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 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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.0
5
+ before_install: gem install bundler -v 1.16.1
@@ -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
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in rotor_machine.gemspec
6
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rotor_machine"
5
+ require "pry"
6
+
7
+ require "pry"
8
+ Pry.start
9
+
10
+ # require "irb"
11
+ # IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/rotor_machine ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rotor_machine"
@@ -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,4 @@
1
+ module RotorMachine
2
+ VERSION_DATA = [1, 0, 0]
3
+ VERSION = VERSION_DATA.join(".")
4
+ 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: []