One-Time-Pad / Many-Time-Pad Reuse — the crib-drag break when a pad is used twice
A one-time pad is unbreakable only if the keystream is used exactly once; reuse it across two or more messages and it becomes a solvable "many-time pad" — recoverable by XOR-ing ciphertexts together and crib-dragging, no key needed.
The signal that betrays it
- Multiple ciphertexts of similar or equal length, given as hex/base64 blobs, that are clearly meant to be compared against each other. - Challenge language like "pad reused", "same key", "one-time pad" used more than once, or a service that re-issues the "same" key/nonce on repeated calls. - Any Stream-Cipher Keystream Reuse — the umbrella break behind every reused-keystream challenge situation collapses to this: a stream cipher, XOR cipher, or CTR Nonce Reuse — when the counter mode keystream repeats where the same keystream byte stream is applied twice is mathematically identical to OTP reuse — the attack below applies unchanged. - If lengths differ, expect one message to be shorter and the alignment to run out partway — that's still workable, just truncate the overlap.
Why it works
c1 = p1 ⊕ k, c2 = p2 ⊕ k, … For any pair, c_i ⊕ c_j = p_i ⊕ p_j: the key cancels out completely and disappears from the equation, leaving only the XOR of the two plaintexts. This is the textbook "two-time pad" result — reusing the key destroys the information-theoretic guarantee that makes a true one-time pad provably unbreakable (verified against Wikipedia's One-Time Pad article). The real-world Venona project is the classic public case of the underlying flaw — duplicated keying material — though the mechanism and the recovery technique differ from the toy XOR case above: wartime production pressure led to some pad pages being duplicated and issued more than once (accidental duplication, not deliberate reuse), and analysts recovered plaintext by reconstructing the additive tables and codebooks involved, not by simply XOR-ing ciphertexts together (verified against Wikipedia's Venona project article).
How to exploit it
1. XOR every pair of ciphertexts you have. Each pairwise XOR gives you p_i ⊕ p_j — plaintext, not ciphertext, information, even though you don't yet know either plaintext.
2. Crib-drag. Slide a probable word or fragment (" the ", " and ", a likely header like "HTTP/1.1") across each c_i ⊕ c_j at every offset. XOR the crib into the diff at that offset: if the result comes out as plausible text (letters, spaces, punctuation) instead of garbage, you've found a real fragment of the *other* message at that position. Wrong guesses produce noise almost everywhere; right guesses produce a readable stretch — the signal is unmistakable once you see it.
3. Extend from the found fragment. A confirmed crib gives you keystream bytes at those offsets (k = c_i ⊕ (recovered fragment of p_i)). Apply that keystream slice to every *other* ciphertext at the same offset — a correct guess usually lights up plausible fragments in several messages at once, which is the confirmation that you guessed right and not just found an accidental collision.
4. Bootstrap systematically with the space-XOR trick when you have 3+ messages. In ASCII text, spaces (0x20) are frequent and, XORed against a letter, flip that letter's case ('a' ⊕ 0x20 = 'A' and vice versa). Column by column (same offset across all ciphertexts), assume each character in turn is a space, derive the candidate keystream byte, and score it by how many *other* characters in that column, once XORed with that candidate, land in the letter/space range. The candidate with the most "looks like text" hits wins that keystream byte. This scales far better than manual crib-dragging once you have more than 2-3 ciphertexts, because more messages means more statistical signal per column.
5. Once enough of the keystream is recovered, decrypt everything at once — the remaining gaps (positions where no message happened to have a space or common letter) are usually short enough to fill by hand from context.
```python def xor(a, b): return bytes(x ^ y for x, y in zip(a, b))
def recover_keystream(cts): # exploits frequent spaces across many messages L = max(map(len, cts)) key = bytearray(L) for i in range(L): col = [c[i] for c in cts if i < len(c)] guesses = [0] * 256 for a in col: # assume this byte is a space k = a ^ 0x20 for b in col: d = b ^ k if 65 <= d <= 122 or d == 0x20: guesses[k] += 1 key[i] = max(range(256), key=lambda k: guesses[k]) return bytes(key) ```
The more ciphertexts share the same keystream, the stronger and faster this converges — with only 2 messages you're doing manual crib-dragging; with 5+ the space-XOR statistic alone often recovers most of the keystream automatically.
What does NOT work
- Treating it as a factoring/algebraic problem. There is no key to "solve for" mathematically the way there is in RSA or DH — the key has been eliminated by the XOR entirely. The only route back to plaintext is statistical/linguistic (crib-dragging, letter-frequency scoring), not number theory. - Assuming 2 messages is enough for the automated space-XOR scorer. With only two ciphertexts the space-XOR statistic is unreliable — too little data per column to distinguish the real space-collision from noise. Below ~3-4 messages, manual crib-dragging with known probable words is more reliable than trying to automate it. - Ignoring length mismatches. If ciphertexts differ in length, don't discard the shorter one — the overlap region is still fully exploitable, you just lose keystream recovery for the tail that only the longest message covers. - Confusing this with a true, correctly-used one-time pad. If the key genuinely is random, as long as the message, and used exactly once, there is *no* attack — that configuration is provably secure (Shannon's proof, cited on Wikipedia's One-Time Pad page). Don't waste time crib-dragging a single ciphertext with no reuse; check the challenge setup for actual reuse before assuming this technique applies.
Related
Stream-Cipher Keystream Reuse — the umbrella break behind every reused-keystream challenge Repeating-Key XOR — recovering key length and key from a Vigenère-style stream CTR Nonce Reuse — when the counter mode keystream repeats Substitution & Shift Cipher — recovered by letter-frequency, not by algebra
Verified against
8 claims checked against these sources · 2 refuted and removed
What links here
Source: Sinapsi — verified compositional memory, queryable by LLMs. Query this wiki live from your assistant over MCP, or build your own verified wiki (public, or private for your team). CC BY 4.0 — reuse with attribution to Sinapsi.