Exploring codes, ciphers, and digital security

Why Reusing IVs and Nonces Breaks Encryption: A Common Mistakes Checklist

Why Reusing IVs and Nonces Breaks Encryption: A Common Mistakes Checklist

Two ciphertexts, one nonce. That single mistake has broken Wi-Fi encryption, TLS sessions, and a long list of homegrown file formats. The block cipher was never the weak point — AES-256 and ChaCha20 held up fine. The nonce is the part of the design everyone assumes is handled, and it is the part that quietly collapses when a counter resets, a process forks, or a developer picks an eight-byte random value because it looks random enough.

Here is what actually goes wrong when a value repeats under the same key, and a checklist you can run against real code.

What an IV or nonce is actually for

An IV or nonce is not a secret. You can publish it, log it, and send it in the clear next to the ciphertext. Its job is uniqueness: for a given key, that value must never be used twice.

In CTR, OFB, CFB, GCM, and ChaCha20-Poly1305, the nonce is fed into the keystream generation. Same key plus same nonce means the same keystream, and once two messages share a keystream, the encryption is effectively gone. CBC is different in a subtle way: the IV must be unpredictable at encryption time, not merely unique, because the first ciphertext block is a function of the plaintext XORed with the IV. Random is fine. Constant is not.

A nonce is a promise to the cipher: under this key, this value will never repeat. The cipher takes your word for it and offers no complaint when you break it.

The failure modes, mode by mode

Stream modes: keystream reuse

Encrypt two messages with the same keystream and an attacker who has both ciphertexts computes:

C1 ⊕ C2 = P1 ⊕ P2

The keys cancel out entirely. What is left is the XOR of two plaintexts, and human language, JSON, and XML carry enough structure to pull them apart. A known plaintext in one message — a standard header, a fixed greeting, the string {"user":" — hands over the corresponding bytes of the other. Attackers do this with crib-dragging and word-frequency scoring, and against structured data it goes fast.

AES-GCM: reuse is worse than you think

GCM combines CTR encryption with GHASH authentication, and both depend on the nonce. Reuse the nonce and an attacker can recover the GHASH authentication key from the tags of two messages. Once that key is known, the attacker can forge valid tags for arbitrary messages and decrypt further traffic under the same key. This is not a subtle statistical leak; it is a full break of confidentiality and integrity. It is the single most common reason to treat nonce management in GCM as a hard requirement rather than a best practice.

CBC: predictable IVs

Reusing a CBC IV leaks whether two messages start with the same block. That is embarrassing but survivable. Letting the attacker choose or predict the IV before you encrypt a secret is more serious: they can test guesses about the first block by comparing ciphertexts. Always generate CBC IVs from a cryptographic random source, fresh per message.

Counter wrap and the birthday bound

Counter-based nonces are fine until they wrap. A 96-bit GCM nonce used as a counter is enormous, but counters get reset by restores, snapshots, and fresh deployments of code that starts at zero. Random nonces have a different ceiling: because of collisions, the number of messages encrypted under a single key with random 96-bit nonces must stay far below 232. Shortening the random portion to 32 or 64 bits makes collisions likely rather than theoretical.

Where reuse hides in real systems

  • Restarts and snapshots. A counter persisted in memory resets to zero after a crash, a container redeploy, or a VM restore from backup — and the key is still the same.
  • Forked processes. A child inherits the parent's counter state and both continue from the same number.
  • Retries and replays. A send fails, the message is re-encrypted with the stored nonce, and the same plaintext goes out twice under the same keystream.
  • Copied rows and databases. Duplicating a record duplicates its stored nonce.
  • Keys shared across devices. Several clients derive the same key from a password and each picks a "random" 8-byte IV.
  • Nonces built from time, PID, or a sequence number. Predictable, and eventually colliding.
  • Keys that never rotate. Uniqueness must hold across the entire lifetime of a key, not just one session.
  • Test code in production. A hardcoded IV left in a config default.

A reuse-prevention checklist

  1. Name the source of nonces explicitly. Write it down in the design doc: random, counter, or derived. "It's handled" is how this bug ships.
  2. Pick a mode whose nonce size fits your source. For random nonces, use XChaCha20-Poly1305 with its 192-bit nonce, or AES-GCM-SIV, which tolerates accidental reuse far better than GCM.
  3. Persist counter state safely. Reserve a range of nonces, write the new high-water mark to durable storage, then encrypt. If you crash, you skip numbers rather than repeat them.
  4. Never store nonces for later reuse. Generate fresh on every encryption call, including retries.
  5. Give each process, node, or device its own nonce space. A random 32-bit prefix plus a local counter is a common, workable pattern; include the prefix in the key derivation if you want stronger separation.
  6. Cap messages per key. Rotate keys before any counter approaches its limit or before random-nonce collision odds matter.
  7. Bind the nonce into anything you authenticate. If you use AAD, make sure the nonce or a key identifier is covered, so mismatches fail loudly.
  8. Test for it. Add a unit test that encrypts thousands of messages with one key and asserts all nonces are distinct. Fuzz the counter across a simulated restart. Review every code path that constructs an IV or nonce with the same suspicion you would give to a password comparison.

Choose failure modes you can see

Tools that fail silently are the dangerous ones. AES-GCM gives no error on nonce reuse — you get correct-looking ciphertext and a false sense of safety. Counter-based nonces give you an audible failure when they wrap, which is why many teams prefer them for high-volume encryption under one key. Random nonces are simplest to deploy but need a generous size and a message-count budget. Decide which tradeoff you are making, and record it.

Make uniqueness a property you can prove

Open one encryption path in your codebase today and answer three questions: where does the nonce come from, what happens after a restart, and who else could be using this key. If any answer is vague, that is your next fix. Rotation is cheap, tests are cheap, and a design that separates nonce spaces by key costs a few lines. Recovering from a nonce reuse incident costs considerably more — you have to rotate the key, invalidate everything encrypted under it, and assume the plaintexts are already known. Uniqueness is not a detail of the encryption; it is the encryption.

Photo: Fernando Arcos / Pexels

Related Articles