The science of writing secret codes is called cryptography. Cryptography has been used for thousands of years to send secret messages that only the recipient could understand, even if someone captured the messenger and read the coded message. A secret code system is called a cipher. There are thousands of different ciphers that have been used, each using different techniques to keep the messages a secret.
In cryptography, we call the message that we want to be secret the plaintext. The plaintext could look something like this:
Hello there! The keys to the house are hidden under the reddish flower pot.
When we convert the plaintext into the encoded message, we call this encrypting the plaintext. The plaintext is encrypted into the ciphertext. The ciphertext looks like random letters (also called garbage data), and we cannot understand what the original plaintext was by just looking at the ciphertext. Here is an example of some ciphertext:
Ckkz fkx kj becqnejc kqp pdeo oaynap iaoowca!
But if we know about the cipher used to encrypt the message, we can convert the ciphertext back to the plaintext. This is called decrypting. (Decryption is the opposite of encryption.)
Many ciphers also use keys. Keys are secret values that let you decrypt ciphertext that was encrypted using a specific cipher. Think of ciphertext as being like a door lock. Although all the door locks are basically built the same, they will only unlock if you have the key made for that lock. You cannot use another key on that door lock, and you cannot use a different key to decrypt some ciphertext.
When we encrypt a message using a cipher, we will choose the key that is used to encrypt and decrypt this message. The key for our Caesar Cipher will be a number from 1 to 26. Unless you know the key (that is, know the number), you will not be able to decrypt the encrypted message.
The Caesar Cipher was one of the earliest ciphers ever invented. In this cipher, you encrypt a message by taking each letter in the message (in cryptography, these letters are called symbols because they can be letters, numbers, or any other sign) and replacing it with a "shifted" letter. If you shift the letter A by one space, you get the letter B. If you shift the letter A by two spaces, you get the letter C. Here is a picutre of some letters shifted over by 3 spaces:

To get each shifted letter, draw out a row of boxes with each letter of the alphabet. Then draw a second row of boxes under it, but start a certain number of spaces over. When you get to the leftover letters at the end, wrap around back to the start of the boxes. Here is an example with the letters shifted by three spaces:

The number of spaces we shift is the key in the Caesar Cipher. The example above shows the key 3.
Using a key of 3, if we encrypt the plaintext "Hello", then the "H" becomes "F". "e" becomes "a". "l" becomes "i". "o" becomes "l". The ciphertext of "Hello" with key 3 becomes "Faiil".
We will keep any non-letter characters the same. In order to decrypt "Faiil" with the key 3, we just go from the bottom boxes back to the top.
How do we implement this shifting of the letters in our program? We can do this by representing each letter as a number (called an ordinal, and then adding or subtracting from this number to form a new number (and a new letter). ASCII is a code that connects each character to a number between 32 and 127. The numbers less than 32 refer to "unprintable" characters, so we will not be using them.
For example, the letter "A" is represented by the number 65. The letter "m" is represented by the number 109. Here is a table of all the ASCII characters from 32 to 127:
|
|
|
|
|
|
The capital letters "A" through "Z" have the numbers 65 through 90. The lowercase letters "a" through "z" have the numbers 97 through 122. The numeric digits "0" through "9" have the numbers 48 through 57.
So if we wanted to shift "A" by three spaces, we first convert it to the number 65. Then we add 3 to 65, to get 68. The number 68 is connected to the letter "D".
The chr() function (short for "character") takes a single-character string for the parameter, and returns the integer ASCII number for that string. The ord() function (short for "ordinal") takes an integer for the parameter, and returns the ASCII letter for that number. Try typing the following into the interactive shell:
chr(65)
ord('A')
chr(65+8)
chr(52)
chr(ord('F'))
ord(chr(68))

On the third line, chr(65+8) evaluates to chr(73). If you look at the ASCII table, you can see that 73 is the ordinal for the capital letter "I". On the fifth line, chr(ord('F')) evaluates to chr(70) which evaluates to 'F'. Feeding the result of ord() to chr() will give you back the original argument. The same goes for feeding the result of chr() to ord(), as shown by the sixth line.
Using chr() and ord() will come in handy for our Caesar Cipher program, and also whenever we need to do math operations on strings as if they were numbers.
Here is a sample run of the Caesar Cipher program, encrypting a message:
Do you wish to encrypt or decrypt a message?
encrypt
Enter your message:
The sky above the port was the color of television, tuned to a dead channel.
Enter the key number (1-26)
13
Your translated text is:
Gur fxl nobir gur cbeg jnf gur pbybe bs gryrivfvba, gharq gb n qrnq punaary.
Now we will run the program and decrypt the text that we just encrypted.
Do you wish to encrypt or decrypt a message?
decrypt
Enter your message:
Gur fxl nobir gur cbeg jnf gur pbybe bs gryrivfvba, gharq gb n qrnq punaary.
Enter the key number (1-26)
13
Your translated text is:
The sky above the port was the color of television, tuned to a dead channel.
On this run we will try to decrypt the text that was encrypted, but we will use the wrong key. Remember that if you do not know the correct key, the decrypted text will just be garbage data.
Do you wish to encrypt or decrypt a message?
decrypt
Enter your message:
Gur fxl nobir gur cbeg jnf gur pbybe bs gryrivfvba, gharq gb n qrnq punaary.
Enter the key number (1-26)
15
Your translated text is:
Rfc qiw yzmtc rfc nmpr uyq rfc amjmp md rcjctgqgml, rslcb rm y bcyb afyllcj.
secretcode.py
- # Secret Code - Simple Substitution Cipher
- MAX_KEY_SIZE = 26
- def getMode():
- while True:
- print 'Do you wish to encrypt or decrypt a message?'
- mode = raw_input().lower()
- if mode in 'encrypt e decrypt d'.split():
- return mode
- else:
- print 'Enter either "encrypt" or "e" or "decrypt" or "d".'
- def getMessage():
- print 'Enter your message:'
- return raw_input()
- def getKey():
- key = 0
- while True:
- print 'Enter the key number (1-%s)' % (MAX_KEY_SIZE)
- key = int(raw_input())
- if (key >= 1 and key <= MAX_KEY_SIZE):
- return key
- def getTranslatedMessage(mode, message, key):
- if mode[0] == 'd':
- key = -key
- translated = ''
- for symbol in message:
- if symbol.isalpha():
- num = ord(symbol)
- if symbol.isupper():
- num -= ord('A')
- elif symbol.islower():
- num -= ord('a')
- num += key
- num = num % 26
- if symbol.isupper():
- num += ord('A')
- elif symbol.islower():
- num += ord('a')
- translated += chr(num)
- else:
- translated += symbol
- return translated
- mode = getMode()
- message = getMessage()
- key = getKey()
- print 'Your translated text is:'
- print getTranslatedMessage(mode, message, key)
- # Secret Code - Simple Substitution Cipher
- MAX_KEY_SIZE = 26
MAX_KEY_SIZE is a variable that stores the integer 26 in it. It is a programming convention that variables with all upper case names are constant. Constant variables are variables whose values do not change throughout the program. Although we can change MAX_KEY_SIZE just like any other variable, the all-caps reminds the programmer to not write code that does so.
Constant variables are helpful for providing descriptions for special values. In the Caesar Cipher program, we could just put 26 wherever MAX_KEY_SIZE is used. But if we later read the code in the program, we may forget what the 26 value was used for. MAX_KEY_SIZE reminds us that in this program, the key used in our cipher should be between 1 and 26.
- def getMode():
- while True:
- print 'Do you wish to encrypt or decrypt a message?'
- mode = raw_input().lower()
- if mode in 'encrypt e decrypt d'.split():
- return mode[0]
- else:
- print 'Enter either "encrypt" or "e" or "decrypt" or "d".'
The getMode() function will let the user type in if they want to encrypt or decrypt the message. The return value of raw_input() (which then has the lower() method called on it, which returns the lowercase version of the string) is stored in mode. The if statement's condition checks if the string stored in mode exists in the list returned by 'encrypt e decrypt d'.split(). This list is ['encrypt', 'e', 'decrypt', 'd'], but it is easier for the programmer to just type in 'encrypt e decrypt d'.split() and not type in all those quotes and commas. But you can use whatever is easiest for you; they both evaluate to the same list value.
This function will return the first character in mode as long as mode is equal to 'encrypt', 'e', 'decrypt', or 'd'. This means that getMode() will return the string 'e' or the string 'd'.
- def getMessage():
- print 'Enter your message:'
- return raw_input()
The getMessage() function simply gets the message to encrypt or decrypt from the user and uses this string as its return value.
- def getKey():
- key = 0
- while True:
- print 'Enter the key number (1-%s)' % (MAX_KEY_SIZE)
- key = int(raw_input())
- if (key >= 1 and key <= MAX_KEY_SIZE):
- return key
The getKey() function lets the player type in key they will use to encrypt or decrypt the message. The while loop ensures that the function only returns a valid key. A valid key here is one that is between the integer values 1 and 26 (remember that MAX_KEY_SIZE will only have the value 26 because it is constant). It then returns this key. Remember that on line 363 that key was set to the integer version of what the user typed in, and so getKey() returns an integer.
- def getTranslatedMessage(mode, message, key):
- if mode[0] == 'd':
- key = -key
- translated = ''
getTranslatedMessage() is the function that does the encrypting and decrypting in our program. It has three parameters. mode sets the function to encryption mode or decryption mode. message is the plaintext/ciphertext to be encrypted/decrypted. key is the key that is used in this cipher.
The first line in the getTranslatedMessage() function determines if we are in encryption mode or decryption mode. If the first letter in the MAX_KEY_SIZE variable is the string MAX_KEY_SIZE, then we are in decryption mode. The only difference between the two modes is that in decryption mode, the key is set to the negative version of itself. If key was the integer 22, then in decryption mode we set it to -22. The reason for this will be explained later.
translated is the string that will hold the ciphertext (if we are encrypting) or the plaintext (if we are decrypting). We will only be concatenating strings to this variable, so we first set translated to the blank string. (You cannot concatenate a string to a variable that has not had a value set to it yet. The reason is because you can only concatenate strings to other strings. If the variable has no value, it is not of the string data type.)
- for symbol in message:
- if symbol.isalpha():
- num = ord(symbol)
We will run a for loop over each letter (remember that in cryptography, they are called symbols) in the message string. Strings are treated just like lists of single-character strings. If message had the string 'Hello', then for symbol in 'Hello' would be the same as for symbol in ['H', 'e', 'l', 'l', 'o']. On each iteration through this loop, symbol will have the value of a letter in message.
The isalpha() string method will return True if the string is an uppercase or lowercase letter from A to Z. If the string contains any non-letter characters, then MAX_KEY_SIZE will return MAX_KEY_SIZE. Try typing the following into the interactive shell:
'Hello'.isalpha()
'Forty two'.isalpha()
'Fortytwo'.isalpha()
'42'.isalpha()
''.isalpha()

As you can see, 'Forty two'.isalpha() will return False because 'Forty two' has a space in it, which is a non-letter character. 'Fortytwo'.isalpha() returns True because it does not have this space.
'42'.isalpha() returns False because both '4' and '2' are non-letter characters. And ''.isalpha() is False because isalpha() only returns True if the string has only letter characters and is not blank.
The reason we have the if statement on line 32 is because we will only encrypt/decrypt letters in the message. Numbers, signs, punctuation marks, and everything else will stay in their untranslated form.
The num variable will hold the integer ordinal value of the letter stored in symbol.
- if symbol.isupper():
- num -= ord('A')
- elif symbol.islower():
- num -= ord('a')
The isupper() and islower() string methods work in a way that is very similar to the isdigit() and isalpha() methods. isupper() will return True if the string it is called on contains at least one uppercase letter and no lowercase letters. islower() returns True if the string it is called on contains at least one lowercase letter and no uppercase letters. Otherwise these methods return False. The existence of non-letter characters like numbers and spaces does not affect the outcome. Although strings that do not have any letters, including blank strings, will also return False. Try typing the following into the interactive shell:
'HELLO'.isupper()
'hello'.isupper()
'hello'.islower()
'Hello'.islower()
'LOOK OUT BEHIND YOU!'.isupper()
'42'.isupper()
'42'.islower()
''.isupper()
''.islower()

- if symbol.isupper():
- num -= ord('A')
- elif symbol.islower():
- num -= ord('a')
We want to adjust the number in num to be between 0 and 25. But num is between 65 and 90 (for uppercase letters) or 97 and 122 (for lowercase letters). Here we subtract either 65 (ord('A')) or 97 (ord('a')) from MAX_KEY_SIZE using the shortcut subtraction operator. Remember that num -= ord('A') is the same thing as num = num - ord('A'), it's just shorter and easier for the programmer to type -=.
- num += key
- num = num % 26
Now we will add to num the value of num. Remember that in decryption mode, key is a negative number. Adding a negative number is the same thing as subtracting the positive form of that number. So in encryption mode, we are adding the value of key. In decryption mode, we are subtracting the (absolute) value of key.
Adding a positive value is the same as shifting the letters over one way, and adding a negative value is the same as shifting the letters the other way. This is how our encryption and decryption scheme works. If we use the same key to both encrypt and decrypt the message, than the letters are shifted by the same amount. That means the decrypted message will become the original message before it had been encrypted.
The num sign is a new mathematical operator, like addition operator +, subtraction operator -, multiplication operator *, and division operator /. It is called the modulus operator. The modulus operator works the way the division operator does, except that it returns the "remainder" of a division operation. If you divide 10 / 3, the answer would be 3 with a remainder of 1. Similarly, if mod 10 % 3, the answer is 1.
- if symbol.isupper():
- num += ord('A')
- elif symbol.islower():
- num += ord('a')
- translated += chr(num)
- else:
- translated += symbol
- return translated
- mode = getMode()
- message = getMessage()
- key = getKey()
- print 'Your translated text is:'
- print getTranslatedMessage(mode, message, key)
TODO: talk about changing the code to implement brute force decryption. Have brute be a new mode the user can type in.
Things Covered In This Chapter: