Breaking the Nihilist Cipher
The Nihilist cipher turns letters into Polybius numbers and adds a repeating keyword, which makes it a Vigenere cipher wearing a numeric disguise. Index of coincidence finds the period, and column-by-column statistics peel off the key. This guide runs the real attack and recovers a 514-letter message.
Pashalis Laoutaris 13 min read
Try it
Interactive Nihilist Cipher Breaker
🔐 Nihilist Cipher Breaker
Breaking the Nihilist Cipher
Introduction
The Nihilist cipher was used by Russian revolutionaries in the 1880s to pass messages past the Tsar’s secret police. It looks unusual, because the ciphertext is a stream of numbers rather than letters. Each plaintext letter becomes a pair of Polybius-square coordinates, and then the digits of a repeating keyword are added on top.
That numeric coat hides a familiar skeleton. Adding a repeating key to each symbol is exactly what the Vigenere cipher does, just with letters. So the Nihilist cipher falls to the same two-step attack: find the period with the index of coincidence, then solve each column on its own. This guide runs that attack against a real Nihilist message and recovers both the keyword and the plaintext from the numbers alone.
Table of Contents
- How the Nihilist Cipher Works
- It Is Vigenere in Disguise
- Finding the Period
- Solving Each Column
- Interactive Visualizer
- A Worked Example
- Python Implementation
- For Fun: The Core Attack in a Few Lines
- Limitations
- Security Status
- FAQ
- References
How the Nihilist Cipher Works
Start with a Polybius square, a 5x5 grid of 25 letters with I and J sharing a cell. Each letter has a row and a column from 1 to 5, which join into a two-digit number. In the standard square, A is 11, E is 15, F is 21, and Z is 55.
Encryption adds a keyword. Write the plaintext as its numbers, write the keyword’s numbers underneath, repeating the keyword to match the length, and add the two streams position by position:
plaintext : A(11) T(44) T(44) A(11) C(13) K(25) ...
key RUSSIA: R(42) U(45) S(43) S(43) I(24) A(11) ...
ciphertext: 53 89 87 54 37 36 ...
The sums run from 22, which is 11 + 11, up to 110, which is 55 + 55. The receiver subtracts the same repeating key numbers and reads the Polybius pairs back into letters.
It Is Vigenere in Disguise
The weakness is the repetition. The keyword has some length, call it the period, and after that many letters the same key number comes around again. So the first, seventh, thirteenth plaintext numbers, and so on, are all encrypted by adding the same constant. Each of those positions forms a column, and within a column the cipher is just the plaintext numbers shifted by one fixed amount.
This is the Vigenere structure exactly. A Vigenere cipher splits into columns that are each a Caesar shift, and the Nihilist cipher splits into columns that are each a constant addition. The only difference is that the symbols are numbers from a 25-letter square instead of the 26 letters of the alphabet. Everything that breaks Vigenere breaks the Nihilist cipher too.
Finding the Period
The index of coincidence measures how often two randomly chosen symbols in a text match. For English it sits near 0.067, far above the 0.04 or so you get from random symbols over a 25-letter set. Adding a constant to a whole column does not change how often its symbols repeat, so a column of correct period keeps English’s high index of coincidence.
So the attack tries each possible period. For each one it splits the ciphertext into that many columns and averages their index of coincidence. The wrong periods mix unrelated positions and wash the value down toward random. The right period, and its multiples, stand out with a value near the English figure. The smallest period that spikes is the keyword length, and it is usually obvious:
period 1: 0.029 period 4: 0.035 period 7: 0.028
period 2: 0.035 period 5: 0.028 period 8: 0.035
period 3: 0.046 period 6: 0.070 period 9: 0.047
Solving Each Column
Once the period is known, each column hides a single constant key number. Recovering it uses a fact that makes the Nihilist cipher especially fragile: subtracting the correct key number from every ciphertext value in a column must leave a valid Polybius number, one whose two digits are both between 1 and 5. A wrong key number produces digits like 0, 6, 7, 8 or 9, which cannot come from the square.
That constraint alone rules out almost every wrong guess. Among the few key numbers that survive, the right one is the one whose recovered plaintext numbers match English letter frequencies best, scored with a chi-squared test. Do this for each column, read the recovered key numbers back through the square into letters, and the keyword appears. Subtract it across the whole message and the plaintext falls out.
Interactive Visualizer
The visualizer above runs the real attack on real Nihilist ciphertext. It shows the index of coincidence for each candidate period, with the winning period highlighted, then recovers the key number for every column and decrypts the message. Every value is computed by the genuine cipher and the genuine statistics, with nothing faked, and the keyword is never supplied.
Edit the message or the keyword and run it again. As long as there is enough text, a few hundred letters, the period spikes cleanly and the columns give up their key numbers. Shorter messages make the statistics noisier, which is the honest limit of the method.
A Worked Example
The hidden keyword is RUSSIA, and the plaintext is a 514-letter passage about the cipher. The attacker sees only the stream of numbers:
ciphertext: 86 68 58 76 48 34 66 76 67 86 68 24 66 80 66 58 66 63 53 88 88 86 39 25 ...
The index of coincidence spikes at period 6, so the keyword has six letters. Solving the six columns recovers the key and the message in full:
recovered period : 6
recovered key : RUSSIA (numbers 42 45 43 43 24 11)
letters correct : 514/514
decryption : THENIHILISTCIPHERWASUSEDBYRUSSIANREVOLUTIONARIES...
Every letter comes back, and the keyword RUSSIA is recovered exactly, not just an equivalent. The numbers that looked like a wall of noise were a Vigenere cipher the whole time.
Python Implementation
The code is self-contained and runnable. It builds the Nihilist cipher on the standard square, finds the period with the index of coincidence, and recovers each column’s key number using the validity constraint and a chi-squared fit.
# nihilist_breaker.py
#
# Ciphertext-only attack on the Nihilist cipher (standard Polybius square). Each letter becomes a two-digit
# Polybius number (A=11..Z=55), then the numbers of a repeating keyword are ADDED on. That is a Vigenere
# cipher on numbers: the key repeats with a period, so the ciphertext splits into columns that each add one
# constant. The index of coincidence finds the period; then in each column the correct key number is the one
# that leaves every plaintext number valid (both digits 1..5) and best matches English letter frequencies.
ALPHA = "ABCDEFGHIKLMNOPQRSTUVWXYZ" # J folded into I
NUM = {c: (i // 5 + 1) * 10 + (i % 5 + 1) for i, c in enumerate(ALPHA)}
INV = {v: k for k, v in NUM.items()}
VALID = set(NUM.values())
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": 7.119, "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}
EXP = {NUM[c]: FREQ[c] / 100 for c in ALPHA}
def clean(text):
return "".join("I" if c == "J" else c for c in text.upper() if c.isalpha())
def encrypt(plaintext, keyword):
pt, k = clean(plaintext), [NUM[c] for c in clean(keyword)]
return [NUM[c] + k[i % len(k)] for i, c in enumerate(pt)]
def ic(column):
n = len(column)
if n < 2:
return 0.0
counts = {}
for x in column:
counts[x] = counts.get(x, 0) + 1
return sum(c * (c - 1) for c in counts.values()) / (n * (n - 1))
def find_period(cipher, max_len=15):
scores = [(L, sum(ic(cipher[i::L]) for i in range(L)) / L) for L in range(1, max_len + 1)]
best = max(s for _, s in scores)
for L, s in scores:
if s >= 0.85 * best and s > 0.055: # smallest period that spikes
return L, scores
return max(scores, key=lambda t: t[1])[0], scores
def recover_column_key(col):
best_k, best_score = None, 1e18
for k in VALID:
plains = [c - k for c in col]
invalid = sum(1 for p in plains if p not in VALID) # correct key keeps all valid
counts = {}
for p in plains:
if p in VALID:
counts[p] = counts.get(p, 0) + 1
n = max(1, len(plains) - invalid)
chi = sum((counts.get(num, 0) - EXP[num] * n) ** 2 / (EXP[num] * n) for num in VALID)
score = invalid * 1000 + chi
if score < best_score:
best_score, best_k = score, k
return best_k
def break_nihilist(cipher, period=None):
if period is None:
period, _ = find_period(cipher)
key_nums = [recover_column_key(cipher[i::period]) for i in range(period)]
keyword = "".join(INV[k] for k in key_nums)
plain = "".join(INV[cipher[i] - key_nums[i % period]] for i in range(len(cipher)))
return period, keyword, key_nums, plain
if __name__ == "__main__":
assert NUM["A"] == 11 and NUM["Z"] == 55
assert encrypt("ATTACKATDAWN", "RUSSIA") == [53, 89, 87, 54, 37, 36, 53, 89, 57, 54, 76, 44]
print("Nihilist self-test: PASS")
pt = clean(
"the nihilist cipher was used by russian revolutionaries in the eighteen eighties to "
"pass secret messages past the tsarist secret police it writes each letter as a pair of "
"polybius square coordinates and then adds the digits of a repeating keyword so it behaves "
"like a vigenere cipher working on numbers instead of letters because the key repeats with a "
"fixed period the ciphertext splits into columns that each hide a single constant and the "
"statistics of the language leak straight through the addition which is exactly what lets a "
"modern analyst peel the keyword and the plaintext back out of the numbers alone today")
cipher = encrypt(pt, "RUSSIA")
period, keyword, knums, dec = break_nihilist(cipher)
print("recovered period :", period)
print("recovered key :", keyword, knums)
print("letters correct :", sum(a == b for a, b in zip(dec, pt)), "/", len(pt))
print("decryption :", dec[:48])
print("MATCH:", dec == pt)
Running it prints the worked example:
Nihilist self-test: PASS
recovered period : 6
recovered key : RUSSIA [42, 45, 43, 43, 24, 11]
letters correct : 514 / 514
decryption : THENIHILISTCIPHERWASUSEDBYRUSSIANREVOLUTIONARIES
MATCH: True
For Fun: The Core Attack in a Few Lines
With the period in hand, recovering a column is tiny, because the validity rule does nearly all the work. Among key numbers, keep the ones that leave every plaintext number inside the square, then pick the most English:
def column_key(col):
ok = [k for k in VALID if all((c - k) in VALID for c in col)] # key must keep all valid
score = lambda k: sum((sum((c - k) == num for c in col) - EXP[num]*len(col))**2 for num in VALID)
return min(ok, key=score)
On a healthy column only one key number usually survives the validity filter, so the chi-squared step is just a tie-breaker. The full listing keeps a softer version that still works when a short column has no perfectly clean key.
Limitations
The attack is a real ciphertext-only break, but it rests on a few assumptions:
- It assumes the standard square. This recovers the keyword while taking the Polybius square to be the plain
AtoZlayout. If the square itself is keyed and unknown, the same period and column work still strips the additive key, but the result is then a monoalphabetic substitution over the numbers, which needs a second solving step to recover the square. - It needs enough text. The index of coincidence and the column frequencies are statistical. A few hundred letters is comfortable; a very short message can leave the period ambiguous or a column underdetermined.
- Very short keywords are almost trivial, very long ones are harder. A long keyword relative to the message gives each column few letters, which weakens the statistics, exactly as with Vigenere.
- The validity rule is specific to this scheme. The trick that a correct key leaves every number inside the 5-by-5 square is what makes column recovery so clean. It is a property of the Polybius-plus-addition design, not a general attack.
Security Status
- The Nihilist cipher is a classical cipher with no modern security. It was reasonable against hand analysis in the 1880s, but the index of coincidence, published by William Friedman in 1922, turns it into a quick exercise. It is a puzzle today, not protection.
- Fractionating into numbers does not help. Writing letters as Polybius pairs and adding a key feels like it scrambles the statistics, but the per-column language structure survives the addition untouched. Hiding letters as numbers changes the costume, not the cryptography.
- The lesson is about key repetition. Any cipher that reuses a short key across a long message splits into columns an analyst can attack one at a time. The fix the twentieth century reached for was the opposite extreme, a key as long as the message and never repeated, which is the one-time pad.
FAQ
Why is the ciphertext made of numbers?
Each letter is written as its Polybius-square coordinates, a row and a column from 1 to 5, which read as a two-digit number. The keyword’s numbers are then added. The result is a stream of sums between 22 and 110, which is why the Nihilist cipher looks numeric rather than alphabetic.
How does the index of coincidence find the period?
It measures how often symbols repeat. English repeats far more than random text, and adding a constant to a column does not change its repetitions. So the correct period, where each column is a shifted slice of English, keeps a high index of coincidence, while wrong periods average down toward random.
Why does subtracting the wrong key give invalid numbers?
Every real plaintext number has both digits between 1 and 5, because the square is 5 by 5. Subtracting the wrong constant pushes some results outside that range, producing a digit like 0 or 7 that no square cell can make. That impossibility flags the guess as wrong almost immediately.
Does this recover the keyword or just the plaintext?
Both. Each column’s key number is a Polybius pair, so reading the recovered numbers back through the square spells the keyword directly. The worked example returns RUSSIA, the exact keyword, along with the full plaintext.
What if the Polybius square is keyed?
Then the period finding and column work still remove the additive keyword, but what remains is a substitution cipher on the numbers, since you no longer know which number is which letter. Solving that substitution, with frequency analysis or a quadgram hill climb, recovers the square and finishes the break.
References
-
Friedman, William F. “The Index of Coincidence and Its Applications in Cryptography,” Riverbank Publication No. 22, 1922.
-
Kahn, David. “The Codebreakers,” 1967 (history of the Nihilist cipher and Russian revolutionary cryptography).
-
The Nihilist cipher guide on this site, whose cipher this breaker attacks: /blog/nihilist-cipher/
-
The Vigenere cipher guide on this site, whose structure the Nihilist cipher shares: /blog/vigenere-cipher/