current position:Home>Write an Enigma machine in Python

Write an Enigma machine in Python

2022-02-02 06:26:08 AHA acridine

Head author :Bob Lord, link :commons.wikimedia.org/w/index.php… CC BY-SA 3.0 Agreement authorization . therefore , This paper also adopts this protocol .

cause

One day , In my spare time , I came across a video :

【 Computer natural history 】 War code ( Part I ) How to recreate an engema machine _ Bili, Bili _bilibili

For enigma (Enigma) machine , Although I've heard of it , But there is no in-depth understanding of . So I ordered in .

The encryption method of this machine is too subtle , So I came up with the idea of reproducing it in programming language .

Briefly, Enigma machine

Simply speaking , Enigma cipher machine is an encryption and decryption machine developed by Germany . During the second world war , It was used on a large scale by the German army .

The encryption method of this machine is very exquisite : The encryption components are several rotors and a reflector . Put aside the knowledge of electricity :

  1. A letter goes into a rotor , Will come out of another letter , It's equivalent to changing a letter ;
  2. Press a letter , The rightmost rotor will turn one grid , It will also drive other rotors , The principle is very similar to that of a clock ;
  3. Because the rotor will rotate , Letters coming out of one rotor go into front of another rotor , The letters will also change ;
  4. To the reflector , The letter will be replaced by another letter , Then reverse through the rotor , It is equivalent to encrypting again ;
  5. The order and initial position of the rotor can be customized .

It can be seen that , Its encryption method is equivalent to setting a changed password table for each letter in the text , One letter for a password table . In theory, , A three rotor enigma cipher machine has 26 × 25 × 26 = 16900 A combination of ( Although it is actually slightly less than this number ).

in addition , Some machines also have terminal blocks . Insert the two letter socket , It's equivalent to exchanging two letters . thus , There are more combinations .

It has the following properties :

  • Enter the same , rotor 、 The terminal block configuration is different , The output may be different ;
  • Input is not equal to output ;
  • rotor 、 When the terminal block configuration is the same , Enter ciphertext , Output the original text .

In short , You first write down the installation method and initial position of the rotor , Press the same button continuously , Write down the output . You'll find that , The output letters are different , But it can never be the letter entered . Turn the rotor back , Enter the last ciphertext , You'll find that , The output is all that letter .

 Enigma cipher works

Because of this nature , Intelligence agents in Britain and France think it can't be cracked , But for a defeated country in World War I , They don't care ; Polish intelligence once found the direction of decoding , However, due to the update of encryption methods and the flash war in Germany , Nor did it change the fate of Poland ; Last , A group of Britons including Turing , Just cracked it well , It also changed the direction of World War II .

Define each class

An Enigma machine , There's input 、 Output 、 Fixed interface 、 rotor 、 It is composed of reflector and terminal block .

Input and output are very simple , Just use the incoming parameters and return values of the method .

Fixed interface 、 rotor 、 The structure of the reflector is very similar , Can be simulated as an input of a letter 、 A device that outputs a letter , At least the corresponding table and location information are required . For rotors , It turns one grid at a time , Change its position , The position can also be adjusted manually . therefore , We can create a class , Including these three , Name it Plate( The disk ). Then create an inheritance from Plate Three classes of .

Every time the terminal block is connected , Will connect several two letters in the alphabet .

As for the relationship between each component and Enigma machine , Because some models of Enigma machines don't have terminal blocks ; If there is a terminal block , Then it is an integral part of the machine , So the terminal block is initialized together when the Enigma machine is initialized . As for the disc , The fixed interface is initialized together when the Enigma machine is initialized , The rotor and reflector are initialized before .

thus , We can initially define the following structure :

class Plate:
    """ The disk , Including fixed interface 、 rotor 、 Reflector """
    def __init__(self, map_table, position): ...
    def encrypt(self, letter): ...  #  Encrypt a single letter 


class EntryPlate(Plate):
    """ Fixed interface """
    ...


class Rotor(Plate):
    """ rotor """
    def __init__(self, map_table, position): ...
    def forward(self): ...  #  turn 


class Reflector(Plate):
    """ Reflector """
    ...


class Plugboard:
    """ Wiring board """
    def __init__(self, parent): ...
    def plug(self, letter_1, letter_2): ...     #  connection 
    def unplug(self, letter): ...   #  Remove the wiring 


class Enigma:
    """ The Enigma machine """
    def __init__(self, rotor_list, reflector): ...
    #  The terminal block is initialized inside during the initialization of Enigma machine , Therefore, it is not introduced into 
    def input(self, string): ...    #  Input string 
    def set_position(self, *position_list): ...     #  Set the position of each rotor 
 Copy code 

Of course , Each class initialization requires more parameters than this . and , These classes are actually distributed in different files , I put them together just for the convenience of explanation .

Realize the function of disc

The basic function of the disc , Is to enter a letter , Output another letter .

The general Enigma machine only processes capital letters . In order to realize the function , When initializing, pass in map_table As A-Z The result of the transformation .

To explore how to transform , We can make a simple machine first , Only ASDF Four letters :

 Simplified Enigma machine

In the initial position , Input A, The circuit is shown in the figure :

 Initial position input  A  The circuit diagram of

Be careful : We're just thinking about how to write the code , In fact, there are some noticeable differences between the principle of Enigma machine and the current simplified model , I'll explain later .

then , The rightmost rotor rotates up one grid , Input A, The circuit is shown in the figure :

 The rotor on the right turns up one grid , Input  A  The circuit diagram of

First compare the two inputs , The current flows through the line of the rightmost rotor :

 Comparison diagram of current passing through the rightmost rotor

It's not hard to see , You can first write a method to represent the current state of the rotor , Then use this method when encrypting .

When initializing the rotor , We will enter the input symbol of the rotor (map_source) And output results (map_table), And its front and rear discs (next_plate, prev_plate):

def __init__(self, map_source, map_table, prev_plate=None, next_plate=None):
    self.map_source = map_source
    dict_table = {}
    for input_letter_index, input_letter in enumerate(map_source):
        dict_table[input_letter] = map_table[input_letter_index]
    self.dict_table = dict_table
    self.prev_plate = prev_plate
    self.next_plate = next_plate
    self.position = 0
 Copy code 

self.dict_table It expresses the corresponding relationship between input and output . For the rightmost rotor , as follows :

{'A': 'D', 'S': 'A', 'D': 'S', 'F': 'F'}
 Copy code 

Try to output self.position Before and after the change , The state of the rotor (current_state):

@property
def current_state(self):
    right = list(self.dict_table.keys())
    left = list(self.dict_table.values())
    current_right = right[-self.position:] + right[:-self.position]
    current_left = left[-self.position:] + left[:-self.position]
    return {'left': current_left, 'right': current_right}
    # self.position == 0: {'left': ['S', 'D', 'A', 'F'], 'right': ['A', 'S', 'D', 'F']}
    # self.position == 1: {'left': ['F', 'S', 'D', 'A'], 'right': ['F', 'A', 'S', 'D']}
 Copy code 

We can notice that , The letter when the current enters the rotor , The output of the previous disc is not necessarily the same . So we also need to find the letter when entering the rotor :

def transform_letter(self, letter):
    letter_index = self.prev_plate.current_state['right'].index(letter)
    transformed_letter = self.current_state['right'][letter_index]
 Copy code 

Then encrypt :

def encrypt(self, letter):
    return self.dict_table[self.transform_letter(letter)]
 Copy code 

The above method can always use electric current to get out of the reflector . however , If the current comes out of the reflector , It's about to change the code . This time we don't have to next_plate and prev_plate, Instead, use left_plate and right_plate, Lest you get confused next . By the way, consider the leftmost and rightmost situations .

in addition , By the way, add the method of adjusting the position :

def __init__(self, map_source, map_table, left_plate=None, right_plate=None):
    self.map_source = map_source
    dict_table = {}
    for input_letter_index, input_letter in enumerate(map_source):
        dict_table[input_letter] = map_table[input_letter_index]
    self.dict_table = dict_table
    self.left_plate = left_plate
    self.right_plate = right_plate
    self.position = 0

def transform_letter(self, letter, from_plate):
    letter_index = from_plate.current_state['right'].index(letter)
    transformed_letter = self.current_state['right'][letter_index]

def encrypt(self, letter, letter_from='right'):
    if letter_from == 'left':
        from_plate = self.left_plate
    else:
        from_plate = self.right_plate
    if from_plate:
        letter = self.transform_letter(letter, from_plate)
    if letter_from == 'left':
        key_index = list(self.dict_table.values()).index(letter)
        left_letter = list(self.dict_table.keys())[key_index]
        return left_letter
    else:
        return self.dict_table[letter]

def set_position(self, position):
    position_int = 0
    if isinstance(position, str):
        if position in self.map_source:
            position_int = self.map_source.index(position)
    elif isinstance(position, int):
        if 0 <= position < len(self.map_source):
            position_int = position
    self.position = position_int
 Copy code 

The above is placed in Plate Methods in class . Next , On demand configuration auto_rotatable Properties of :

class Plate:
    def __init__(self, map_source, map_table, left_plate=None, right_plate=None, auto_rotatable=False):
        self.map_source = map_source
        dict_table = {}
        for input_letter_index, input_letter in enumerate(map_source):
            dict_table[input_letter] = map_table[input_letter_index]
        self.dict_table = dict_table
        self.left_plate = left_plate
        self.right_plate = right_plate
        self.position = 0
        self.auto_rotatable = auto_rotatable

class Rotor(Plate):
    def __init__(self, map_source, map_table, left_plate=None, right_plate=None, ...):
        super().__init__(map_source, map_table, left_plate, right_plate, auto_rotatable=True)

class Reflector(Plate):
    def __init__(self, map_source, map_table, right_plate=None, ...):
        super().__init__(map_source, map_table, left_plate=None, right_plate=right_plate, auto_rotatable=False)

class EntryPlate(Plate):
    def __init__(self, map_source, map_table, left_plate=None, ...):
        super().__init__(map_source, map_table, left_plate, right_plate=None, auto_rotatable=False)
 Copy code 

Set this property , It's for you Rotor Class add method forward(), Turn one grid , Along with processing carry :

def forward(self):
    original_position = self.position
    if original_position == len(self.map_source) - 1:
        self.position = 0
    else:
        self.position += 1
    if self.left_plate and self.left_plate.position == len(self.map_source) - 1:
        if self.left_plate.auto_rotatable:
            self.left_plate.forward()
    return self.position
 Copy code 

The part of the disc is over . Let's start building the terminal block .

Build the terminal block

The construction of the terminal block is relatively simple , Is to create a correspondence table .

class Plugboard:
    """ Wiring board """
    def __init__(self):
        self.map_dict = {}

    def plug(self, letter_1, letter_2):
        self.map_dict[letter_1] = letter_2
        self.map_dict[letter_2] = letter_1

    def unplug(self, letter):
        another_letter = self.map_dict[letter]
        del self.map_dict[letter]
        del self.map_dict[another_letter]
 Copy code 

Assemble Enigma machine

Due to the initialization sequence mentioned earlier , When we initialize the Enigma machine , Do the following :

  1. List of incoming rotors ( The rotors in this list are from right to left )、 Reflector ;
  2. Define the disk on the right side of the reflector as the leftmost rotor ;
  3. Define fixed interface ;
  4. Define the left and right discs of each rotor ;
  5. Initialize the patch panel .

So there is the following code :

class Enigma:
    def __init__(self, rotor_list, reflector):
        self.rotors = rotor_list
        self.reflector = reflector
        self.reflector.right_plate = self.rotors[-1]
        self.entry_plate = EntryPlate(
            map_source=self.reflector.map_source, map_table=self.reflector.map_source, left_plate=self.rotors[0]
        )
        for rotor_index, rotor in enumerate(self.rotors):
            if rotor_index < len(self.rotors) - 1:
                rotor.left_plate = self.rotors[rotor_index + 1]
            else:
                rotor.left_plate = self.reflector
            if rotor_index > 0:
                rotor.right_plate = self.rotors[rotor_index - 1]
            else:
                rotor.right_plate = self.entry_plate
        self.plugboard = Plugboard()
 Copy code 

Let's write down the method of setting the position of each rotor set_position(). If no parameter is filled in , Set to 0( We define the initial position as 0, This is different from the actual Enigma machine , In this case, the corresponding number on the rotor is 01):

def set_position(self, *position_list):
    if not position_list:
        position_list = (0, ) * len(self.rotors)
    elif len(position_list) != len(self.rotors):
        raise AttributeError('Position list length is not equal to rotors number')
    for rotor, position in zip(self.rotors, position_list):
        rotor.set_position(position)
 Copy code 

Then there is the input method input(). Finally, it's the last link , But there's not much to say , The previous principle is quite clear .

def input(self, string):
    new_string_list = []
    for letter in string:
        if letter in self.plugboard.map_dict:
            letter = self.plugboard.map_dict[letter]
        for rotor in self.rotors:
            letter = rotor.encrypt(letter, 'right')
        letter = self.reflector.encrypt(letter, 'right')
        for rotor in reversed(self.rotors):
            letter = rotor.encrypt(letter, 'left')
        letter = self.entry_plate.encrypt(letter, 'left')
        if letter in self.plugboard.map_dict:
            letter = self.plugboard.map_dict[letter]
        new_string_list.append(letter)
        self.rotors[0].forward()
    return ''.join(new_string_list)
 Copy code 

test

Then let's try to run ! Don't tube the patch panel yet .

if __name__ == '__main__':
    main_map_source = 'ASDF'
    e = Enigma(
        [
            Rotor(map_source=main_map_source, map_table='DASF'),
            Rotor(map_source=main_map_source, map_table='SDAF'),
            Rotor(map_source=main_map_source, map_table='DFAS'),
        ],
        Reflector(map_source=main_map_source, map_table='DFAS')
    )
    print(e.input('AA'))     # 'DS'
 Copy code 

Output :

DS
 Copy code 

As expected .

however , The previous simplified model is a simplified model after all , Let's find a practical example .

I found a simulator of Enigma machine :Enigma Simulator. It comes with file , This will better restore the real situation .

However, the connection method mentioned in the document is not convenient for input :

 The connection method mentioned in the document

But I found another page about Enigma machine :Enigma Rotor and Umkehrwalze Wirings, It puts the connection mode in one line , Easier to enter .

 This kind of connection is easier

then :

if __name__ == '__main__':
    main_map_source = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    e = Enigma(
        [
            Rotor(map_source=main_map_source, map_table='BDFHJLCPRTXVZNYEIWGAKMUSQO'),
            Rotor(map_source=main_map_source, map_table='AJDKSIRUXBLHWTMCQGZNPYFVOE'),
            Rotor(map_source=main_map_source, map_table='EKMFLGDQVZNTOWYHXUSPAIBRCJ'),
        ],
        Reflector(map_source=main_map_source, map_table='YRUHQSLDPXNGOKMIEBFZCWVJAT')
    )
    print(e.input('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'))
 Copy code 

Output results :

UOBEFMBPLDCMTBSKTLXCWOGZDBUOBE
 Copy code 

Compare the results with the simulator :

 The results of the simulator

BDZGOWCXLTKSBTMCDLPBMUQOFXYHCX
 Copy code 

It can't be as like as two peas , At least it's irrelevant .

What's going on ?

Several points that cannot be ignored

In fact, when writing programs , I don't know how many pits I stepped on . The first is how to judge the direction from the output of the previous disc to the input of the next disc . It only says the results , But it took me three days , Only from the A website Inspired by :

 The video of that website

Then the output mentioned before is inconsistent .

Browsing Enigma code machine - Wikipedia , An encyclopedia of freedom when , I found a sentence in it :

When you press a key , The rotor will turn before the circuit is connected .

in other words , First turn the rotor , Then turn on the circuit .

So change the code , Adjust the rotor rotation command before translation , Run again , Output :

OBEFMBPLDCMTBSKTLXCWOGZDBUOBEF
 Copy code 

It's still different . Don't worry. , We open the cover of the simulator :

 Open the cover of the simulator

You can find , From this perspective , The rotor of Enigma machine rotates downward . And the picture we drew before , It turns upward .

This is more troublesome . I want to be compatible with the previous situation , So modify the code :

class Plate:
    def __init__(self, map_source, map_table, left_plate=None, right_plate=None, auto_rotatable=False, rotate_up=False):
        ...
        #  add 
        self.rotate_up = rotate_up
   ...
 @property
    def current_state(self):
        right = list(self.dict_table.keys())
        left = list(self.dict_table.values())
        transform_position = (-2 * self.rotate_up + 1) * self.position
        current_right = right[transform_position:] + right[:transform_position]
        current_left = left[transform_position:] + left[:transform_position]
        return {'left': current_left, 'right': current_right}
 Copy code 

Inside -2 * self.rotate_up + 1, When self.rotate_up by True when , The value is -1; When self.rotate_up by False when , The value is 1.

Run again :

BDZGOWCXLTKSBTMCDLPBMFEBOUBDZG
 Copy code 

It looks right . wait :

BDZGOWCXLTKSBTMCDLPBMFEBOUBDZG  #  Code 
BDZGOWCXLTKSBTMCDLPBMUQOFXYHCX  #  Simulator 
 Copy code 

From 22 A letter starts , Not so . What's going on ?

Take a look at the simulator :

 Simulator operation

You see ? Carry is not carried at the end of a turn . As for how to carry , The documents mentioned before are :

 Carry mode

Change it .

class Plate:
    def __init__(self, map_source, map_table, left_plate=None, right_plate=None, auto_rotatable=False, rotate_up=False):
        self.map_source = map_source
        dict_table = {}
        for input_letter_index, input_letter in enumerate(map_source):
            dict_table[input_letter] = map_table[input_letter_index]
        self.dict_table = dict_table
        self.left_plate = left_plate
        self.right_plate = right_plate
        self.position = 0
        self.auto_rotatable = auto_rotatable
        self.rotate_up = rotate_up
        self.turnover = []


class Rotor(Plate):

    def turnover_single_check_and_standardize(self, turnover_single, map_source):
        if isinstance(turnover_single, str):
            return map_source.index(turnover_single)
        elif isinstance(turnover_single, int):
            return turnover_single

    def __init__(self, map_source, map_table, left_plate=None, right_plate=None, rotate_up=False, turnover=None):
        super().__init__(map_source, map_table, left_plate, right_plate, auto_rotatable=True, rotate_up=rotate_up)
        if turnover is None:
            turnover = map_source[-1]
        if isinstance(turnover, list) or isinstance(turnover, tuple):
            turnover_list = []
            for turnover_item in turnover:
                turnover_list.append(self.turnover_single_check_and_standardize(turnover_item, map_source))
        else:
            turnover_list = [self.turnover_single_check_and_standardize(turnover, map_source)]
        self.turnover = turnover_list

    def forward(self):
        original_position = self.position
        if original_position == len(self.map_source) - 1:
            self.position = 0
        else:
            self.position += 1
        if original_position in self.turnover:
            if self.left_plate:
                if self.left_plate.auto_rotatable:
                    self.left_plate.forward()
        return self.position
 Copy code 

function , Compared with the results of the simulator :

BDZGOWCXLTKSBTMCDLPBMFEBOXYHCX  #  Code 
BDZGOWCXLTKSBTMCDLPBMUQOFXYHCX  #  Simulator 
 Copy code 

There is a small difference in the middle . Oh , The rotor tested is not set to carry , Re reform :

if __name__ == '__main__':
    main_map_source = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    e = Enigma(
        [
            Rotor(map_source=main_map_source, map_table='BDFHJLCPRTXVZNYEIWGAKMUSQO', turnover='V'),
            Rotor(map_source=main_map_source, map_table='AJDKSIRUXBLHWTMCQGZNPYFVOE', turnover='E'),
            Rotor(map_source=main_map_source, map_table='EKMFLGDQVZNTOWYHXUSPAIBRCJ', turnover='Q'),
        ],
        Reflector(map_source=main_map_source, map_table='YRUHQSLDPXNGOKMIEBFZCWVJAT')
    )
    print(e.input('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'))
 Copy code 

function :

BDZGOWCXLTKSBTMCDLPBMUQOFXYHCX  #  Code 
BDZGOWCXLTKSBTMCDLPBMUQOFXYHCX  #  Simulator 
 Copy code 

It's finally the same .

Try to see if the output results can return to the original results :

if __name__ == '__main__':
    main_map_source = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    e = Enigma(
        [
            Rotor(map_source=main_map_source, map_table='BDFHJLCPRTXVZNYEIWGAKMUSQO', turnover='V'),
            Rotor(map_source=main_map_source, map_table='AJDKSIRUXBLHWTMCQGZNPYFVOE', turnover='E'),
            Rotor(map_source=main_map_source, map_table='EKMFLGDQVZNTOWYHXUSPAIBRCJ', turnover='Q'),
        ],
        Reflector(map_source=main_map_source, map_table='YRUHQSLDPXNGOKMIEBFZCWVJAT')
    )
    print(e.input('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'))
    e.set_position()
    print(e.input('BDZGOWCXLTKSBTMCDLPBMUQOFXYHCX'))
 Copy code 

function :

BDZGOWCXLTKSBTMCDLPBMUQOFXYHCX
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
 Copy code 

Everything is all right .

You can also test the results of inserting the patch panel . Because there's too much in the front , Let's leave it to you .

project

actually , We also need to consider information such as the correctness of the input data . But it's not written here , Too much space . If you are interested, you can see me in GitHub Project on :DingJunyao/myenigma: Python-based Enigma., Welcome to Fork、Star.

I've written the project into a Python package , You can install... From the following command line :

pip install myenigma
 Copy code 

Only support Python 3.10 Later versions , file :myenigma - Acridine

Actually, I wanted to register as enigma Of , In limine stay PyPI No duplicate names were found on the . Until I came across A namesake , But I don't know why , It's not in the search record .

copyright notice
author[AHA acridine],Please bring the original link to reprint, thank you.
https://en.pythonmana.com/2022/02/202202020626014385.html

Random recommended