Fun with Python: Intro to Cryptography - Playfair Cipher

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.

What is 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.

How it Works: The Manual Method

The process has a few more rules than our previous ciphers, but it's very systematic. Let's walk through it.

  • Keyword: PLAYFAIR EXAMPLE
  • Plaintext: 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.

  1. Keyword PLAYFAIR EXAMPLE becomes PLAYFIREXM after removing duplicates.
  2. Fill the grid with these letters, then the rest of the alphabet.
PLAYF
I/JREXM
BCDGH
KNOQS
TUVWZ

Step 2: Prepare the Plaintext
Next, we prepare the message for encryption.

  1. Break the plaintext into pairs of letters: HI DE TH EG OL DI NT HE TR EE ST UM P
  2. If any pair has two of the same letter (like 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.
  3. If the message has an odd number of letters, add a filler 'X' at the end. Our example has an even number, so we're good.

Step 3: Encrypt the Pairs using the Key Square
Now we encrypt each pair based on three simple rules:

  • Rule 1: If the letters are in the same row, replace each with the letter to its right (wrapping around to the first column if necessary).
    • Example: The pair EX is in the second row. E becomes X, and X becomes M. So, EX becomes XM.
  • Rule 2: If the letters are in the same column, replace each with the letter below it (wrapping around to the top row if necessary).
    • Example: The pair 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.
  • Rule 3: If the letters form a rectangle, replace each with the letter in the same row but at the opposite corner of the rectangle.
    • Example: The pair 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.


Automating with Python

playfair.png

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

Sort:  

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..