RotorMachine
The RotorMachine
gem provides a simple Ruby implementation of the
Enigma rotor encryption machine.
I wrote RotorMachine primarily as an exercise in Test-Driven Development with RSpec. It is not intended to be efficient or performant, and I wasn't striving much for idiomatic conciseness. My aims were fairly modular code and a relatively complete RSpec test suite.
Many thanks to Kevin Sylvestre, whose blog post helped me understand some aspects of the internal workings of the Enigma and how the signals flowed through the pieces of the machine.
Installation
Add this line to your application's Gemfile:
gem 'rotor_machine'
And then execute:
$ bundle
Or install it yourself as:
$ gem install rotor_machine
Architecture
The RotorMachine::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. In this simulation, the Machine class serves to encapsulate all of these components.
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 Enigma Machine (Wikipedia).
The Signal Path of Letters
Here's a visual depiction of the signal path of a single character through a (physical) Enigma machine:
As you can see, 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.
The result of the machine's signal path being a loop 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 during World War II.
Usage
To use the RotorMachine Enigma machine, you need to perform the following steps:
- Create a new
RotorMachine::Machine
object. - Add one or more
RotorMachine::Rotor
s to therotors
array. - Set the
reflector
to an instance of theRotorMachine::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.
You can also create a new RotorMachine::Machine
(or its various parts)
using the RotorMachine::Factory
methods, as shown in the second example.
Example
require 'rotor_machine'
machine = RotorMachine::Machine.empty_machine
machine.rotors << RotorMachine::Rotor.new(RotorMachine::Rotor::ROTOR_I, "A", 1)
machine.rotors << RotorMachine::Rotor.new(RotorMachine::Rotor::ROTOR_II, "A", 1)
machine.rotors << RotorMachine::Rotor.new(RotorMachine::Rotor::ROTOR_III, "A", 1)
machine.reflector = RotorMachine::Reflector.new(RotorMachine::Reflector::REFLECTOR_A)
machine.plugboard.connect("A", "M")
machine.plugboard.connect("Q", "K")
machine.set_rotors("CFL")
plaintext = "This is a super secret message".upcase
ciphertext = machine.encipher(plaintext) # => "MYGMS ZLTWS AAIDD VTGOC RFKFO"
machine.set_rotors("CFL")
new_plaintext = machine.encipher(ciphertext) # => "THISI SASUP ERSEC RETME SSAGE"
Example - Simplified Setup Using the Factory
require 'rotor_machine'
machine = RotorMachine::Factory.build_machine(
rotors: [:ROTOR_I, :ROTOR_II, :ROTOR_III],
reflector: :REFLECTOR_A,
connections: {"A" => "M", "Q" => "K" }
)
machine.set_rotors("CFL")
plaintext = "This is a super secret message".upcase
ciphertext = machine.encipher(plaintext) # => "MYGMS ZLTWS AAIDD VTGOC RFKFO"
machine.set_rotors("CFL")
new_plaintext = machine.encipher(ciphertext) # => "THISI SASUP ERSEC RETME SSAGE"
Using the Wrapper DSL
A simple wrapper DSL (domain-specific language) is provided, primarily for
testing and other "conversational" or interactive uses. This DSL is defined in
the RotorMachine::Session
class. Usage is similar to the following:
RotorMachine.Session do
default_machine
set_rotors "AAA"
connect "A", "G"
encipher "THIS IS A SUPER SECRET MESSAGE"
ct = last_result
set_rotors "AAA"
encipher ct
puts last_result # THISI SASUP ERSEC RETME SSAGE
end
After the operations in the block are executed, the RotorMachine.Session
method
will return the RotorMachine::Session
object, which can be further reused if
needed.
Using the rotor_machine
REPL
The rotor_machine
executable instantatiates an instance of the
RotorMachine::Shell
class and then runs its repl()
method. This creates an
interactive shell whereby you can interact with a rotor machine. The
RotorMachine::Shell
is an interactive wrapper around RotorMachine::Session
with help, readline and ANSI colorization added.
Either run the rotor_machine
executable, or run the following code to instantiate
a REPL:
`RotorMachine::Shell.new().repl`
The REPL provides interactive usage help. Type help
at the REPL prompt for more
details.
Documentation
The classes in
lib/rotor_machine/
all contain documentation that
pretty exhaustively describe their operation.
The RSpec tests in the spec/
directory are also instructive for how the library works and how to use it.
Development
After checking out the repo, run bin/setup
to install dependencies. Then,
run rake spec
to run the tests. You can also run bin/console
for an
interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
.
To release a new version, update the version number in version.rb
, and then
run bundle exec rake release
, which will create a git tag for the version,
push git commits and tags, and push the .gem
file to rubygems.org.
This gem depends on the tcravit_ruby_lib
gem, which provides Rake tasks to update the version number. You can use the
bundle exec rake version:bump:build
, bundle exec version:bump:minor
and
bundle exec rake version:bump:major
tasks to increment the parts of
the version number. (These tasks rewrite the file
lib/rotor_machine/version.rb
.
After using them, you'll need to run a git add lib/rotor_machine/version.rb
and git commit -m "version bump"
.
Contributing
Bug reports and pull requests are welcome on GitHub at [https://github.com/tammymakesthings/rotor_machine]. Pull requests for code changes should include RSpec tests for the new/changed features. Pull requests for documentation and other updates are also welcome.
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct. Contributions from people who identify as women, BIPOC folx, LGBT folx, and members of other marginalized communities are especially welcomed.
Code of Conduct
Everyone interacting in the RotorMachine project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
License
The gem is available as open source under the terms of the Apache 2.0 license.
Image Credits
-
Enigma image - from Wikimedia Commons, provided by Das Bundesarchiv (German Federal Archives).
-
Enigma signal path image - from Wikimedia Commons, by MesserWoland