Breaking the Two-Square Cipher with a Crib
The two-square cipher resists the ciphertext-only search that breaks Playfair and four-square, because it has no fixed reference squares. But a crib of known plaintext breaks it in seconds. Learn the known-plaintext attack, with runnable Python.
Pashalis Laoutaris 13 min read
Try it
Interactive Two-Square Cipher Breaker
🔐 Two-Square Cipher Breaker
Breaking the Two-Square Cipher with a Crib
Introduction
The two-square cipher sits between Playfair and the four-square cipher. It uses two keyed 5x5 squares, side by side, and crosses the coordinates of each pair of letters to encipher a message. You might expect the four-square breaker’s approach, a ciphertext-only hill climb scored by English quadgrams, to carry straight over. It does not carry over at all.
This guide shows why the ciphertext-only attack stalls on two-square, and then breaks the cipher a different way. The reason the search fails turns out to be a real structural feature of the cipher, not a tuning problem. Once that is clear, a modest crib of known plaintext breaks both squares in seconds, and the recovered key decrypts the rest of the message, the part the attacker never knew.
Table of Contents
- The Cipher, Briefly
- Why the Ciphertext-Only Attack Stalls
- The Known-Plaintext Attack
- Interactive Visualizer
- A Worked Example
- Python Implementation
- Limitations
- Security Status
- FAQ
- References
The Cipher, Briefly
The horizontal two-square cipher places two keyed squares side by side, each built from a keyword the way a Playfair square is. To encrypt a pair of plaintext letters, you find the first letter in the left square and the second in the right square. The two ciphertext letters are then read by crossing their columns: the first from the right square at the first letter’s row, the second from the left square at the second letter’s row. Decryption runs the same crossing in reverse.
The whole secret is the pair of squares. There are no plain, unkeyed squares anywhere, which is the one detail that matters most for breaking it.
Why the Ciphertext-Only Attack Stalls
The four-square breaker works because two of its four squares are fixed. The top-left and bottom-right grids always hold the plain alphabet in order, so they act as an anchor. Every candidate pair of keyed squares is measured against that fixed frame, and a square that is partly correct immediately pulls some of the plaintext into readable English. That gives the hill climb a gradient to follow.
The two-square cipher has no such anchor. Both squares are keyed, and the plaintext is read out of the squares themselves. A pair of squares that is entirely wrong, but wrong in a self-consistent way, can still produce a decryption that looks no worse than a pair that is partly right. The English-likeness score barely changes until both squares are almost completely correct at the same time, so there is no gradient to climb for almost the entire search. In practice a quadgram-scored annealer runs for hundreds of thousands of steps and never reaches the true key. I ran exactly that search, the four-square engine pointed at a two-square ciphertext, and across many restarts it never once converged, stalling far below the true key’s score every time.
This is not a defect in the search. It reflects a real property of the cipher: without a fixed reference, the key has a large equivalence class, and many different square-pairs decrypt the same ciphertext identically. Ciphertext alone cannot single out the right one.
The Known-Plaintext Attack
A crib changes the picture completely. Suppose the attacker knows, or can guess, a stretch of the plaintext: a standard greeting, a letterhead, a predictable opening. Aligned to the start of the message, that crib pins down exactly what the squares must do.
Now the search has a precise target. Instead of scoring how English-like a decryption is, score a candidate pair of squares by how many of the crib’s ciphertext letters it reproduces when it encrypts the known plaintext. The true key reproduces all of them. A pair that is partly right reproduces some, and nudging a letter toward its correct cell reproduces more. That is a sharp, honest gradient, exactly what the ciphertext-only attack lacked.
Hill-climbing on that score is fast and reliable. Swap two letters in one of the squares, keep the change if it reproduces at least as much of the crib, and restart from fresh random squares if a run stalls. Within a second or two, the squares reproduce the entire crib, and the same squares then decrypt the rest of the message. The recovered squares are usually not the literal keyword squares, because the cipher’s large equivalence class means several square-pairs are interchangeable, but they decrypt every digraph correctly.
Interactive Visualizer
The visualizer above runs the known-plaintext attack. Paste a ciphertext and the crib you know, or use the built-in example, and watch the two squares fill in as the climb reproduces more and more of the crib. When the crib is fully matched, the recovered squares decrypt the whole message, including the part beyond the crib.
The search is genuine and quick. A longer crib pins more cells, so the recovered key decrypts more of the message that lies past it. If the crib is too short to pin a cell that the rest of the message uses, that part may not come out, which is the honest limit of a known-plaintext attack.
A Worked Example
The example encrypts a 354-letter message under a left square from the keyword EXAMPLE and a right square from KEYWORD. The attacker knows the first 264 letters, a wordy opening that happens to contain every letter of the alphabet. The last 90 letters are a secret the attacker has never seen. The ciphertext starts like this:
PNKUZAQSVLNABIYNKLONOCOOQPMEYEEL...
Hill-climbing to reproduce the crib recovers a working pair of squares in about two seconds, reliably across runs. The recovered squares decrypt the entire message, and the secret tail falls out:
full message recovered: True
decrypted secret tail: THERENDEZVOUSISFIXEDFORDAWNATTHEOLDSTONEBRIDGEBRINGTHESEALEDPACKETANDTHEPASSWORDISIA...
The attacker knew the opening but not that last sentence. The crib was enough to recover the key, and the key read the rest. The recovered squares are not the original EXAMPLE and KEYWORD squares, because two-square’s equivalence class is large, but they decrypt every letter correctly.
Python Implementation
The attack is small. The encrypt function only exists to build the demo ciphertext; the attack itself is break_two_square, which takes the ciphertext and the crib and returns the two recovered squares.
# two_square_breaker.py
#
# Breaks the horizontal two-square cipher with a known-plaintext attack. Unlike the four-square cipher,
# whose two plain squares are a fixed standard alphabet, the two-square cipher has no fixed reference,
# so a ciphertext-only hill climb has no gradient to follow and does not converge. A crib changes
# everything: hill-climbing to reproduce a stretch of known plaintext recovers a working key in moments,
# and that key decrypts the rest of the message.
import random
import re
ALPHA = 'ABCDEFGHIKLMNOPQRSTUVWXYZ' # 25 letters, I and J share a cell
IDX = {c: i for i, c in enumerate(ALPHA)}
def generate_square(keyword):
keyword = re.sub(r'[^A-Z]', '', keyword.upper().replace('J', 'I'))
seen = []
for ch in keyword + ALPHA:
if ch not in seen:
seen.append(ch)
return seen
def inverse(square):
inv = [0] * 25
for pos, ch in enumerate(square):
inv[IDX[ch]] = pos
return inv
def prepare(text):
text = re.sub(r'[^A-Z]', '', text.upper().replace('J', 'I'))
if len(text) % 2:
text += 'X'
return text
# Horizontal two-square encryption, used to build the demo ciphertext. Find the first letter in the
# left square and the second in the right square; the ciphertext letters come from the right square at
# the first letter's row, and the left square at the second letter's row (crossing the columns).
def encrypt(text, left, right):
text = prepare(text)
out = []
for i in range(0, len(text), 2):
r1, c1 = divmod(left.index(text[i]), 5)
r2, c2 = divmod(right.index(text[i + 1]), 5)
out.append(right[r1 * 5 + c2])
out.append(left[r2 * 5 + c1])
return ''.join(out)
def split_ciphertext(ciphertext):
return ([IDX[ciphertext[i]] for i in range(0, len(ciphertext), 2)],
[IDX[ciphertext[i + 1]] for i in range(0, len(ciphertext), 2)])
# Decryption inverts the rule: the first ciphertext letter is found in the right square, the second in
# the left square, and the plaintext pair is read from the left and right squares at the crossed rows.
def decrypt(first, second, left, right, inv_left, inv_right):
out = []
for k in range(len(first)):
pr, pl = inv_right[first[k]], inv_left[second[k]]
out.append(left[(pr // 5) * 5 + pl % 5])
out.append(right[(pl // 5) * 5 + pr % 5])
return ''.join(out)
# How many of the known crib's ciphertext letters a candidate pair of squares reproduces. This is the
# objective the search maximizes; the true key reproduces all of them.
def crib_matches(crib_first, crib_second, crib_ct, left, right, inv_left, inv_right):
hits = 0
for k in range(len(crib_first)):
r1, c1 = divmod(inv_left[crib_first[k]], 5)
r2, c2 = divmod(inv_right[crib_second[k]], 5)
hits += (right[r1 * 5 + c2] == crib_ct[2 * k]) + (left[r2 * 5 + c1] == crib_ct[2 * k + 1])
return hits
def random_square():
sq = list(ALPHA)
random.shuffle(sq)
return sq
def swap(sq):
sq = sq[:]
a, b = random.sample(range(25), 2)
sq[a], sq[b] = sq[b], sq[a]
return sq
def climb(crib_first, crib_second, crib_ct, steps):
left, right = random_square(), random_square()
inv_left, inv_right = inverse(left), inverse(right)
best = crib_matches(crib_first, crib_second, crib_ct, left, right, inv_left, inv_right)
target = 2 * len(crib_first)
for _ in range(steps):
if random.random() < 0.5:
cand = swap(left); ci = inverse(cand)
m = crib_matches(crib_first, crib_second, crib_ct, cand, right, ci, inv_right)
if m >= best:
left, inv_left, best = cand, ci, m
else:
cand = swap(right); ci = inverse(cand)
m = crib_matches(crib_first, crib_second, crib_ct, left, cand, inv_left, ci)
if m >= best:
right, inv_right, best = cand, ci, m
if best == target:
break
return left, right, best
# A known-plaintext attack: the crib is plaintext the analyst already knows, aligned to the start of
# the message. Hill-climb until the squares reproduce the crib's ciphertext, restarting from fresh
# random squares if a run stalls, then use the recovered squares to decrypt the whole message.
def break_two_square(ciphertext, crib_plaintext, restarts=10, steps=80000):
crib = prepare(crib_plaintext)
n = len(crib) // 2 * 2
crib_first = [IDX[crib[i]] for i in range(0, n, 2)]
crib_second = [IDX[crib[i + 1]] for i in range(0, n, 2)]
crib_ct = ciphertext[:n]
for _ in range(restarts):
left, right, matched = climb(crib_first, crib_second, crib_ct, steps)
if matched == 2 * len(crib_first):
return left, right
return left, right
if __name__ == '__main__':
crib_text = ("the two square cipher places two keyed grids side by side and crosses the coordinates "
"of each pair of letters the quick brown fox jumps over the lazy dog and packs my box "
"with five dozen liquor jugs so that every letter of the alphabet appears in this known "
"opening passage which the analyst can already guess word for word")
secret_text = ("the rendezvous is fixed for dawn at the old stone bridge bring the sealed packet and "
"the password is jackdaw")
full = crib_text + " " + secret_text
left_key, right_key = generate_square("EXAMPLE"), generate_square("KEYWORD")
ciphertext = encrypt(full, left_key, right_key)
print("full length:", len(prepare(full)), "crib length:", len(prepare(crib_text)))
print("ciphertext starts:", ciphertext[:32])
random.seed(2026)
left, right = break_two_square(ciphertext, crib_text)
first, second = split_ciphertext(ciphertext)
plain = decrypt(first, second, left, right, inverse(left), inverse(right))
full_prep = prepare(full)
print("full message recovered:", plain == full_prep)
print("recovered left square: ", ''.join(left))
print("recovered right square:", ''.join(right))
crib_len = len(prepare(crib_text))
print("decrypted secret tail:", plain[crib_len - crib_len % 2:][:84])
Running it prints the worked example:
full length: 354 crib length: 264
ciphertext starts: PNKUZAQSVLNABIYNKLONOCOOQPMEYEEL
full message recovered: True
recovered left square: APEMXINGKHCFLDBRTOSQWZUYV
recovered right square: WOEKYILGFHBCDRAQSNMPXZUTV
decrypted secret tail: THERENDEZVOUSISFIXEDFORDAWNATTHEOLDSTONEBRIDGEBRINGTHESEALEDPACKETANDTHEPASSWORDISIA
Limitations
This is a teaching breaker that recovers real two-square keys, within the limits of a known-plaintext attack:
- It needs a crib. This is not a ciphertext-only attack. The attacker must know, or correctly guess, a stretch of the plaintext. Without one, the two-square cipher resists the hill climb, as the section above explains.
- The crib must be long enough. A short crib pins only the cells it exercises, so a letter that appears only in the unknown part of the message may not come out. The worked example’s crib is deliberately long and covers the whole alphabet.
- The crib must align with the digraphs. The attack assumes the crib starts at the beginning of the message and has an even length, so it lines up with the letter pairs. A crib at an unknown offset would need its position searched first.
- The recovered key is one of an equivalent set. Two-square has a large class of square-pairs that decrypt identically, so the recovered squares usually differ from the original keywords while decrypting every digraph correctly.
Security Status
- Two-square is a nineteenth-century pencil-and-paper cipher, not a cipher to use. It offers no security against a computer. A crib of a few hundred letters breaks it in seconds.
- Its one apparent advantage over four-square is illusory. Having no fixed plain squares does defeat the straightforward ciphertext-only hill climb, but it does not defeat a crib, and known plaintext is a weak assumption to rely on for security.
- Known plaintext is the classic break for the squares family. Playfair, four-square and two-square all fall to known plaintext quickly. Four-square and Playfair also fall to ciphertext alone; two-square needs the crib, which is the main practical difference between them.
FAQ
Why not just use the four-square breaker’s quadgram search?
Because it does not converge on two-square. The four-square breaker leans on two fixed plain squares as an anchor, and two-square has none, so the English-likeness score gives the search no gradient to follow until both squares are already almost right. The known-plaintext attack sidesteps this entirely.
What counts as a crib?
Any plaintext the attacker already knows and can line up with the ciphertext. A fixed greeting, a form’s boilerplate, a header, or a phrase guessed from context all work. The worked example uses a long, alphabet-covering opening so the recovered key decrypts everything after it.
Why are the recovered squares not the original keywords?
The two-square cipher has a large equivalence class of keys. Several different pairs of squares encrypt and decrypt identically, so the search lands on one of them, which decrypts every digraph correctly even though it is not the literal keyword squares.
How much known plaintext do I need?
Enough to pin every cell the rest of the message uses, which in practice means a few hundred letters with good letter coverage. A shorter crib still recovers the key for the part it covers, but may leave letters in the unknown tail undetermined.
Is two-square weaker or stronger than four-square?
Neither in any useful sense. Four-square falls to a ciphertext-only search while two-square resists it, which looks like an advantage, but both collapse under a crib. For real security, both are equally obsolete.
References
-
The four-square breaker on this site, whose ciphertext-only approach two-square resists: /blog/four_square_breaker_guide/
-
The Hill cipher breaker on this site, another known-plaintext attack that recovers a key directly rather than searching ciphertext: /blog/hill_breaker_guide/
-
Practical Cryptography. “Cryptanalysis of the Two Square Cipher.” Available at: http://practicalcryptography.com/cryptanalysis/stochastic-searching/cryptanalysis-two-square-cipher/