Cryptography Best Practices for Developers

Chapter 2 · Practice 2 of 4

Never reuse a nonce with the same key

From Encrypting data with AEAD

Why it matters

AEAD security depends on each (key, nonce) pair being used once. With AES-GCM and ChaCha20-Poly1305, reusing a nonce under the same key can reveal the XOR of two plaintexts and let an attacker forge messages. This is the most common way AEAD code goes wrong.

libsodium's documentation gives the limits per construction:

ConstructionNonce sizeSafe ways to pick noncesMessages per key with random nonces
XChaCha20-Poly1305192 bitscounter, permutation or randomno practical limit
ChaCha20-Poly1305 (IETF)96 bitscounter or permutation2^32
AES256-GCM96 bitscounter or permutation2^32

In practice:

  • If your library picks nonces for you (Tink, libsodium's secretbox and crypto_aead_* with helpers), let it.
  • With 96-bit nonces and random generation, count encryptions per key and rotate the key well before 2^32. OWASP lists approaching a mode's usage limits as a reason to rotate.
  • With counters, make sure two servers or two restarts can never start from the same value. Random 192-bit nonces avoid that coordination problem.
  • Never derive the nonce from data that can repeat, such as a timestamp in seconds or a record ID.

The nonce is not secret. Store it next to the ciphertext, usually as a prefix.

Example

TypeScript
import { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';

// AES-256-GCM: fresh 12-byte nonce per message, stored as a prefix
export function encrypt(key: Buffer, plaintext: Buffer, aad: Buffer) {
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', key, iv);
  cipher.setAAD(aad);
  const body = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  return Buffer.concat([iv, cipher.getAuthTag(), body]); // 12 + 16 + n bytes
}

export function decrypt(key: Buffer, blob: Buffer, aad: Buffer) {
  const decipher = createDecipheriv('aes-256-gcm', key, blob.subarray(0, 12));
  decipher.setAAD(aad);
  decipher.setAuthTag(blob.subarray(12, 28));
  // final() throws if anything was tampered with
  return Buffer.concat([decipher.update(blob.subarray(28)), decipher.final()]);
}