Turn-in Checklist:
  1. Bring in to class a stapled turn-in containing your written answers to all the questions. If you worked with a partner, you and your partner should submit a single writeup with both of your names and UVA Email Ids on it. You must clearly indicate both names and UVA IDs in big block letters if you are working in a team.
  2. Use our automatic adjudication service to submit a single Python file (a modification of ps4.py) containing all your code answers. You must define an author list. Each partner must separately submit a copy of your joint file.

Collaboration Policy - Read Carefully

This problem set is intended to help you prepare for Exam 1. You may work on it by yourself or with any number of other students of your choice, but you will need to do the exam on your own.

Regardless of whether you work alone or not, you are encouraged to discuss this assignment with other students in the class and ask and provide help in useful ways.

Remember to follow the pledge you read and signed at the beginning of the semester. For this assignment, you may consult any outside resources, including books, papers, web sites and people, you wish except for materials from previous cs1120, cs150, and cs200 courses. You may consult an outside person (e.g., another friend who is a CS major but is not in this class) who is not a member of the course staff, but that person cannot type anything in for you and all work must remain your own. That is, you can ask general questions such as "can you explain recursion to me?" or "how do lists work in Python?", but outside sources should never give you specific answers to problem set questions. If you use resources other than the class materials, lectures and course staff, explain what you used in your turn-in.

You are strongly encouraged to take advantage of the scheduled help hours and office hours for this course.

You have one week to complete this problem set. We recommend you start early and take advantage of the help hours.



Cryptography means secret writing. The goal of much cryptography is for one person to send a message to another person over a channel that an adversary may be eavesdropping on without the eavesdropper understanding the message. We do this by having functions that encrypt and decrypt messages. The encrypt function takes a plaintext message and produces a ciphertext message. Encrypt scrambles and alters the letters of the plaintext message so that an eavesdropper will not be able to understand the message. The decrypt function takes a ciphertext message and produces the corresponding plaintext message. Encryption works as intended if the only person who can perform the decrypt function is the intended recipient of the message.

Since making up good encrypt and decrypt functions and keeping them secret is hard, most cryptosystems are designed to be secure even if the encryption and decryption algorithms are revealed. The security relies on a key which is kept secret and known only to the sender and receiver. The key alters the encryption and decryption algorithm in some way that would be hard for someone who doesn't know the key to figure out. If the sender and receiver use the same secret key we call it a symmetric cipher. If the sender and receiver can use different keys we call it an asymmetric cipher.

Ciphertext = encrypt(KE, Plaintext)
Plaintext = encrypt( KD, Ciphertext)

If KE = KD it is symmetric encryption. If KE is different from KD it is asymmetric encryption.

In this problem set, you will explore a symmetric cipher based on the Lorenz Cipher that was used by the German Army High Command to send some of the most important and secret messages during World War II. The Lorenz Cipher was broken by British Cryptographers at Bletchley Park. Arguably the first electronic programmable computer, Colossus, was designed and built by Tommy Flowers, a Post Office engineer working at Bletchley Park during the war. (There is a lot of arguing about what should be considered the first computer. Who Invented the Computer? The Legal Battle That Changed Computing History supports John Atanasoff's case; this Scientific American post summarizes some of the other candidates.)

Ten Collosi were built in 1943 and 1944, and used to break some of the most important German messages during World War II. Messages broken by Colossus were crucial to the D-Day invasion since the allies were able to learn that their campaign to deceive Hitler about where the attack would come was succeeding and knew where German troops were positioned.

Bletchley Park (Summer 2004)
It is regretted that it is not possible to give an adequate idea of the fascination of a Colossus at work: its sheer bulk and apparent complexity; the fantastic speed of thin paper tape round the glittering pulleys; the childish pleasure of not-not, span, print main heading and other gadgets; the wizardry of purely mechanical decoding letter by letter (one novice thought she was being hoaxed); the uncanny action of the typewriter in printing the correct scores without and beyond human aid; the stepping of display; periods of eager expectation culminating in the sudden appearance of the longed-for score; the strange rhythms characterizing every type of run; the stately break-in, the erratic short run, the regularity of wheel-breaking, the stolid rectangle interrupted by the wild leaps of the carriage-return, the frantic chatter of a motor run, the ludicrous frenzy of hosts of bogus scores.

D. Mitchie, J. Good, G. Timms. General Report on Tunny, 1945. (Released to the Public Record Office in 2000). http://www.alanturing.net/tunny_report

Measuring Cost

Optional Reading: Before answering question 1, you should read Chapters 6 and 7 of the coursebook.

Optional Reading: Before answering question 1, you should complete Unit 5 of Udacity CS 101. (Note that we have not yet assigned Udacity Unit 4. You can complete 4 before 5 if you like, at your option.)

For each of the following subquestions, you will be given two functions, g and f that take a single parameter n (which must be a non-negative integer) and asked to determine which of O (f(n)), Ω (f(n)), and Θ (f(n)) contain g (n). It is possible that more than one of the sets contain g(n).

In addition to identifying the properties that hold, you should justify your answer by:

For example, if g is n2 and f is n3 you would argue:
Question 1: For each g and f pair below, argue convincingly whether or not g is (1) in O(f), (2) in Ω(f), and (3) in Θ(f) as explained above. For all questions, assume n is a non-negative integer.
  1. g: n + 3, f: n
  2. g: n2 + n, f: n2
  3. g: 2n, f: 3n
  4. g: 2n, f: nn
  5. g: the federal debt n years from today, f: the US population n years from today (this one requires a more informal argument)
Download: Download ps4.zip to your machine and unzip it into your home directory J:\cs1120\ps4.

This file contains:

  • ps4.py — A template for your answers. You should do the problem set by editing this file.
  • lorenz.py — Some helpful definitions for this problem set, including code to convert between characters and baudot codes, and some functions we defined in class.

The Exclusive Or (XOR)

The exclusive-or (xor, sometimes pronounced "zor") function is one of the most useful functions for cryptography. The xor function evaluates to true if exactly one of the inputs is true.

In cryptography, it is usually easier to deal with 1 and 0 instead of true and false. For this problem set, use 1 to represent true and 0 to represent false. We will call each 0 or 1 a bit, since this is the smallest unit of information.

Question 2: Define the xor function that takes two bits as parameters and evaluates to 1 if exactly one of the parameters is 1 and evaluates to 0 otherwise. (Note that this xor function may be slightly different from others you may see, since the inputs are not Booleans.)

Your xor function should produce these evaluations:

>>> xor(0,0)
>>> xor(0,1)
>>> xor(1,0)
>>> xor(1,1)
The xor function has several properties that make it useful in cryptography: The second property means that if a message is encrypted by converting the message into a sequence of bits, and xor-ing each bit in the message with a perfectly random, secret bit known only to the sender and receiver, then we can send a message with perfect secrecy! This is known as a one-time pad.

Fortunately for the allies, the Nazis didn't have a way of generating and distributing perfectly random bit sequences. Instead, they generated non-random sequences of bits using rotors. The Soviet Union also used a one-time pad cipher following WWII. Because their key bits were not truly random, and because they sometimes reused keys, the NSA was able to decrypt many important KGB messages. Details of this were released in 1995, and are available at the NSA's web site. We will look at how the Lorenz cipher used xor to encrypt messages soon. First, we consider how to turn messages into sequences of bits.

The Baudot Code

The Nazis used the Baudot code to represent the letters in their messages as sequences of bits. The Baudot code translates letters and other common characters into a 5-bit sequences. With five bits, we have 25 = 32 possible values, so this is enough to give each letter in the alphabet a different code and have a few codes left over for spaces and other symbols. Modern computers typically used either 7-bit ASCII encodings to have 27 = 128 possible characters so that lowercase and additional punctuation characters can be represented, or 16-bit Unicode encodings that are able to represent 216 = 65536 different characters to support languages like Chinese that have many different characters.

Table 1 shows the letter mappings for the Baudot code. For example, the letter H is represented by 10100 while I is 00110. We can put letters together just by concatenating their encodings. For example, the string "HI" is represented in Baudot as 10100 00110. There are some values in the Baudot code that are awkward to print: carriage return, line feed, letter shift, figure shift and error. For our purposes we will use printable characters unused in the Baudot code to represent those values so we can print encrypted messages as normal strings. Table 1 shows the replacement values in parenthesis.

A 00011      H 10100      O 11000      V 11110      space 00100
B 11001      I 00110      P 10110      W 10011      carriage return (,) 01000
C 01110      J 01011      Q 10111      X 11101      line feed (-) 00010
D 01001      K 01111      R 01010      Y 10101      letter shift (.) 11111
E 00001      L 10010      S 00101      Z 10001      figure shift (!) 11011
F 01101      M 11100      T 10000           error (*) 00000
G 11010      N 01100      U 00111     
Table 1. Baudot Code mappings.

We can use lists of ones and zeros to represent Baudot codes. H is represented as the list [1, 0, 1, 0, 0]. A string is represented as a list of these lists: "HI" is
   [ [1, 0, 1, 0, 0], [0, 0, 1, 1, 0] ].

We have provided these two functions in lorenz.py:

char_to_baudot: Character → Baudot
Takes a character as input, and outputs the corresponding Baudot code represented as a list of five bits.
baudot_to_char: Baudot → Character
Takes a baudot code, represented as a list of five bits, as input and outputs the corresponding character.

Question 3:
a. Define a procedure, string_to_baudot, that takes a string and transforms it into a list of Baudot codes. [Hints]
b. Write the inverse function, baudot_to_string, that takes a list of lists of baudot codes and transforms it back into a string.

You may find the functions string_to_char_list and char_list_to_string provided in ps4.py helpful for writing answering question 3.

Your functions should be inverses. Hence, you can test your code by evaluating baudot_to_string composed with string_to_baudot. For example,
should evaluate to "HELLO".

Question 4: Describe the running time of your baudot_to_string procedure using Θ notation. Make sure to explain carefully what every variable you use means. [Hint]

Lorenz Cipher Machine

The Lorenz Cipher

The Lorenz cipher was an encryption algorithm developed by the Germans during World War II. It was used primarily for communications between high commanders. The original Lorenz machine consisted of 12 wheels, each one having 23 to 61 unique positions. Each position of a wheel represented either a one or a zero.

The first 5 wheels were called the K wheels. Each bit of the Baudot representation of a letter was xor-ed with the value showing on the respective wheel. The same process was repeated with the next 5 wheels, named the S wheels. The resulting value represented the encrypted letter. After each message letter the K wheels turn one rotation. The movement of the S wheels was determined by the positions of the final two wheels, called the M wheels.

Lorenz Cipher Wheels

Like most ciphers, the Lorenz machine also required a key. The key was the starting position of each of the 12 wheels. To decipher the message you simply need to start the wheels with the same position as was used to encrypt and enter the ciphertext. There were 16,033,955,073,056,318,658 possible starting positions. This made the Nazis very confident that without knowing the key (starting positions of the wheels), no one would be able to break messages encrypted using the Lorenz machine. (But their confidence was misplaced.)

Since the full Lorenz cipher would be too hard for this problem set, we will implement a simplified version. You should be suitably amazed that the allied cryptographers in 1943 were able to build a computer to solve a problem that is still hard for us to solve today! (Of course, they did have more that a week to solve it, and more serious motivation than we can provide in this course.)

Our Lorenz machine will use 11 wheels, each with only 5 positions. The first five wheels will be the K wheels and the second five the S wheels. Each of these will only have a single starting position for all 5 — that is, unlike the real Lorenz machine, for this problem set we will assume all five K wheels must start in the same position and all five S wheels must start in the same position.

The final wheel will act as the M wheel. After each letter all the K wheels and the M wheel should always rotate. Before the M wheel rotates, if it shows a 1 the S wheels should also rotate, but if the M wheel shows a 0 the S wheels do not rotate.

We have provided 3 lists that represent the wheels in lorenz.py. The first is called K_wheels and is a list of lists, each of the inner lists containing the 5 settings.

K_wheels = [[1,1,0,1,0],[0,1,0,0,1],[1,0,0,1,0],[1,1,1,0,1],[1,0,0,0,1]].

There is a similar list called S_wheels to represent the S wheels of our simulated machine.

S_wheels = [[0,0,0,1,1],[0,1,1,0,0],[0,0,1,0,1],[1,1,0,0,0],[1,0,1,1,0]].

The final list represents the M wheels and is just a single list.

M_wheel = [0,0,1,0,1]

To rotate our wheels, we will take the number at the front of the list and move it to the back. Thus, the first number in the list represents the current position of the wheel.

Question 5: Define a procedure rotate_wheel that takes as input a wheel (which is a represented as a list). It should return the wheel rotated once. For example, rotate_wheel([1, 2, 3, 4, 5]) should evaluate to [2, 3, 4, 5, 1]. Although all the wheels in our simulated Lorenz cipher machine have five bits, your rotate_wheel procedure should work for any non-empty list. [Hint]

Note: Do not use Python functions like list.remove() that you may have learned from Google or from some other source. They work well with lists like [1,2,3,4] but may not do what you expected on lists like [1,1,0,1] with duplicate elements. You can solve all of the problems here using just the information covered in the lectures.

We also want a procedure that can rotate a wheel zero or more times.
Question 6: Define a procedure rotate_wheel_by that takes two inputs: a wheel as the first input and a number as the second. It should output result of rotating the wheel the number of times passed in. For example, rotate_wheel_by([1, 0, 0, 1, 0], 2) should evaluate to [0, 1, 0, 1, 0] and rotate_wheel_by(wheel, 0) should evaluate to wheel. [Hint]

Next, we define similar procedures that work on a list of wheels at a time instead of a single wheel.

Question 7: Define a procedure rotate_wheel_list that takes a list of wheels (like K_wheels) as its input and evaluates to a list of wheels where each of the wheels in the parameter list of wheels has rotated once. For example, rotate_wheel_list(K_wheels) should evaluate to [ [1, 0, 1, 0, 1], [1, 0, 0, 1, 0], [0, 0, 1, 0, 1], [1, 1, 0, 1, 1], [0, 0, 0, 1, 1] ]. [Hint]

Question 8: Define a procedure rotate_wheel_list_by that takes a list of wheels and a number as parameters, and evaluates to a list where each of the wheels in the parameter list of wheels has rotated the number parameter times. For example, rotate_wheel_list_by(K_wheels, 5) should evaluate to the same list as K_wheels.
Question 9: Describe the running time of your rotate_wheel_list_by procedure using Θ notation. Be sure to explain what all variables you use mean. [Hints]

Simulating the Lorenz Machine

Now that we can rotate our wheels, we simulate the Lorenz machine using our K and S wheels. Since both sets of wheels are doing the same thing, we should be able to write one procedure that will work with either the K wheels or the S wheels.

Question 10: Define a function wheel_encrypt that takes a Baudot-encoded letter (a list of 5 bits) and a list of 5 wheels as parameters. The function should xor each bit of the Baudot list with the first value of its respective wheel and return the resulting list. For example, wheel_encrypt([0, 0, 0, 1, 1], K_wheels) should produce [1, 0, 1, 0, 0]. Your wheel_encrypt procedure should have running time in Θ(n) where n is the length of the list passed as the first parameter to wheel_encrypt. Even though our Lorenz crypotography will always involve lists of length five, your answer must work for any length inputs as long as both lists have the same length. [Hints]
Question 11: Argue convincingly why the running time of your wheel_encrypt procedure is linear in the size of its input.

We now have all the procedures we need to implement our simplified Lorenz machine. A quick review of how the machine should work:

Question 12: Define a function called do_lorenz that takes a list of Baudot values, the K wheels, S wheels and M wheel. The function should encrypt the first Baudot code with the K and S wheels, then recursively encrypt the rest of the Baudot codes with the wheels rotated. The function should return the encrypted values in the form of a list of Baudot values. For example:

>>> do_lorenz(string_to_baudot("COOKIE"), K_wheels, S_wheels, M_wheel)
[ [1, 1, 0, 1, 0], [0, 0, 0, 0, 1], [1, 1, 0, 0, 1], [1, 0, 0, 0, 1], \ 
  [0, 0, 1, 1, 1], [1, 1, 0, 1, 1] ]

Question 13: Define a function lorenz_encrypt that takes four parameters: a string and three integers. The integers represent the starting positions of the three wheels (zero indexed), respectively. The function should call do_lorenz with the string converted to Baudot and the wheels rotated to the correct starting positions. The function should return the ciphertext in the form of a string.

You should now be able to encrypt strings using the simplified Lorenz cipher. To test it, call your lorenz_encrypt function with a string and offsets of your choice to produce ciphertext. Since our encryption and decryption functions are the same, if you evaluate lorenz_encrypt again using the ciphertext and the same offsets you should get your original message back. For example:

>>> lorenz_encrypt("CAKE", 1, 2, 3)
>>> lorenz_encrypt("BNR!", 1, 2, 3)

Cracking the Code

The first Lorenz-encrypted messages were intercepted by the British in early 1940. The intercepts were sent to Bletchley Park, the highly secret British base set up specifically to break enemy codes. The code-breakers at Bletchley Park had little success with the Lorenz Cipher until the Germans made a major mistake in late 1941. A German operator had nearly finished sending a long message using a Lorenz machine when the receiver radioed back to tell him that the message had not been received correctly. The operator then reset his machine back to the same starting position and began sending the message again. But the operator, probably frustrated at having to resend the message, abbreviated some of the words he had typed out completely the first time. This led to two nearly identical messages encrypted using the same starting positions.

The messages were sent to John Tiltman at Bletchley Park. Tiltman was able to discern both messages and determine the generated key. The messages were then passed on to Bill Tutte who, after two months of work, figured out the complete structure of the Lorenz machine only from knowing the key it generated. The British were then able to break the Lorenz codes, but much of the work needed to be done by hand, which took a number of weeks to complete. By the time the messages were decrypted they were mostly useless.

The problem was given to Tommy Flowers, an electronics engineer from the Royal Post Office. Flowers designed and built a device called Colossus that worked primarily with electronic valves. The Colossus was the first electronic programmable computer. It was able to decrypt the Lorenz messages in a matter of hours, a huge improvement from the previous methods. The British built ten more Colossi and were able to decrypt an enormous amount of messages sent between Hitler and his high commanders. The British kept Colossus secret until the 1970s. After the war, eight of the Colossi were quickly destroyed and the remaining two were destroyed in 1960 and all drawings were burnt. The details of the breaking of the Lorenz Cipher were kept secret until 2000, but are now available at http://www.codesandciphers.org.uk/documents/newman/newmix.htm.

Our simplified cipher will be much easier to break. Since there are only 5 starting positions for the K wheels, 5 for the S wheels, and 5 for the M wheel, there are only 125 different keys. This is such a small number that we can simply try every possibility and look at the results to find the original message. Breaking a cipher by trying all possible key values is called a brute-force attack.

Question 14: Define a procedure, brute_force_lorenz, that takes two inputs: a procedure and a ciphertext string. Your procedure should evaluate lorenz_encrypt on the ciphertext for all 125 possible starting positions, passing in the resulting string to the input procedure. For example, consider the following procedure printout, which is defined in ps4.py:
def printout(x):  
    print x         
When brute_force_lorenz( printout, ciphertext) is evaluated, it should call printout on each of the 125 possible decoded strings for the ciphertext input (defined in lorenz.py), causing each of these strings to be printed to the screen. If your procedure works correctly, one of the messages generated will look like sensible English. [Hints]

Your procedure should not return any value. This can be achieved either by not including a return statement at all or by ending the procedure with the line return (no value after the return).

Remember to call the passed-in procedure once on each decoded string (i.e., many times total), not once on a big list of strings.

You may be wondering why we needed to define the fairly silly-looking printout procedure in the previous question. After all, can't we just pass print into brute_force_lorentz? It turns out that in Python, print is not a function, but rather a special kind of Python statement. According to the Python Grammar, print is defined with the following rule:

print_stmt ::=  "print" ([expression ("," expression)* [","]]
                | ">>" expression [("," expression)+ [","]])
This means that when we evaluate a print statement in Python, it is not treated as a function but instead as a print_stmt. Since print is not a function, we can't pass it to brute_force_lorentz directly. We can get aroudn this, however, by enclosing the print statement in a simple function printout and passing that into brute_force_lorentz instead.

Question 15: The actual Lorenz cipher operated similarly to the one in this problem set except instead of only having 5 positions in each wheel, each wheel had many more positions (up to 61). Suppose n is the number of possible positions for each Lorenz cipher wheel, and the procedure used to cryptanalyze the cipher is the same as your code (except extended as necessary to handle the extra wheel positions).

a. Describe how the amount of work needed to break a Lorenz-encrypted message grows as a function of the number of wheel positions, n, using Θ notation. Assume the message length and number of wheels are fixed.

b. Suppose instead that it was possible to add S and K wheels to the Lorenz cipher, and w represents the number of S and K wheels (for example, for the cipher machine in the problem set, w is 5). Describe how the amount of work needed to break a Lorenz-encrypted message grows as a function of w, using Θ notation. Assume the message length and number of wheel positions are fixed.

c. If the Nazis had learned of Bletchley Park's success in breaking the Lorenz cipher during the war, what changes that could be done without building and redistributing completely new cipher machines, would be most likely to prevent further successful cryptanalysis?

Question 16: Define a variable concept_comments that is a string containing your thoughts on what the main concepts have been so far in class. Your string may contain line breaks if you like, or it can be one long line. Let us know what you think the key points of the class have been so far. Your answers will help me determine what you have retained, and may shape the material on the first exam. For example, if you suggest a very good exam question, I may use it on the exam. We will not think less of you (or grade you more harshly later on) for negative comments. You will receive full credit for this question for any comment longer than 40 characters, but we appreciate more helpful comments.

If you are working with a partner, you should each submit your own answer to this in the version of ps4.py you submit electronically.

Question 17: Complete the following survey:
  1. Computer Science Diversity Committee Survey (pick CS1xxx)
Define a variable surveys_completed that is an integer indicating the number of surveys you completed.

If you are working with a partner, you should each submit your own answer to this in the version of ps4.py you submit electronically.

Question 18: Define a variable favorite_fractals that is a list of three integers. Each integer should correspond to a Fractal from the CS 1120 Fractal Gallery. You may use any evaluation criteria you like. You may vote for a fractal multiple times. If you are working with a partner, you and your partner may (should?) vote for different fractals — since each partner submits a separate copy of the assignment, each partner gets three separate votes. You are on your honor not to vote for your own factal. The order of your list does not matter.

The creators of the fractals that get the most votes will receive extra credit and public acclaim.

Automatic Adjudication: Submit a single Python Definition file (your ps4.py) including all of the code you wrote, as well as your answer to Question 16. Each partner must submit the file separately.

Challenge Bonus Question: Decrypt the ciphertext defined as challenge_ciphertext in lorenz.py.

This message was encrypted using a Lorenz-like cipher, but unlike the ciphertext for Question 14 (and the machine you simulated to solve that), the wheels in each group are not necessarily all rotated by the same amount. So, each of the five K_wheels, each of the five S_wheels, and the one M_wheel can be rotated by any amount. This means instead of having 125 possible initial wheel settings, this machine has 511 = 48828125 possible settings.

Solving this challenge is worth at least one extra credit point (and potentially multiple depending on how you solve it), and is open until solved. Email your solution to cs1120-staff@cs.virginia.edu.

Note: this is definitely a challenging problem, but trivially easy compared to breaking the actual Lorenz cipher as was done at Bletchley Park in 1943! (Hint: The encrypted message is in English, but you definitely don't want to look at 48 million output strings by hand to see which one is English!)

Colossus (Original, 1943)

Colossus (Rebuilt, 2004)


Avail yourself of these means to communicate to us at seasonable intervals
a copy of your journal, notes & observations of every kind,
putting into cipher whatever might do injury if betrayed.

Thomas Jefferson's instructions to Captain Lewis for the Expedition to the Pacific.
The complete text is available from

Credits: This problem set was created for CS200 Spring 2002 by Jon Erdman and David Evans, and tested by Stephen Liang. It was revised for CS200 Spring 2003 and CS150 Fall 2005 by David Evans, for CS 150 Spring 2009 by Wes Weimer, and for cs1120 Fall 2009 by David Evans. It was revised for Python in 2012 by Jonathan Burket and Lenny Li.