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_keywordfunction pairs each letter with its originalindex (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.