Chapter 2 · Practice 2 of 4
Never reuse a nonce with the same key
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:
| Construction | Nonce size | Safe ways to pick nonces | Messages per key with random nonces |
|---|---|---|---|
| XChaCha20-Poly1305 | 192 bits | counter, permutation or random | no practical limit |
| ChaCha20-Poly1305 (IETF) | 96 bits | counter or permutation | 2^32 |
| AES256-GCM | 96 bits | counter or permutation | 2^32 |
In practice:
- If your library picks nonces for you (Tink, libsodium's
secretboxandcrypto_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
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()]);
}