I recently wrote about the multiplication code used by WWII double agent Eddie Chapman, famously known as Agent Zigzag. By using just a memorised keyword and the day’s date, Chapman could generate a unique numerical shift key for his Gronsfeld cipher every single day, leaving zero physical evidence behind for counter-intelligence to find.

To see how it works in practice, I’ve translated his manual tradecraft into Python. Below is a complete script that replicates Chapman’s algorithm, featuring functions to process the keyword, generate the daily shift, and both encrypt and decrypt messages.

The code

def process_keyword(keyword):
    """
    Step 1: Assigns numerical values to the keyword based on alphabetical order.
    Returns the combined integer.
    """
    # Sanitize keyword to uppercase letters only
    keyword = "".join(c for c in keyword.upper() if c.isalpha())

    # Create a list of tuples: (character, original_index)
    indexed_chars = [(char, i) for i, char in enumerate(keyword)]

    # Sort alphabetically. Python's sort is stable, meaning duplicates
    # automatically stay in their original left-to-right order.
    sorted_chars = sorted(indexed_chars)

    # Reconstruct the sequence based on the original positions
    base_seq = [0] * len(keyword)
    for rank, (char, orig_index) in enumerate(sorted_chars, start=1):
        base_seq[orig_index] = str(rank)

    return int("".join(base_seq))

def generate_shift_key(keyword, day):
    """
    Step 2: Multiplies the base sequence by the day of the month.
    """
    base_int = process_keyword(keyword)
    shift_key_int = base_int * day
    return str(shift_key_int)

def agent_zigzag_encrypt(keyword, day, message):
    """
    Steps 3 & 4: Pads and encrypts the message using the generated shift key.
    """
    shift_key_str = generate_shift_key(keyword, day)

    # Sanitize and pad the message with 'X's to a multiple of 5
    message = "".join(c for c in message.upper() if c.isalpha())
    remainder = len(message) % 5
    if remainder != 0:
        message += 'X' * (5 - remainder)

    ciphertext = ""
    key_len = len(shift_key_str)

    # Encrypt by shifting forward
    for i, char in enumerate(message):
        shift = int(shift_key_str[i % key_len])
        # Calculate new character wrapping around the alphabet
        new_char = chr(((ord(char) - ord('A') + shift) % 26) + ord('A'))
        ciphertext += new_char

    # Group into blocks of 5 for standard radio transmission
    blocks = [ciphertext[i:i+5] for i in range(0, len(ciphertext), 5)]
    return " ".join(blocks)

def agent_zigzag_decrypt(keyword, day, ciphertext):
    """
    Decrypts the message by reversing the shift.
    """
    shift_key_str = generate_shift_key(keyword, day)

    # Remove any spaces from transmission blocks
    ciphertext = ciphertext.replace(" ", "")
    plaintext = ""
    key_len = len(shift_key_str)

    # Decrypt by shifting backward
    for i, char in enumerate(ciphertext):
        shift = int(shift_key_str[i % key_len])
        # Calculate original character wrapping around the alphabet
        orig_char = chr(((ord(char) - ord('A') - shift) % 26) + ord('A'))
        plaintext += orig_char

    return plaintext

if __name__ == "__main__":
    keyword = "RADIO"
    day = 15
    message = "SEND HELP"

    print("--- Multiplication code example ---")
    print(f"Keyword: {keyword}")
    print(f"Day: {day}")
    print(f"Original message: {message}\n")

    # 1. Show the generated shift key
    shift_key = generate_shift_key(keyword, day)
    print(f"Generated shift key: {shift_key}")

    # 2. Encrypt
    encrypted_message = agent_zigzag_encrypt(keyword, day, message)
    print(f"Ciphertext: {encrypted_message}")

    # 3. Decrypt
    decrypted_message = agent_zigzag_decrypt(keyword, day, encrypted_message)
    print(f"Decrypted message: {decrypted_message} (includes padding)")

How the code works

  • Sorting for duplicates: The process_keyword function pairs each letter with its original index (char, i) before sorting. Because Python uses a “stable” sorting algorithm (Timsort), if a word has duplicate letters like “APPLE”, the two ‘P’s will automatically be numbered left-to-right just as they appeared in the original word.
  • Arithmetic for wrapping: The line chr(((ord(char) - ord('A') + shift) % 26) + ord('A')) handles the alphabet wrap-around mathematically. It converts the letter to a 0–25 index, adds the shift, calculates the remainder divided by 26 (to loop past ‘Z’ back to ‘A’), and converts it back to a character.
  • Block grouping: Python easily slices the string into 5-character blocks at the very end to match the standard Morse code transmission format used by operatives.