The Vigenère Cipher Breaking Guide
For three centuries the Vigenère cipher was called 'le chiffre indéchiffrable.' Learn the two statistical techniques, Kasiski examination and the Index of Coincidence, that finally broke it, and watch them recover a real key letter by letter.
Interactive Vigenère Cipher Breaker
🔐 Vigenère Cipher Breaker
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.
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 of those interleaved streams was shifted by one fixed amount: a Caesar cipher in disguise. Solve each one with chi-squared frequency analysis, same as breaking a Caesar cipher, one key letter at a time.
The Vigenère Cipher Breaking Guide
Introduction
The Vigenère cipher resisted the Caesar cipher’s fatal weakness (a single, guessable shift) by using a whole keyword’s worth of shifts instead, cycling through them as it went. For roughly three hundred years, that was enough. No systematic method existed to crack it, and it earned the nickname “le chiffre indéchiffrable”: the indecipherable cipher.
Breaking it doesn’t take brute force, the way the Caesar Cipher Breaker tries all 25 shifts. Vigenère’s key space is astronomically larger: a 10-letter keyword alone has over 141 trillion possibilities. Instead, breaking it takes two statistical insights, discovered independently by Charles Babbage and Friedrich Kasiski in the 19th century: repeated patterns in the ciphertext leak the length of the key. Once you know the length, each position collapses back into a plain Caesar cipher you already know how to solve.
Table of Contents
- Why Brute Force Doesn’t Work Here
- Step 1: Kasiski Examination
- Step 2: The Index of Coincidence
- Step 3: Chi-Squared Frequency Analysis
- A Worked Example
- Python Implementation
- Vigenère vs. Beaufort vs. Autokey
- Why This Needs a Reasonable Amount of Ciphertext
- FAQ
- References
Why Brute Force Doesn’t Work Here
The Caesar cipher has exactly 25 possible keys, so trying all of them and picking the one that produces readable English is trivial. Vigenère’s key is a whole word, repeated to cover the message: a 5-letter key already has 26⁵ (roughly 11.9 million) possibilities, and real keywords are often longer. Brute-forcing the key itself is a dead end.
The actual weakness is more subtle: because the key repeats, the same key letter eventually lines up with the same plaintext letter again, somewhere later in the message. That produces the exact same ciphertext letters, some multiple of the key length apart. That repetition is the crack in the armor.
Step 1: Kasiski Examination
Friedrich Kasiski formalized this in 1863 (Charles Babbage had privately worked out the same idea a decade earlier, but never published it). The method:
- Scan the ciphertext for repeated sequences of three or more letters.
- Record the distance between each repeat’s occurrences.
- Factor those distances. If a sequence repeats because the key cycled back to the same alignment, the distance between the repeats must be a multiple of the key length. So the key length is almost always one of the common factors shared across many of these distances.
A single repeated sequence could be coincidence (two unrelated stretches of plaintext just happening to produce the same three ciphertext letters). But when dozens of repeats all share the same handful of factors, that’s a strong signal: the true key length is almost always the most frequently occurring factor.
Step 2: The Index of Coincidence
Kasiski examination narrows things down to a shortlist of candidate lengths, but it can be noisy: short texts don’t produce many repeats. A length’s multiples (10 when the real answer is 5, for instance) tend to show up as false positives too. The Index of Coincidence (IC), introduced by William F. Friedman in the 1920s, confirms which candidate is actually correct.
The IC measures how likely two randomly chosen letters from a text are to be identical. English text has an IC of about 6.67%, because letters like E and T show up far more often than Q and Z. Matching pairs are more likely than in a uniform distribution. Purely random letters (or ciphertext scrambled by many different shifts at once) have an IC closer to 3.85%, since every letter is equally likely.
To test a candidate key length N: split the ciphertext into N interleaved streams (every Nth letter forms one stream). If N is the real key length, each individual stream was shifted by only one fixed amount (a plain Caesar cipher). So it still carries English’s natural letter-frequency fingerprint, and its IC lands close to 6.67%. If N is wrong, the streams mix multiple different shifts together, flattening the distribution toward the random baseline.
Step 3: Chi-Squared Frequency Analysis
Once the key length is known, each of the N interleaved streams is genuinely just a Caesar cipher, meaning each one can be solved exactly the way the Caesar Cipher Breaker works, one position at a time.
This visualizer uses chi-squared frequency analysis rather than the Caesar breaker’s “count common words” trick, because a single stream is a scattered subsequence of letters, not readable text. There are no words to search for. Instead, for each of the 26 possible shifts, it decodes the stream and compares the resulting letter distribution against standard English letter frequencies using the chi-squared statistic:
χ² = Σ (observed − expected)² / expected
The shift that produces a letter distribution closest to real English (lowest χ²) is almost always the correct one for that position. Do this once per position, and the recovered shifts spell out the key itself.
Interactive Visualizer
Try it above: paste any Vigenère-encrypted text (or use the default). 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.
A Worked Example
Using the visualizer’s default ciphertext (268 letters, encrypted with a 5-letter key):
- Kasiski examination finds several repeated 3-letter sequences:
"ELQ"recurs 3 times (25 and 40 letters apart from its first appearance), and"GTG"reappears 60 letters later elsewhere in the message. Every one of these distances (25, 40, 60) is a multiple of 5. - Factoring those distances, the length 5 collects far more votes than any other candidate.
- The Index of Coincidence confirms it: splitting the ciphertext into 5 interleaved streams gives an average IC close to 6.6%, while shorter lengths sit near the random baseline of 3.85%.
- Chi-squared analysis solves each of the 5 streams independently, recovering the letters L-E-M-O-N one at a time.
- Decrypting the full ciphertext with key LEMON reveals a coherent English passage, the same keyword used in the Vigenère Cipher Guide’s own worked example, this time recovered rather than assumed.
Python Implementation
The interactive visualizer above runs this exact pipeline in JavaScript. Here’s the same three-stage attack in Python: no shortcuts, no dictionary of common words, just Kasiski examination, the Index of Coincidence, and chi-squared frequency analysis, the same as a real cryptanalyst would use.
Unlike the Caesar Cipher Breaker’s progression down to a 5-line one-liner, this one stays as a single, fully worked implementation. Vigenère’s break is a genuine three-stage statistical pipeline, and compressing that into a golfed one-liner would trade away the very steps that make it work, not just the whitespace.
Key Features
- No word lists required: Steps 1 and 2 work purely from letter statistics: no dictionary, no scoring against common words, because the intermediate columns from Step 2 aren’t readable text at all.
- Same key-length heuristic as the visualizer: rather than trusting whichever candidate length scores closest to English’s Index of Coincidence, it scans from the smallest length upward and takes the first one that clearly clears the random-vs-English threshold. This is verified to avoid the multiple-of-the-true-length trap that a naive “pick the best score” approach falls into on shorter texts.
- Genuine chi-squared scoring: each column is solved by comparing its decoded letter distribution against real English letter frequencies for all 26 possible shifts, not a heuristic approximation.
Code
# vigenere_breaker.py
#
# Breaks a Vigenere cipher using the same three real cryptanalysis
# techniques as the interactive visualizer: Kasiski examination,
# the Index of Coincidence, and chi-squared frequency analysis.
# No dictionary, no brute-forcing the key itself -- the key space
# is far too large for that (a 10-letter key alone has over 141
# trillion possibilities).
import re
from itertools import cycle
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 vigenere_decrypt(ciphertext, key):
plaintext = []
for i, c in enumerate(ciphertext):
shift = ord(key[i % len(key)]) - ord('A')
plaintext.append(chr((ord(c) - ord('A') - shift) % 26 + ord('A')))
return ''.join(plaintext)
# --- Step 1: Kasiski examination ---
# Repeated sequences in the ciphertext usually mean the key cycled back to
# the same alignment -- so the distance between repeats is almost always a
# multiple of the key length.
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 ---
# Splits the ciphertext into `key_len` interleaved streams. If key_len is
# correct, each stream is a plain Caesar cipher and keeps English's natural
# letter-frequency "clumpiness" (IC close to 6.67%). If it's wrong, the
# streams mix multiple shifts together and flatten toward the random
# baseline (IC close to 3.85%).
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):
# A wrong length that's a multiple of the true one still scores well on
# IC, so picking the single closest-to-English score is unreliable on
# shorter texts. Instead: scan from the smallest length upward and take
# the first one that clearly beats the random baseline. 0.55 is a
# tunable constant, not a derived value: it just needs to sit between
# RANDOM_IC and ENGLISH_IC, closer to the English end, so a genuine key
# length clears it while a too-short, undershooting length doesn't.
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 ---
# Once the key length is known, each column is just a Caesar cipher in
# disguise. Try all 26 shifts 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_shift_for_column(column):
best_shift, best_chi2 = 0, float('inf')
for shift in range(26):
decoded = [chr((ord(c) - ord('A') - shift) % 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_shift = chi2, shift
return best_shift
def break_vigenere(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_shift_for_column(c) + ord('A')) for c in columns)
plaintext = vigenere_decrypt(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 quick brown fox jumps over the lazy dog while the "
"cryptographer sits by the window studying an old cipher "
"that once protected royal secrets for centuries before "
"mathematicians finally found the pattern hidden inside "
"its repeating alphabetic shifts and broke the code that "
"had fooled generations of curious readers"
)
key = 'LEMON'
clean_plain = clean(plaintext_source)
ciphertext = ''.join(
chr((ord(p) - ord('A') + ord(k) - ord('A')) % 26 + ord('A'))
for p, k in zip(clean_plain, cycle(key))
)
print('Ciphertext:', ciphertext)
print()
result = break_vigenere(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 same LEMON-encrypted passage from the worked example above produces:
Kasiski top factors (length -> vote count): [(5, 7), (2, 2), (4, 2), (10, 2), (20, 2)]
Key length found: 5 (IC = 0.0579)
Recovered key: LEMON
This matches the visualizer’s own result exactly, letter for letter.
For Fun: The Same Thing in ~20 Lines
The Caesar Cipher Breaker post compresses its whole attack down to a 5-line one-liner, purely to show that it’s possible. In that same spirit (not as something to actually learn the algorithm from, that’s what the version above is for), here’s the entire three-stage pipeline (Kasiski examination, Index of Coincidence, chi-squared frequency analysis) compressed into about 20 lines. It’s verified to produce byte-for-byte identical output to the readable version above: same ciphertext, same Kasiski votes, same recovered key, same plaintext.
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(x)-65+ord(k[i%len(k)])-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 s: sum((Counter([chr((ord(x)-65-s)%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(x)-65-ord(key[i%len(key)])+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 quick brown fox jumps over the lazy dog while the "
"cryptographer sits by the window studying an old cipher "
"that once protected royal secrets for centuries before "
"mathematicians finally found the pattern hidden inside "
"its repeating alphabetic shifts and broke the code that "
"had fooled generations of curious readers"
)
clean_plain = C(plaintext)
ciphertext = enc(clean_plain, 'LEMON')
r = b(ciphertext)
print('Ciphertext:', ciphertext)
print(f"Key: {r['key']} (length {r['len']}, IC {r['ic']:.4f})")
print('Kasiski top factors:', r['kasiski'])
print('Recovered plaintext:', r['plain'])
Running this prints the same ciphertext, key, and Kasiski votes as the full version above, and confirms the recovered plaintext matches the source exactly. One line in there (the one building key) is doing the entire chi-squared column-solving step from Step 3 in a single expression: 26 candidate shifts, decode, count, compare against English frequencies, pick the minimum. It’s a genuine testament to how expressive Python’s comprehensions are, and a pretty good demonstration of exactly why the version above uses actual variable names. The enc lambda is new here too, added only so this snippet is self-contained and runnable on its own.
Vigenère vs. Beaufort vs. Autokey
This breaker’s three-stage pipeline isn’t unique to Vigenère. Two other repeating-key ciphers on this site share most of it directly:
| Vigenère (this breaker) | Beaufort | Autokey | |
|---|---|---|---|
| Key structure | Short keyword, repeats indefinitely | Short keyword, repeats indefinitely | Short priming key, then the key is the plaintext |
| Kasiski + IC find the key length? | Yes | Yes, identical code | No: the key never repeats, so there’s no periodicity to detect |
| What changes in the final step | P = (C − K) mod 26 |
P = (K − C) mod 26, the one line that differs from Vigenère |
N/A: Autokey needs a completely different attack (bounded brute force over the priming key, scored with digram statistics) |
Beaufort’s breaker is, quite literally, this one with a single decode formula swapped out; everything upstream of that (Kasiski examination and the Index of Coincidence) is copied over unchanged. Autokey looks similar on the surface, another repeating-ish key over a Vigenère-style shift, but its key stream never actually cycles, which is exactly what defeats both of this breaker’s key-length-finding techniques and forces a completely different attack strategy.
Why This Needs a Reasonable Amount of Ciphertext
Every step above is a statistical technique: it relies on patterns that only become visible across enough text. Kasiski examination needs enough length for repeated sequences to actually occur; the Index of Coincidence needs enough letters per stream for the frequency distribution to resemble English rather than a noisy small sample; chi-squared analysis needs the same.
In practice, texts shorter than roughly 100 letters (after removing spaces and punctuation) start producing unreliable results, not because the technique is wrong, but because there simply isn’t enough signal yet. Very short ciphertexts can also produce ambiguous Kasiski factors (too few repeats to vote confidently) or an Index of Coincidence that doesn’t clearly separate the true key length from its multiples. This is a genuine, well-documented limitation of classical cryptanalysis, not a simplification made for this visualizer; real cryptanalysts run into exactly the same wall with short intercepted messages.
Finally, the chi-squared step assumes English letter frequencies specifically. ENGLISH_FREQ would need to be swapped for the target language’s own letter distribution to break Vigenère-encrypted text in another language; Kasiski examination and the Index of Coincidence, which don’t reference any particular language, would carry over unchanged.
FAQ
Why can’t you just brute-force every possible Vigenère key?
A Caesar cipher has only 25 keys, small enough to try all of them. A Vigenère key is a word: even a modest 8-letter keyword has over 200 billion possibilities. Brute force is computationally hopeless; the actual break comes from the key’s repetition leaking its length, not from guessing the key directly.
What’s the difference between Kasiski examination and the Index of Coincidence?
Kasiski examination looks for literal repeated sequences in the ciphertext and factors the distances between them. It’s a direct, if noisy, signal. The Index of Coincidence is a statistical measurement of letter-frequency “clumpiness” that doesn’t need any repeats to exist at all; it works even on ciphertext where Kasiski finds too few repeats to be conclusive. Real cryptanalysis typically uses both together, exactly as this visualizer does.
Why chi-squared instead of counting common words, like the Caesar breaker does?
The Caesar breaker decrypts the entire message with each candidate key and can check for recognizable English words. Here, each of the interleaved streams from Step 2 is just every Nth letter of the ciphertext: a scattered sequence with no words in it at all, only a letter-frequency fingerprint. Chi-squared analysis is the right tool for comparing a frequency distribution, not a word list.
Is the Vigenère cipher secure if the key is as long as the message?
No. That describes the One-Time Pad, a different (and, done correctly, information-theoretically unbreakable) construction. A repeating Vigenère key, no matter how long, is still vulnerable to this exact method. A longer key only means you need more ciphertext before the statistics become reliable.
Does this visualizer implement a shortcut version of real cryptanalysis?
No. Kasiski examination, the Index of Coincidence, and chi-squared frequency analysis here are the genuine, unmodified techniques used historically and in real cryptanalysis tools, not a simplified toy version. The only thing scaled for a browser demo is the amount of ciphertext, since realistic intercepted messages are often much longer.
References
- Kahn, David. The Codebreakers: The Story of Secret Writing. Macmillan, 1967. The standard historical account of Kasiski’s and Babbage’s independent discoveries.
- Friedman, William F. The Index of Coincidence and Its Applications in Cryptography. Riverbank Publication No. 22, 1922.
- Wikipedia. “Kasiski examination.” https://en.wikipedia.org/wiki/Kasiski_examination
- Wikipedia. “Index of coincidence.” https://en.wikipedia.org/wiki/Index_of_coincidence
- Practical Cryptography. “Vigenère Cipher Breaking.” http://practicalcryptography.com/cryptanalysis/stochastic-searching/cryptanalysis-vigenere-cipher/