Hey everyone,
About a week ago in our "Fun with Python" series, we explored the Vigenère and Beaufort ciphers, which use a keyword to create more secure substitutions. At the end of that post, I teased the idea of ciphers that work on pairs of letters. Today, we're going to dive into the most famous of them: the Playfair cipher.
Invented by Charles Wheatstone in 1854, but promoted by his friend Lord Playfair, this cipher was a major leap forward in security. Instead of substituting single letters, it encrypts pairs of letters at a time. This clever trick masks the frequency of individual letters (counting the number of Es for example), making it much more resistant to simple cryptanalysis. It works by using a 5x5 grid of letters generated from a keyword.
The process has a few more rules than our previous ciphers, but it's very systematic. Let's walk through it.
PLAYFAIR EXAMPLE
HIDE THE GOLD IN THE TREE STUMP
Step 1: Create the Key Square
We create a 5x5 grid and fill it with the unique letters of our keyword. Then, we fill the remaining spaces with the rest of the alphabet in order. Since there are 26 letters and only 25 spaces, we combine 'I' and 'J' into a single cell.
PLAYFAIR EXAMPLE
becomes PLAYFIREXM
after removing duplicates.P | L | A | Y | F |
---|---|---|---|---|
I/J | R | E | X | M |
B | C | D | G | H |
K | N | O | Q | S |
T | U | V | W | Z |
Step 2: Prepare the Plaintext
Next, we prepare the message for encryption.
HI DE TH EG OL DI NT HE TR EE ST UM P
EE
), insert a filler letter (like 'X') between them and regroup. Our example becomes HI DE TH EG OL DI NT HE TR EX ES TU MP
.Step 3: Encrypt the Pairs using the Key Square
Now we encrypt each pair based on three simple rules:
EX
is in the second row. E
becomes X
, and X
becomes M
. So, EX
becomes XM
.DE
is in the third column. D
is in row 3, so it becomes the letter below it, O
. E
is in row 2, so it becomes the letter below it, D
. So, DE
becomes OD
.TH
forms a rectangle. T
is in row 5, and H
is in row 3. The other corners of their rectangle are Z
and B
. T
is replaced by the corner on its row, Z
. H
is replaced by the corner on its row, B
. So, TH
becomes ZB
.Applying these rules to our whole message HI DE TH EG OL DI NT HE TR EX ES TU MP
, we get the ciphertext:
BM OD ZB XD NA BE KU DM UI XM MO UV IF
Decryption is the reverse: for same-row pairs move left, for same-column pairs move up, and the rectangle rule is the same.
Now, let's automate that process. Here is a Python script that can generate the key square, prepare the message, and handle both encryption and decryption.
#!/usr/bin/env python3
import argparse
def generate_key_square(key):
"""Generates a 5x5 Playfair key square."""
key = key.upper().replace("J", "I")
alphabet = "ABCDEFGHIKLMNOPQRSTUVWXYZ"
square = []
# Add unique key characters to the square
for char in key:
if char in alphabet and char not in square:
square.append(char)
# Add remaining alphabet characters
for char in alphabet:
if char not in square:
square.append(char)
return [square[i:i+5] for i in range(0, 25, 5)]
def find_position(square, char):
"""Finds the (row, col) position of a character in the square."""
char = char.upper().replace("J", "I")
for r, row in enumerate(square):
for c, letter in enumerate(row):
if letter == char:
return r, c
return -1, -1
def prepare_text(text):
"""Prepares plaintext for Playfair encryption."""
text = text.upper().replace("J", "I").replace(" ", "")
prepared_text = ""
i = 0
while i < len(text):
a = text[i]
if i + 1 < len(text):
b = text[i+1]
if a == b:
prepared_text += a + "X"
i += 1
else:
prepared_text += a + b
i += 2
else:
prepared_text += a + "X"
i += 1
return prepared_text
def playfair_cipher(text, key, decrypt=False):
"""Encrypts or decrypts text using the Playfair cipher."""
square = generate_key_square(key)
pairs = [text[i:i+2] for i in range(0, len(text), 2)]
result = ""
shift = -1 if decrypt else 1
for pair in pairs:
r1, c1 = find_position(square, pair[0])
r2, c2 = find_position(square, pair[1])
if r1 == r2: # Same row
result += square[r1][(c1 + shift) % 5] + square[r2][(c2 + shift) % 5]
elif c1 == c2: # Same column
result += square[(r1 + shift) % 5][c1] + square[(r2 + shift) % 5][c2]
else: # Rectangle
result += square[r1][c2] + square[r2][c1]
return result
def main():
parser = argparse.ArgumentParser(description="Playfair Cipher")
parser.add_argument("text", help="Text to encrypt/decrypt")
parser.add_argument("key", help="Cipher key")
parser.add_argument("--decrypt", action="store_true", help="Decrypt instead of encrypt")
args = parser.parse_args()
prepared_input = prepare_text(args.text) if not args.decrypt else args.text.replace(" ", "")
result = playfair_cipher(prepared_input, args.key, args.decrypt)
# Format output for readability
formatted_result = ' '.join(result[i:i+2] for i in range(0, len(result), 2))
print(f"Result: {formatted_result}")
if __name__ == "__main__":
main()
This cipher is a great example of how a few clever rules can create a system that's much stronger than the sum of its parts.
As always,
Michael Garcia a.k.a. TheCrazyGM
Thank you for my continued cryptographic education, my friend. Even though I don't understand it well, it's still very interesting. 😁🙏💚✨🤙
That shift flip in pla;yfair_cipher for decrypt made it click for me, simple and tidy. Love how prepare_text handles doubles with X and you even print the output in neat pairs, it reads like hte manual method :) As an accountant this kind of rule based flow scratches the ledger brain, now I want to encrypt my coffee orders..