What Is a Nonce, and Why Does Reusing One Break AES-GCM

By Freddy ·

Every AES-GCM encryption call takes a nonce, usually named iv in code and documentation. It is short (96 bits by default), it travels alongside the ciphertext in plain sight, and unlike the key, it is not a secret. That combination makes it easy to treat as an afterthought, a piece of boilerplate you generate and forget about. It is the opposite. A nonce is the one value in the whole scheme that has exactly one rule attached to it, and breaking that rule does not degrade AES-GCM a little. It removes the security guarantee entirely, for every message encrypted under the repeated value.

What “nonce” actually means

Nonce is short for “number used once.” The name is the entire specification: under a given key, a nonce must never be used for more than one encryption. It does not need to be random, it does not need to be unpredictable, and it does not need to be kept secret from anyone, including an attacker. A simple incrementing counter is a perfectly valid nonce. The only property that matters is uniqueness, per key, for the full lifetime of that key. Confuse this with an encryption key, which must stay secret, and you will reach for the wrong protection. Confuse it with a password salt, which tolerates rare duplication without catastrophic failure, and you will underestimate how strict the rule actually is.

Why GCM specifically falls apart on reuse

AES-GCM is built on a stream cipher construction. Internally, the key and nonce together generate a long pseudorandom keystream, and encryption is just XOR-ing that keystream against the plaintext. Decryption XORs the same keystream back. The keystream depends entirely on the key and nonce pair, so if the same key and nonce ever encrypt two different messages, both ciphertexts were produced by XOR-ing different plaintexts against the exact same keystream.

That is the failure condition, and it is a well-known one: XOR two ciphertexts encrypted under the same keystream together, and the keystream itself cancels out, leaving the XOR of the two plaintexts.

ciphertext_A = plaintext_A XOR keystream
ciphertext_B = plaintext_B XOR keystream

ciphertext_A XOR ciphertext_B
  = (plaintext_A XOR keystream) XOR (plaintext_B XOR keystream)
  = plaintext_A XOR plaintext_B

The XOR of two plaintexts is not the plaintexts themselves, but it is enough. If either message has any known or guessable structure, a JSON key name, a common header, repeated whitespace, an attacker can peel that structure apart and recover meaningful chunks of both plaintexts using nothing but frequency analysis and guesswork. This is not a theoretical weakness. It is exactly the technique that broke multiple real-world systems that reused nonces, going back to the original two-time-pad attacks against stream ciphers decades before GCM existed. GCM did not fix that underlying weakness, it inherited it, and layered authentication on top that assumes the nonce rule is never broken.

The authentication side breaks too, and arguably worse. GCM derives its authentication subkey from the same key-and-nonce pair used for the keystream. Reuse a nonce and an attacker who observes two ciphertext and tag pairs under it can, with enough algebra, recover that authentication subkey outright. Once they have it, they can forge valid tags for ciphertexts they construct themselves, meaning the “this data has not been tampered with” guarantee that is GCM's entire reason for existing over plain AES-CBC stops holding for that key. A single nonce reuse does not just leak plaintext. It can hand over the ability to forge future messages.

Random nonces are safe, but not infinitely so

The standard fix, generating a fresh random 96-bit nonce with crypto.getRandomValues() for every encryption, is correct and is what almost every AES-GCM implementation does by default. But “random” is a probabilistic guarantee, not an absolute one, and it is worth knowing where the edge actually sits. With a 96-bit random nonce, collisions become a real risk once you have encrypted somewhere in the neighborhood of 232 messages under a single key, roughly 4.3 billion. That sounds like a lot until you consider a busy backend service rotating keys yearly rather than per session, or a logging pipeline that never rotates a key at all. Past that volume, the birthday bound makes an accidental collision plausible, not just theoretically possible.

The practical guidance follows directly from that: rotate keys well before you approach that message volume under any single key, and never fall back to a fixed or predictable nonce as a way to “simplify” key management. If a system genuinely needs to encrypt at very high volume under one key, a counter-based nonce (guaranteed unique by construction, not by chance) is the safer choice over pure randomness, though it requires durable state to track the counter across restarts, which is exactly the kind of complexity that makes random nonces the more common default for anything that is not that high-throughput.

How reuse actually creeps into real code

Nobody sets out to reuse a nonce. It happens through a handful of specific, recurring mistakes:

A nonce hoisted outside the encryption loop. Generating one IV before a loop that encrypts multiple records, intending to reuse the variable name but not the value, and accidentally reusing the value too. This is the single most common version of the bug in application code.

A hardcoded or example IV left in from testing. Copying a code sample that uses a fixed IV for clarity, shipping it unchanged because the tests passed and the round-trip worked.

Deriving the nonce from something that is not actually unique. Using a timestamp truncated to a coarse unit, a request ID that resets on restart, or a hash of content that can itself repeat, none of these guarantee uniqueness the way a fresh random value or an ever-incrementing counter does.

A key that outlives its intended encryption volume. A key provisioned once and never rotated, quietly approaching the birthday-bound message count described above, turns a correct random-nonce implementation into an eventual reuse purely through scale.

What to actually do about it

Generate a new 96-bit nonce with crypto.getRandomValues() immediately before each call to crypto.subtle.encrypt(), never once and reused. Store or transmit that nonce alongside its ciphertext, since it is not secret and the decrypting side needs the exact value back. Rotate the underlying key on a schedule well short of the billions-of-messages collision threshold for your actual traffic volume. And treat any code path that hardcodes, caches, or derives a nonce from a non-unique source as a bug to fix immediately, not a style preference to leave alone. If you want to see the nonce and ciphertext pairing work correctly end to end, the Encrypt / Decrypt tool on this site generates a fresh IV for every AES-GCM operation automatically, entirely in your browser, which is the same behavior your own code should replicate. If a decrypt is instead failing outright rather than silently leaking, that is a different problem with a different set of causes, covered in why AES-GCM decryption fails with OperationError.

← Back to KeyForge