Class: RotorMachine::Machine

Inherits:
Object
  • Object
show all
Defined in:
lib/rotor_machine/machine.rb

Overview

The Machine class serves as the entrypoint and orchestrator for an Enigma machine.

Components of an Enigma machine

The Enigma machine, as represented by the RotorMachine module, consists of the following components:

  • One or more rotors, which perform the transposition ciphering and also rotate to produce a polyalphabetic (rather than simple substitution) cipher.

  • A reflector, which performs a simple symmetric substitution of letters

  • A plugboard, which allows pairs of letters to be transposed on a per-message basis.

On an actual Enigma machine, these components are all electromechanical, and the Enigma also included a keyboard, a grid of lights to show the results, and in some cases a printer. Since this is a simulated Enigma, obviously, no keyboard/printer are supplied here.

The polyalphabetic encryption of the Enigma comes from the fact that the rotors are linked (mechanically in a real Enigma) so that they rotate one or more “steps” after each character, changing the signal paths and transpositions. This means that a sequence of the same plaintext character will encipher to different ciphertext characters.

The rotors are designed to advance such that each time a rotor completes a full revolution, it will advance the rotor to its left once. The rotors allow you to configure how many positions they advance when they do. So, assuming all rotors are advancing one position at a time, if the rotors have position “AAZ”, their state after the next character is typed will be “ABA”.

To learn much more about the inner workings of actual Enigma machines, visit https://en.wikipedia.org/wiki/Enigma_machine.

The Signal Path of Letters

On a physical Enigma machine, the electrical signal from a keypress is routed through the plugboard, then through each of the rotors in sequence from left to right. The signal then passes through the reflector (where it is transposed again), then back through the rotors in reverse order, and finally back through the plugboard a second time before being displayed on the light grid and/or printer.

One important consequence of this signal path is that encryption and decryption are the same operation. That is to say, if you set the rotors and plugboard, and then type your plaintext into the machine, you’ll get a string of ciphertext. If you then reset the machine to its initial state and type the ciphertext characters into the machine, you’ll produce your original plaintext.

One consequence of the Enigma’s design is that a plaintext letter will never encipher to itself. The Allies were able to exploit this property to help break the Enigma’s encryption in World War II.

Usage

To use the RotorMachine Enigma machine, you need to perform the following steps:

  1. Create a new Machine object.

  2. Add one or more Rotors to the ‘rotors` array.

  3. Set the ‘reflector` to an instance of the Reflector class.

  4. Make any desired connections in the Plugboard.

  5. Optionally, set the rotor positions with #set_rotors.

You’re now ready to encipher and decipher your text using the #encipher method to encode/decode, and #set_rotors to reset the machine state.

The #default_machine and #empty_machine class methods are shortcut factory methods whcih set up, respectively, a fully configured machine with a default set of rotors and reflector, and an empty machine with no rotors or reflector.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeMachine

Initialize a RotorMachine object.

This object won’t be usable until you add rotors, a reflector and a plugboard. Using the #default_machine and #empty_machine helper class methods is the preferred way to initialize functioning machines.



88
89
90
91
92
# File 'lib/rotor_machine/machine.rb', line 88

def initialize()
  @rotors = []
  @reflector = nil
  @plugboard = nil
end

Instance Attribute Details

#plugboardObject

Returns the value of attribute plugboard.



80
81
82
# File 'lib/rotor_machine/machine.rb', line 80

def plugboard
  @plugboard
end

#reflectorObject

Returns the value of attribute reflector.



80
81
82
# File 'lib/rotor_machine/machine.rb', line 80

def reflector
  @reflector
end

#rotorsObject

Returns the value of attribute rotors.



80
81
82
# File 'lib/rotor_machine/machine.rb', line 80

def rotors
  @rotors
end

Class Method Details

.default_machineObject

Generates a default-configuration RotorMachine, with the following state:

  • Rotors I, II, III, each set to A and configured to advance a single step at a time

  • Reflector A

  • An empty plugboard with no connections

This method is just a proxy for the equivalently-named factory method in the Factory class, and is maintained here for backward compatibility.



106
107
108
# File 'lib/rotor_machine/machine.rb', line 106

def self.default_machine
  RotorMachine::Factory.default_machine
end

.empty_machineObject

Generates an empty-configuration RotorMachine, with the following state:

  • No rotors

  • No reflector

  • An empty plugboard with no connections

A RotorMachine in this state will raise an ArgumentError until you outfit it with at least one rotor and a reflector.

This method is just a proxy for the equivalently-named factory method in the Factory class, and is maintained here for backward compatibility.



124
125
126
# File 'lib/rotor_machine/machine.rb', line 124

def self.empty_machine
  RotorMachine::Factory.empty_machine()
end

.from_yaml(config) ⇒ RotorMachine::Machine

Create a new RotorMachine::Machine from a YAML configuration file.

This class method is a one-step shortcut for creating an empty RotorMachine::Machine and then loading its machine state.

hash generated by #machine_state. supplied config hash.

Parameters:

  • config (Hash)

    A configuration hash for the new machine, such as a config

Returns:



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/rotor_machine/machine.rb', line 313

def self.from_yaml(config)
  unless config.keys.include?(:serialization_version)
    raise ArgumentError, "Serialization Data Version Mismatch"
  end
  unless config[:serialization_version].is_a?(Numeric)
    raise ArgumentError, "Serialization Data Version Mismatch"
  end
  if (config[:serialization_version] > RotorMachine::VERSION_DATA[0]) || (config[:serialization_version] < 1)
    raise ArgumentError, "Serialization Data Version Mismatch"
  end

  m = self.empty_machine
  m.set_machine_config_from(config)
  return m
end

Instance Method Details

#==(another_machine) ⇒ Boolean

Compare another RotorMachine::Machine instance to this one.

Returns true if the provided RotorMachine::Machine has the same configuration as this one, and false otherwise.

this one. otherwise.

Parameters:

Returns:

  • (Boolean)

    True if the machines have identical configuration, false



385
386
387
388
389
# File 'lib/rotor_machine/machine.rb', line 385

def ==(another_machine)
  @rotors == another_machine.rotors &&
    @reflector == another_machine.reflector &&
    @plugboard == another_machine.plugboard
end

#encipher(text) ⇒ String

Encipher (or decipher) a string.

Each character of the string is, in turn, passed through the machine. This process is documented in the class comment for the RotorMachine::Machine class.

Because the Enigma machine did not differentiate uppercase and lowercase letters, the source string is upcase’d before processing.

Parameters:

  • text (String)

    the text to encipher or decipher

Returns:

  • (String)

    the enciphered or deciphered text

Raises:

  • (ArgumentError)


139
140
141
142
143
# File 'lib/rotor_machine/machine.rb', line 139

def encipher(text)
  raise ArgumentError, "Cannot encipher; no rotors loaded" if (@rotors.count == 0)
  raise ArgumentError, "Cannot encipher; no reflector loaded" if (@reflector.nil?)
  text.upcase.chars.collect { |c| self.encipher_char(c) }.join("").in_blocks_of(5)
end

#encipher_char(c) ⇒ String

Encipher a single character.

Used by #encipher to walk a single character of text through the signal path of all components of the machine.

Parameters:

  • c (String)

    a single-character string containing the next character to encipher/decipher

Returns:

  • (String)

    the enciphered/deciphered character. After the character passes through the machine, a call is made to #step_rotors to advance the rotors.



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/rotor_machine/machine.rb', line 201

def encipher_char(c)
  ec = c

  unless @plugboard.nil?
    ec = @plugboard.transpose(ec)
  end

  @rotors.each { |rotor| ec = rotor.forward(ec) }
  ec = @reflector.reflect(ec)
  @rotors.reverse.each { |rotor| ec = rotor.reverse(ec) }

  unless @plugboard.nil?
    ec = @plugboard.transpose(ec)
  end

  unless ec == c
    self.step_rotors
  end
  ec
end

#load_machine_state_from(filepath) ⇒ Object

Read the internal machine state from a YAML file.

The YAML file can be created using the ##save_machine_state_to method to save the machine state of an existing RotorMachine::Machine object.

The internal state is captured as is, so if you save the state from a machine that’s not validly configured (no rotors, no reflector, etc.), the reconstituted machine will also have an invalid state.

state should be saved.

Parameters:

  • filepath (String)

    The path to the YAML file to which the machine

Raises:

  • (ArgumentError)


296
297
298
299
300
301
# File 'lib/rotor_machine/machine.rb', line 296

def load_machine_state_from(filepath)
  raise ArgumentError, "File path \"#{filepath}\" not found!" unless File.exist?(filepath)
  c = YAML.load(File.open(filepath))
  self.set_machine_config_from(c)
  return true
end

#machine_stateHash

Create a Ruby hash containing a snapshot of the current machine state.

The hash returned by this method contains enough information to capture the current internal state of the machine. Although you can invoke it directly if you want to, it is primarily intended to be accessed via the #save_machine_state_to and #load_machine_state_from methods, which save and load machine state to YAML files.

Returns:

  • (Hash)

    A Hash representing the internal state of the machine.



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/rotor_machine/machine.rb', line 232

def machine_state
  machine_state = {}
  machine_state[:serialization_version] = RotorMachine::VERSION_DATA[0]

  machine_state[:rotors] = []
  self.rotors.each do |r|
    rstate = {
      kind: r.rotor_kind_name,
      position: r.position,
      step_size: r.step_size
    }
    if r.rotor_kind_name == :CUSTOM
      rstate[:letters] = r.rotor_kind
    end

    machine_state[:rotors] << rstate
  end
  machine_state[:reflector] = {
    kind: self.reflector.reflector_kind_name,
    position: self.reflector.position
  }
  if (self.reflector.reflector_kind_name == :CUSTOM)
    machine_state[:reflector][:letters] = self.reflector.letters
  end

  machine_state[:plugboard] = {
    connections: self.plugboard.connections.clone
  }
  return machine_state
end

#save_machine_state_to(filepath) ⇒ Boolean

Write the internal machine state to a YAML file.

The generated YAML file can be loaded using the ##load_machine_state_from method to restore a saved machine state.

state should be saved. if an error was raised.

Parameters:

  • filepath (String)

    The path to the YAML file to which the machine

Returns:

  • (Boolean)

    True if the save operation completed successfully, false



273
274
275
276
277
278
279
280
281
282
# File 'lib/rotor_machine/machine.rb', line 273

def save_machine_state_to(filepath)
  begin
    File.open(filepath, "w") do |f|
      f.puts machine_state.to_yaml
    end
    return true
  rescue
    return false
  end
end

#set_machine_config_from(config) ⇒ RotorMachine::Machine

Set the state of the machine based on values in a config hash.

Any config hash (such as that generated by #machine_state) can be provided as an argument, but this method is primarily intended to be accessed by the #from_yaml and #load_config_state_from methods to deserialize a machine state hash.

RotorMachine::Machine. configured. def set_machine_config_from(config)

Parameters:

  • config (Hash)

    The configuration hash describing the state of the

Returns:



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/rotor_machine/machine.rb', line 341

def set_machine_config_from(config)
  @rotors = []
  @reflector = nil
  @plugboard = RotorMachine::Plugboard.new()

  # Create rotors
  config[:rotors].each do |rs|
    if rs[:kind] == :CUSTOM
      r = RotorMachine::Rotor.new(rs[:letters], rs[:position], rs[:step_size])
    else
      letters = RotorMachine::Rotor.const_get(rs[:kind])
      r = RotorMachine::Rotor.new(letters, rs[:position], rs[:step_size])
    end
    @rotors << r
  end

  # Create reflector
  if config[:reflector][:kind] == :CUSTOM
    letters = config[:reflector][:letters]
  else
    letters = RotorMachine::Reflector.const_get(config[:reflector][:kind])
  end
  @reflector = RotorMachine::Reflector.new(letters, config[:reflector][:position])

  # Plugboard mappings
  config[:plugboard][:connections].keys.each do |l|
    unless @plugboard.connected?(l)
      @plugboard.connect(l, config[:plugboard][:connections][l])
    end
  end

  return self
end

#set_rotors(init_val) ⇒ Object

Set the initial positions of the set of rotors before begining an enciphering or deciphering operation.

This is a helper method to avoid having to manipulate the rotor positions individually. Starting with the leftmost rotor, each character from this string is used to set the position of one rotor.

If the string is longer than the number of rotors, the extra values (to the right) are ignored. If it’s shorter, the values of the “extra” rotors will be unchanged.

for the rotors.

Parameters:

  • init_val (String)

    A string containing the initial values



170
171
172
173
174
# File 'lib/rotor_machine/machine.rb', line 170

def set_rotors(init_val)
  init_val.chars.each_with_index do |c, i|
    @rotors[i].position = c if (i < @rotors.length)
  end
end

#step_rotorsObject

Coordinate the stepping of the set of rotors after a character is enciphered.



148
149
150
151
152
153
# File 'lib/rotor_machine/machine.rb', line 148

def step_rotors
  @rotors.reverse.each do |rotor|
    rotor.step
    break unless rotor.wrapped?
  end
end

#to_sString

Describe the current state of the machine in human-readable form.

state.

Returns:

  • (String)

    A description of the Rotor Machine’s current internal



181
182
183
184
185
186
187
188
# File 'lib/rotor_machine/machine.rb', line 181

def to_s
  buf = "a RotorMachine::Machine with the following configuration:\n"
  buf += "  Rotors: #{@rotors.count}\n"
  @rotors.each { |r| buf += "    - #{r.to_s}\n" }
  buf += "  Reflector: #{@reflector.nil? ? "none" : @reflector.to_s}\n"
  buf += "  Plugboard: #{@plugboard.nil? ? "none" : @plugboard.to_s}"
  return buf
end