rotor_machine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +47 -0
- data/LICENSE.txt +21 -0
- data/README.md +44 -0
- data/Rakefile +6 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/exe/rotor_machine +3 -0
- data/lib/rotor_machine/machine.rb +221 -0
- data/lib/rotor_machine/plugboard.rb +102 -0
- data/lib/rotor_machine/reflector.rb +97 -0
- data/lib/rotor_machine/rotor.rb +201 -0
- data/lib/rotor_machine/version.rb +4 -0
- data/lib/rotor_machine.rb +27 -0
- data/rotor_machine.gemspec +30 -0
- metadata +150 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5a55afe85f516f188aa8964e6c8bbebfd737c55b07869433c5d733a3c6b0f72a
|
|
4
|
+
data.tar.gz: c846899187ea3ff7d4decfb67304a6088adf89ccde26e9e5dd31684938a1433d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 59394be7631a1a94ce384698b39447ef20d23a2bd21160728d48461342dc6eff2b56480d0702f4289482c980a164f26bf1f672bf2eaf0b616c632ee864634a62
|
|
7
|
+
data.tar.gz: 7f08cab18c273394e4ac4227d8fae60e8cf1c941866ccb801924efd725ba4dbb7ad8969f5e89b546ac12e02bc88fc412b2e917e2bdba655023257e4d434e6551
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-gemset
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rotor_machine
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby-2.5.0
|
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
|
10
|
+
orientation.
|
|
11
|
+
|
|
12
|
+
## Our Standards
|
|
13
|
+
|
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
|
15
|
+
include:
|
|
16
|
+
|
|
17
|
+
* Using welcoming and inclusive language
|
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
|
19
|
+
* Gracefully accepting constructive criticism
|
|
20
|
+
* Focusing on what is best for the community
|
|
21
|
+
* Showing empathy towards other community members
|
|
22
|
+
|
|
23
|
+
Examples of unacceptable behavior by participants include:
|
|
24
|
+
|
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
26
|
+
advances
|
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
28
|
+
* Public or private harassment
|
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
|
30
|
+
address, without explicit permission
|
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
32
|
+
professional setting
|
|
33
|
+
|
|
34
|
+
## Our Responsibilities
|
|
35
|
+
|
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
|
38
|
+
response to any instances of unacceptable behavior.
|
|
39
|
+
|
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
|
44
|
+
threatening, offensive, or harmful.
|
|
45
|
+
|
|
46
|
+
## Scope
|
|
47
|
+
|
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
|
49
|
+
when an individual is representing the project or its community. Examples of
|
|
50
|
+
representing a project or community include using an official project e-mail
|
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
|
53
|
+
further defined and clarified by project maintainers.
|
|
54
|
+
|
|
55
|
+
## Enforcement
|
|
56
|
+
|
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
58
|
+
reported by contacting the project team at tammycravit@me.com. All
|
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
|
63
|
+
|
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
|
66
|
+
members of the project's leadership.
|
|
67
|
+
|
|
68
|
+
## Attribution
|
|
69
|
+
|
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
|
72
|
+
|
|
73
|
+
[homepage]: http://contributor-covenant.org
|
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
rotor_machine (1.0.0)
|
|
5
|
+
pry (~> 0.11)
|
|
6
|
+
tcravit_ruby_lib
|
|
7
|
+
thor (~> 0.20)
|
|
8
|
+
|
|
9
|
+
GEM
|
|
10
|
+
remote: https://rubygems.org/
|
|
11
|
+
specs:
|
|
12
|
+
coderay (1.1.2)
|
|
13
|
+
diff-lcs (1.3)
|
|
14
|
+
method_source (0.9.0)
|
|
15
|
+
pry (0.11.3)
|
|
16
|
+
coderay (~> 1.1.0)
|
|
17
|
+
method_source (~> 0.9.0)
|
|
18
|
+
rake (10.5.0)
|
|
19
|
+
rspec (3.7.0)
|
|
20
|
+
rspec-core (~> 3.7.0)
|
|
21
|
+
rspec-expectations (~> 3.7.0)
|
|
22
|
+
rspec-mocks (~> 3.7.0)
|
|
23
|
+
rspec-core (3.7.1)
|
|
24
|
+
rspec-support (~> 3.7.0)
|
|
25
|
+
rspec-expectations (3.7.0)
|
|
26
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
27
|
+
rspec-support (~> 3.7.0)
|
|
28
|
+
rspec-mocks (3.7.0)
|
|
29
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
30
|
+
rspec-support (~> 3.7.0)
|
|
31
|
+
rspec-support (3.7.0)
|
|
32
|
+
simple-password-gen (0.1.5)
|
|
33
|
+
tcravit_ruby_lib (0.2.8)
|
|
34
|
+
simple-password-gen (~> 0.1)
|
|
35
|
+
thor (0.20.0)
|
|
36
|
+
|
|
37
|
+
PLATFORMS
|
|
38
|
+
ruby
|
|
39
|
+
|
|
40
|
+
DEPENDENCIES
|
|
41
|
+
bundler (~> 1.16)
|
|
42
|
+
rake (~> 10.0)
|
|
43
|
+
rotor_machine!
|
|
44
|
+
rspec (~> 3.0)
|
|
45
|
+
|
|
46
|
+
BUNDLED WITH
|
|
47
|
+
1.16.1
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Tammy Cravit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# RotorMachine
|
|
2
|
+
|
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rotor_machine`. To experiment with that code, run `bin/console` for an interactive prompt.
|
|
4
|
+
|
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'rotor_machine'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
$ bundle
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
$ gem install rotor_machine
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
TODO: Write usage instructions here
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
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](https://rubygems.org).
|
|
32
|
+
|
|
33
|
+
## Contributing
|
|
34
|
+
|
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rotor_machine. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
40
|
+
|
|
41
|
+
## Code of Conduct
|
|
42
|
+
|
|
43
|
+
Everyone interacting in the RotorMachine project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/rotor_machine/blob/master/CODE_OF_CONDUCT.md).
|
|
44
|
+
# rotor_machine
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
data/exe/rotor_machine
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
module RotorMachine #-nodoc-#
|
|
2
|
+
##
|
|
3
|
+
# The {RotorMachine::Machine} class serves as the entrypoint and orchestrator
|
|
4
|
+
# for an Enigma machine.
|
|
5
|
+
#
|
|
6
|
+
# == Components of an Enigma machine
|
|
7
|
+
#
|
|
8
|
+
# The Enigma machine, as represented by the RotorMachine module, consists
|
|
9
|
+
# of the following components:
|
|
10
|
+
#
|
|
11
|
+
# - One or more rotors, which perform the transposition ciphering and also
|
|
12
|
+
# rotate to produce a polyalphabetic (rather than simple substitution)
|
|
13
|
+
# cipher.
|
|
14
|
+
#
|
|
15
|
+
# - A reflector, which performs a simple symmetric substitution of letters
|
|
16
|
+
#
|
|
17
|
+
# - A plugboard, which allows pairs of letters to be transposed on a
|
|
18
|
+
# per-message basis.
|
|
19
|
+
#
|
|
20
|
+
# On an actual Enigma machine, these components are all electromechanical,
|
|
21
|
+
# and the Enigma also included a keyboard, a grid of lights to show the
|
|
22
|
+
# results, and in some cases a printer. Since this is a simulated Enigma,
|
|
23
|
+
# obviously, no keyboard/printer are supplied here.
|
|
24
|
+
#
|
|
25
|
+
# The polyalphabetic encryption of the Enigma comes from the fact that the
|
|
26
|
+
# rotors are linked (mechanically in a real Enigma) so that they rotate
|
|
27
|
+
# one or more "steps" after each character, changing the signal paths and
|
|
28
|
+
# transpositions. This means that a sequence of the same plaintext character
|
|
29
|
+
# will encipher to different ciphertext characters.
|
|
30
|
+
#
|
|
31
|
+
# The rotors are designed to advance such that each time a rotor completes
|
|
32
|
+
# a full revolution, it will advance the rotor to its left once. The rotors
|
|
33
|
+
# allow you to configure how many positions they advance when they do. So,
|
|
34
|
+
# assuming all rotors are advancing one position at a time, if the rotors
|
|
35
|
+
# have position "AAZ", their state after the next character is typed will
|
|
36
|
+
# be "ABA".
|
|
37
|
+
#
|
|
38
|
+
# To learn much more about the inner workings of actual Enigma machines,
|
|
39
|
+
# visit {https://en.wikipedia.org/wiki/Enigma_machine}.
|
|
40
|
+
#
|
|
41
|
+
# == The Signal Path of Letters
|
|
42
|
+
#
|
|
43
|
+
# On a physical Enigma machine, the electrical signal from a keypress is
|
|
44
|
+
# routed through the plugboard, then through each of the rotors in sequence
|
|
45
|
+
# from left to right. The signal then passes through the reflector (where it
|
|
46
|
+
# is transposed again), then back through the rotors in reverse order, and
|
|
47
|
+
# finally back through the plugboard a second time before being displayed on
|
|
48
|
+
# the light grid and/or printer.
|
|
49
|
+
#
|
|
50
|
+
# One important consequence of this signal path is that encryption and
|
|
51
|
+
# decryption are the same operation. That is to say, if you set the rotors
|
|
52
|
+
# and plugboard, and then type your plaintext into the machine, you'll get
|
|
53
|
+
# a string of ciphertext. If you then reset the machine to its initial state
|
|
54
|
+
# and type the ciphertext characters into the machine, you'll produce your
|
|
55
|
+
# original plaintext.
|
|
56
|
+
#
|
|
57
|
+
# One consequence of the Enigma's design is that a plaintext letter will
|
|
58
|
+
# never encipher to itself. The Allies were able to exploit this property
|
|
59
|
+
# to help break the Enigma's encryption in World War II.
|
|
60
|
+
#
|
|
61
|
+
# == Usage
|
|
62
|
+
#
|
|
63
|
+
# To use the RotorMachine Enigma machine, you need to perform the following
|
|
64
|
+
# steps:
|
|
65
|
+
#
|
|
66
|
+
# 1. Create a new {RotorMachine::Machine} object.
|
|
67
|
+
# 2. Add one or more {RotorMachine::Rotor Rotors} to the `rotors` array.
|
|
68
|
+
# 3. Set the `reflector` to an instance of the {RotorMachine::Reflector Reflector} class.
|
|
69
|
+
# 4. Make any desired connections in the {RotorMachine::Plugboard Plugboard}.
|
|
70
|
+
# 5. Optionally, set the rotor positions with {#set_rotors}.
|
|
71
|
+
#
|
|
72
|
+
# You're now ready to encipher and decipher your text using the {#encipher}
|
|
73
|
+
# method to encode/decode, and {#set_rotors} to reset the machine state.
|
|
74
|
+
#
|
|
75
|
+
# The {#default_machine} and {#empty_machine} class methods are shortcut
|
|
76
|
+
# factory methods whcih set up, respectively, a fully configured machine
|
|
77
|
+
# with a default set of rotors and reflector, and an empty machine with
|
|
78
|
+
# no rotors or reflector.
|
|
79
|
+
class Machine
|
|
80
|
+
attr_accessor :rotors, :reflector, :plugboard
|
|
81
|
+
|
|
82
|
+
##
|
|
83
|
+
# Generates a default-configuration RotorMachine, with the following
|
|
84
|
+
# state:
|
|
85
|
+
#
|
|
86
|
+
# - Rotors I, II, III, each set to A and configured to advance a single
|
|
87
|
+
# step at a time
|
|
88
|
+
# - Reflector A
|
|
89
|
+
# - An empty plugboard with no connections
|
|
90
|
+
def self.default_machine
|
|
91
|
+
machine = self.empty_machine
|
|
92
|
+
machine.rotors << RotorMachine::Rotor.new(RotorMachine::Rotor::ROTOR_I, "A", 1)
|
|
93
|
+
machine.rotors << RotorMachine::Rotor.new(RotorMachine::Rotor::ROTOR_II, "A", 1)
|
|
94
|
+
machine.rotors << RotorMachine::Rotor.new(RotorMachine::Rotor::ROTOR_III, "A", 1)
|
|
95
|
+
machine.reflector = RotorMachine::Reflector.new(RotorMachine::Reflector::REFLECTOR_A)
|
|
96
|
+
machine
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# Generates an empty-configuration RotorMachine, with the following
|
|
101
|
+
# state:
|
|
102
|
+
#
|
|
103
|
+
# - No rotors
|
|
104
|
+
# - No reflector
|
|
105
|
+
# - An empty plugboard with no connections
|
|
106
|
+
#
|
|
107
|
+
# A RotorMachine in this state will raise an {ArgumentError} until you
|
|
108
|
+
# outfit it with at least one rotor and a reflector.
|
|
109
|
+
def self.empty_machine
|
|
110
|
+
machine = RotorMachine::Machine.new()
|
|
111
|
+
machine.rotors = []
|
|
112
|
+
machine.reflector = nil
|
|
113
|
+
machine.plugboard = RotorMachine::Plugboard.new()
|
|
114
|
+
machine
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
##
|
|
118
|
+
# Initialize a RotorMachine object.
|
|
119
|
+
#
|
|
120
|
+
# This object won't be usable until you add rotors, a reflector and a
|
|
121
|
+
# plugboard. Using the {#default_machine} and {#empty_machine} helper class
|
|
122
|
+
# methods is the preferred way to initialize functioning machines.
|
|
123
|
+
def initialize()
|
|
124
|
+
@rotors = []
|
|
125
|
+
@reflector = nil
|
|
126
|
+
@plugboard = nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
##
|
|
130
|
+
# Encipher (or decipher) a string.
|
|
131
|
+
#
|
|
132
|
+
# Each character of the string is, in turn, passed through the machine.
|
|
133
|
+
# This process is documented in the class comment for the
|
|
134
|
+
# {RotorMachine::Machine} class.
|
|
135
|
+
#
|
|
136
|
+
# Because the Enigma machine did not differentiate uppercase and lowercase
|
|
137
|
+
# letters, the source string is upcase'd before processing.
|
|
138
|
+
# @param text [String] the text to encipher or decipher
|
|
139
|
+
# @return [String] the enciphered or deciphered text
|
|
140
|
+
def encipher(text)
|
|
141
|
+
raise ArgumentError, "Cannot encipher; no rotors loaded" if (@rotors.count == 0)
|
|
142
|
+
raise ArgumentError, "Cannot encipher; no reflector loaded" if (@reflector.nil?)
|
|
143
|
+
text.upcase.chars.collect { |c| self.encipher_char(c) }.join("")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
##
|
|
147
|
+
# Coordinate the stepping of the set of rotors after a character is
|
|
148
|
+
# enciphered.
|
|
149
|
+
def step_rotors
|
|
150
|
+
@rotors.reverse.each do |rotor|
|
|
151
|
+
rotor.step
|
|
152
|
+
break unless rotor.wrapped?
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
##
|
|
157
|
+
# Set the initial positions of the set of rotors before begining an
|
|
158
|
+
# enciphering or deciphering operation.
|
|
159
|
+
#
|
|
160
|
+
# This is a helper method to avoid having to manipulate the rotor
|
|
161
|
+
# positions individually. Starting with the leftmost rotor, each
|
|
162
|
+
# character from this string is used to set the position of one
|
|
163
|
+
# rotor.
|
|
164
|
+
#
|
|
165
|
+
# If the string is longer than the number of rotors, the extra
|
|
166
|
+
# values (to the right) are ignored. If it's shorter, the values of
|
|
167
|
+
# the "extra" rotors will be unchanged.
|
|
168
|
+
#
|
|
169
|
+
# @param init_val [String] A string containing the initial values
|
|
170
|
+
# for the rotors.
|
|
171
|
+
def set_rotors(init_val)
|
|
172
|
+
init_val.chars.each_with_index do |c, i|
|
|
173
|
+
@rotors[i].position = c if (i < @rotors.length)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
##
|
|
178
|
+
# Describe the current state of the machine in human-readable form.
|
|
179
|
+
#
|
|
180
|
+
# @return [String] A description of the Rotor Machine's current internal
|
|
181
|
+
# state.
|
|
182
|
+
def to_s
|
|
183
|
+
buf = "a RotorMachine::Machine with the following configuration:\n"
|
|
184
|
+
buf += " Rotors: #{@rotors.count}\n"
|
|
185
|
+
@rotors.each { |r| buf += " - #{r.to_s}\n" }
|
|
186
|
+
buf += " Reflector: #{@reflector.nil? ? "none" : @reflector.to_s}\n"
|
|
187
|
+
buf += " Plugboard: #{@plugboard.nil? ? "none" : @plugboard.to_s}"
|
|
188
|
+
return buf
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
##
|
|
192
|
+
# Encipher a single character.
|
|
193
|
+
#
|
|
194
|
+
# Used by {#encipher} to walk a single character of text through the
|
|
195
|
+
# signal path of all components of the machine.
|
|
196
|
+
#
|
|
197
|
+
# @param c [String] a single-character string containing the next
|
|
198
|
+
# character to encipher/decipher
|
|
199
|
+
# @return [String] the enciphered/deciphered character. After the
|
|
200
|
+
# character passes through the machine, a call is made to
|
|
201
|
+
# {#step_rotors} to advance the rotors.
|
|
202
|
+
def encipher_char(c)
|
|
203
|
+
ec = c
|
|
204
|
+
|
|
205
|
+
unless @plugboard.nil?
|
|
206
|
+
ec = @plugboard.transpose(ec)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
@rotors.each { |rotor| ec = rotor.forward(ec) }
|
|
210
|
+
ec = @reflector.reflect(ec)
|
|
211
|
+
@rotors.reverse.each { |rotor| ec = rotor.reverse(ec) }
|
|
212
|
+
|
|
213
|
+
unless @plugboard.nil?
|
|
214
|
+
ec = @plugboard.transpose(ec)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
self.step_rotors
|
|
218
|
+
ec
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
module RotorMachine
|
|
2
|
+
##
|
|
3
|
+
# Plugboard implementaion for the {RotorMachine} Enigma simulation.
|
|
4
|
+
#
|
|
5
|
+
# The Plugboard was an enhancement to the original Enigma machine to add an
|
|
6
|
+
# additional layer of transposition into the signal path. Signals passed
|
|
7
|
+
# through the plugboard as they were leaving the keyboard and, to maintain
|
|
8
|
+
# the symmetry of the Enigma's encryption, before being displayed on the
|
|
9
|
+
# lightboard.
|
|
10
|
+
#
|
|
11
|
+
# The properties of the {Plugboard} which are relevant to how the encryption
|
|
12
|
+
# works are:
|
|
13
|
+
#
|
|
14
|
+
# - Each letter may only be connected to one other letter.
|
|
15
|
+
# - Connections are reciprocal. Connecting A to B also implies a connection
|
|
16
|
+
# from B to A.
|
|
17
|
+
# - A letter cannot be connected to itself.
|
|
18
|
+
class Plugboard
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Create a new, empty Plugboard object.
|
|
22
|
+
#
|
|
23
|
+
# By default, no letters are connected in the plugboard, and all input
|
|
24
|
+
# characters are passed through unchanged.
|
|
25
|
+
def initialize
|
|
26
|
+
@connections = {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# Connect a pair of letters on the {Plugboard}.
|
|
31
|
+
#
|
|
32
|
+
# The designations of "from" and "to" are rather arbitrary, since the
|
|
33
|
+
# connection is reciprocal.
|
|
34
|
+
#
|
|
35
|
+
# An {ArgumentError} will be raised if either +from+ or +to+ are already
|
|
36
|
+
# connected, or if you try to connect a letter to itself.
|
|
37
|
+
#
|
|
38
|
+
# @param from [String] A single-character string designating the start
|
|
39
|
+
# of the connection.
|
|
40
|
+
# @param to [String] A single-character string designating the end
|
|
41
|
+
# of the connection.
|
|
42
|
+
def connect(from, to)
|
|
43
|
+
from.upcase!
|
|
44
|
+
to.upcase!
|
|
45
|
+
raise ArgumentError, "#{from} is already connected" if (connected?(from))
|
|
46
|
+
raise ArgumentError, "#{to} is already connected" if (connected?(to))
|
|
47
|
+
raise ArgumentError, "#{from} cannot be connected to itself" if (to == from)
|
|
48
|
+
|
|
49
|
+
@connections[from] = to
|
|
50
|
+
@connections[to] = from
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# Disconnect a plugboard mapping for a letter.
|
|
55
|
+
#
|
|
56
|
+
# Because the {Plugboard} mappings are reciprocal (they were represented by
|
|
57
|
+
# a physical wire on the actual machine), this also removes the reciprocal
|
|
58
|
+
# mapping.
|
|
59
|
+
#
|
|
60
|
+
# An {ArgumentError} is raised if the specified letter is not connected.
|
|
61
|
+
#
|
|
62
|
+
# @param letter [String] The letter to disconnect. You may specify the
|
|
63
|
+
# letter at either end of the mapping.
|
|
64
|
+
def disconnect(letter)
|
|
65
|
+
letter.upcase!
|
|
66
|
+
if (connected?(letter))
|
|
67
|
+
other_end = @connections.delete(letter)
|
|
68
|
+
@connections.delete(other_end)
|
|
69
|
+
else
|
|
70
|
+
raise ArgumentError, "#{letter} is not connected"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# Feed a string of characters through the {Plugboard} and return the mapped
|
|
76
|
+
# characters. Characters which are not mapped are passed through unchanged
|
|
77
|
+
# (but the parameter string is upcased before processing.
|
|
78
|
+
#
|
|
79
|
+
# @param the_string [String] The string being enciphered.
|
|
80
|
+
# @return [String] The enciphered text.
|
|
81
|
+
def transpose(the_string)
|
|
82
|
+
the_string.chars.collect { |c| @connections[c.upcase] || c.upcase }.join("")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Test if a particular letter is connected on the {Plugboard}.
|
|
87
|
+
#
|
|
88
|
+
# @param letter [String] The letter to test.
|
|
89
|
+
# @return True if the letter is connected, nil otherwise.
|
|
90
|
+
def connected?(letter)
|
|
91
|
+
@connections.keys.include?(letter.upcase)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
##
|
|
95
|
+
# Produce a human-readable representation of the #{Plugboard}'s state.
|
|
96
|
+
#
|
|
97
|
+
# @return [String] A description of the current state.
|
|
98
|
+
def to_s
|
|
99
|
+
"a RotorMachine::Plugboard with connections: #{@connections.to_s}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module RotorMachine
|
|
2
|
+
##
|
|
3
|
+
# Implementation of the Reflector rotor.
|
|
4
|
+
#
|
|
5
|
+
# A {Reflector} behaves similarly to a {RotorMachine::Rotor Rotor}, except
|
|
6
|
+
# that the {Reflector} did not rotate. Its purpose is to reflect the
|
|
7
|
+
# signal path back through the rotor stack in the opposite direction,
|
|
8
|
+
# thereby ensuring that the encryption algorithm is symmetric.
|
|
9
|
+
#
|
|
10
|
+
# The module defines constants for the standard German Enigma reflectors,
|
|
11
|
+
# but you can create a reflector with any string of 26 alphabetic
|
|
12
|
+
# characters. However, you may not repeat a given letter more than once
|
|
13
|
+
# in your string, or else the symmetry of the encipherment algorithm will
|
|
14
|
+
# be broken.
|
|
15
|
+
class Reflector
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# The letter mapping for the German "A" reflector.
|
|
19
|
+
REFLECTOR_A = "EJMZALYXVBWFCRQUONTSPIKHGD".freeze
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# The letter mapping for the German "B" reflector.
|
|
23
|
+
REFLECTOR_B = "YRUHQSLDPXNGOKMIEBFZCWVJAT".freeze
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# The letter mapping for the German "C" reflector.
|
|
27
|
+
REFLECTOR_C = "FVPJIAOYEDRZXWGCTKUQSBNMHL".freeze
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# The letter mapping for the German "B Thin" reflector.
|
|
31
|
+
REFLECTOR_B_THIN = "ENKQAUYWJICOPBLMDXZVFTHRGS".freeze
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# The letter mapping for the German "C Thin" reflector.
|
|
35
|
+
REFLECTOR_C_THIN = "RDOBJNTKVEHMLFCWZAXGYIPSUQ".freeze
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# The letter mapping for the German "ETW" reflector.
|
|
39
|
+
REFLECTOR_ETW = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".freeze
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# Initialize a new {Reflector}.
|
|
43
|
+
#
|
|
44
|
+
# @param selected_reflector [String] The character sequqnece for the
|
|
45
|
+
# reflector. You can use one of the class constants which define
|
|
46
|
+
# the standard German reflectors, or pass a custom sequence of
|
|
47
|
+
# 26 letters.
|
|
48
|
+
# @param start_position [Integer] The start position of the reflector.
|
|
49
|
+
# Because the reflector does not rotate, this is essentially just
|
|
50
|
+
# an additional permutation factor for the encipherment.
|
|
51
|
+
def initialize(selected_reflector, start_position = 0)
|
|
52
|
+
@letters = selected_reflector.chars.freeze
|
|
53
|
+
@alphabet = REFLECTOR_ETW.chars.freeze
|
|
54
|
+
@position = start_position
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Feed a sequence of characters through the reflector, and return the
|
|
59
|
+
# results.
|
|
60
|
+
#
|
|
61
|
+
# Any characters which are not present on the reflector will be passed
|
|
62
|
+
# through unchanged.
|
|
63
|
+
#
|
|
64
|
+
# @param input [String] The string of characters to encipher.
|
|
65
|
+
# @return [String] The results of passing the input string through the
|
|
66
|
+
# {Reflector}.
|
|
67
|
+
def reflect(input)
|
|
68
|
+
input.upcase.chars.each.collect { |c|
|
|
69
|
+
if @alphabet.include?(c) then
|
|
70
|
+
@letters[(@alphabet.index(c) + @position) % @alphabet.length]
|
|
71
|
+
else
|
|
72
|
+
c
|
|
73
|
+
end }.join("")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# Return the reflector kind.
|
|
78
|
+
#
|
|
79
|
+
# If the {Reflector} is initialized with one of the provided rotor type
|
|
80
|
+
# constants (such as {REFLECTOR_A}), the name of the reflector will be
|
|
81
|
+
# returned as a symbol. If not, the symbol `:CUSTOM` will be returned..
|
|
82
|
+
#
|
|
83
|
+
# @return [Symbol] The kind of this {Reflector} object.
|
|
84
|
+
def reflector_kind_name
|
|
85
|
+
self.class.constants.each { |r| return r if (@letters.join("") == self.class.const_get(r)) }
|
|
86
|
+
return :CUSTOM
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# Return a human-readable representation of the {Reflector}
|
|
91
|
+
#
|
|
92
|
+
# @return [String] A description of the Reflector.
|
|
93
|
+
def to_s
|
|
94
|
+
"a RotorMachine::Reflector of type '#{self.reflector_kind_name.to_s}'"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
module RotorMachine
|
|
2
|
+
##
|
|
3
|
+
# Implment an Enigma machine rotor.
|
|
4
|
+
#
|
|
5
|
+
# The {Rotor} is the central component of the Enigma machine's polyalphabetic
|
|
6
|
+
# substitution cipher. Each rotor consisted of a ring with a series of
|
|
7
|
+
# internal connections and wiring which mapped input letters on the left side
|
|
8
|
+
# of the rotor to (different) output letters on the right side. The signal
|
|
9
|
+
# from the Enigma's keyboard would pass twice through the rotor/reflector
|
|
10
|
+
# stack (and plugboard) in opposite directions before being displayed. This
|
|
11
|
+
# ensured the algorithm was symmetrical; without this property, the Enigma
|
|
12
|
+
# could not both encipher and decipher text.
|
|
13
|
+
#
|
|
14
|
+
# Adding to the complexity of the algorithm, the rotors rotated after
|
|
15
|
+
# enciphering each character. In a standard 3-rotor Enigma machine, the
|
|
16
|
+
# rightmost rotor advanced position for each character. The middle rotor
|
|
17
|
+
# advanced one position with each full revolution of the right rotor, and
|
|
18
|
+
# the left rotor advanced one position with each full rotation of the middle
|
|
19
|
+
# rotor. These rotations permuted the signal path, so a sequence of several
|
|
20
|
+
# of the same input character would produce different output characters.
|
|
21
|
+
#
|
|
22
|
+
# The {Rotor} as implemented here allows the `step_size` (the number of
|
|
23
|
+
# positions each rotor advances when it's stepped) to be varied.
|
|
24
|
+
class Rotor
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# Query the current numeric position (0-based) of the rotor. The {#position=}
|
|
28
|
+
# method provides a setter for this property to allow for setting the
|
|
29
|
+
# {Rotor} based on either a numeric position or a letter position.
|
|
30
|
+
attr_reader :position
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Get or set the `step_size` - the number of positions the rotor should
|
|
34
|
+
# advance every time it's stepped.
|
|
35
|
+
attr_accessor :step_size
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# Provides the configuration of the German IC Enigma {Rotor}.
|
|
39
|
+
ROTOR_IC = "DMTWSILRUYQNKFEJCAZBPGXOHV".freeze
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# Provides the configuration of the German IIC Enigma {Rotor}.
|
|
43
|
+
ROTOR_IIC = "HQZGPJTMOBLNCIFDYAWVEUSRKX".freeze
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Provides the configuration of the German IIIC Enigma {Rotor}.
|
|
47
|
+
ROTOR_IIIC = "UQNTLSZFMREHDPXKIBVYGJCWOA".freeze
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# Provides the configuration of the German I Enigma {Rotor}.
|
|
51
|
+
ROTOR_I = "JGDQOXUSCAMIFRVTPNEWKBLZYH".freeze
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# Provides the configuration of the German II Enigma {Rotor}.
|
|
55
|
+
ROTOR_II = "NTZPSFBOKMWRCJDIVLAEYUXHGQ".freeze
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Provides the configuration of the German III Enigma {Rotor}.
|
|
59
|
+
ROTOR_III = "JVIUBHTCDYAKEQZPOSGXNRMWFL".freeze
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
# Provides the configuration of the German UKW Enigma {Rotor}.
|
|
63
|
+
ROTOR_UKW = "QYHOGNECVPUZTFDJAXWMKISRBL".freeze
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# Provides the configuration of the German ETW Enigma {Rotor}.
|
|
67
|
+
ROTOR_ETW = "QWERTZUIOASDFGHJKPYXCVBNML".freeze
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# Provides the alphabet in order. Used for mapping rotor indices, but
|
|
71
|
+
# could also be used as a {Rotor} configuration.
|
|
72
|
+
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".freeze
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# Initialize a new rotor.
|
|
76
|
+
#
|
|
77
|
+
# @param rotor [String] The letter sequence used for the new rotor. In
|
|
78
|
+
# normal use, this should be one of the class constants which
|
|
79
|
+
# define the standard German Enigma rotors, but any sequence of
|
|
80
|
+
# 26 unique letters can be used.
|
|
81
|
+
# @param start_on [Integer] The (0-based) starting position for the rotor.
|
|
82
|
+
# Defaults to 0. To start on a specific letter, use {#position=} to
|
|
83
|
+
# set the rotor after creating it.
|
|
84
|
+
# @param step_size [Integer] The number of positions to step the rotor
|
|
85
|
+
# each time it is advanced. Defaults to 1.
|
|
86
|
+
def initialize(rotor, start_on=0, step_size=1)
|
|
87
|
+
@letters = rotor.chars.freeze
|
|
88
|
+
self.position = start_on
|
|
89
|
+
@step_size = step_size
|
|
90
|
+
@wrapped = nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# Set the position of the {Rotor}.
|
|
95
|
+
#
|
|
96
|
+
# If a numeric position is provided, an {ArgumentError} will be raised if
|
|
97
|
+
# the position is outside the bounds of the rotor. If an alphabetic position
|
|
98
|
+
# is provided, an {ArgumentError} will be raised if the supplied character
|
|
99
|
+
# is not a character represented on the rotor.
|
|
100
|
+
#
|
|
101
|
+
# @param pos [Numeric, String] The position of the rotor.
|
|
102
|
+
def position=(pos)
|
|
103
|
+
if pos.class.to_s == "String"
|
|
104
|
+
raise ArgumentError, "#{pos[0]} is not a character on the rotor" unless @letters.include?(pos[0])
|
|
105
|
+
@position = @letters.index(pos[0])
|
|
106
|
+
elsif pos.class.to_s == "Integer"
|
|
107
|
+
raise ArgumentError, "Position #{pos} is invalid" if (pos < 0 or pos > @letters.length)
|
|
108
|
+
@position = pos
|
|
109
|
+
else
|
|
110
|
+
raise ArgumentError, "Invalid argument to position= (#{pos.class.to_s})"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
##
|
|
115
|
+
# Return the "forward" (left-to-right) transposition of the supplied letter.
|
|
116
|
+
#
|
|
117
|
+
# @param letter [String] The letter to encipher.
|
|
118
|
+
# @return [String] The enciphered letter.
|
|
119
|
+
def forward(letter)
|
|
120
|
+
if ALPHABET.include?(letter)
|
|
121
|
+
@letters[((ALPHABET.index(letter) + self.position) % @letters.length)]
|
|
122
|
+
else
|
|
123
|
+
letter
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
##
|
|
129
|
+
# Return the "reverse" (right-to-left) transposition of the supplied letter.
|
|
130
|
+
#
|
|
131
|
+
# @param letter [String] The letter to encipher.
|
|
132
|
+
# @return [String] The enciphered letter.
|
|
133
|
+
def reverse(letter)
|
|
134
|
+
if ALPHABET.include?(letter)
|
|
135
|
+
ALPHABET[((@letters.index(letter) - self.position) % @letters.length)]
|
|
136
|
+
else
|
|
137
|
+
letter
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
##
|
|
142
|
+
# Step the rotor.
|
|
143
|
+
#
|
|
144
|
+
# @param step_size [Integer] The number of positions to step the rotor.
|
|
145
|
+
# Defaults to the value of {#step_size} if not provided.
|
|
146
|
+
def step(step_size=@step_size)
|
|
147
|
+
old_position = @position
|
|
148
|
+
@position = (@position + step_size) % @letters.length
|
|
149
|
+
@wrapped = (old_position > @position)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
##
|
|
153
|
+
# Get the current letter position of the rotor.
|
|
154
|
+
#
|
|
155
|
+
# @return [String] The current letter position of the rotor.
|
|
156
|
+
def current_letter
|
|
157
|
+
@letters[@position]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
##
|
|
161
|
+
# Return the current rotor's "kind" (a string containing the mappings of
|
|
162
|
+
# the rotor.
|
|
163
|
+
#
|
|
164
|
+
# @return [String] The sequence of letters on the {Rotor}.
|
|
165
|
+
def rotor_kind
|
|
166
|
+
@letters.join("")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
##
|
|
170
|
+
# Return the name of this kind of rotor.
|
|
171
|
+
#
|
|
172
|
+
# If the rotor's sequence matches one of the defined class constants for a
|
|
173
|
+
# standsard Enigma rotor, the name of the constant will be returned as a
|
|
174
|
+
# symbol. Otherwise, :CUSTOM is returned.
|
|
175
|
+
#
|
|
176
|
+
# @return [Symbol] The name of the kind of this rotor.
|
|
177
|
+
def rotor_kind_name
|
|
178
|
+
self.class.constants.each { |k| return k if (self.class.const_get(k) == rotor_kind) }
|
|
179
|
+
return :CUSTOM
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
##
|
|
183
|
+
# Check if the last {#step} operation caused the rotor to wrap around in
|
|
184
|
+
# position. This is used by the {RotorMachine::Machine Machine} to determine
|
|
185
|
+
# whether to advance the adjacent rotor.
|
|
186
|
+
#
|
|
187
|
+
# @return [Booleam] True if the last {#step} operation caused the rotor to
|
|
188
|
+
# wrap around.
|
|
189
|
+
def wrapped?
|
|
190
|
+
@wrapped
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
##
|
|
194
|
+
# Generate a human-readable representation of the {Rotor}'s state.
|
|
195
|
+
#
|
|
196
|
+
# @return [String] The current state of the Rotor
|
|
197
|
+
def to_s
|
|
198
|
+
return "a RotorMachine::Rotor of type '#{self.rotor_kind_name}', position=#{self.position} (#{self.current_letter}), step_size=#{@step_size}"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
|
2
|
+
|
|
3
|
+
require "rotor_machine/version"
|
|
4
|
+
|
|
5
|
+
Dir[File.join(File.dirname(__FILE__), "rotor_machine", "*.rb")].reject { |x| File.basename(x) == "version.rb" }.each do |f|
|
|
6
|
+
require File.join("rotor_machine", File.basename(f))
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# The RotorMachine gem is a relatively simple implementation of the German
|
|
11
|
+
# WWII "Enigma"-style of rotor-based encryption machine.
|
|
12
|
+
#
|
|
13
|
+
# I wrote RotorMachine primarily as an exercise in Test-Driven Development
|
|
14
|
+
# with RSpec. It is not intended to be efficient or performant, and I wasn't
|
|
15
|
+
# striving much for idiomatic conciseness. My aims were fairly modular code
|
|
16
|
+
# and a relatively complete RSpec test suite.
|
|
17
|
+
#
|
|
18
|
+
# The documentation for {RotorMachine::Machine} shows an example of how to
|
|
19
|
+
# use the module.
|
|
20
|
+
#
|
|
21
|
+
# Many thanks to Kevin Sylvestre, whose {https://ksylvest.com/posts/2015-01-03/the-enigma-machine-using-ruby blog post}
|
|
22
|
+
# helped me understand some aspects of the internal workings of the Enigma
|
|
23
|
+
# and how the signals flowed through the pieces of the machine.
|
|
24
|
+
#
|
|
25
|
+
#@author Tammy Cravit <tammycravit@me.com>
|
|
26
|
+
module RotorMachine
|
|
27
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
|
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require "rotor_machine/version"
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "rotor_machine"
|
|
8
|
+
spec.version = RotorMachine::VERSION
|
|
9
|
+
spec.authors = ["Tammy Cravit"]
|
|
10
|
+
spec.email = ["tammycravit@me.com"]
|
|
11
|
+
|
|
12
|
+
spec.summary = %q{Simple Enigma-like rotor machine in Ruby}
|
|
13
|
+
spec.homepage = "https://github.com/tammycravit/rotor_machine"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
|
18
|
+
end
|
|
19
|
+
spec.bindir = "exe"
|
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
21
|
+
spec.require_paths = ["lib"]
|
|
22
|
+
|
|
23
|
+
spec.add_dependency "thor", "~> 0.20"
|
|
24
|
+
spec.add_dependency "pry", "~> 0.11"
|
|
25
|
+
spec.add_dependency "tcravit_ruby_lib"
|
|
26
|
+
|
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rotor_machine
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tammy Cravit
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-02-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: thor
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.20'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.20'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: pry
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.11'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.11'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: tcravit_ruby_lib
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: bundler
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.16'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.16'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rake
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '10.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '10.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rspec
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '3.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '3.0'
|
|
97
|
+
description:
|
|
98
|
+
email:
|
|
99
|
+
- tammycravit@me.com
|
|
100
|
+
executables:
|
|
101
|
+
- rotor_machine
|
|
102
|
+
extensions: []
|
|
103
|
+
extra_rdoc_files: []
|
|
104
|
+
files:
|
|
105
|
+
- ".gitignore"
|
|
106
|
+
- ".rspec"
|
|
107
|
+
- ".ruby-gemset"
|
|
108
|
+
- ".ruby-version"
|
|
109
|
+
- ".travis.yml"
|
|
110
|
+
- CODE_OF_CONDUCT.md
|
|
111
|
+
- Gemfile
|
|
112
|
+
- Gemfile.lock
|
|
113
|
+
- LICENSE.txt
|
|
114
|
+
- README.md
|
|
115
|
+
- Rakefile
|
|
116
|
+
- bin/console
|
|
117
|
+
- bin/setup
|
|
118
|
+
- exe/rotor_machine
|
|
119
|
+
- lib/rotor_machine.rb
|
|
120
|
+
- lib/rotor_machine/machine.rb
|
|
121
|
+
- lib/rotor_machine/plugboard.rb
|
|
122
|
+
- lib/rotor_machine/reflector.rb
|
|
123
|
+
- lib/rotor_machine/rotor.rb
|
|
124
|
+
- lib/rotor_machine/version.rb
|
|
125
|
+
- rotor_machine.gemspec
|
|
126
|
+
homepage: https://github.com/tammycravit/rotor_machine
|
|
127
|
+
licenses:
|
|
128
|
+
- MIT
|
|
129
|
+
metadata: {}
|
|
130
|
+
post_install_message:
|
|
131
|
+
rdoc_options: []
|
|
132
|
+
require_paths:
|
|
133
|
+
- lib
|
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - ">="
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '0'
|
|
144
|
+
requirements: []
|
|
145
|
+
rubyforge_project:
|
|
146
|
+
rubygems_version: 2.7.3
|
|
147
|
+
signing_key:
|
|
148
|
+
specification_version: 4
|
|
149
|
+
summary: Simple Enigma-like rotor machine in Ruby
|
|
150
|
+
test_files: []
|