Class: RotorMachine::Machine
- Inherits:
-
Object
- Object
- RotorMachine::Machine
- 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:
-
Create a new Machine object.
-
Add one or more Rotors to the ‘rotors` array.
-
Set the ‘reflector` to an instance of the Reflector class.
-
Make any desired connections in the Plugboard.
-
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
-
#plugboard ⇒ Object
Returns the value of attribute plugboard.
-
#reflector ⇒ Object
Returns the value of attribute reflector.
-
#rotors ⇒ Object
Returns the value of attribute rotors.
Class Method Summary collapse
-
.default_machine ⇒ Object
Generates a default-configuration RotorMachine, with the following state:.
-
.empty_machine ⇒ Object
Generates an empty-configuration RotorMachine, with the following state:.
-
.from_yaml(config) ⇒ RotorMachine::Machine
Create a new Machine from a YAML configuration file.
Instance Method Summary collapse
-
#==(another_machine) ⇒ Boolean
Compare another Machine instance to this one.
-
#encipher(text) ⇒ String
Encipher (or decipher) a string.
-
#encipher_char(c) ⇒ String
Encipher a single character.
-
#initialize ⇒ Machine
constructor
Initialize a RotorMachine object.
-
#load_machine_state_from(filepath) ⇒ Object
Read the internal machine state from a YAML file.
-
#machine_state ⇒ Hash
Create a Ruby hash containing a snapshot of the current machine state.
-
#save_machine_state_to(filepath) ⇒ Boolean
Write the internal machine state to a YAML file.
-
#set_machine_config_from(config) ⇒ RotorMachine::Machine
Set the state of the machine based on values in a config hash.
-
#set_rotors(init_val) ⇒ Object
Set the initial positions of the set of rotors before begining an enciphering or deciphering operation.
-
#step_rotors ⇒ Object
Coordinate the stepping of the set of rotors after a character is enciphered.
-
#to_s ⇒ String
Describe the current state of the machine in human-readable form.
Constructor Details
#initialize ⇒ Machine
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
#plugboard ⇒ Object
Returns the value of attribute plugboard.
80 81 82 |
# File 'lib/rotor_machine/machine.rb', line 80 def plugboard @plugboard end |
#reflector ⇒ Object
Returns the value of attribute reflector.
80 81 82 |
# File 'lib/rotor_machine/machine.rb', line 80 def reflector @reflector end |
#rotors ⇒ Object
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_machine ⇒ Object
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_machine ⇒ Object
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.
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.
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.
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.
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.
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_state ⇒ Hash
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.
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.
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)
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.
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_rotors ⇒ Object
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_s ⇒ String
Describe the current state of the machine in human-readable form.
state.
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 |