The system used by Eddie Chapman was an effective tool for a field agent in the 1940s, but it cannot withstand modern computing power. While this cipher once protected secrets from manual codebreakers, a laptop can now find the answer in a fraction of a second.

Why the code is vulnerable

Here is why this cipher is highly vulnerable to modern computing, and how a computer would go about cracking it.

A small key space

Security in cryptography often depends on the number of possible keys, known as the key space. Modern standards like AES-256 have keyspaces so large they are practically impossible to guess. Chapman’s code is much more limited.

If an operative uses a standard dictionary word for their secret:

  • There are approximately 200,000 common English words to choose from.
  • There are only 31 possible multipliers based on the day of the month.
  • The resulting 6.2 million combinations are few enough for a Python script to test in a few seconds.

The limitations of digits

Most Vigenère-style codes use the entire alphabet for shifts, but this Gronsfeld variation only uses the digits 0 to 9. This restriction creates a statistical opening. For instance, an encrypted “A” will only ever appear as a letter from “A” to “J”. It will never be a “W” or “X”. A program can use frequency analysis to spot these restricted patterns and quickly narrow down the possibilities.

Repeating sequences

Because the shift key is relatively short and repeats throughout the message, it leaves a trail of recurring patterns. Techniques like the Kasiski examination allow a computer to determine the length of the key. Once the length is fixed, the complexity of the problem drops significantly, making the keyword and date much easier to find.

Automated cracking with Python

A computer can use a dictionary attack and a fitness function to break the code automatically:

  1. Loading a word list: The script begins with a file containing thousands of English words.
  2. Trying every combination: It generates a shift key for every word and every date.
  3. Applying a fitness function: To pick the correct decryption out of millions of attempts, the program scores each result. It looks for n-grams, common letter pairings like “TH” or “ER”, that appear in natural English.
  4. Ranking the results: The decryptions with the highest scores for natural language patterns are presented as the most likely candidates.

Below is a complete Python script to automatically crack the cipher by ranking the top five most likely results.

def process_keyword(keyword):
    """
    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):
    """
    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_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

def score_fitness(text):
    """
    Checks for common bigrams and trigrams,
    but also rewards the presence of common vowels.
    """
    common_patterns = [
        "TH", "HE", "IN", "ER", "AN", "RE", "ON", "AT", "EN",
        "THE", "AND", "ING", "ENT", "ION", "HER", "FOR", "THA"
    ]
    score = 0
    for pattern in common_patterns:
        score += text.count(pattern) * (len(pattern) ** 2) # Reward longer patterns more

    # Bonus for common English vowels appearing frequently
    vowels = "AEIOU"
    vowel_count = sum(text.count(v) for v in vowels)
    if vowel_count > (len(text) * 0.3): # English is usually ~38% vowels
        score += 5

    return score

def crack_zigzag(ciphertext, word_list, top_n=5):
    """
    Brute-forces the cipher and returns the top N unique results.
    """

    total_permutations = len(word_list) * 31
    print(f"Cracking... Analyzing {total_permutations:,} possible combinations.\n")

    all_results = []

    for word in word_list:
        for day in range(1, 32):
            decrypted = agent_zigzag_decrypt(word, day, ciphertext)
            score = score_fitness(decrypted)
            all_results.append({
                "keyword": word,
                "day": day,
                "text": decrypted,
                "score": score
            })

    # Sort by highest score first
    all_results.sort(key=lambda x: x['score'], reverse=True)

    # Filter for unique plaintext results (different keys can result in the same text)
    unique_results = []
    seen_texts = set()
    for res in all_results:
        if res['text'] not in seen_texts:
            unique_results.append(res)
            seen_texts.add(res['text'])
        if len(unique_results) >= top_n:
            break

    return unique_results


if __name__ == "__main__":
    # 1. We start with an intercepted message (Ciphertext)
    # This says "THE INVASION WILL HAPPEN AT DAWN" padded with "X"s
    # Encrypted with keyword: "PARIS", Day: 6
    intercepted_ciphertext = "UPMNS VBAQT SWJTT MFPQM VFYDB EVCCX"

    # 2a. We load a small dictionary
    # In a real scenario, you would load a text file with 100,000+ words here.
    dictionary = [
        "APPLE", "TRAIN", "BERLIN", "PARIS", "RADIO",
        "LONDON", "CHURCHILL", "SPY", "ZIGZAG", "SECRET"
    ]

    # 2b. Full dictionary loading example (commented out for this test)
    # with open("english_words.txt", "r") as file:
    #     dictionary = [line.strip().upper() for line in file]

    top_hits = crack_zigzag(intercepted_ciphertext, dictionary, top_n=5)

    print(f"{'Rank':<5} | {'Score':<6} | {'Day':<3} | {'Keyword':<16} | {'Decrypted Message'}")

    for i, hit in enumerate(top_hits, 1):
        print(f"{i:<5} | {hit['score']:<6} | {hit['day']:<3} | {hit['keyword']:<16} | {hit['text']}")

How can the “Multiplication code” be strengthened?

To strengthen the cipher, you must balance simplicity with the need to obscure patterns from modern scripts. Here are three methods to update the process for better security.

Expanding the keyspace

The original multiplier, which uses the date 1–31, is a weak point that a computer can quickly overcome. Using a second memorised word as the multiplier increases the difficulty. For instance, if the second word is “ORANGE” (Base: 451326), the multiplier becomes a six-digit number instead of a two-digit one. This shifts the number of combinations from millions into the trillions, slowing down brute-force attempts.

Removing the shift limit

One of the primary flaws in this system is that shifts are restricted to the digits 0 to 9, which means certain letters may never appear in specific contexts. This can be corrected by using digits in pairs, such as 76, 85, and 10. By applying a modulo 26 calculation, where 76 (mod 26) leaves a remainder of 24, the shifts are spread across the whole alphabet. This prevents statistical analysis from ruling out specific characters.

Using double encryption

Encrypting the message twice, a technique known as the Nihilist method, adds another layer of protection. By using different keywords or dates for each pass, the relationship between letters is further hidden. This produces a result that is much more difficult for automated frequency analysis to break.