In this tutorial you will learn about three ciphers and how you can implement these yourself in Python. The ciphers you will learn about are the Caesar cipher, the Polybius square and the Vigenère cipher. Of course none of these ciphers offer any kind of security in the modern age of computers. But for all of you interested in cryptography: Have fun with some DIY cryptography in Python!

The Caesar cipher

Maybe the easiest (and maybe also the most insecure) way to encipher a message is the so-called Caesar cipher. The Caesar cipher is named after Julius Caesar, who is said to have used it to encrypt some of his private correspondence. But the technique is so simple, that probably every school child has already played around with it. In its most basic version, the Caesar cipher encrypts every “A” as a “B”, every “B” as a “C” and so on. Of course you can also shift every letter of the plain text by two positions in the alphabet. In that case, every “A” would become a “C” and every “B” would become a “D”. The obvious problem with this method is that using some simple frequency analysis (or simply trial and error), you can quickly work out which letters in the cipher text correspond to which letters in the plain text. But despite the fact that the Caesar cipher and other simple substition ciphers were so easy to crack, they were used for centuries to come.

To program this cipher in python, we first create a plain text alphabet called ‘alpha’ and a substitution alphabet called ‘subst’. An integer ‘s’ determines by how many positions the two alphabets differ.

import string

alpha = string.ascii_lowercase
s = 2 # number of letters to shift by
subst = alpha[s:] + alpha[:s]
print(alpha, subst)
## abcdefghijklmnopqrstuvwxyz cdefghijklmnopqrstuvwxyzab

Next, we specify an arbitrary plain text message, for example: ‘secretmessage’. Then, we create an empty list called ‘cipher’. We loop through each letter in the plain text, find its position in the plain text alphabet and then output the letter at that position in the substitution alphabet. Lastly, we merge the elements in the cipher list into a single string.

plain = "secretmessage"
cipher = []
for i in plain:
  letter = subst[alpha.find(i)]
  cipher.append(letter)
print(''.join(cipher))
## ugetgvoguucig

To decipher a message, that has been enciphered using the Caesar cipher, we simply apply the procedure backwards.

cipher = "ugetgvoguucig"
plain = []
for i in cipher:
  letter = alpha[subst.find(i)]
  plain.append(letter)
print(''.join(plain))
## secretmessage

Now, we let’s turn this into two methods within in a class that we will call ‘Caesar’. Of course, we don’t really have to write two methods. To decrypt messages, we can simply use the encryption method with a negative shift integer:

class Caesar:

    def encipher(plain, s):
        alpha = string.ascii_lowercase
        subst = alpha[s:] + alpha[:s]
        cipher = []
        for i in plain:
            letter = subst[alpha.find(i)]
            cipher.append(letter)
        return ''.join(cipher)

    def decipher(cipher, s):
        plain = Caesar.encipher(cipher,-s)
        return plain
print(Caesar.encipher("anothersecretmessage",5))
## fstymjwxjhwjyrjxxflj
print(Caesar.decipher("fstymjwxjhwjyrjxxflj",5))
## anothersecretmessage

The Polybius square

Another ancient cipher is the so-called Polybius square. This cipher is based on a 25-letter cipher alphabet which is arranged in a \(5 \times 5\) matrix. One typically drops the letter ‘j’ to fit our 26-letter alphabet into this matrix. The rows and columns of this matrix are labeled 1 through 5. In the most basic version of the cipher, this Polybius square looks like this:

\[\begin{equation} \begin{array} ~ & 1 & 2 & 3 & 4 & 5 \\ 1 & A & B & C & D & E \\ 2 & F & G & H & I & K \\ 3 & L & M & N & O & P \\ 4 & Q & R & S & T & U \\ 5 & V & W & X & Y & Z \\ \end{array} \end{equation}\]

Every letter is now enciphered according to its position in the matrix. For example, an “L” would be “31”, an “R” would be “42”. The first digit always refers to the row position, the second digit to the column position of the letter.

Of course, at its core, the Polybius square is again just a simple substitution cipher. To allow for different alphabets, one could, just as with the Caesar cipher, simply shift square’s alphabet by a couple of positions. To make things a little bit more interesting, we will introduce a keyword, which will occupy the top left fraction of the square. If we use the word “CIPHER”, the Polybius square will look like this: \[\begin{equation} \begin{array} ~ & 1 & 2 & 3 & 4 & 5 \\ 1 & C & I & P & H & E \\ 2 & R & A & B & D & F \\ 3 & G & K & L & M & N \\ 4 & O & Q & R & S & T \\ 5 & U & V & W & Y & Z \\ \end{array} \end{equation}\]

In Python, we create this square by starting with an empty list. Then we loop through all letters of the keyword and the alphabet and, if they haven’t been added yet, add them to the list:

keyword = "cipher"
square = []
for c in keyword + string.ascii_lowercase:
    if c not in square and c != "j":
        square.append(c)
square = ''.join(square)
print(square)
## cipherabdfgklmnoqstuvwxyz

Then, we loop through each letter of the plain text and determine its row and column position in the matrix using the divmod() function.

plain = "secretmessage"
cipher = []
for i in plain:
  n = square.find(i) + 1
  row,col = divmod(n,5)
  cipher.append(str(row+1)+str(col))
print(cipher)
## ['43', '20', '11', '21', '20', '44', '34', '20', '43', '43', '22', '31', '20']

To decipher the message, we reverse the process by inferring the plain text letters from their positions in the Polybius square:

plain = []
for i in range(len(cipher)):
    row = int(cipher[i][0])
    col = int(cipher[i][1])
    letter = square[(row-1)*5 + col-1]
    plain.append(letter)
print(''.join(plain))
## secretmessage

As before, we can wrap these steps into a class:

class Polybius:

    def encipher(plain, keyword):

        # create secret alphabet => 5x5 square
        square = []
        for c in keyword + string.ascii_lowercase:
            if c not in square and c != "j":
                square.append(c)
        square = ''.join(square)

        # loop through plain text to encipher
        cipher = []
        for i in plain:
            n = square.find(i) + 1
            row,col = divmod(n,5)
            cipher.append(str(row+1)+str(col))

        # return
        return cipher

    def decipher(cipher, keyword):

        # create secret alphabet => 5x5 square
        square = []
        for c in keyword + string.ascii_lowercase:
            if c not in square and c != "j":
                square.append(c)
        square = ''.join(square)

        # loop through cipher text to decipher
        plain = []
        for i in range(len(cipher)):
            row = int(cipher[i][0])
            col = int(cipher[i][1])
            letter = square[(row-1)*5 + col-1]
            plain.append(letter)

        # return
        return "".join(plain)
print(Polybius.encipher("anothersecretmessage","polybius"))
## ['24', '43', '12', '51', '40', '32', '50', '23', '32', '30', '50', '32', '51', '42', '32', '23', '23', '24', '34', '32']
print(Polybius.decipher(['24', '43', '12', '51', '40', '32', '50', '23', '32', '30', '50', '32', '51', '42', '32', '23', '23', '24', '34', '32'],"polybius"))
## anothersecretmessage

Le chiffre indéchiffrable

In the 16th century a new type of cipher emerged. Unlike substitution ciphers with a single alphabet, this new Vigenère cipher is be based on what’s called polyalphabetic substitution. Given a keyword, e.g., “ORANGE”, the letters of a plain text, e.g., “secretmessage”, are not all enciphered using the same substitution alphabet. Instead, the first letter of the plain text is shifted by 14 positions, because the first letter of the keyword is an “O”, which is the 15th letter of the alphabet. The second letter of the plain text is then shifted by 17 positions, because the second letter of the keyword is a “R”, which is the 18th letter of the alphabet. This processes is repeated for all letters in the plain text. In case the plain text is longer than the keyword, which is typically the case, one simply re-uses the keyword over and over again until all letters of the plain text have been enciphered.

For roughly three hundred years, this cipher was never cracked, earning it the title “the indecipherable cipher”. What made this cipher so much stronger than its monoalphabetic precursors is the fact that the same letter in the plain text can be enciphered in different ways. Thus, traditional frequency analysis no longer works. Of course, the cipher’s weakness is the fact that the keyword gets repeated over and over again. And in the 19th century Charles Babbage and Friedrich Kasiski figured out that, once you worked out the length of the keyword, you can use frequency analysis for each letter in the keyword and thus crack “le chiffre indéchiffrable” nonetheless.

Fortunately, we can use some of our earlier code from the Caesar cipher to also program the Vigenère cipher. All we have to do is dynamically changing the shift value “s” throughout the encryption process.

plain = "secretmessage"
keyword = "orange"
n = 0
alpha = string.ascii_lowercase
cipher = []
for i in plain:
    if n > len(keyword)-1:
        n = 0 # re-use keyword
    s = alpha.find(keyword[n])
    letter = Caesar.encipher(i, s)
    print(i, s, letter)
    cipher.append(letter)
    n += 1
## s 14 g
## e 17 v
## c 0 c
## r 13 e
## e 6 k
## t 4 x
## m 14 a
## e 17 v
## s 0 s
## s 13 f
## a 6 g
## g 4 k
## e 14 s
print("".join(cipher))
## gvcekxavsfgks

Similarly, we can use the “decipher” method from the Caesar cipher when writing the code necessary to decipher a Vigenère-enciphered text:

cipher = "gvcekxavsfgks"
keyword = "orange"
n = 0
alpha = string.ascii_lowercase
plain = []
for i in cipher:
    if n > len(keyword)-1:
        n = 0 # re-use keyword
    s = alpha.find(keyword[n])
    letter = Caesar.decipher(i, s)
    print(i, s, letter)
    plain.append(letter)
    n += 1
## g 14 s
## v 17 e
## c 0 c
## e 13 r
## k 6 e
## x 4 t
## a 14 m
## v 17 e
## s 0 s
## f 13 s
## g 6 a
## k 4 g
## s 14 e
print("".join(plain))
## secretmessage

As before, we wrap these two chunks of code into a class:

class Vigenere:

    def encipher(plain, keyword):
        n = 0
        alpha = string.ascii_lowercase
        cipher = []
        for i in plain:
            if n > len(keyword)-1:
                n = 0
            s = alpha.find(keyword[n])
            letter = Caesar.encipher(i, s)
            cipher.append(letter)
            n += 1
        return ''.join(cipher)

    def decipher(cipher, keyword):
        n = 0
        alpha = string.ascii_lowercase
        plain = []
        for i in cipher:
            if n > len(keyword)-1:
                n = 0
            s = alpha.find(keyword[n])
            letter = Caesar.decipher(i, s)
            plain.append(letter)
            n += 1
        return ''.join(plain)
print(Vigenere.encipher("anothersecretmessage","vigenere"))
## vvuxuiiwzkxigqvwnimi
print(Vigenere.decipher("vvuxuiiwzkxigqvwnimi","vigenere"))
## anothersecretmessage