Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
Gilbert Vernam's 1917 patent described a cipher that XORed a teleprinter tape of the message with a tape of random key characters — the first mechanised one-time pad and the first time anyone built encryption around the XOR operation. XOR is the fundamental operation of all modern symmetric cryptography for two reasons: it is its own inverse (encrypt and decrypt are the same circuit), and it is information-theoretically the only operation under which a uniformly random key bit produces a uniformly random ciphertext bit. Every stream cipher (RC4, ChaCha20, AES-CTR) is a Vernam cipher whose 'random tape' is replaced by a deterministic pseudorandom keystream. Understanding why XOR sits at the heart of this design — and what goes catastrophically wrong when you reuse the keystream (the so-called 'two-time pad') — is essential for understanding the long history of stream-cipher disasters from Venona to WEP.
The Vernam cipher operates bit by bit: each plaintext bit is XORed with the corresponding key bit. With a fresh random key per message it is a one-time pad; with a deterministic keystream from a short key it is a stream cipher. Either way, the encrypt and decrypt operations are identical — XOR is an involution.
Use these three in order. Each builds on the one before.
In one paragraph, explain what the Vernam cipher is and why XOR is the natural operation for a one-time pad rather than addition modulo 26.
Walk me through XOR-based encryption step by step at the bit level, and explain why a uniformly random key bit produces a uniformly random ciphertext bit irrespective of the plaintext bit's value.
Explain why reusing a Vernam keystream (the 'two-time pad' problem) is catastrophic, how Venona exploited it against Soviet diplomatic traffic, and what specifically goes wrong in modern stream ciphers like AES-CTR if a nonce is reused.
// main.go — run: go run main.go
package main
import (
"crypto/rand"
"fmt"
)
func vernam(data, key []byte) []byte {
out := make([]byte, len(data))
for i := range data {
out[i] = data[i] ^ key[i]
}
return out
}
func main() {
// Single-message Vernam (= OTP)
pt := []byte("the quick brown fox")
key := make([]byte, len(pt))
rand.Read(key)
ct := vernam(pt, key)
if string(vernam(ct, key)) != string(pt) {
panic("round-trip failed")
}
// The "two-time pad" disaster: reusing the same key for two messages
pt1 := []byte("attack at dawn 06")
pt2 := []byte("retreat at noon00")
ct1 := vernam(pt1, key[:len(pt1)])
ct2 := vernam(pt2, key[:len(pt2)])
// Attacker computes pt1 XOR pt2 with no knowledge of the key
leaked := vernam(ct1, ct2) // this is pt1 XOR pt2
fmt.Println("pt1 XOR pt2:", leaked)
// If the attacker guesses any portion of pt1, they get pt2 for free in that range
guess := []byte("attack at dawn 06")
recoveredPt2 := vernam(leaked, guess)
fmt.Println("recovered pt2:", string(recoveredPt2))
}go run main.go