Nichols and Sense

All Things Coding and CyberSecurity

This Project is an Enigma

Quite literally actually...

We are going to be making a recreation of the Enigma Machine from scratch. For those who do not know, the Enigma machine was a device used by the Germans during WWII in order to encrypt messages. The device looked sort of like a typewriter, and the person who was writing the message would type it out, and the machine would encrypt it. Then when the recipient received the message, they would type the encoded message into their own enigma machine, which would then decrypt it. How the machine actually did this encrypting and decrypting is actually very fascinating, but I don't think I would be the best at explaining it. Instead, watch this quick video that shows how the machine works with visual examples. This will greatly help you understand some of the code later.

Now that we understand how it works, let's try to build one with code!


Step 1: Making the Plugboard

As you saw in the video, the plugboard was just a bunch of wires that connected keys on the keyboard. For example, if A and X were connected via the plugboard, pressing A would output X and vice versa. This is something we can easily simulate in code. Below is the function I wrote that generates the plugboard. It randomly matches two letters, then adds them to a dictionary. Since the connection goes both ways, I also put the inverse pairing in the dictionary as well, so if A -> X then X -> A. The plugboard often used different numbers of plugs, so the number of plugs is passed in as a parameter.

#Generates plugboard connections for n number of plugs
def generate_plugboard(plugs):
    plugboard = {}
    indexes = random.sample(range(0,26), plugs * 2)
    for i in range(0, plugs * 2, 2):
        k = letters[indexes[i]]
        v = letters[indexes[i+1]]
        plugboard[k] = v
        plugboard[v] = k
    return plugboard

Step 2: Making the Rotors

The rotors were the key factor in what made the Enigma Machine so uncrackable. As you saw in the video, each rotor had 26 input connections and 26 output connections, and each input was paired to another output. That meant any letter going into a rotor would come out as another, which would then enter the next rotor and change again. On top of this, the rotation of the rotors made it even more complicated, but we'll come back to that later. For now, let's just make the rotor itself. To simulate the wiring, we will just make an Ordered Dictionary that pairs letters together. Unlike the plugboard however, these connections are not bidirectional so each letter needs to have it's own unique pairing. Using an Ordered Dictionary is important as well because since we are going to be simulating the stepping of each rotor, the ordering of these pairings matter as well. Below is an example of a rotor made in code. I have also created two more, but they are exactly the same with just different pairings so I will not show them here.

(Note: there are actually three more rotors in my code that swap the keys and values of these dictionaries in order to simulate the reverse path, but this is just to make the coding easier. There are really only 3 rotors.)

#These dictionaries simulate the internal wiring of the rotors and reflector
rotor1 = OrderedDict([
    ('A','Q'),
    ('B','G'),
    ('C','Z'),
    ('D','U'),
    ('E','N'),
    ('F','S'),
    ('G','V'),
    ('H','E'),
    ('I','K'),
    ('J','B'),
    ('K','X'),
    ('L','C'),
    ('M','T'),
    ('N','M'),
    ('O','A'),
    ('P','D'),
    ('Q','I'),
    ('R','R'),
    ('S','O'),
    ('T','H'),
    ('U','L'),
    ('V','Y'),
    ('W','P'),
    ('X','J'),
    ('Y','F'),
    ('Z','W')
])

Step 3: Making the Reflector

The reflector was a device located at the output of the third rotor, which took the output of the final rotor, changed it's value and sent it back through the rotors in reverse. This works largely like the plugboard, and can be simulated with another dictionary. Note that the reflector is bidirectional as well so each pairing is mirrored in the dictionary.

reflector = OrderedDict([
    ('A','P'),
    ('P','A'),
    ('B','K'),
    ('K','B'),
    ('C','T'),
    ('T','C'),
    ('D','J'),
    ('J','D'),
    ('E','M'),
    ('M','E'),
    ('F','O'),
    ('O','F'),
    ('G','N'),
    ('N','G'),
    ('H','R'),
    ('R','H'),
    ('I','Y'),
    ('Y','I'),
    ('L','S'),
    ('S','L'),
    ('Q','V'),
    ('V','Q'),
    ('U','X'),
    ('X','U'),
    ('W','Z'),
    ('Z','W')
])

Step 4: Putting it all Together

Now that we have all of our parts made, it's now time to fit them all together. We'll create a class named Enigma and initialize it with two parameters. These are the intital positions of the rotors and the state of the plugboard. Each rotor was turned to a certain position before encoding a message, and the receiver would have to mirror these positions on their own Enigma machine in order to decipher the message. We will pass a string of three values that represent the offset of each rotor along with the state of the plugboard we created. We will also initialize it with the rotors the we created in step 2. 

class Enigma:
    def __init__(self, rotor_idxs, plugboard):
        self.rotors = [rotor1,rotor2,rotor3]
        self.inverted_rotors = [rotor1_inverted,rotor2_inverted,rotor3_inverted]
        self.rotor_idxs = rotor_idxs.copy()
        self.rotor_init_pos = rotor_idxs.copy()
        self.plugboard = plugboard
 

 

Next we make a method that simulates a character going through a plugboard switch. We pass in a character to the method, which then looks in our plugboard dictionary and outputs the correct character that it is linked to. If the character is not linked to anything, meaning no plug is inserted for that letter on the plugboard, we just return the letter itself.

#simulate the change of character when key is connected to plugboard wire
def swap_plugboard(self, char):
    return self.plugboard.get(char,char)

 

Next we'll simulate the turning of a rotor. When the rotor turns, it shifts one character. To simulate this, we just need to increase the current index of our rotor's dictionary by 1. We also need make sure that when we reach the end of our dictionary, we wrap back around to the beginning. This is why using an Ordered Dictionary is so important.

#simulate the turning of a rotor
def rotate_rotor(self, rotor_number):
    self.rotor_idxs[rotor_number] = (self.rotor_idxs[rotor_number] + 1) % ROTOR_IDX_LENGTH

 

Now that we have rotating completed, we need to simulate how all the rotors step together. When the first rotor makes a full revolution, it then makes the second rotor rotate one position. When the second rotor makes a full rotation, it then makes the third rotor rotate one position. To do this in code, we just check when each rotor has reached the first index, then have it increase it's neighboring rotor's current index by 1. 

#simulate the stepping of rotors when a neighbor completes a full revolution
def step_rotors(self):
    self.rotate_rotor(0)
    if self.rotor_idxs[0] == 0:
        self.rotate_rotor(1)
        if self.rotor_idxs[1] == 0:
            self.rotate_rotor(2)

 

Now we need to simulate what actually happens when a letter goes through a rotor. This is by far the most complicated part so I will try to explain it as best as I can. If you would like a more in depth dive into how they worked, this wikipedia article is what I mainly used in my research to understand the wiring. Here is the full method and I will explain what each line does.

#simulate how the character changes while passing through a rotor forwards.
    def forward_scramble(self, char, rotor_number):
        idx = letters.index(char)
        offset = idx + self.rotor_idxs[rotor_number]
        wrap = offset % 26
        char = letters[wrap]
        rotor_swapped = self.rotors[rotor_number][char]
        swapped_idx = letters.index(rotor_swapped)
        output_idx = (swapped_idx - self.rotor_idxs[rotor_number]) % 26
        rotor_final = letters[output_idx]
        return rotor_final

 

As seen in the video, the letter goes through the input, and comes out as a different letter depending on the internal wiring of the rotor. But we can't forget that since the rotor is rotating, we need to account for that as it will affect the final output as well. So to start with, we will need to get the index of the incoming character in the alphabet (It's position in the alphabet, A=0,B=1,C=3, etc...).

idx = letters.index(char)

 

We then take this value and add it to current index of our rotor to account for the rotation of the rotor. This gives us the distance between the original letter sent into the rotor and it's current offset.

offset = idx + self.rotor_idxs[rotor_number]

 

We then mod the result by 26 in order to wrap back around to the beginning if the offset is greater than 26. We now have the index of the correct input letter of the rotor.

wrap = offset % 26

 

Next, we again get the letter at that index of the alphabet.

char = letters[wrap]

 

Finally we get the corresponding letter in our rotor dictionary. This is the output of where the input wire is connected.

rotor_swapped = self.rotors[rotor_number][char]

 

We have just simulated the wiring of the rotor, but we aren't quite done yet. Since we added the offset of the rotor for the input, our output is also still rotated. We must reverse this to get the true final output of the rotor. So we will just do the same steps again, but subtracting the offset instead of adding. With that, we have the final output of our rotor.

swapped_idx = letters.index(rotor_swapped)
output_idx = (swapped_idx - self.rotor_idxs[rotor_number]) % 26
rotor_final = letters[output_idx]
return rotor_final

 

Whew...

The last thing we have to do now is simulate a character passing through the reflector. Luckily this is much easier than the rotors as the reflector is static and all the pairings are bidirectional. All we have to do for this is just look up the cooresponding charactor in the reflector dictionary.

char = reflector[char]

 

I know we said we are done now, but that wasn't quite correct. We now have to simulate the character going back through all three rotors and the plugboard again on its reverse trip. Luckily though we can just use all the same code as it works the exact same way going backwards as it does forwards. the only difference is that when we go through the rotors backwards, the mappings of the wiring is reversed which I have simulated just by making three more rotors that are identical to the original three, just with the keys and values swapped. We can now simulate the entire process of a character moving through the Enigma Machine! In code, it looks something like this.

    #full simulation of how a character changes when a button is pressed on the keyboard
    def simulate_keypress(self, char):
        char = self.swap_plugboard(char)
        char = self.forward_scramble(char,0)
        char = self.forward_scramble(char,1)
        char = self.forward_scramble(char,2)
        char = reflector[char]
        char = self.reverse_scramble(char,2)
        char = self.reverse_scramble(char,1)
        char = self.reverse_scramble(char,0)
        char = self.swap_plugboard(char)
        self.step_rotors()
        return char

As you can see, we first simulate the letter going through the plugboard, through all three rotors, through the reflector, back through all three rotors, and back out through the plugboard. We also can't forget to rotate the rotors at the end of each keypress as well.

Now we just have to write a simple method that takes some text, simulates all the keypresses with our previous method, and outputs the encrypted message. 

    def encrypt_message(self, message):
        try:
            message = message.replace(" ","").upper()
            if message.isalpha():
                encrypted_message = ''
                for char in message:
                    encrypted_char = self.simulate_keypress(char)
                    encrypted_message += encrypted_char
                return(encrypted_message)
        except:
            print("Only Letters are allowed")

 

That's it! We really are done now. All that is left now is to test it out. Here I am simulating two Enigma Machines, one of which is encrypting our message, and the other is decrypting it. As you can see below, both machines have the exact same rotor posititions and plugboard configurations passed to them. This ensures that the second machine is able to decrypt the first machines output.

enigma1 = Enigma([2,12,15,],plugboard)     
enigma2 = Enigma([2,12,15,],plugboard)

plaintext = "I SPENT WAY TOO LONG WORKING ON THIS"

encrypted = enigma1.encrypt_message(plaintext)
decrypted = enigma2.encrypt_message(encrypted)


print("Encoded Text: ",encrypted)
print("Decoded Text: ",decrypted)

And here is our output:

Encoded Text:  BQJUPJKFQMLQCTLNQNHLJEOAWNQLP
Decoded Text:  ISPENTWAYTOOLONGWORKINGONTHIS

Thank you for sticking through it if you've made it this far. I've always been a History nerd so this was a really fun project for me. In the next post, we're going to explore how this machine was eventually cracked by the great Alan Turing, so stay tuned

 

As always, you can view the full source code here