A1: Enigma
This assignment asks you to develop a software replica of the Enigma encryption machine used by the German military in World War II. Efforts to break the Enigma cipher led to the development of the first electronic computers at Bletchley Park, England. Using computers, the Allies were eventually able to break the Enigma code, giving them an intelligence edge that changed the balance of the war. The most famous of those computers is the Bombe, which was designed by Alan Turing (pictured to the right). Turing is arguably the founder of modern computer science.
This assignment also asks you to learn about some modern software development tools and methodologies.
This assignment is considerably more difficult than A0. On a similar assignment last year, the mean hours worked was 10.2, and the standard deviation was 3.7. Please get started right away and make steady progress each day. Please track the time that you spend. I will ask you to report it.
Collaboration policy: This assignment is to be completed as an individual. You may have only limited collaboration with other students as described in the syllabus.
What you’ll do: Implement half a dozen functions, and test them with OUnit; maintain your code in Git, a source control tool; and use test-driven development, an agile software development technique.
Objectives:
- Use OCaml data types.
- Model a stateful computation in the functional style.
- Get more practice with constructing an OUnit test suite.
- Become familiar with the basic Git operations of adding, committing, pushing, and pulling.
- Train yourself to write test cases before writing code.
Table of contents:
- FAQs
- Getting Started
- Step 1: Get Git Going
- Step 2: Make Sure Make Works
- Step 3: Index
- Step 4: Enigma Tutorial
- Step 5: Rotors
- Step 6: Reflector
- Step 7: Plugboard
- Step 8: Ciphering a character
- Step 9: Stepping
- Step 10: Ciphering a string
- Rubric
- Submission
FAQs
-
Q: I’m trying to use an online Enigma simulator for test cases but it doesn’t seem to get the right answers.
A: It’s probably because of stepping or turnovers. Read all about those later in this handout for the details.
-
All the FAQs from A0 still apply.
-
Q: May I use functions from the
String
andChar
modules?A: Yes, except for the deprecated functions in
String
. Please note that answer already follows from the A0 FAQ:String
andChar
are in the standard library, so you may use them. But Imperative Features Prohibition linked from the Canvas front page says the deprecated functions inString
may not be used. (Also, just in general any time a library marks a function as deprecated you shouldn’t write new code that uses it!)
Getting Started
Although the assignment could seem intimidating at first, just take it a step at a time and you’ll do great! The rest of the handout below gives you instructions on each step you should take. There is a series of functions to implement, each of which builds on the ones that came before. By the time you’ve implemented all the functions, you will have finished your Enigma simulator.
The mathematics of the Enigma cipher itself could seem mysterious at first. But I’ve carefully designed the functions to break it down into smaller pieces, so that you can focus on each piece in isolation. I’ve also supplied many examples below that will help you to check your understanding. Developing software in the real world always challenges you to become a little bit of an expert in your client’s domain; think of learning about Enigma as practicing that. As a side benefit, you’ll come away from this assignment understanding an important artifact in the history of computer science.
A highly recommended arts and crafts project: As you proceed with building a simulation of the Enigma machine in this assignment, it could be useful to have a completed simulator at your side. The best Enigma simulator I can recommend is this one. It’s totally worth your while to assemble this simulator. It provides excellent insight into the physical machine, especially the wiring of the rotors and the reflector. To assemble it, you need to buy a tube of Pringles potato chips, print out the paper at 100% scaling (which can be tricky because it’s A4; to help with that we provide these close-ups [1] [2] of the important parts that should be printable on letter paper), then cut out the paper pieces and wrap them around the tube. Of course, it might be difficult to arrange all this during pandemic, but this physical simulator offers considerably more insight than any virtual simulator I’ve found online.
Step 1: Get Git Going
If you already know Git, great! Otherwise, watch this 20 minute video tutorial:
For an interactive tutorial, you can also download and run the latest release of Git-it for your operating system (Linux, Mac, or Windows).
Do the following to create your git repository:
-
Login to https://github.coecis.cornell.edu/, which is the Cornell-hosted GitHub site. Login with your Cornell NetID and password. Please use your Cornell NetID as your username—it will make your life much easier. Also, please do use the Cornell GitHub, not the public GitHub (whose URL we deliberately omit).
- From the terminal, run these commands:
git config --global user.name "Your Name" git config --global user.email "your_netid@cornell.edu"
Do this both on Ugclinux and your local machine, if you have a local install. If you get an error message that says
command not found
, you don’t have Git installed on your local machine. That actually should not happen on either Linux or Mac, and on Windows if you’re inside Ubuntu (as you should be) it shouldn’t happen either. -
Create a new repository, or “repo” for short. On the top right of any GitHub page, click the “+” icon, then select “New repository”. Give the repo a name, perhaps “cs3110-a1”, and make sure the radio button is set to Private, not Public. A public repo would share your source code with the entire Cornell community, which would violate the Academic Integrity policies of this course. Also check “Initialize this respository with a README”. Do not add a
.gitignore
now; one is provided as part of the assignment release code. Click “Create repository”. -
On the new repo’s page, double-check: does it say “Private” next to the name of the repo? If not, go to “Settings”, “Danger Zone”, “Make this repository private”, and click “Make private”.
-
Next, on the page for the repo, click “Clone or download”. The box that opens can be set to “Clone with HTTPS” or “Clone with SSH”. Copy the URL from the box.
The HTTPS option will have you authenticate to Cornell GitHub with your NetID and password. The SSH option can be used to authenticate with an SSH key pair, so if you did the optional “Ditch the password” steps for Ugclinux authentication you can also ditch it here. You’ll have to upload your public key to Cornell GitHub: go to the top-right menu, click Settings, go to SSH and GPG keys, add a new SSH key, give it whatever title you want, and copy-paste the contents of your SSH public key, which is probably located at
~/.ssh/id_rsa.pub
. -
In your terminal on Ugclinux, or own your local machine if you have a local install, navigate to whatever directory you want to store the repo in on your local filesystem. Type
git clone URL
, replacing URL with the URL you copied from the box. That will download the repo into a directory with the same name that you gave it before. Change into that directory by typingcd DIR
, replacing DIR with that name. - Download the starter code for this assignment from CMS and put the zip file
on Ugclinux, or your local machine if you have a local install. Suppose you save
it in
~/a1.zip
. Then from your repo directory, issue the following commands to move the files into your repo and clean away the downloaded file:$ mv ~/a1.zip . $ unzip a1.zip $ rm ~/a1.zip
A common mistake is to use a graphical file browser to copy files. If you do that, you will most likely omit copying some dotfiles: files whose names start with
.
and most of the time are hidden from you by graphical tools. The most likely symptoms of this mistake will are that VS Code is unable to understand your OUnit test suite because the.merlin
file is missing, and that when you launchutop
your definitions have not already been loaded because the.ocamlinit
file is missing. - Continuing in the terminal in that same directory, put the starter code
into source control by running:
$ git add a1 $ git commit -m "Import release code" $ git push
Congratulations! You now have a repository for this assignment.
Step 2: Make Sure Make Works
Run make check
from the folder containing the release code. Double check that
you aren’t getting any warnings about your environment, or your names or types.
The make
, make build
, make test
, make clean
, and make finalcheck
commands are also available, as in A0.
Tip: if you ever get errors in VS Code that say something about an “Unbound
module”, you probably need to rebuild your code with make build
.
Step 3: Index
Your goal is to implement the function index : char -> int
in the file
enigma.ml
.
-
Open
enigma.ml
in VS Code. Read the specification comment forindex
. -
Open
enigma_test.ml
and locate the unit test forindex
. Read it. Invent one new unit test for the function. Add that unit test to the suite. Runmake test
and verify that the unit tests forindex
are currently failing. That’s good! It means you now have a concrete goal: get those two tests to pass. -
Implement
index
. Hint 1: your solution should need only about one line of code. Hint 2: read the documentation of theChar.code
function and think about how it could help. Runmake
to compile and run the test suite. Make sure that both the tests pass. -
Add any other unit tests that you would like to have in the suite. Make sure all tests pass.
-
Read the Testing Rubric and Code Quality Rubric at the end of this handout. Consider whether you need to add more test cases. Also take some time to polish the code you just wrote before moving on. Don’t leave this to the end! Human tendencies being what they are, chances are if you don’t do it now, you’ll never do it.
-
Finally, re-run
make check
to double-check that you haven’t changed the names and types of any required functions. Continue to do that after implementing each function below.
Congratulations! You’ve implemented and tested your first function. The methodology you used is called test-driven development (TDD). You write an automated test, and check that the test fails. Then you write code to make the test pass. Then you clean up your code. Working this way ensure that, by the time you’re done coding, you already have an automated test suite; it’s not just something you write at the end. It also capitalizes on your brain’s desire to be rewarded, because you keep getting the sweet, sweet satisfaction of a test case going from failing to passing. Finally, if you ever happen to break older code as you are writing new code, you will instantly be alerted by your older automated tests suddenly failing.
TDD is a hugely helpful skill to develop as a programmer. Please, practice it now, even if it seems a bit strange at first. It’s not that you will always code that way 100% of the time: it’s that when you get lost and need to code that way you’ll have built up the skills to do so.
Now let’s save the work that you did in your repo:
-
Go to View -> SCM in VS Code. It’s also available as an icon on the left that looks like a tree with three nodes. It probably has a blue circle with a number in it, too.
-
You will see a list of Changes in the left-hand pane. You should see that
enigma.ml
andenigma_test.ml
have been changed since your last commit. -
Click the “+” icon next to Changes to add all the changed files to the staging area of work you want to commit.
-
In the Message textbox, note the key stroke identified to make the commit: probably Control+Enter on Windows or Command+Enter on Mac. In that textbox, type a brief description of the changes you made. This will be recorded along with them as a explanation you can read and (if ever needed) could help identify a previous point in your development that you’d like to recover. Press the keystroke when you’re done.
-
Click the three dots at the top-right of the left-hand pane in VS Code to open the Git menu. Select Push. This will push your changes up to Cornell GitHub, where they are safely backed up.
Alternatively, here’s how to do all that from the command line (which is how I prefer to do it, but YMMV):
-
Run
git status
. You should see thatenigma.ml
andenigma_test.ml
have been modified. -
Run
git add enigma.ml enigma_test.ml
to add those files to the staging area. -
Run
git commit
. That will open the Vim editor, in which you can type a commit message. Pressi
to enter insert mode. Now type your commit message. -
Press the Escape key, then type
:wq
, and press the Enter key. Escape puts Vim into normal mode, the colon moves you to the command-line mode, andwq
says to write the file you are editing to disk (i.e., save it) then quit. Vim will close. -
Back at the command line, type
git push
. Check the GitHub website to see that your code is now saved there.
In real-world projects, commit messages are expected to be more than a few brief words. I’m not going to require that here. But, the following is a good template if you want to practice writing commit messages:
Implement Enigma.index
- Replaced release code implementation with my own
- Added a test case
The first line of a good commit message should be just a few words, with the first one capitalized. The words should complete the sentence “If you pulled this commit into your own copy of the repo, this commit would…” You’re informing the reader what value they’re getting from your commit. The second line should be blank. The remaining lines should supply details.
Congratulations! That was a pretty simple function, but you’ve accomplished so much more. Time for a dance party! ┏(・o・)┛♪┗ (・o・) ┓
Step 4: Enigma Tutorial
Please watch this 12 minute video, which introduces the Enigma machine:
(The video ends abruptly; you can watch the second part here. It explains how the Enigma cipher was broken. You don’t need to understand that to complete the assignment, but it’s fun!)
The Enigma was not just a single machine, but a family of closely related machines. So keep in mind that there might be details that differ from one video to another that you watch online, or even between a video and this writeup.
Substitution ciphers. The Enigma machine implemented a substitution cipher, which encrypts a message by substituting one character for another. Such ciphers go back at least as far as Julius Caesar, who used a simple substitution cipher to encrypt military orders. The Caesar cipher specified that each letter in the alphabet would be encrypted using the letter three positions later in the alphabet. For example, ‘A’ would be encrypted as ‘D’, ‘B’ would be encrypted as ‘E’, ‘C’ would be encrypted as ‘F’, and so on. The code wraps around at the end of the alphabet, so ‘X’, ‘Y’ and ‘Z’ would be encrypted as ‘A’, ‘B’, and ‘C’, respectively. Although the Caesar cipher was effective in its time (when few people could read at all), its simple pattern of encrypting letters seems pretty obvious today.
The Enigma cipher. The Enigma machine, shown below, implemented a more complex keyed substitution cipher. The key is a secret presumably known only to the message sender and receiver; it defines the mapping between a plaintext letter and a ciphertext letter. For the Caesar cipher, the key amounts to the “rotate by 3” operation. The key for the Enigma cipher involved many aspects of the physical configuration of the machine; we describe these below.
(Click on that image and any of the images below to link to the website where we found the image.)
After the machine was configured with the key, an operator pressed a letter on a typewriter-like keyboard, which caused a circuit to close and light up a letter on a lampboard aka lightboard, a kind of simple display screen. (Note the two different uses of “key” in the previous sentence: the first refers to a cryptographic key, which is a secret used as an input to encryption and decryption algorithms; the second is in the word “keyboard”, which refers to a button that is pressed.) To encrypt a message, the operator typed the plaintext letters, the ciphertext letters would light up, and the operator would record each ciphertext letter on paper. Likewise, to decrypt a message, the operator typed the ciphertext letters, the plaintext letters would light up, and the operator would record each plaintext letter on paper.
Here is a diagram showing more of the internals of the Enigma machine:
What the diagram calls a wheel is also called a rotor in literature about the Enigma. Henceforth, we will use the term rotor. The rotors were installed next to each other on a spindle, around which they rotated. Here is a picture of three rotors:
You’ll see the alphabet, in order, on each rotor in that picture. Sometimes rotors were labeled with the alphabet, and sometimes (as in the video linked above) they were instead labeled with the integers 1..26. It makes no difference to the operation of the machine which labeling scheme was used: ‘A’ corresponds to 01, ‘B’ to 02, and so forth.
In the wiring diagram above, there is another disc at the far left of the spindle that looks something like a rotor but in fact does not rotate; it is called the reflector. Here is a picture of a reflector:
As the diagram shows, when the operator typed a key on the keyboard, electrical current would flow through the plugboard to the rotors, through the reflector, back through the rotors and plugboard, and light up a letter on the lightboard. The reflector, wheels, and plugboard are all mechanisms that could be installed in different ways into the machine. The way in which they all were collectively installed determined the key that was being used for encryption or decryption.
Here is a brief description of each of the mechanisms; we go into greater detail in later parts of this writeup:
-
Each rotor implements its own, distinct substitution cipher by wires that connect the 26 contacts on its left side with the 26 contacts on its right side. The combination of many rotors together on the spindle creates an even more sophisticated substitution cipher. The original Enigma machines used at the beginning of World War II had three rotors, known by the Roman-numeral designations I, II, and III, which could be placed on the spindle in any order. Later, other rotors with different wirings were manufactured, as were machines that could accept up to five rotors on the spindle.
-
The reflector also implements a substitution cipher, though a less sophisticated one. It has 26 contacts only on its right side, but none on its left. Current that it receives from the right side is wired to be reflected back out that side, but in a different position. The reflector was always inserted at the leftmost side of the spindle. A keypress caused current to flow right-to-left through the rotors, through the reflector, then back through the rotors, left-to-right. The reflector’s substitution pattern caused the entire circuit to be symmetric, meaning that if ‘E’ mapped to ‘Q’ in a particular machine state, then ‘Q’ would likewise be mapped to ‘E’ in that same state. The minor advantage of this design was that the machine did not need separate modes of operation for decryption and encryption. But it turned out to be a major cryptographic weakness, enabling the code to be cracked.
-
The plugboard implements yet another substitution cipher, which simply swaps pairs of letters. Though the wiring of the rotors and reflector was pre-determined by how they were manufactured, the wiring of the plugboard could easily be changed by the operator by plugging cables into ports. The plugboard could have up to 13 cables inserted in it. A cable inserted between two letters caused those letters to be swapped. Even though it seems simpler than a rotor, the plugboard was a major source of cryptographic strength for the Enigma cipher.
-
The static wheel shown above is of no interest to us. (It is effectively a no-op.)
What made the Enigma cipher especially difficult to break was that just after the operator typed a letter, and just before that letter was enciphered, the rotors moved, changing the substitution pattern. Thus, the letter ‘E’ might get mapped to a ‘Q’ the first time it was enciphered, but to a completely different letter each successive time. The rightmost rotor stepped for every letter typed, and the other rotors stepped conditionally based on notches on the rotors: when a rotor stepped past a notch, not only did that rotor step, but also the rotor to its left. This stepping mechanism made the rotors work somewhat like a mechanical odometer.
When a rotor was installed, the operator could manually rotate it to be in any of the 26 possible orientations, based on the 26 labels appearing on the rotor (either ‘A’..’Z’ or 01..26). There was a window in the machine through which the operator could observe the top letter showing on each rotor. The letter that would be on top of the rotor at the point in time when the notch engaged was known as the turnover.
(Note: we are omitting one more Enigma mechanism, the ring setting. You can safely ignore it. Aficionados: assume the ring setting is always ‘A’ for each rotor.)
The key for the Enigma machine comprises all those physical settings: which rotors were installed, the order they were on the spindle, their initial top letters, and which pairs of letters were connected on the plugboard. An Enigma operator would set all those, then encipher a message. That message would be sent (perhaps by Morse code on a radio channel). The receiver would set their Enigma machine with the same key, type in the received ciphertext, and out would come the plaintext.
Check your understanding: Answer these questions. (There’s no need to turn in your answers to be graded.)
- Define the following terms: plaintext, ciphertext, key.
- Describe the physical design of a rotor. How does it differ from a reflector?
- How many rotors could be installed on the spindle? Were they always installed in the same order?
- How does the plugboard affect the cipher?
- What is the top letter of a rotor? What causes it to change? How often does it change?
Don’t worry if you are not yet 100% solid on how the Enigma cipher works. You now know enough to get started building your simulator, and we’ll fill in the gaps as we proceed.
Step 5: Rotors
Our goal in this step is to implement functions that model how current passes through rotors.
Please note that all the examples and unit tests in this section have been carefully double checked; they do not contain any typos.
Rotor wiring. Here is a picture of a disassembled rotor showing some of the internal wiring:
Rather than using complicated diagrams, the wiring of a rotor is often specified
in literature on the Enigma as a 26-character string that is a permutation of
the uppercase letters of the alphabet. For example, EKMFLGDQVZNTOWYHXUSPAIBRCJ
is a wiring specification. Let’s give that a formal definition:
A valid wiring specification is a 26-character uppercase string that is a permutation of the letters of the alphabet.
A wiring specification has nothing to do with the order in which letters are printed on the exterior of the rotor. (Indeed, you will note that all the pictures above show the letters in alphabetical order.) Rather, the specification describes the interior wiring.
To understand a wiring specification, first forget about the letters. They are
really just shorthand for a number—specifically, the index of that letter
in the alphabet. So wiring specification EKMFLGDQVZNTOWYHXUSPAIBRCJ
is better
understood as the following list of numbers: 4, 10, 12, 5, 11, … (Now you know
why we had you implement index
.)
Next, let’s associate each of those numbers with its index in the list:
index in list | index in alphabet |
---|---|
0 | 4 |
1 | 10 |
2 | 12 |
3 | 5 |
4 | 11 |
… | … |
Finally, think of that table as defining a function \(w\), such that \(w(0) = 4\), \(w(1) = 10\), \(w(2) = 12\), \(w(3) = 5\), \(w(4) = 11\), etc.
The function \(w\) we have thus constructed is what the wiring specification really denotes. A rotor has a left side and a right side, and each side has 26 contacts from which electrical current can enter or leave the rotor. If current enters the rotor from the right side at contact \(i\), it leaves the rotor from the left side at contact \(w(i)\). And if current enters the rotor from the left side at contact \(j\), it leaves the rotor from the right side at contact \(w^{-1}(j)\), where \(w^{-1}\) denotes the inverse of \(w\).
Rotor orientation. There are 26 fixed positions at which current can enter a rotor. (For example, if the plugboard is empty, typing ‘A’ causes current to enter the right-most rotor at position 0, ‘B’ at 1, etc.) But current entering at position \(i\) does not necessarily connect with contact \(i\) on the rotor, because rotors turn around the spindle. That rotation causes an offset between the positions and the contacts.
Recall that the operator can see on the top of the machine, for each rotor, one of the letters that appears as exterior label on the rotor. Also recall that those letters are printed on the rotor in alphabetical order. So the offset of the rotor can be can be determined by looking at the letter that is showing. We call that the top letter of the rotor. Here, for example, is a closeup of a three-rotor Enigma machine in which the top letters are currently RDK:
When a rotor is in its default orientation, with ‘A’ showing as the top letter, the offset of the rotor is 0: current entering at position 0 touches contact 0, position 1 touches contact 1, and so forth. But when ‘B’ is showing as the top letter, the offset of the rotor is 1: current entering from at position 0 touches contact 1, position 1 touches contact 2, and so forth.
Likewise, as current exits from a contact on the opposite side of the rotor, that contact is offset from the actual exit position. For example, when the offset is 1, current flowing to contact 1 exits at position 0, contact 2 exits at position 1, and so forth.
Note that the way the offset works is thus independent of the direction that current is flowing—which makes sense, as the offset is determined by the movement of the rotor around the spindle, not by the flow of current through the rotor.
Examples. Now that we know about how wiring and orientation affect how current flows through a rotor, let’s look at some examples. Here is where a 3D model is worth a million words: the Pringles-can model linked above makes these examples considerably easier to follow. Please note that these examples have been carefully checked to ensure there are no typos.
Example 1. Suppose the rotor has wiring specification
EKMFLGDQVZNTOWYHXUSPAIBRCJ
, its top letter is ‘A’, current is flowing from
right to left, and current enters at position 0:
-
Current entering at right-hand position 0 flows to right-hand contact 0 of the rotor, because the rotor’s offset is 0.
-
Current entering right-hand contact 0 flows to left-hand contact 4, because \(w(0) = 4\) for this rotor’s wiring specification.
-
Current exiting left-hand contact 4 flows to left-hand position 4, because the rotor’s offset is 0.
Example 2. But now suppose that the rotor’s top letter is ‘B’. How would Example 1 change?
-
Current entering at right-hand position 0 flows to contact 1 of the rotor, because the rotor’s offset is now 1.
-
Current entering right-hand contact 1 flows to left-hand contact 10, because \(w(1) = 10\).
-
Current exiting left-hand contact 10 flows to left-hand position 9, because the rotor’s offset is 1.
Example 3. Now let’s redo Example 1, but with current flowing from left to right.
-
Current entering at left-hand position 0 flows to left-hand contact 0 of the rotor, because the rotor’s offset is 0.
-
Current entering left-hand contact 0 flows to right-hand contact 20, because \(w^{-1}(0) = 20\) for this rotor’s wiring specification. (That’s a fact we haven’t previously established, but you should be able to work it out yourself from the fact that ‘A’, whose index in the alphabet is 0, appears at position 20 in the wiring specification.)
-
Current exiting right-hand contact 20 flows to right-hand position 20, because the rotor’s offset is 0.
Example 4. Finally let’s redo Example 2, but with current flowing from left to right.
-
Current entering at left-hand position 0 flows to left-hand contact 1 of the rotor, because the rotor’s offset is 1.
-
Current entering left-hand contact 1 flows to right-hand contact 22, because \(w^{-1}(1) = 22\) for this rotor’s wiring specification.
-
Current exiting right-hand contact 22 flows to right-hand position 21, because the rotor’s offset is 1.
It’s time to write some code. Complete the functions map_r_to_l
and
map_l_to_r
in the starter code. Note that these functions do not perform any
stepping of the rotor; rather, they model how current passes through the rotor
when it’s in a particular orientation. We’ll implement stepping later.
Use test-driven development to implement these functions.
Begin by implementing map_r_to_l
. Here is a suggested workflow:
-
Start by adding this very simple test: The wiring specification
ABCDEFGHIJKLMNOPQRSTUVWXYZ
with top letter ‘A’ should cause current to flow from position 0 to position 0. Program a unit test, and verify that it fails. -
Now design the algorithm that you want to implement. Note that for this special wiring specification, you actually don’t have to implement anything to account for orientation. So don’t bother with that part of the algorithm yet.
-
Having sketched out the algorithm (verbally, on paper, on a white board, etc.), implement it. Hint 1: take a look at the
get
andindex
functions in theString
module. Hint 2: use your ownindex
function that you implemented above.
- Verify that the unit test passes.
Now repeat that workflow for these progressively harder unit tests:
-
Example 1 above. You still don’t have to account for orientation. If your algorithm is correct, this will pass already.
-
The wiring specification
BACDEFGHIJKLMNOPQRSTUVWXYZ
with top letters ‘A’, ‘B’, and ‘C’. That will at last require you to implement code to handle orientation. Hint: You will probably want to implement a modulo operator that differs from the built-in operatormod
by always producing a positive remainder. -
Example 2 above. If your algorithms are correct, this will pass already.
Finally, develop map_l_to_r
using the same workflow as before, but with
Examples 3 and 4. Hint: You might want to implement a function that is the
inverse of your index
function.
Ideas for more unit tests. Finally, add some even harder unit tests. For example, here are the wiring specifications for the three standard rotors used by the 1930 Enigma I:
rotor I: EKMFLGDQVZNTOWYHXUSPAIBRCJ
rotor II: AJDKSIRUXBLHWTMCQGZNPYFVOE
rotor III: BDFHJLCPRTXVZNYEIWGAKMUSQO
Note: To head off any potential confusion, be aware that I, II, and III are the historical names of the rotors, not their order on the spindle. They could be installed in any order—e.g., I-II-III, III-II-I, III-I-II, etc.
You’ll recognize rotor I as the rotor we have used in our examples above. As test cases with those, you could try the following:
-
If
rotor_III = "BDFHJLCPRTXVZNYEIWGAKMUSQO"
, thenmap_r_to_l rotor_III 'O' 14
should be17
. -
If
rotor_I = "EKMFLGDQVZNTOWYHXUSPAIBRCJ"
, thenmap_l_to_r rotor_I 'F' 10
should be14
.
Rotors I, II, and III are not the only possible rotors. We will test your solution with other historical rotors and shiny new rotors. So, you should create test cases of your own based on other rotors.
Great work! You’ve now implemented a simulation of Enigma rotors. Finish up by
running make check
, then committing and pushing your code to your repo.
This is the stopping point for satisfactory scope.
Step 6: Reflector
Reflectors are a lot like rotors. Reflector wiring is specified with the same kind of 26-character string as a rotor, and that string is used to define a function \(w\) as before: when current enters a reflector at input position \(i\), it exits at output position \(w(i)\). But reflectors must satisfy one additional property:
A valid reflector specification must be a valid wiring specification, and it also must hold that if \(w(i) = j\) then \(w(j) = i\).
It is this property that makes the wiring a reflection, in the sense that it swaps \(i\) and \(j\).
Beyond that property, reflectors are actually simpler than rotors. Unlike rotors, the reflector does not rotate, so we don’t have to worry about the orientation or offset of the reflector. Also, current flows only one direction through a reflector, so we don’t have to worry about right-to-left vs. left-to-right.
Use test-driven development to complete the function map_refl
in the release
code. Make sure to keep writing failing unit tests first, then implementing code
to make those tests pass. As a first unit test, try
ABCDEFGHIJKLMNOPQRSTUVWXYZ
, which it turns out is a valid reflector
specification. Hint: you can implement map_refl
in just one line by using
map_r_to_l
as a helper.
After that, here are the wiring specifications of the standard reflectors B and C from the 1930 Enigma I:
reflector B: YRUHQSLDPXNGOKMIEBFZCWVJAT
reflector C: FVPJIAOYEDRZXWGCTKUQSBNMHL
Use those to write more unit tests, which if all goes well will already pass.
Reflectors B and C are not the only possible reflectors. We will test your solution with historical reflectors and shiny new reflectors.
Finish up by running make check
, then committing and pushing
your code to your repo.
Step 7: Plugboard
The plugboard swaps pairs of letters before and after current passes through the
rest of the machine. The operator configured the plugboard by inserting cables
between pairs of letters. We’ll represent a plugboard by a list of pairs of
characters. For example, here’s a plugboard in which two cables have been
inserted; one between ‘A’ and ‘Z’, the other between ‘X’ and ‘Y’: [('A', 'Z');
('X', 'Y')]
. The order of characters in the pairs should not matter and the
order of pairs in the list should not matter. So that plugboard has an effect
that is equivalent to this one: [('Y', 'X'); ('Z', 'A')]
.
A valid plugboard is a list of pairs of characters in which only the letters ‘A’..’Z’ appear, and no letter appears more than once.
A valid plugboard therefore may have anywhere from 0 to 13 elements in its list.
The plugboard with no cables inserted would be represented by the empty list,
[]
.
Complete the function map_plug
, continuing to use test-driven development. The
empty plugboard would be a good first test case, followed by a plugboard with
one cable inserted. Hint: write a recursive function that pattern-matches
against the input list.
After that, the two equivalent plugboards above would be good as a basis for several unit tests. Those tests could make sure that the two plugboards do indeed behave the same. Then you could try a plugboard with 13 cables inserted.
Finish up by running make check
, then committing and pushing your code to your
repo.
Step 8: Ciphering a character
As before, all the examples and unit tests in this section have been carefully double checked; they do not contain any typos.
Now it’s time to assemble all the functions you’ve written and tested. We’ll first implement the ciphering of just a single character based on the current configuration of the Enigma machine, including the plugboard, the reflector, and the rotors and their orientations. But for now, we continue to leave out any stepping of the rotors.
The cipher algorithm:
-
The operator types a letter \(X\) as input. The design of Enigma limits it to the uppercase letters ‘A’..’Z’. Lowercase letters, numerals, punctuation, spaces, etc. are not supported.
-
(The machine would now step. But we omit stepping for now. We’ll come back to it in a later part of the assignment.)
-
The input letter \(X\) is potentially transformed to a different letter \(Y\) because of the plugboard. The result of that transformation becomes an electrical current at position \(p\), where \(p\) is the index of \(Y\) in the alphabet.
-
That current passes through the rotors, the reflector, and back through the rotors.
At each of those, the output position from one rotor becomes the input position to the next. Eventually current exits the rotors at position \(q\). -
Let \(Z\) be the letter whose index in the alphabet is \(q\). The plugboard potentially transforms \(Z\) into an output letter \(W\), which is the letter that lights up on the lampboard.
An example: Consider the following configuration: reflector B is installed; rotors I, II, and III are installed in that order from left to right; the top letter of every rotor is ‘A’; and the plugboard is empty. Then input letter ‘G’ would be enciphered to output letter ‘P’, as follows:
-
The input letter is ‘G’. The plugboard is empty, so no transformation occurs. Current begins flowing into the rotors at the position of the index of ‘G’, which is 6.
-
Rotor III with top letter ‘A’ maps current from right-hand position 6 to left-hand position 2.
-
Rotor II with top letter ‘A’ maps current from right-hand position 2 to left-hand position 3.
-
Rotor I with top letter ‘A’ maps current from right-hand position 3 to left-hand position 5.
-
Reflector B maps current from position 5 to position 18.
-
Rotor I with top letter ‘A’ maps current from left-hand position 18 to right-hand position 18.
-
Rotor II with top letter ‘A’ maps current from left-hand position 18 to right-hand position 4.
-
Rotor III with top letter ‘A’ maps current from left-hand position 4 to right-hand position 15.
-
Current flows out of the rotors at position 15, which is the index of ‘P’. The plugboard is empty, so no transformation occurs. The output letter is ‘P’.
Expanding on that example, the following table shows how any individual character would be enciphered in that machine configuration. You’ll see, for example, that input letter ‘G’ lines up with output letter ‘P’, as we just explained in detail:
input letter: ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
V
output letter: UEJOBTPZWCNSRKDGVMLFAQIYXH
Data types. The release code defines a config
type to represent the
configuration of the Enigma machine: the reflector, the rotors that are
installed and the order they are on the spindle, and the top letter of each
rotor.
A valid configuration is a configuration in which all the wiring specifications are valid, the plugboard is valid, and the turnovers and top letters are all in ‘A’..’Z’.
Note that there might be any number of rotors in a valid configuration: perhaps 0, perhaps the standard 3, perhaps more. And the rotors need not be distinct; there could be duplicates.
Here are a couple notes on the config
type:
-
The
refl
field is the wiring specification of the reflector. -
The field named
turnover
in therotor
type won’t actually be used in any meaningful way in this part of the assignment; you can ignore it or just set it to an arbitrary letter for now. -
The order of elements in the
rotors
list in theconfig
type represents the order in which the rotors are installed on the spindle, from left to right. So, the head of the list is the leftmost rotor on the spindle, and the last element of the list is the rightmost rotor on the spindle. For example, if there are two rotors installed, and if the left-most rotor on the spindle (i.e., the rotor next to the reflector) isr
, and if the right-most rotor on the spindle (i.e., the rotor next to the static wheel) isq
, then that configuration would be represented by the list[r; q]
.
Function to implement. Complete the function cipher_char
that computes how
the Enigma machine ciphers a single letter. Note that this function still does
not perform any stepping of the rotors; rather, it models how the machine
transforms an input letter into an output letter when all the rotors are in a
particular orientation.
Continue to use test-driven development. Here are some ideas for unit tests:
-
When the plugboard is empty, the list of rotors is empty, and the reflector is wired as
ABCDEFGHIJKLMNOPQRSTUVWXYZ
, thencipher_char
will behave like the identity function: ‘A’ ciphers to ‘A, ‘B’ to ‘B’, etc. -
The example worked above gives you many possible test cases.
-
Using the Pringles-can model, or simply pencil and paper, you could work out many more test cases yourself.
Hint: consider writing two helper functions,
map_rotors_r_to_l : oriented_rotor list -> int -> int
map_rotors_l_to_r : oriented_rotor list -> int -> int
which compute the output position that results when current enters and passes through an entire list of rotors. They will be recursive functions over the list. Can you factor out a common helper function from them so that there isn’t any duplicated code?
Caution: Something confusing will happen if you attempt to use an
online Enigma simulator to construct test cases for your cipher_char
.
Those simulators implement stepping, but cipher_char
omits it.
So you might have to use different top letters to get the results you expect
with those simulators. For the details, you’ll have to keep reading.
Finish up by running make check
, then committing and pushing
your code to your repo.
Well done! You’ve accomplished a lot. Give yourself some time to recharge. Have you done anything from this list? Some of them are harder to do in the midst of pandemic, sadly.
This is the stopping point for good scope. It’s totally fine to end here. The rest of the assignment is worth only 5 points, but it will take a fair amount of work. So:
- If you’d rather work on other courses or just chill, that’s completely okay.
- If you’re having fun and want to dig deeper, that’s awesome.
- If you’re not sure, how about this: try implementing just “Rule 1” in Step 9 below, then skipping ahead to Step 10. Even without “Rule 2” and “Rule 3”, you’ll get some partial credit toward excellent scope. More importantly, you’ll also learn more about functional programming—in particular, how to model what seems like mutable state in an idiomatic functional way.
Let’s go!
Step 9: Stepping
The intuition of stepping is that the rotors behave mostly like the numbers on an odometer: the rightmost completes a full revolution, and in so doing, causes the next rotor to its left to take a single step, and so forth. But the stepping system on the Enigma is a little more complicated. Recall that each rotor has a turnover, which is the top letter that is showing when a notch is reached in the rotor. Those notches cause the rotors to step according to the following rules:
Rule 1: Just before each letter is enciphered, the rightmost rotor always steps.
Rule 2: Just before each letter is enciphered, if the top letter of any rotor is its turnover, then that rotor and the rotor to its left step. This rule does not apply to the leftmost rotor, however, since it has no rotor to its left.
Rule 3: No rotor steps more than once per letter enciphered, even if the above rules could be construed as suggesting that a rotor would step twice.
Some Enigma machines or rotors were built with different stepping rules; in case of discrepancy, the rules above are in effect for this assignment.
Here are the turnovers of some of the historical rotors:
turnover of I: Q
turnover of II: E
turnover of III: V
Example 1. Suppose the installed rotors are III-II-I (i.e., III is the left-most rotor, then II, then I is the right-most), starting in orientations where the top letters are KDO. Then as each character is enciphered, they would step as follows: KDO, KDP, KDQ, KER, LFS, LFT, LFU, …
Example 2. Suppose the installed rotors are again III-II-I, starting with top letters VDP. Then as each character is enciphered, they would step as follows: VDP, VDQ, VER, WFS, WFT, …
There are no typos in those examples. Really. 😀
Function to implement. Complete the function step
, which computes how the
Enigma machine steps just before enciphering a letter. Its type
config -> config
is worth considering: instead of mutating state, we produce a
new value out of an old one. This is a hallmark of the functional style of
programming.
This function is the most algorithmically complex part of the assignment. Continue to use test-driven development. For unit tests, first implement just rule 1; you should have no trouble inventing some tests for that. Then add in rules 2 and 3, using the examples we supplied above as unit tests.
Finish up by running make check
, then committing and pushing your code to your
repo.
Step 10: Ciphering a string
At last, it’s time to encipher an entire string. The order of operations as they occur on the Enigma machine is as follows:
- The operator types a letter.
- The machine immediately steps to a new configuration.
- The letter is enciphered according to that new configuration.
- The enciphered letter is lit on the lampboard.
- The operator repeats until finished.
Complete the function cipher
, which computes how the Enigma machine ciphers a
string. Continue to use test-driven development.
It is up to you to invent test cases for this function. There are many online simulators that could help you, such as these: 1, 2. We don’t know of any that allow arbitrary rotor wirings or numbers of rotors, though. Just do your best!
Actually, here is one test case for you: use reflector B, rotors I, II, and III (installed in that order—I is leftmost, then II, then III is rightmost), with starting top letters F, U, and N, and a plugboard with a single cable connecting ‘A’ and ‘Z’. Encipher YNGXQ; it should produce the name of a pretty good programming language.
Finish up by running make check
, then committing and pushing your code to your
repo.
YAAAAAAAY! All done!
Rubric
- 25 points: submitted and compiles
- 25 points: satisfactory scope
- 25 points: good scope
- 10 points: testing
- 10 points: code quality
- 5 points: excellent scope
Testing Rubric. We’re still taking it fairly easy on this assignment. Your test suite needs to satisfy these requirements:
-
Each required function has at least five unit tests. That magic number of five is in part still just about building proficiency with OUnit. It’s fine to use any of the test cases identified above in the handout or shipped in the release code as part of your five per function. The required functions are those that shipped in the release code with a
failwith "Unimplemented"
, namely,index
,map_r_to_l
,map_l_to_r
,map_refl
,map_plug
,cipher_char
,step
, andcipher
. That’s eight functions, so you need to write 40 total test cases. You should do your best to invent test cases that are not redundant, that explore boundary cases, etc., but the graders are not going to assess that on this assignment. You do not need to add tests for helper function you introduce yourself. -
Each unit test has a descriptive name that gives the reader an idea of what the test is about that is more specific than just the name of the function being tested. The name of a test is the string bound to it with OUnit’s
>::
operator. For example, the name of the provided test in the release code is"index of A is 0"
. That’s a good name; a name such asindex0
would not be sufficient. Think of the name as a comment that would help hypothetical future maintainers of the test suite to understand the test. -
Each unit test is constructed by a helper function to minimize code duplication. The
index_test
function is an example of the sort of function you should write.
As on A0, your test suite will be graded independently from the rest of your implementation. So you can gain or lose points for it regardless of which required functions you complete.
Make sure your test suite can be run with make test
. It’s okay if the OUnit
report contains some test cases that fail: you do not have to comment out
failing test cases. But OUnit must run to completion and produce that report
about how many test cases passed, failed, etc.
As a special case, you may violate the 80-column rule for long string literals in test cases. But even those can be avoided by using a multi-line string literal:
let s = "A camel is an even-toed ungulate in the genus Camelus that bears \
distinctive fatty deposits known as humps on its back. Camels have \
long been domesticated and, as livestock, they provide food (milk and \
meat) and textiles (fiber and felt from hair). Camels are working \
animals especially suited to their desert habitat and are a vital \
means of transport for passengers and cargo. There are three \
surviving species of camel. The one-humped dromedary makes up 94% of \
the world's camel population, and the two-humped Bactrian camel makes \
up 6%. The Wild Bactrian camel is a separate species and is now \
critically endangered."
(source: Wikipedia)
Code Quality Rubric. The graders will assess the following aspects of your source code:
-
Everything that was already assessed on A0.
-
Indentation of all constructs follows the OCaml Community guidelines.
-
Variable names should be self-documenting. Either they should be very short and idiomatically indicative (e.g.,
lst
for lists;h
andt
for head and tail), or they should be longer and self-documenting (e.g.,offset
orrotor
; notfoo
orvar1
). -
Individual functions should be at most about 10 or 20 lines and should never overflow what is visible on the screen. The human eye should be able to take in the entire function at once.
-
Nested match and if expressions should be avoided; or if sometimes used, an inner expression should be surrounded by
begin
..end
. -
Do not use
List.hd
,List.tl
, orList.nth
. These functions raise exceptions and are the cause of many coding errors. Though there are times their usage is reasonable, on A1 please avoid them altogether.
And here are a few aspects of your code that the graders will not assess but you should still consider:
-
Any helper functions you add should have specification comments.
-
There should be no compiler warnings about pattern matching errors. Such warnings are almost certainly a result of buggy code that will cause you to lose points from the autograder.
-
A deeply nested chain of function applications such as
f4 (f3 (f2 (f1 x)))
should be rewritten with the pipeline operator asx |> f1 |> f2 |> f3 |> f4
, which depending on the length of the function names might better be written asx |> really_long_name_f1 |> another_really_long_name_f2 |> short_f3 |> f4
Submission
Set the hours_worked
variable at the end of enigma.ml
to the number of
person-hours you spent working on this assignment. Ensure that your solution
passes make finalcheck
.
If you’re not working on a local install, you will need to get your code back on your own machine to upload it from your browser. Right click on the file in the VS Code Explorer pane, choose Download, and save it on your local drive. Don’t drag and drop; that can result in an empty file (which you will be able to detect by looking in it).
Submit your enigma.ml
and enigma_test.ml
on CMS. Double-check that
the MD5 sums are what you expected. Re-download your submission from CMS and
double-check before the deadline that the contents of the files are what
you intended.
Congratulations! You’ve cracked the Enigma!
Acknowledgement: Adapted from Prof. David Reed (Creighton University).