Nonce Management
GCM nonce-reuse, deterministic vs random, the birthday bound, XChaCha20 fix.
Nonce Management
A nonce is a number used once. With a stream cipher or AEAD, it's the difference between a system that's secure and one whose ciphertexts are catastrophically forgeable. Most production crypto bugs in the last decade are nonce-management bugs — not because the math is weak, but because "use it once" is harder than it sounds.
The GCM nonce-reuse catastrophe
Encrypt two messages with the same (key, nonce) under AES-GCM and you don't just leak P1 ⊕ P2 — that's the stream-cipher consequence. GCM does worse: an attacker can recover H = AES_K(0), the authentication subkey.
The reason is that GCM's authentication tag is a polynomial evaluation in GF(2^128):
T = H · X_1 + H · X_2 + ... + H · X_n + EK(J0)
Where X_i are blocks of (associated_data || ciphertext) and EK(J0) depends on the nonce. With one ciphertext you can't isolate H. With two ciphertexts under the same nonce, you can subtract them — EK(J0) cancels — and what's left is a polynomial in H whose roots include H. Solve, and you have H.
Once you have H, you can forge any new ciphertext-and-tag pair. Confidentiality and integrity are both gone, and on TLS-style channels that means session hijack.
This is the core of Joux's "forbidden attack" — the reason NIST SP 800-38D writes in capital letters: THE PROBABILITY THAT THE AUTHENTICATED ENCRYPTION FUNCTION EVER WILL BE INVOKED WITH THE SAME (KEY, IV) PAIR ON TWO DIFFERENT SETS OF INPUT DATA SHALL BE NO GREATER THAN 2⁻³².
Two nonce strategies, both safe
There are exactly two nonce strategies that preserve security for any reasonable workload:
1. Deterministic / counter-based
Take a 96-bit counter, start at zero, increment by one per message, never reset. With proper care you can encrypt up to 2^96 messages under one key — enough for the lifetime of every key you'll ever generate.
This is what TLS does (sequence number folded into the nonce). It's what most file-format encryption does (block index = nonce). It's the default, and it's perfectly safe if you have somewhere to store the counter durably.
# AES-GCM with explicit counter — the TLS pattern
counter = 0
def encrypt(plaintext: bytes) -> bytes:
global counter
counter += 1
nonce = counter.to_bytes(12, "big")
return gcm.encrypt(nonce, plaintext, associated_data=b"")
The hard part is durability. If a process crashes and restarts the counter from 0, you're guaranteed nonce reuse on the first message. Every counter-based scheme needs persistent storage and atomic increment.
2. Random nonces — the birthday problem
If you can't keep a counter (distributed systems, fanout, no shared state), generate the nonce randomly. The trap is the birthday bound: after roughly 2^(n/2) random n-bit values, you'll see a collision with non-negligible probability.
For a 96-bit AES-GCM nonce that's 2^48 ≈ 281 trillion messages. Sounds huge, but a busy CDN can hit 2^32 (~4 billion) per day on a single key. The NIST bound (< 2^32 random nonces per key, probability < 2^-32 of collision) is tight.
192-bit nonces fix this entirely. XSalsa20 and XChaCha20 extend the original 96-bit ChaCha20 nonce to 192 bits by hashing the first 16 bytes into a sub-key with HChaCha20(key, n[0:16]), then running standard ChaCha20 with n[16:24] as the regular nonce. The sub-key construction is collision-resistant; with 192 random bits, the birthday bound is 2^96 — astronomical.
Use XChaCha20-Poly1305 (or AES-GCM-SIV, or any AEAD with extended nonces) anywhere you can't guarantee strict counter management.
Side-by-side decision table
| Constraint | Pick |
|---|---|
| You have a clean counter (TLS records, file blocks, sequence #) | AES-GCM or ChaCha20-Poly1305 with counter nonce |
| You don't have a counter, plenty of CPU | XChaCha20-Poly1305 with random 192-bit nonce |
| Tiny embedded device, no AES hardware | XChaCha20-Poly1305 (also no AES needed) |
| You absolutely must keep AES-GCM and have no counter | AES-GCM-SIV (misuse-resistant, but slower) |
What modern libraries do for you
The current generation of "crypto for the rest of us" libraries hides nonces entirely:
// libsodium's secretstream: counter is internal, you don't see it.
const { state, header } = sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
const c1 = sodium.crypto_secretstream_xchacha20poly1305_push(state, m1, ...);
const c2 = sodium.crypto_secretstream_xchacha20poly1305_push(state, m2, ...);
// Each push uses a fresh internal nonce. No way to misuse it.
# AWS Encryption SDK derives a fresh data key per message.
client = aws_encryption_sdk.EncryptionSDKClient()
ct, _ = client.encrypt(source=b"hello", key_provider=...)
# Internal: per-message data key + nonce. Application sees neither.
The lesson: let the library manage nonces. When you're forced to deal with them yourself (low-level protocols, custom formats), pick one of the two safe strategies and bolt the type system to enforce it.
Real-world incidents
- TLS 1.2 BEAST/Lucky13 — wasn't strictly a nonce reuse, but a related class: predictable IVs in CBC mode let attackers chosen-plaintext their way through cookies. Killed by TLS 1.3's mandatory AEAD with sequence-number nonces.
- WPA2 KRACK (2017) — Vanhoef et al. forced the WPA2 4-way handshake to retransmit, which reset the nonce counter. Same nonce twice = stream-cipher leakage.
- Sweet32 (2016) — not nonce reuse but a related birthday bound: 64-bit block ciphers (3DES) reuse blocks after
2^32blocks, leaking authentication cookies in long-lived TLS sessions. Killed by deprecating 64-bit block ciphers.
Each of these was prevented by a single rule: never let two messages share (key, nonce). Implementations get this wrong; the rule itself is simple.
Summary
- Nonce-reuse with GCM = key-recovery for the auth subkey, not just plaintext leak.
- 96-bit random nonces are safe up to
~2^32messages per key; not nearly enough for high-traffic systems. - 192-bit nonces (XChaCha20) are safe for any practical workload — the birthday bound becomes
2^96. - Deterministic counters are simpler and safer when you have one. Make sure they survive process restarts.
- Modern libraries hide all of this. Use them.
Tools in the wild
4 tools- librarylibsodiumfree tier
Provides XChaCha20-Poly1305 for safe random nonces. The simplest correct AEAD interface.
- library
Per-message data-key derivation hides nonce management entirely. Pick this when key reuse worries you.
- libraryTinkfree tier
Google's misuse-resistant crypto library — handles nonces internally, never exposes the raw API.
- libraryRustCrypto aes-gcmfree tier
Rust AEAD implementations with explicit nonce types. Type system catches reuse bugs at compile time.