Project 1

The Playfair Cipher

Project 1 is to implement the encoding and decoding of the Playfair cipher. A description of the cipher and a good visual walkthrough is available on Wikipedia.

Due date is Friday October 30.

The rules of the Playfair cipher

The rules are listed on Wikipedia, but here they are again with the specific choices we will use.

Creating the table

Given a secret word, create a 5 by 5 grid of the 25 capital letters A-Z with J omitted. Form the table as follows: given a secret word, first convert any J to an I and then write it letter by letter starting with the top-left and skipping over a letter if you have already written it down. For example, the key word "MATHEMATICS" would produce the table:

M A T H E
I C S * *
* * * * *
* * * * *
* * * * *

Next, fill in the starred locations with the capital letters in order, skipping the letter J and the letters you have already used. (So the first letter entered in the example is B since A has already been used and next comes D.)

M A T H E
I C S B D
F G K L N
O P Q R U
V W X Y Z

Encrypting

Given a table and a message to encrypt, the rules are.

Wikipedia has a visual example of encoding "Hide the gold in the tree stump." Notice how the initial breaking into pairs is

HI DE TH EG OL DI NT HE TR EE ST UM P

The EE pair is the same so an X is inserted and the letters are rebroken into pairs

HI DE TH EG OL DI NT HE TR EX ES TU MP
                            ^

Now that all pairs are distinct, we check if the length is even or odd. Since it is even, nothing needs to be added to the end.

What you need to do.

Create a file playfair.py which contains a function playfair which takes two parameters: one for the scret key and one for the plaintext. Your function must return the encrypted ciphertext. That is, you must fill in the body of the following function:

def playfair(key, plaintext):
    # Code to encode and return the ciphertext

if __name__ == "__main__":
    s = input("Enter the secret key: ")
    p = input("Enter the plaintext: ")
    print(playfair(s, p))

The name/main stuff will cause that code to only be executed if you run the python file playfair.py directly.

Some suggestions

I suggest you work on the problem in stages, testing that the code works before moving on to the next stage. These are just suggestions, you can implement it in any way you like, although if you only complete a few of the stages you can get partial credit.

Stage 1

First, write enough python code to prompt only for the secret key, compute the table, and print out the table. I suggest you store the table as a list of lists. Now run your program several times, checking by hand that the correct table is created. Your code could look like

# Initialize a 5 by 5 table of stars
table = [['*', '*', '*', '*', '*'],
         ['*', '*', '*', '*', '*'],
         ['*', '*', '*', '*', '*'],
         ['*', '*', '*', '*', '*'],
         ['*', '*', '*', '*', '*']]

def print_table():
    for row in range(5):
        for col in range(5):
            print(table[row][col],end="")
        print("")

def clear_table():
    for row in range(5):
        for col in range(5):
            table[row][col] = "*"

def table_has_letter(letter):
    # Write code here to search the table for the given letter
    # and return True or False depending on if the letter exists

def create_table(secret):
    clear_table()

    # Write code here to fill in the table using the secret string.
    # The secret must be converted to upper case, the letter J replaced by I.

create_table("Mathematics")
print_table()

print("")

create_table("Playfair example")
print_table()  # Should match wikipedia

As a hint on create_table, there is a way to fill in the secret plus all the rest of the letters all at once by modifiying the secret before you start filling in letters. After changing the secret to upper case and replacing J by I, you can append all the letters besides J to the end of the secret. Since you are checking for preexisting letters, this will cause the table to be filled completly.

Stage 2

Write a function encode_pair(a,b) which takes two distinct characters as a block and runs the encoding rules for this block.

# Functions from Stage 1, plus

def find_letter(letter):
    # Search the table for the letter, returning the row and column

def encode_pair(a, b):
    if a == b:
        print("ERROR: letters to encode_pair must be distinct")

    # Code here to encode using the table using find_letter to lookup
    # the positions.  This should return the two encoding characters.


s = "Mathematics"
create_table(s)
print_table()
print(encode_pair('A', 'L')) # Should print HG

As a hint on encode_pair, you should first compute variables a_row, a_col, b_row, b_col using find_letter. Next, use an if-elif-else statement to fill in variables a_new_row, a_new_col, b_new_row, and b_new_col depending on the different cases. You might want to write down the formulas for the new rows and columns on paper before programming. Finally, return the contents of the new rows and columns.

Try different characters to pass to encode_pair on the very last line until you are confident that encode_pair works correctly for all distinct letters. Make sure you check all the cases (same row, same column, box, wrapping around the edge of the table).

Stage 3

Write functions cleanup and encode that implement the Playfair cipher, ignoring the duplicate letter and odd length text stuff.

# Functions from previous two stages plus

def cleanup(plaintext):
    # Code to remove spaces, convert to upper case, and change J to I.
    # Return the cleaned up plaintext

def encode(plaintext):
    # Code here to break into pairs and call encode_pair.
    # Return the cyphertext.

s = "Mathematics"
create_table(s)
print_table()
print(cleanup("a message with j in it"))
print(encode(cleanup("abcd"))) # Should print HCSI

Write the code so it works when each block in the original input is already distinct and has even length. If you put in a plaintext which has duplicate pairs you can leave your code with an error message, which is OK. Just concentrate first on when the plaintext is already distinct and even length. Try different strings inside the call to cleanup and encode until you are confident it works.

Stage 4

Finally, expand the cleanup function to deal with the duplicate pairs and odd length.

# Functions from the previous stages except cleanup, plus

def fix_one_problem(plaintext):
    # Code to fix a single problem with the plaintext.
    #
    # First, check for a duplicate block.  The first block that is
    # found is fixed and then the function returns the new plaintext
    # (which might still have issues).
    #
    # If no duplicate block is found, the input is checked for odd
    # length, fixed, and returned.
    #
    # If no problem is detected, return None

def cleanup(plaintext):
    # To the cleanup function from Stage 3, add calls fix_one_problem
    # in a loop until it returns None.  Return the cleaned up plaintext.

print(fix_one_problem("ABCD")) # Should return None
print(fix_one_problem("AA")) # Should return AXA
print(cleanup("ZWAABCC")) # Should return ZWAXABCXCZ
print(cleanup("Hide the gold in the tree stump"))

Try several other messages to make sure you cover all the cases.

The submission

Once you finish the above stages and are confident your code works, your submission needs a playfair function. You should first remove all of the test code from your final submission. Next, add a playfair function plus some code to prompt for the key and plaintext only when run directly.

# Functions from above, plus

def playfair(secret, plaintext):
    create_table(secret)
    return encode(cleanup(plaintext))

if __name__ == "__main__":
    s = raw_input("Enter the secret key: ")
    p = raw_input("Enter the plaintext: ")
    print(playfair(s, p))

Extra Credit

For extra credit, add a function playfair_decode which takes two parameters, the secret and the ciphertext and returns the plaintext.

Some Tests

Here are some tests. Nothing here needs to be submitted, the following code is only to help you make sure your encryption is correct. The following code is similar to what we will use for grading (we will use different keys and ciphertexts). The following code should be stuck in a file called test_playfair.py in the same directory as your submission. It imports your encryption code and then runs some tests.

import playfair

def test(key, plain, expected):
    cipher = playfair.playfair(key, plain)
    if cipher != expected:
        print("%s : %s != %s" % (plain, cipher, expected))

# The message from wikipedia
test("playfair example", "Hide the gold in the tree stump",
     "BMODZBXDNABEKUDMUIXMMOUVIF")

# A traditional ciphertext message
# See http://en.wikipedia.org/wiki/The_Magic_Words_are_Squeamish_Ossifrage
test("mathematics", "The Magic Words are Squeamish Ossifrage",
     "HEMACPCSVPUBCTUHKXZDTACBMRKTBCLOCPDE")

# A message on the statue at CIA headquarters (although using a different cipher)
# See http://en.wikipedia.org/wiki/Kryptos
test("Central Intelligence Agency", 
     "BETWEEN SUBTLE SHADING AND THE ABSENCE OF LIGHT LIES THE NUANCE OF IQLUSION",
     "DCNXTVNTMZHCDLQKLBFIYLEFGQCLKMNTENPDIGHQEGLNQRDTCWICENPDGPAVPYPE")

# Same row and column wrap around the table correctly
test("Mathematics",
     "GLPUDCTSAWYB",
     "KNQOISSKCAHL")

# Some duplicate letter and odd length checks
test("Mathematics",
     "aaa",
     "TWTWEW")
test("Mathematics",
     "jjrxxtzz",
     "SVBOTXTSVYXU")

Stage Tests

If our grading tests similar to the above do not pass on your submission, we will run some tests on the various stages outlined above to see about partial credit. These tests are only run if our encryption tests fail, because the stages outlined above are optional. In any case, if you decide to follow my outline above of stages, tests similar to the following will be run. You should use these to make sure your stages are correct. These files should be stuck in the same directory as your playfair.py.