Nonce Reuse in AES-GCM: Why Reusing It Once Is Enough to Break Everything
AES-GCM is the default authenticated encryption mode for a huge share of what runs today: TLS, SSH, disk encryption, half the crypto libraries you've ever imported. It's fast, it's a NIST standard, and it gives you confidentiality and integrity in a single pass. It also has exactly one operating rule that, if you break it even once, doesn't degrade gracefully. It fails completely. The rule is: never use the same key and nonce pair twice.
Not "try not to". Not "avoid where practical". Once, and the security guarantees for the whole construction are gone, not just for the two messages involved, but potentially for the key itself.
What the nonce is actually doing
GCM is built from two pieces: CTR mode for encryption, and GHASH for authentication. The key point for this article is that the CTR keystream depends on nothing except the key and the nonce. Given a 12-byte nonce N, GCM builds a sequence of counter blocks (N with an incrementing 32-bit counter appended) and encrypts each one with AES under the key to get keystream blocks. The plaintext is then just XORed against that keystream to produce ciphertext.
Nothing about the plaintext feeds into the keystream. That's deliberate: it's what makes CTR mode parallelisable and fast. But it also means that if you ever call GCM with the same key and nonce twice, you get the exact same keystream twice. GCM's security proof assumes the (key, nonce) pair is a fresh, never-repeated identifier for a one-time pad. Repeat it, and you no longer have a one-time pad. You have a two-time pad, which is a textbook broken cipher.
Break one: confidentiality, via a two-time pad
If an attacker sees two ciphertexts produced under the same key and nonce, XORing them cancels the keystream entirely and leaves the XOR of the two plaintexts. If the attacker knows or can guess any part of one plaintext, whether from a fixed header, a predictable field, or plain crib-dragging, they recover the corresponding bytes of the other plaintext for free, with no need to touch the key at all.
Here's the failure made concrete. Go's crypto/cipher package will not stop you doing this; it has no way of knowing you've reused a nonce across two separate Seal calls.
package main
import (
"crypto/aes"
"crypto/cipher"
"fmt"
)
func main() {
key := []byte("0123456789abcdef") // 16 bytes -> AES-128, demo only
nonce := []byte("unique-nonce") // 12 bytes, reused below - that's the bug
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
panic(err)
}
pt1 := []byte("Transfer 100 GBP to Alice.")
pt2 := []byte("Transfer 999 GBP to Mallory.")
ct1 := gcm.Seal(nil, nonce, pt1, nil)
ct2 := gcm.Seal(nil, nonce, pt2, nil) // same nonce as ct1: the bug
// Strip the 16-byte tags, leaving the raw keystream-masked bytes.
body1 := ct1[:len(ct1)-gcm.Overhead()]
body2 := ct2[:len(ct2)-gcm.Overhead()]
n := min(len(body1), len(body2))
xored := make([]byte, n)
for i := 0; i < n; i++ {
xored[i] = body1[i] ^ body2[i] // keystream cancels out
}
// Anyone who can guess part of pt1 recovers the same bytes of pt2.
known := pt1[:n]
recovered := make([]byte, n)
for i := range recovered {
recovered[i] = xored[i] ^ known[i]
}
fmt.Printf("recovered from second message: %q\n", recovered)
}
No key material was touched anywhere in that recovery. The attacker never needed to know K; they just needed the two ciphertexts and a crib. Scale this up to something like a database of encrypted records, log lines, or session tokens with a fixed nonce (a mistake that is depressingly easy to make when someone hardcodes a nonce "temporarily" during development), and an attacker with a large enough corpus can often recover most of the plaintext through statistical crib-dragging alone, exactly as with classic one-time pad reuse attacks against Venona-era ciphertext.
Break two: authentication, via the forbidden attack
The confidentiality break is bad, but the authentication break is worse, because it doesn't just leak data, it lets an attacker forge valid ciphertexts that GCM will accept as genuine.
GHASH computes its tag by evaluating a polynomial over GF(2^128) at a secret point H, where H = E_K(0), the AES encryption of the all-zero block under the session key. Crucially, H depends only on the key, not the nonce, so it's the same for every message ever encrypted under that key. The polynomial's coefficients are built from the ciphertext blocks, the associated data, and their lengths. Normally you never see two evaluations of this polynomial that share the unknown H without also sharing the one-time keystream mask that hides the result, because the tag is XORed with a nonce-dependent mask before being sent.
Reuse a nonce, though, and that mask is identical across both messages. Subtract (XOR) the two tags and the mask cancels, leaving an equation purely in terms of H with everything else known. With two such equations, an attacker can solve for H directly, since it's a root of a polynomial they can compute in full. Once H is recovered, GCM's authentication is finished: the attacker doesn't need the AES key at all to construct valid tags for arbitrary chosen ciphertext, meaning they can inject or modify data protected under that key and have it verify successfully. This is Antoine Joux's "forbidden attack" against GCM, described not long after GCM was standardised, and it's one of the reasons GCM implementations are so unforgiving about nonce handling: the moment you allow reuse, you've handed the attacker the one secret value (H) that the whole authentication scheme depends on.
Where this actually happens
Nobody sets out to reuse a nonce. It happens through infrastructure accidents:
- A counter-based nonce that resets to zero after a process restart, container respawn, or VM snapshot/restore, silently repeating a sequence that was supposed to be strictly monotonic.
- Cloning a VM or container image after it has already generated (but not yet used) some nonce state, so two "different" instances start from the same point.
- Multiple writers sharing one key without partitioning the nonce space between them, so two processes independently pick overlapping counter or timestamp values.
- Deriving a nonce from something that looks unique but isn't, such as a coarse timestamp, under load high enough that two messages land in the same tick.
- A weak or misused random source for nonces that are supposed to be uniformly random, shrinking the effective entropy far below 96 bits.
What to actually do
Two structurally sound options, and don't mix them within the same key:
Random 96-bit nonces from a CSPRNG are fine, but they carry a birthday-bound limit. NIST Special Publication 800-38D caps random-nonce GCM usage at roughly 2^32 encryptions per key to keep collision probability acceptably low; if you're anywhere near that volume, rotate the key before you get there rather than after.
Deterministic counter-based nonces avoid the birthday bound entirely, but only if uniqueness is structurally guaranteed: a single writer, a monotonic counter that is never allowed to reset without also rotating the key, and no cloning of state that includes an unused counter value. This is how TLS 1.2/1.3 use GCM safely: the nonce is built from the connection's sequence number, and a fresh connection means a fresh key.
If you can't confidently guarantee either of those in your architecture, two alternatives are worth knowing about. XChaCha20-Poly1305 uses a 192-bit nonce, large enough that random generation is safe for the lifetime of essentially any realistic key without a rotation schedule; it isn't yet a formal IETF standard but is widely implemented, including in libsodium. AES-GCM-SIV (RFC 8452) is specifically designed to degrade gracefully under nonce misuse: reusing a nonce with the same key only reveals whether two plaintexts were identical, not their content and not the key. It's not a licence to be careless, but it's a meaningfully softer failure mode than plain GCM's total collapse.
Go's standard library doesn't ship AES-GCM-SIV, and crypto/cipher's GCM implementation will never check your nonce for you. It trusts you completely on the one thing GCM cannot forgive getting wrong.