Skip to main content
Basic Classic Symmetric Breakers Intermediate

Breaking the Beaufort Cipher

The Beaufort cipher's reciprocal formula (C = K − P instead of Vigenère's C = P + K) looks like it should matter to an attacker. It doesn't: the same Kasiski examination, Index of Coincidence, and chi-squared pipeline that breaks Vigenère breaks Beaufort just as cleanly.

PL
Pashalis Laoutaris
August 21, 2026
16 min read

Interactive Beaufort Cipher Breaker

🔐 Beaufort Cipher Breaker

7
Beaufort is reciprocal (C = K − P, same formula both ways), but that changes nothing about this attack: it's the identical Kasiski / IC / chi-squared pipeline used to break Vigenère.
Enter text and click a button to start!

Step 1: Kasiski Examination

Find repeated sequences in the ciphertext. They usually mean the same plaintext letters lined up with the same key letters again, some multiple of the key length apart.

Click "Break Cipher" to begin.

Step 2: Index of Coincidence Scan

Test each candidate key length: split the ciphertext into that many interleaved streams and measure how "clumped" the letters are. English text clumps more than random noise (~6.67% vs. ~3.85%). The true key length is the shortest one where that jump appears.

Step 3: Recovering the Key

Each interleaved stream was combined with one fixed key letter via Beaufort's K − P formula. Try all 26 candidate key letters per column and keep whichever produces a letter distribution closest to real English, via chi-squared.

ZAEDHDYCUNSBKSBAUSETYMLSGTQLDAOUPWJDHOUJDXOUENHDZCANTKMWIRJZRNPWSLBDGWWMEFITGPBVOTLBLHVYHLFTPXDQMHHQRKLTMEYDFHXGTKSOPEJTAZPMDQQDRAWZSOEBTZCNERJZAZVLEZQZTXHMZDLLTKKWXDHKBHKLEZIDKTHQMOBLEMENCXBDAZQUDZVHWWYDZZURLQPOBADQPDLQGBEZVCDAOUGATKKWXMHOSQILHLZAECXMBDGLAZFBPXGMEVPXHHBTVYXQOPZKTKSPRAAVSGXGXQQDPXHBEWOSYLSQEMWSKOIPLMZOBAJWKPSKLMOEQLLKKPPWJYKUFMHDQAYANSOOPAUZVHGLAFZAEILFKOMQRSPCUNLQUTPXHMDTXGLSDAIDHKKFGWWWOQHKDSZCRQZLKVTTHREEOTLMAAAZSL

Breaking the Beaufort Cipher: Same Statistics, Flipped Formula

Introduction

The Beaufort cipher looks like a small variation on Vigenère: instead of C = (P + K) mod 26, it uses C = (K − P) mod 26. That’s subtraction instead of addition, and it’s reciprocal. So the exact same formula decrypts as encrypts. It’s tempting to assume that flipped sign changes how an attacker approaches it. It doesn’t, meaningfully. Cryptanalysis of Beaufort uses the identical three-stage statistical pipeline that breaks Vigenère: Kasiski examination, the Index of Coincidence, and chi-squared frequency analysis. Only the final per-letter formula is adjusted to match.

Table of Contents

Why the Reciprocal Formula Doesn’t Help

Beaufort’s defining feature, being reciprocal so the same operation encrypts and decrypts, is a convenience for whoever’s using it by hand (no separate decryption procedure to remember). It provides no additional resistance to cryptanalysis. The two properties an attacker actually exploits are:

  1. The key repeats. Just like Vigenère, a short keyword is cycled to cover the whole message, so the same key letter periodically lines up with different plaintext letters. Crucially, distinctive ciphertext sequences repeat whenever the key happens to realign with a repeated stretch of plaintext.
  2. Each position is still a simple modular shift. Whether that shift is expressed as P + K or K − P doesn’t matter. Once you isolate a single key position, decoding it is a 26-way search. It’s a Caesar cipher in disguise either way.

Neither property depends on which direction the subtraction goes. So the attack doesn’t change shape at all; only one small piece of arithmetic in the final step does.

One naming note, to avoid confusing this with a similarly-named cipher: this article covers classical Beaufort, C = (K − P) mod 26. A separate cipher called Variant Beaufort exists too, which encrypts with Vigenère’s decryption formula, C = (P − K) mod 26. Variant Beaufort isn’t reciprocal the way classical Beaufort is, and everything below assumes the classical formula.

Step 1 & 2: Finding the Key Length (Unchanged from Vigenère)

Kasiski examination and the Index of Coincidence work purely on the ciphertext’s statistical structure: how often sequences repeat, how “clumped” the letter distribution is. Neither of these cares what formula produced that ciphertext. So both steps carry over from the Vigenère breaker completely unchanged:

  1. Scan the ciphertext for repeated 3-letter sequences and record the distances between repeats. These are almost always multiples of the key length, since that’s when the key realigns with itself.
  2. Factor those distances and collect votes for each candidate key length.
  3. For each candidate length, split the ciphertext into that many interleaved streams and measure the average Index of Coincidence. The true length is the shortest one whose IC clearly jumps toward English’s ~6.67% baseline, away from random noise’s ~3.85%.

Step 3: Chi-Squared Analysis, Adjusted for K − P

This is the one step that actually changes. For Vigenère, decrypting a column with candidate key letter K means computing P = (C − K) mod 26 for every letter C in that column. For Beaufort, it’s P = (K − C) mod 26 instead. That’s the same reciprocal formula used for encryption, run again.

The search procedure is otherwise identical: try all 26 possible values of K for a given column, decode the column with each, and score the resulting letter distribution against real English frequencies using the chi-squared statistic:

χ² = Σ (observed − expected)² / expected

The K that produces the lowest χ², the distribution closest to real English, is almost always correct for that position. Do this once per column, and the recovered key letters spell out the keyword.

A Worked Example

Using the visualizer’s default ciphertext (438 letters, encrypted with a 6-letter key):

  1. Kasiski examination finds several repeated 3-letter sequences: "LEZ" reappears 24 letters later, "PXH" reappears 30 letters later, and a chain of overlapping repeats ("TKK", "KKW", "KWX") all recur 72 letters apart. Every one of these distances (24, 30, 72) is a multiple of 6.

  2. Factoring those distances, the length 6 collects the most votes, tied with its own divisors 2 and 3. Those will always tag along, since any multiple of 6 is also a multiple of 2 and 3.

  3. The Index of Coincidence breaks that tie: splitting the ciphertext into 6 interleaved streams gives an average IC of 0.0654, close to English’s 6.67%. Length 2 or 3 alone stay much closer to the random baseline.

  4. Chi-squared analysis solves each of the 6 streams independently using Beaufort’s P = (K − C) mod 26 formula, recovering the letters S-H-I-E-L-D one at a time.

  5. Decrypting the full ciphertext with key SHIELD reveals a coherent English passage about Beaufort’s cryptanalysis. It’s the same passage this article uses to demonstrate the attack, recovered rather than assumed:

    “The Beaufort cipher looks almost identical to Vigenere at first glance but its reciprocal formula changes nothing about how an analyst attacks it since repeated sequences in the ciphertext still betray the key length through Kasiski examination and the index of coincidence still separates the correct length from the wrong ones just as reliably once the columns are split apart the chi squared statistic finds each key letter exactly the way it would for any other polyalphabetic cipher built from simple modular shifts…”

Python Implementation

The interactive visualizer above runs this exact pipeline in JavaScript. Here’s the same attack in Python. Kasiski examination and Index of Coincidence are copied essentially verbatim from the Vigenère breaker, with only the chi-squared step’s decode formula adjusted for Beaufort’s K − P.

Key Features

  • Steps 1 and 2 are unmodified: kasiski_factors and index_of_coincidence don’t reference the encryption formula at all. They operate purely on ciphertext statistics, so they carry over from Vigenère without a single change.
  • Reciprocal encrypt/decrypt in one function: beaufort_transform implements C = (K − P) mod 26 once. Because Beaufort is reciprocal, the exact same function both builds the demo ciphertext and recovers the plaintext from a cracked key.
  • Chi-squared scoring, Beaufort’s formula: best_key_letter_for_column tries all 26 candidate key letters using P = (K − C) mod 26, the one piece of arithmetic that actually differs from the Vigenère version.

Code

# beaufort_breaker.py
#
# Breaks a Beaufort cipher using the same three-stage statistical pipeline
# as the Vigenere breaker -- Kasiski examination, the Index of Coincidence,
# and chi-squared frequency analysis -- adapted for Beaufort's reciprocal
# formula: C = (K - P) mod 26, so decryption is P = (K - C) mod 26, the
# SAME formula run again (K - C instead of C - K).

import re

ENGLISH_FREQ = {
    'A': 8.167, 'B': 1.492, 'C': 2.782, 'D': 4.253, 'E': 12.702, 'F': 2.228,
    'G': 2.015, 'H': 6.094, 'I': 6.966, 'J': 0.153, 'K': 0.772, 'L': 4.025,
    'M': 2.406, 'N': 6.749, 'O': 7.507, 'P': 1.929, 'Q': 0.095, 'R': 5.987,
    'S': 6.327, 'T': 9.056, 'U': 2.758, 'V': 0.978, 'W': 2.360, 'X': 0.150,
    'Y': 1.974, 'Z': 0.074,
}
RANDOM_IC = 0.0385   # expected IC for a uniform 26-letter alphabet
ENGLISH_IC = 0.0667  # expected IC for real English text
MAX_KEY_LENGTH = 20


def clean(text):
    return re.sub(r'[^A-Z]', '', text.upper())


def beaufort_transform(text, key):
    # Reciprocal: the same formula encrypts and decrypts.
    result = []
    for i, c in enumerate(text):
        k = ord(key[i % len(key)]) - ord('A')
        x = ord(c) - ord('A')
        result.append(chr((k - x) % 26 + ord('A')))
    return ''.join(result)


# --- Step 1: Kasiski examination (identical to Vigenere's) ---
def kasiski_factors(ciphertext, seq_len=3):
    positions = {}
    for i in range(len(ciphertext) - seq_len + 1):
        seq = ciphertext[i:i + seq_len]
        positions.setdefault(seq, []).append(i)

    factor_counts = {}
    for occurrences in positions.values():
        if len(occurrences) < 2:
            continue
        for i in range(1, len(occurrences)):
            distance = occurrences[i] - occurrences[0]
            for factor in range(2, min(distance, MAX_KEY_LENGTH) + 1):
                if distance % factor == 0:
                    factor_counts[factor] = factor_counts.get(factor, 0) + 1

    return factor_counts


# --- Step 2: Index of Coincidence (identical to Vigenere's) ---
def index_of_coincidence(text):
    n = len(text)
    if n < 2:
        return 0.0
    counts = {ch: text.count(ch) for ch in set(text)}
    return sum(c * (c - 1) for c in counts.values()) / (n * (n - 1))


def split_into_columns(ciphertext, key_len):
    columns = [''] * key_len
    for i, ch in enumerate(ciphertext):
        columns[i % key_len] += ch
    return columns


def best_key_length_by_ic(ciphertext, max_len=MAX_KEY_LENGTH):
    # 0.55 is a tunable constant, not a derived value: it just needs to sit
    # somewhere between RANDOM_IC and ENGLISH_IC, closer to the English end,
    # so a genuine key length clears it while shorter, undershooting lengths
    # don't. The same threshold shape is used in the Vigenere breaker.
    threshold = RANDOM_IC + (ENGLISH_IC - RANDOM_IC) * 0.55
    scores = []
    for length in range(1, max_len + 1):
        columns = split_into_columns(ciphertext, length)
        avg_ic = sum(index_of_coincidence(c) for c in columns) / length
        scores.append((length, avg_ic))
        if length > 1 and avg_ic >= threshold:
            return length, avg_ic, scores
    return max(scores, key=lambda s: s[1]) + (scores,)


# --- Step 3: Chi-squared frequency analysis, Beaufort's formula ---
# Each column is a Beaufort-shifted stream: P = (K - C) mod 26. Try all 26
# candidate K values and pick whichever produces a letter distribution
# closest to real English.
def chi_squared(observed_counts, n):
    chi2 = 0.0
    for letter, freq in ENGLISH_FREQ.items():
        expected = (freq / 100) * n
        observed = observed_counts.get(letter, 0)
        chi2 += (observed - expected) ** 2 / expected
    return chi2


def best_key_letter_for_column(column):
    best_k, best_chi2 = 0, float('inf')
    for k in range(26):
        decoded = [chr((k - (ord(c) - ord('A'))) % 26 + ord('A')) for c in column]
        counts = {ch: decoded.count(ch) for ch in set(decoded)}
        chi2 = chi_squared(counts, len(column))
        if chi2 < best_chi2:
            best_chi2, best_k = chi2, k
    return best_k


def break_beaufort(ciphertext):
    factor_counts = kasiski_factors(ciphertext)
    top_factors = sorted(factor_counts.items(), key=lambda kv: kv[1], reverse=True)[:5]

    key_len, ic, _ = best_key_length_by_ic(ciphertext)

    columns = split_into_columns(ciphertext, key_len)
    key = ''.join(chr(best_key_letter_for_column(c) + ord('A')) for c in columns)

    plaintext = beaufort_transform(ciphertext, key)
    return {
        'kasiski_top_factors': top_factors,
        'key_length': key_len,
        'ic_at_key_length': ic,
        'key': key,
        'plaintext': plaintext,
    }


if __name__ == '__main__':
    plaintext_source = (
        "The Beaufort cipher looks almost identical to Vigenere at first "
        "glance but its reciprocal formula changes nothing about how an "
        "analyst attacks it since repeated sequences in the ciphertext "
        "still betray the key length through Kasiski examination and the "
        "index of coincidence still separates the correct length from the "
        "wrong ones just as reliably once the columns are split apart the "
        "chi squared statistic finds each key letter exactly the way it "
        "would for any other polyalphabetic cipher built from simple "
        "modular shifts"
    )
    key = 'SHIELD'

    clean_plain = clean(plaintext_source)
    ciphertext = beaufort_transform(clean_plain, key)

    print('Ciphertext:', ciphertext)
    print()

    result = break_beaufort(ciphertext)
    print('Kasiski top factors (length -> vote count):', result['kasiski_top_factors'])
    print(f"Key length found: {result['key_length']} (IC = {result['ic_at_key_length']:.4f})")
    print('Recovered key:', result['key'])
    print()
    print('Recovered plaintext:')
    print(result['plaintext'])

Running this against the passage from the worked example above produces:

Kasiski top factors (length -> vote count): [(2, 24), (3, 24), (6, 24), (4, 13), (12, 13)]
Key length found: 6 (IC = 0.0654)
Recovered key: SHIELD

This matches the visualizer’s own result exactly, letter for letter.

For Fun: The Same Thing in ~20 Lines

In the same spirit as the Caesar and Vigenère breakers’ compressed versions, here’s the entire three-stage pipeline squeezed into about 20 lines. This isn’t something to learn the algorithm from; that’s what the version above is for. It’s verified to produce byte-for-byte identical output to the readable version above.

import re; from collections import Counter
F=[8.167,1.492,2.782,4.253,12.702,2.228,2.015,6.094,6.966,0.153,0.772,4.025,2.406,6.749,7.507,1.929,0.095,5.987,6.327,9.056,2.758,0.978,2.36,0.15,1.974,0.074]; R=0.0385; E=0.0667
C=lambda t: re.sub(r'[^A-Z]','',t.upper())
I=lambda t: sum(c*(c-1) for c in Counter(t).values())/(len(t)*(len(t)-1)) if len(t)>1 else 0
enc=lambda t,k: ''.join(chr((ord(k[i%len(k)])-65-(ord(x)-65))%26+65) for i,x in enumerate(t))
def b(c):
    p={}
    for i in range(len(c)-2): p.setdefault(c[i:i+3],[]).append(i)
    freq=Counter(x for o in p.values() if len(o)>1 for i in range(1,len(o)) for x in range(2,min(o[i]-o[0],20)+1) if (o[i]-o[0])%x==0)
    L=1; ic=0
    for l in range(1,21):
        cols=[c[j::l] for j in range(l)]; ic=sum(I(col) for col in cols)/l
        if l>1 and ic>=R+(E-R)*0.55: L=l; break
    else: L=max(range(1,21), key=lambda l: sum(I(c[j::l]) for j in range(l))/l); ic=sum(I(c[j::L]) for j in range(L))/L
    cols=[c[j::L] for j in range(L)]
    key=''.join(chr(min(range(26), key=lambda k: sum((Counter([chr((k-(ord(x)-65))%26+65) for x in col]).get(chr(65+i),0)-F[i]/100*len(col))**2/(F[i]/100*len(col)) for i in range(26))) + 65) for col in cols)
    plain=''.join(chr((ord(key[i%len(key)])-65-(ord(x)-65))%26+65) for i,x in enumerate(c))
    return {'key':key,'plain':plain,'len':L,'ic':ic,'kasiski':sorted(freq.items(), key=lambda x:x[1], reverse=True)[:5]}

if __name__ == '__main__':
    plaintext = "The Beaufort cipher looks almost identical to Vigenere at first glance but its reciprocal formula changes nothing about how an analyst attacks it since repeated sequences in the ciphertext still betray the key length through Kasiski examination and the index of coincidence still separates the correct length from the wrong ones just as reliably once the columns are split apart the chi squared statistic finds each key letter exactly the way it would for any other polyalphabetic cipher built from simple modular shifts"
    clean_plain = C(plaintext)
    ciphertext = enc(clean_plain, 'SHIELD')
    result = b(ciphertext)
    print(f"Ciphertext: {ciphertext}\n")
    print(f"Key: {result['key']} (length {result['len']}, IC {result['ic']:.4f})")
    print(f"Kasiski top factors: {result['kasiski']}")
    print(f"Recovered plaintext: {result['plain']}")

Running this prints the ciphertext, the recovered key SHIELD, the same Kasiski votes and IC value as the full version, and confirms the recovered plaintext matches the source exactly. Only one line of actual logic changed from the Vigenère version’s compressed form: the key line now searches K − C instead of C − K inside its chi-squared minimization. The plain line mirrors the same flip. Everything else, the Kasiski scan and the IC threshold search, is untouched. That’s exactly the point this whole article has been making. The enc lambda is new here too, added only so this snippet is self-contained and runnable on its own. It reuses the exact same reciprocal formula as the plain line inside b(), since Beaufort encrypts and decrypts with the same operation.

Interactive Visualizer

Try it above: paste any Beaufort-encrypted text (or use the default), and watch all three steps run: the repeated sequences it finds, the Index of Coincidence bars for each candidate length, and each key letter locking into place as chi-squared analysis solves it.

Beaufort vs. Vigenère: What Actually Changes for an Attacker

Vigenère Beaufort
Encryption formula C = (P + K) mod 26 C = (K − P) mod 26
Decryption formula P = (C − K) mod 26 P = (K − C) mod 26 (same as encryption)
Kasiski examination Unchanged Unchanged
Index of Coincidence Unchanged Unchanged
Chi-squared step Try 26 shifts of C − K Try 26 shifts of K − C
Self-reciprocal? No, needs a separate decryption formula Yes, the same formula both encrypts and decrypts
Overall attack difficulty Same Same

The reciprocal formula is a genuine, interesting design choice. It’s the same idea later reused, in a much more elaborate form, by the Enigma machine, where reciprocity was a practical necessity for a mechanical device used in the field. But as far as cryptanalysis is concerned, it’s cosmetic. Every structural weakness that makes Vigenère breakable survives the flip from addition to subtraction completely intact. That includes the repeating key and the fact that each position is a simple modular shift.

Limitations of This Attack

This attack shares Vigenère’s limitations almost exactly, since the first two stages are literally the same code:

  • Short ciphertexts starve Kasiski and blur the IC. Repeated trigraphs need enough text to actually recur, and the Index of Coincidence needs enough letters per column to be statistically meaningful. A short message can leave Kasiski with too few distances to vote confidently, and push the IC curve for every candidate length closer together.
  • The key length is assumed to be moderate. This breaker searches lengths 1 through 20, matching the Vigenère breaker’s bound. A much longer repeating key, or a true one-time pad where the key never repeats at all, falls outside what Kasiski and the IC can detect in the first place.
  • The chi-squared step assumes English. ENGLISH_FREQ encodes English letter frequencies specifically. Beaufort-encrypted text in another language would need that table swapped for the target language’s own letter distribution; the Kasiski and IC stages, which don’t reference any specific language, would still work unchanged.

FAQ

What’s the actual difference between Vigenère and Beaufort?

Vigenère encrypts with C = (P + K) mod 26. Beaufort encrypts with C = (K − P) mod 26. Beaufort’s formula happens to be reciprocal: applying it twice with the same key returns the original text. So the same operation both encrypts and decrypts. Vigenère, by contrast, needs a separate subtraction step to decrypt.

Does Beaufort’s reciprocal design make it harder to break?

No. Kasiski examination and the Index of Coincidence, which recover the key length, don’t reference the encryption formula at all. They only look at statistical patterns in the ciphertext. Only the final chi-squared step needs a one-line adjustment (K − C instead of C − K) to account for the flipped formula.

Why does the Index of Coincidence work the same way for both ciphers?

The IC measures how “clumped” a text’s letter distribution is: a property of the ciphertext itself, not of how it was produced. A correctly-split column is a plain Caesar-shifted stream either way. That means it carries English’s natural letter-frequency fingerprint regardless of whether that shift was applied by addition or subtraction.

Is Beaufort used anywhere real Enigma-adjacent?

Historically, yes. Beaufort’s reciprocal property made it attractive for manual field cryptography for the same reason reciprocity mattered to Enigma’s design: an operator needed only one procedure, not two, to both encrypt and decrypt. It offers no modern security either way.

Could this same three-stage approach break other polyalphabetic ciphers?

Yes, with the right final-step adjustment. Any cipher built from a repeating key and a simple per-letter modular operation (addition, subtraction, or otherwise) is vulnerable to the same Kasiski-plus-IC key-length recovery. Only the chi-squared decode formula needs to match the cipher’s actual arithmetic.

References

  1. Wikipedia. “Beaufort cipher.” Available at: https://en.wikipedia.org/wiki/Beaufort_cipher

  2. Practical Cryptography. “Beaufort Cipher.” Available at: http://practicalcryptography.com/ciphers/beaufort-cipher/

  3. Singh, Simon. “The Code Book.” Doubleday, 1999.

  4. Kasiski, Friedrich. “Die Geheimschriften und die Dechiffrir-Kunst.” 1863.