The Enigma Machine using Ruby

    An Enigma machine is a German engineered mechanical and electrical cipher machine used commercially in the 1920s and more famously during the Second World War. The machine's enciphering and deciphering played the role of the antagonist in the recent film The Imitation Game (loosely) following Alan Turings work at Bletchley Park. The machine is spellbinding because of its 159 million million million combinations it can cipher to (all before the invention of the transistor).

    The machine is composed of three different 'scrambling' parts: rotors, a plugboard, and a reflector. The machine's input comes from a keyboard, travels through the electromechanical components and is finally displayed on a lampboard as a highlighted letter.

    The Plugboard

    The plugboard's role is to swap pairs of letters. An electronic signal will pass through the plugboard twice (after leaving the keyboard and before entering the lampboard). Any letter that isn't "plugged" will output as itself.

    module Enigma
      class Plugboard
    
        attr_accessor :mappings
    
        def initialize()
          self.mappings = {}
        end
    
        def plug(a,b)
          self.mappings[a] = b
          self.mappings[b] = a
        end
    
        def swap(letter)
          self.mappings[letter] || letter
        end
    
      end
    end
    
    plugboard = new Enigma::Plugboard
    plugboard.plug('A','B')
    plugboard.swap('A') # 'B'
    plugboard.swap('B') # 'A'
    plugboard.swap('C') # 'C'
    

    The Reflector

    The reflector's role is to perform a substitution. It is a mapping of every letter to a different letter (i.e. A to B and B to A, C to D and D to C, etc). The relationship is defined in Mathematics to be reciprocal (that is f(f(x)) = x).

    module Enigma
      ALPHABET = %w(A B C D E F G H I J K L M N O P Q R S T U V W X Y Z).freeze
    
      class Reflector
    
        module Mappings
          SAMPLE = %w(Y R U H Q S L D P X N G O K M I E B F Z C W V J A T).freeze
        end
    
        attr_accessor :mappings
    
        def initialize(mappings: Mappings::SAMPLE)
          self.mappings = mappings
        end
    
        def reflect(letter)
          ALPHABET[(self.mappings.index(letter)) % ALPHABET.length]
        end
    
      end
    end
    
    reflector = new Enigma::Reflector
    reflector.reflect('A') # 'Y'
    reflector.reflect('Y') # 'A'
    reflector.reflect('B') # 'R'
    reflector.reflect('R') # 'B'
    

    The Machine

    These two components are enough to build a simple (ineffective) cipher machine known as a substitution cipher. A letter flows into the plugboard then to the reflector and finally back to the plugboard. To decrypt, the encrypted message is sent through the same processes.

    module Enigma
    
      class Machine
        SEPARATOR = ''
    
        attr_accessor :reflector
        attr_accessor :plugboard
    
        def initialize(reflector: Reflector.new, plugboard: Plugboard.new)
          self.reflector = reflector
          self.plugboard = plugboard
        end
    
        def convert(string)
          string.split(SEPARATOR).map { |letter| process(letter) }.join(SEPARATOR)
        end
    
      private
    
        def process(letter)
          letter = plug(letter)
          letter = reflect(letter)
          letter = plug(letter)
    
          return letter
        end
    
        def plug(letter)
          self.plugboard.swap(letter)
        end
    
        def reflect(letter)
          self.reflector.reflect(letter)
        end
    
      end
    end
    

    The Rotor

    Unfortunately a simple substitution cipher is easy to break because letters always map to the same values. The cleverness behind the Enigma machine is that it uses three (or sometimes four) rotors to perform multiple layers of substitution. Rotors are bijective functions that have one to one mappings for every letter. In the enigma machine the rotors step like an odometer or a clock before each letter is processed. Only when the first rotor performs a full rotation will the second rotor turn. Similarly each subsequent rotor will turn after the previous rotor cycles through all 26 positions. The processes of a rotor 'stepping' changes the bijective mapping. Each rotor acts as a substitution cypher and has a forward and reverse (inverse) mapping. The combination of multiple rotors is known as a polyalphabetic substitution cipher.

    module Enigma
      class Rotor
    
        module Mappings
          A = %w(E K M F L G D Q V Z N T O W Y H X U S P A I B R C J).freeze
          B = %w(A J D K S I R U X B L H W T M C Q G Z N P Y F V O E).freeze
          C = %w(B D F H J L C P R T X V Z N Y E I W G A K M U S Q O).freeze
        end
    
        attr_accessor :mappings
        attr_accessor :offset
    
        def initialize(mappings)
          self.mappings = mappings
          reset!
        end
    
        def reset!
          self.offset = 0
        end
    
        def rotate
          self.offset = self.offset.next % ALPHABET.length
        end
    
        def turnover?
          ((self.position + self.offset) % ALPHABET.length).zero?
        end
    
        def forward(letter)
          return letter unless ALPHABET.include?(letter)
          return self.mappings[(ALPHABET.index(letter) + self.offset) % ALPHABET.length]
        end
    
        def reverse(letter)
          return letter unless ALPHABET.include?(letter)
          return ALPHABET[(self.mappings.index(letter) - self.offset) % ALPHABET.length]
        end
    
      end
    end
    

    The Stepping

    The example can have a few rotors added in. In addition to the existing steps the machine now rotates the rotors and sends the letter forward and reverse through the rotors before and after being reflected. The rotation or stepping means that 26 cubed (17,576) different bijective functions can be selected to map each letter in a string. The functions will change on each rotation making it very difficult to find reoccurring patterns.

    module Enigma
    
      class Machine
        SEPARATOR = ''
    
        attr_accessor :rotors
        attr_accessor :reflector
        attr_accessor :plugboard
    
        def initialize(reflector: Reflector.new, plugboard: Plugboard.new)
          self.rotors = [
            Rotor.new(mappings: Rotor::Mappings::A),
            Rotor.new(mappings: Rotor::Mappings::B),
            Rotor.new(mappings: Rotor::Mappings::C),
          ]
          self.reflector = reflector
          self.plugboard = plugboard
        end
    
        def convert(string)
          string.split(SEPARATOR).map { |letter| process(letter) }.join(SEPARATOR)
        end
    
      private
    
        def process(letter)
          rotate
    
          letter = plug(letter)
          letter = forward(letter)
          letter = reflect(letter)
          letter = reverse(letter)
          letter = plug(letter)
    
          return letter
        end
    
        def plug(letter)
          self.plugboard.swap(letter)
        end
    
        def reflect(letter)
          self.reflector.reflect(letter)
        end
    
        def forward(letter)
          self.rotors.inject(letter) do |letter, rotor|
            rotor.forward(letter)
          end
        end
    
        def reverse(letter)
          self.rotors.reverse.inject(letter) do |letter, rotor|
            rotor.reverse(letter)
          end
        end
    
        def rotate
          self.rotors.each do |rotor|
            rotor.rotate
            break unless rotor.turnover?
          end
        end
    
      end
    end
    

    Conclusion

    The above example shows a simplified version of how an Enigma machine works in Ruby. For a more complete example see: https://github.com/ksylvest/enigma. The number of permutations the Enigma machine allows for is so large because three of five rotors can be selected in any order, two reflectors can be swapped, and any number of letter mappings can be supplied with the plugboard. In addition the initial settings for the machine allowed for the rotors and reflectors to each start with 26 different offsets.