The one-time pad and why keys must never be reused

Use XOR as a cipher and see how reusing a key leaks the relationship between two messages.

Python
def xor(data, key):
    return bytes(d ^ k for d, k in zip(data, key))


plain1 = b"ATTACK AT DAWN"
plain2 = b"RETREAT AT TEN"
key = bytes(range(1, 15))
cipher1 = xor(plain1, key)
cipher2 = xor(plain2, key)
print(xor(cipher1, key) == plain1)
leak = xor(cipher1, cipher2)
print(leak == xor(plain1, plain2))
print(xor(leak, plain1))
Output
True
True
b'RETREAT AT TEN'

Explanation

Symmetric encryption uses the same secret key to encrypt and decrypt. The simplest example is XOR: combining each message byte with a key byte using exclusive-or, and repeating the same operation with the same key to decrypt. The one-time pad does exactly this with a key that is truly random, as long as the message, kept secret, and used only once. Under those conditions it is provably unbreakable.

The conditions matter. If the key is used twice, XORing the two ciphertexts cancels the key and leaves plaintext1 ⊕ plaintext2. Whoever knows or guesses part of one message can then recover the other. This is the two-time pad flaw. The example uses a fixed, predictable key so the output is reproducible; a real pad key would come from a secure random generator, and the same weakness appears whenever any stream cipher reuses a key and nonce.

Do not invent your own cipher for real data. Use a vetted authenticated encryption scheme from a maintained library, such as AES-GCM or ChaCha20-Poly1305 from the cryptography package, with a fresh nonce for every message. Python's standard library offers hashing and secure randomness but not ciphers, which is why the lesson uses XOR to explain the idea.

Explore the dry run
Guided walkthroughReady
1def xor(data, key):
2 return bytes(d ^ k for d, k in zip(data, key))
3
4
5plain1 = b"ATTACK AT DAWN"
6plain2 = b"RETREAT AT TEN"
7key = bytes(range(1, 15))
8cipher1 = xor(plain1, key)
9cipher2 = xor(plain2, key)
10print(xor(cipher1, key) == plain1)
11leak = xor(cipher1, cipher2)
12print(leak == xor(plain1, plain2))
13print(xor(leak, plain1))

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Encrypt by XORing each message byte with a key byte.—
—
XOR is its own inverse: applying the same key again recovers the plaintext.—
True
XOR of two ciphertexts made with the same key cancels the key, leaving plaintext1 XOR plaintext2.key cancelled
True
The key is gone from the result: this is information the attacker gets for free.key cancelled
True
True
An attacker who guesses or knows one plaintext recovers the other completely.key cancelled
True
True
b'RETREAT AT TEN'

Cost and common mistakes

XOR encryption is O(n) time for n bytes. The key must be at least as long as the message, which makes one-time pads impractical for most uses.

  • Never reuse a one-time pad key. Reusing a key, or a key and nonce in a stream cipher, breaks the security.
  • The key used here is predictable on purpose. A real key must come from a cryptographically secure random generator.
  • Encryption alone does not detect tampering. Use authenticated encryption (AEAD) so changes to the ciphertext are rejected.

Try it yourself

Why does a one-time pad key have to be as long as the message?

Reveal answer and reasoning

A shorter key must repeat, and repeating key bytes has the same weakness as reusing a key: it leaks relationships between plaintext parts.

Further reading