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.
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))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
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))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
| Step / line | What happens | State after step | Output 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.