class EnigmaMachine(Encoder): def __init__(self, *configs): self.plugboard = Plugboard(configs[-1]) self.rotors = [] # initially no rotors for rotor_path in configs[:-1]: self.rotors.append(Rotor(rotor_path)) self.reflector = Reflector(13) def encode(self, input): input = ord(input) - ord('A') # A -> 0, ..., Z -> 25 output = self.plugboard.encode(input) output = self.pass_rotors('encode', output) output = self.reflector.encode(output) output = self.pass_rotors('decode', output) output = self.plugboard.decode(output) if len(self.rotors) > 0: # check for rotors self.rotate_rotors(0) # rotate necessary rotors return chr(output + ord('A')) def encode_message(self, message): encoded_message = '' for letter in message: if letter == ' ': encoded_message += ' ' else: encoded_message += self.encode(letter) return encoded_message def decode(self, output): return self.encode(output) # reciprocal cipher def rotate_rotors(self, rotor_i): if self.rotors[rotor_i].rotate(): rotor_i += 1 if rotor_i < len(self.rotors): self.rotate_rotors(rotor_i) def pass_rotors(self, transformer, input): direc = -1 if transformer == 'decode' else 1 for rotor_num, rotor in enumerate(self.rotors[::direc]): # get output of current rotor if transformer == 'encode': input = rotor.encode(input) else: # decode input = rotor.decode(input) return input
class Enigma: """ This class simulates the Enigma machine As models M3 and M4 of the Enigma machine, it supports 3 or 4 rotors configuration. The order of reflector and rotors definition respect the physical order (left-to-right). :param reflector: The reflector associated to the Enigma machine :type reflector: Reflector :param *rotors: The rotors associated to the Enigma machine :type *rotors: Variable length rotor list :raises: :class:`TypeError, ValueError`: If one of the provided parameter is invalid Example:: enigma = Enigma(ReflectorB(), RotorI(), RotorII(), RotorIII("V")) enigma.encode("HELLOWORLDHELLOWORLDHELLOWORLD") enigma = Enigma(ReflectorA(), RotorIV("E", 18), RotorV("Z", 24), RotorBeta("G", 3), RotorI("P", 5)) enigma.plugboard.set("PC XZ FM QA ST NB HY OR EV IU") enigma.encode("BUPXWJCDPFASXBDHLBBIBSRNWCSZXQOLBNXYAXVHOGCUUIBCVMPUZYUUKHI") """ def __init__(self, reflector, *rotors): self._rotor_chain = RotorChain(reflector, *rotors) self.plugboard = Plugboard() self.__alpha_string_validator = Validator( TypeValidator(str), LengthValidator(1, lambda x, y: x >= y), AlphaValidator()) def encode(self, string): """ Perform the whole encoding of a string on the Enigma machine Each character of the string is first encoded by the plug board then the character is encoded through the rotor chain and finally the character is encoded by the plug board again. :param char: The string to encode :type char: str :return: The encoded string :rtype: str """ self.__alpha_string_validator.validate(string) encoded_string = "" for letter in string: encoded_string += self.plugboard.encode( self._rotor_chain.encode(self.plugboard.encode(letter))) return encoded_string def reset(self): """ Reset all rotors of the machine to position to "A" and the ring setting to 1 """ self._rotor_chain.reset() @property def plugboard(self): """ The plug board associated to the Enigma machine :getter: Returns the plug board :setter: Sets the plug board :type: Plugboard """ return self.__plugboard @plugboard.setter def plugboard(self, plugboard): self.__plugboard = plugboard @property def reflector(self): """ The reflector associated to the Enigma machine :getter: Returns the reflector :setter: Sets the reflector :type: Reflector """ return self._rotor_chain.reflector @reflector.setter def reflector(self, reflector): self._rotor_chain.reflector = reflector @property def rotors(self): """ The rotors associated to the Enigma machine :getter: Returns the list of rotors :setter: Sets the list of rotors :type: Rotors list """ return self._rotor_chain.rotors @rotors.setter def rotors(self, rotors): self._rotor_chain.rotors = rotors
class Enigma: """ The core class for the Enigma machine. Takes in a settings dictionary as provided by the user, and constructs the required machine from the settings blueprints. The structure of the machine is as follows: 1. Plugboard with required plugleads 2. Housing - a simple list from A-Z, never rotates 3. Rotors - 3 or 4 with respective pins and contacts, these rotate with each 'key press' 4. 1 Reflector - which will map the final rotor output to a different letter before passing the signal rightwards. """ def __init__(self, settings: dict): self.settings = settings self.root = None self.rotor_box = { "Housing": { 'contacts': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'notch': None }, "Beta": { 'contacts': 'LEYJVCNIXWPBQMDRTAKZGFUHOS', 'notch': None }, "Gamma": { 'contacts': 'FSOKANUERHMBTIYCWLQPZXVGJD', 'notch': None }, "I": { 'contacts': 'EKMFLGDQVZNTOWYHXUSPAIBRCJ', 'notch': 'Q' }, "II": { 'contacts': 'AJDKSIRUXBLHWTMCQGZNPYFVOE', 'notch': 'E' }, "III": { 'contacts': 'BDFHJLCPRTXVZNYEIWGAKMUSQO', 'notch': 'V' }, "IV": { 'contacts': 'ESOVPZJAYQUIRHXLNFTGKDCMWB', 'notch': 'J' }, "V": { 'contacts': 'VZBRGITYUPSDNHLXAWMJQOFECK', 'notch': 'Z' }, "A": { 'contacts': 'EJMZALYXVBWFCRQUONTSPIKHGD', 'notch': None }, "B": { 'contacts': 'YRUHQSLDPXNGOKMIEBFZCWVJAT', 'notch': None }, "C": { 'contacts': 'FVPJIAOYEDRZXWGCTKUQSBNMHL', 'notch': None }, } def create_machinery(self): """ Adds in turn each part of the machine as specified in the user settings. """ # Add plugboard with respective pluglead pairs self.board = Plugboard(self.settings) # Add housing self.add("Housing") # Add rotors. Rotors are 'inserted' in reverse order from right to left for i in range(len(self.settings['rotors'].split(' ')) - 1, -1, -1): rotor_name = self.settings['rotors'].split(' ')[i] ring_setting = self.settings['ring_settings'].split(' ')[i] initial_position = self.settings['initial_positions'].split(' ')[i] self.add(rotor_name, ring_setting, initial_position) # Add reflector self.add(self.settings['reflector']) @abstractmethod def add(self, name: str, ring_setting: int = 1, initial_position: str = 'A'): """ Adds a named mechanical part to the machine from right to left, and then adjusts the default part setting to reflect the desired ring setting and initial position :param name: Name of the part being added (see self.rotor_box in the __init__ method for details) :param ring_setting: An integer from 1-26 determining the pin-to-contact mapping for the rotor :param initial_position: The starting position for the rotor """ if self.root is None: self.root = Rotor(name, self.rotor_box, ring_setting, initial_position) self.root.adjust_ring_setting() self.root.adjust_starting_positions() else: ptr = self.root while True: if ptr.left is None: ptr.left = Rotor(name, self.rotor_box, ring_setting, initial_position, right=ptr) ptr.left.adjust_ring_setting() ptr.left.adjust_starting_positions() break else: ptr = ptr.left def encode(self, message: str) -> str: """ Takes the users input message and runs each character through the enigma machine from right to left, and back again, to produce an encoded character. Encoded characters are combined into a single list and returned/printed as one encoded/decoded message. :param message: The message to be decoded/encoded :return: decoded/encoded message string """ i = 0 encoded_phrase = [] # For each character in the message while i < len(message): character = message[i] # Run the character through the plugboard character = self.board.encode(character) ptr = self.root # Rotate all rotors as necessary ptr.left.key_press() # Assign the Housing (first "ptr.contacts") index of the character to input_index inside the first rotor object ptr.left.input_index = ptr.contacts.index(character.upper()) ptr = ptr.left while True: # If there is another rotor to the left if ptr.left is not None: # If Housing is to the right of the current rotor... if ptr.right.name == 'Housing': # ..pass the Housing/input index to the first rotor pins, to get the associated contact/output index ptr.rotor_encode_left(ptr.input_index) else: # Else if it is a rotor to the right, grab that rotors encoded/output character ptr.rotor_encode_left(ptr.right.output_index) # Set pointer to the next part to the left and repeat ptr = ptr.left else: # No more rotors, now run signal through the reflector ptr.rotor_encode_left(ptr.right.output_index) break # Pass signal from reflector back through to the plugboard ptr = ptr.right while True: if ptr.right: # If there is another machine element to the right position, encode as normal ptr.rotor_encode_right(ptr.left.output_index) ptr = ptr.right else: # If there are no more parts to pass the signal through, encode, run through the plugboard if it # exists, then append the output to the output phrase list ptr.rotor_encode_right(ptr.left.output_index) if self.board: ptr.output_char = self.board.encode(ptr.output_char) encoded_phrase.append(ptr.output_char) break # Increase i by one to then loop back and start again for the next character i += 1 print(f"Input Phrase: {message}") print(f"Encoded Phrase: {''.join(encoded_phrase)}") if len(self.settings['rotors'].split(' ')) == 3: print(f'Final Rotor Positions:{self.root.left.left.left.position}' f'{self.root.left.left.position}' f'{self.root.left.position}') else: print( f'Final Rotor Positions:{self.root.left.left.left.left.position}' f'{self.root.left.left.left.position}' f'{self.root.left.left.position}' f'{self.root.left.position}') return ''.join(encoded_phrase)