La máquina Enigma en Python

Vamos a simular la máquina Enigma usando Python. Para empezar, vamos a simplificar la máquina como una serie de permutaciones que nos envían letras del alfabeto a otras letras. Vamos a escribirlas usando la notación usual donde, por ejemplo, la permutación que nos lleva una letra a la siguiente será:

\[\begin{pmatrix} abcdefghijklmnopqrstuvwxyz \\ bcdefghijklmnopqrstuvwxyza \end{pmatrix}\]

Y, en Python, esto lo representaremos solo como la segunda fila, por tanto, necesitaremos alguna función auxiliar para llevar esto:

def letra_a_num(a):
  return ord(a.upper())-ord("A")
def num_a_letra(a):
  return "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[a]

Esto lo usaremos para pasar letras a números, y así poder buscarlas en nuestras cadenas que usaremos para representar las permutaciones.

Configuración de los rotores

La máquina que estamos emulando es el modelo E-Eins, o sea, la primera de uso militar. Esta máquina tiene tres rotores, a elegir entre cinco. Estos rotores se sitúan entre los contactos de entrada/salida y el llamado reflector (que en esta máquina es uno entre dos).

Cuando pulsamos una tecla del teclado de la máquina, el rotor de la derecha gira una posición, y después se cierra un circuito que pasa por cada disco, luego vuelve por el reflector (que es otra permutación) para luego volver por los rotores y llegar a una bombilla asignada a una letra, que será la letra cifrada.

Por tanto, necesitamos una manera de simular el giro de los rotores, aquí es donde entrará la siguiente función, la cual cicla las letras de la permutación y luego les asigna la letra anterior:

def cicla_permuta(perm, n=1):
  for _ in range(0,n):
    perm = perm[1:]+str(perm[0])
    perm = "".join([num_a_letra((letra_a_num(i)-1)%26) for i in perm])
  return perm

Así como una función que nos de la imagen de una letra por una permutación:

def permuta(perm, direc, a):
    if direc == 1:
        return perm[letra_a_num(a)]
    else:
        return num_a_letra(perm.index(a))

Esto nos permite tomar la imagen tanto de la permutación como de su inversa.

Por último, cada rotor tiene dos configuraciones, la llamada configuración interna (Ringstellung), que es un desfase del disco indicador del rotor con respecto al cableado interno y la configuración externa (Grundstellung) que es el desfase del rotor una vez introducido. A su vez, cada rotor tiene un punto llamado el turnover, cuando un disco gira pasado su punto de turnover, girará el siguiente. Para modelizarlo en Python, usaremos la siguiente clase:

class Rotor:
    def __init__(self, to, perm):
        self.rs = 0 # Ringstellung
        self.gs = 0 # Grundstellung
        self.perm_init = perm
        self.to = to # Donde se hace el turnover
        self.perm = perm # Permutación del anillo
        self.isTurnover = False # Si se tiene o no que hacer turnover
    def set_rs(self, rs):
        self.rs = rs
        self.perm = cicla_permuta(self.perm, -rs%26)
    def set_gs(self, gs):
        self.gs = gs
        self.perm = cicla_permuta(self.perm_init, gs)
    def permuta_ida(self, a):
        return permuta(self.perm, 1, a)
    def permuta_vuelta(self, a):
        return permuta(self.perm, 0, a)
    def turn(self):
        self.gs = (self.gs + 1) % 26
        self.perm = cicla_permuta(self.perm)#, 25)
        if (self.gs)%26 == self.to:
            self.isTurnover = True
    def turnover(self):
        if self.isTurnover:
            self.isTurnover = False
            return True
        else:
            return False

También se pueden combinar el Grundstellung y el Ringstellung en una sola configuración total que sería la resta de ambos, pero aquí estoy intentando hacer una reproducción fiel de su uso.

El clavijero

El clavijero era una fase extra de seguridad que permutaba una letra por otra a la entrada y salida. Lo modelizamos usando un diccionario de Python, siendo None el clavijero vacío. Por tanto, usaremos la siguiente función para pasar el clavijero:

def pasa_clavijero(pares, a):
    if pares == None:
        return a
    elif not (a in pares.keys() or a in pares.values()):
        return a
    else:
        for k, v in pares.items():
            if k == a:
                return v
            elif v == a:
                return k

La máquina

Usaremos una clase que toma como campos los tres rotores (ordenados de derecha a izquierda), el reflector y el clavijero, y aplica las permutaciones dadas antes. Notar que los rotores giran antes de que se cifre cada letra:

class Maschine:
    def __init__(self, r1, r2, r3, ukw, kl):
        self.r1, self.r2, self.r3 = r1, r2, r3
        self.ukw = ukw
        self.kl = kl
        
    def settings(self, gss, rss):
        self.r1.set_gs(gss[2])
        self.r2.set_gs(gss[1])
        self.r3.set_gs(gss[0])
        
        self.r1.set_rs(rss[2])
        self.r2.set_rs(rss[1])
        self.r3.set_rs(rss[0])
    def cifrar_letra(self, a):
        self.r1.turn()
        if self.r1.turnover():
            self.r2.turn()
            if self.r2.turnover():
                self.r3.turn()
        ret = pasa_clavijero(self.kl, a)
        ret = self.r1.permuta_ida(ret)
        ret = self.r2.permuta_ida(ret)
        ret = self.r3.permuta_ida(ret)
        ret = permuta(self.ukw,1,ret)
        ret = self.r3.permuta_vuelta(ret)
        ret = self.r2.permuta_vuelta(ret)
        ret = self.r1.permuta_vuelta(ret)
        ret = pasa_clavijero(self.kl, ret)
        return ret
    def cifrar_mensaje(self,X):
        return "".join([self.cifrar_letra(i) for i in X])
    def __repr__(self):
        return "("+num_a_letra(M.r3.gs) + ", "+ num_a_letra(M.r2.gs)+", "+num_a_letra(M.r1.gs)+")"

Aquí, usamos el método mágico __repr__ para ver los Grundstellung de los rotores como se verían en las ventanillas de la máquina al usar la función print con un objeto de esta clase.

Cableados y ejemplo

Los cableados que se usaban durante la guerra (con la notación que estamos usando) eran:

permR1 = "EKMFLGDQVZNTOWYHXUSPAIBRCJ"
permR2 = "AJDKSIRUXBLHWTMCQGZNPYFVOE"
permR3 = "BDFHJLCPRTXVZNYEIWGAKMUSQO"
permR4 = "ESOVPZJAYQUIRHXLNFTGKDCMWB"
permR5 = "VZBRGITYUPSDNHLXAWMJQOFECK"
UKW_C  = "FVPJIAOYEDRZXWGCTKUQSBNMHL"
UKW_B  = "YRUHQSLDPXNGOKMIEBFZCWVJAT"

Con esas permutaciones podemos generar los rotores:

R1 = Rotor(letra_a_num("Q")+1, permR1)
R2 = Rotor(letra_a_num("E")+1, permR2)
R3 = Rotor(letra_a_num("V")+1, permR3)
R4 = Rotor(letra_a_num("J")+1, permR3)
R5 = Rotor(letra_a_num("Z")+1, permR3)

Aquí estamos poniendo el punto de turnover que tenían los rotores de la época, usamos +1 porque el turnover pasa en la letra de después del punto de turnover.

Ahora, si tomamos un clavijero AK HL, rotores I-II-III configuración externa DIE y configuración interna GOV, configuramos la máquina usando:

KL = {"A":"K","H":"L"}
M = Maschine(R3,R2,R1,UKW_B,KL) # recordar que los rotores van de dcha a izda
GS = ["D","I","E"]
RS = ["G","O","V"]
M.settings(list(map(letra_a_num, GS)), list(map(letra_a_num, RS)))

Podemos ahora cifrar un mensaje, por ejemplo: Máquina enigma programada en Python. Primero, quitamos las tildes y cambiamos los espacios por X, obteniendo MAQUINAXENIGMAXPROGRAMADAXENXPYTHON, y lo ciframos así:

M.cifrar_mensaje("MAQUINAXENIGMAXPROGRAMADAXENXPYTHON")

Obteniendo (yo esto lo tengo en un cuaderno de ipython, así que la salida me sale inmediatamente debajo):

'NRPILCIMQVCLXYWBBJZDETHXUZXBTVONEYB'

Después, si reseteamos la máquina y volvemos a pasar el mensaje, lo desciframos:

M.settings(list(map(letra_a_num, GS)), list(map(letra_a_num, RS)))
M.cifrar_mensaje(mensaje_cifrado)
'MAQUINAXENIGMAXPROGRAMADAXENXPYTHON'