Wednesday, April 22nd, 2026¶
On Monday, we discussed the encryption/decryption scheme used for our next project.
Project 5 - Code breakers¶
We developed functions to convert strings to sequences of ASCII codes and vice versa.
def str_to_ascii(s):
ascii_codes = []
for c in s:
ascii_codes.append(ord(c))
return ascii_codes
def ascii_to_str(ascii_codes):
char_list = []
for n in ascii_codes:
char_list.append(chr(n))
return ''.join(char_list)
We can test these by converting a string to a sequence of ASCII codes and converting the resulting list back into a string, which should be the same as the original string.
s = 'This is MTH 337!'
ascii_codes = str_to_ascii(s)
new_s = ascii_to_str(ascii_codes)
print(s)
print(new_s)
This is MTH 337! This is MTH 337!
Recall the encryption scheme described in the project page:
- One selects a secret key, which is sequence of characters. This key is used to both encrypt and decrypt the message.
- Characters of the secret key and characters of the message are converted into ASCII codes. In this way the key is transformed into a sequence of integers $(k_1, k_2, \dots, k_r)$, and the message becomes another sequence of integers $(m_1, m_2, \dots, m_s)$. If $r<s$, then the secret key sequence is extended by repeating it as many times as necessary until it matches the length of the message.
- Let $c_i$ be the reminder from the division of $m_i + k_i$ by $128$. The sequence of numbers $(c_1, c_2, \dots, c_s)$ is the encrypted message.
We discussed the need for a get_padded_key_ascii function that can be used to lengthen our key to match the length of the message, along with a few approaches for how to accomplish this.
Exercise: Write a function get_padded_key_ascii that takes in arguments key_ascii and length and returns a padded version of key_ascii of length length, obtained by repeating key_ascii as many times as necessary.
Exercise: Write a function encrypt(message_ascii, key_ascii) that return the encrypted version of message_ascii using the secret key key_ascii (based on the code above).
Exercise: Write a function decrypt(encrypted_ascii, key_ascii) that returns the decrypted message.
Exercise: Test your functions by defining your own message and key strings (with message longer than key), and:
- Converting both strings into sequences of ASCII codes (using
str_to_asciifunction), - Generating the encrypted message as a sequence of ASCII codes (using the
encryptfunction), - Decrypting the encrypted message (using the
decryptfunction), - Converting the decrypted message back into a string (using the
ascii_to_strfunction).
The final string should match the original message string.
We also discussed how to load your assigned file into Python and obtain the corresponding list of ASCII codes.
Exercise: Read the contents of your assigned file and generate the corresponding list of ASCII codes.
Finally, we discussed a possible strategy for breaking the encryption. In particular, we discussed:
- Iterating through all of the possible key words that may have been used to encrypt the original message (coming from the
dictionary.txtfile) and decrypting the assigned message with each word, - Quantifying how "good" each of these different decrypted messages are, and
- Choosing the key word that gave the "best" decryption.
One way to quantify how "good" a given decryption is is to count how many actual English words appear in the decrypted message (using the words in dictionary.txt again as a collection of English words). In other words, we can iterate through each "word" from our decrypted message and test whether that "word" is in our list of English words.
Note: We saw that it is much faster to check for membership in a Python set than in a Python list, so we will want to generate a set version of our collection of English words.
Exercise: Iterate through all possible keywords and decrypt your assigned message for each keyword. Generate a dictionary where the keys are the keywords used to decrypt and the values are "how good" the decryption is. Then find the keyword that gives the "best" decryption.