The Nonce Has One Job
Authenticated encryption is supposed to give two answers at once:
keep the bytes secret
reject modified bytes
GCM gets there by combining counter-mode encryption with a polynomial authentication function called GHASH. That combination is fast and widely deployed. It also puts a hard operational requirement on the nonce.
Under one key, a GCM nonce has one job:
never name two different encryptions
The Counter Stream Repeats
Counter-mode encryption turns a block cipher into a stream of pads:
C[i] = P[i] xor pad(key, nonce, counter+i)
If the same key and nonce are reused, the pad repeats. For two ciphertexts:
\[C_1 = P_1 \oplus S\] \[C_2 = P_2 \oplus S\]Xor them:
\[C_1 \oplus C_2 = P_1 \oplus P_2.\]The stream has disappeared from the equation. If one plaintext is known or guessable, the other plaintext is exposed byte for byte:
\[P_2 = C_1 \oplus C_2 \oplus P_1.\]This is the same failure as a reused one-time pad. GCM’s authentication layer does not repair it; the authentication layer has its own nonce-reuse problem.
The Authentication Mask Repeats Too
GCM’s tag has the shape:
tag = encrypted_nonce_block xor GHASH_H(aad, ciphertext)
The details matter in real GCM: GHASH works in \(\mathrm{GF}(2^{128})\), uses a hash subkey \(H=E_K(0^{128})\), includes associated data, includes ciphertext, and includes lengths. The structural point is enough for the failure:
same key + same nonce -> same encrypted_nonce_block
So repeated nonces do not merely reveal a plaintext xor. They also give an attacker equations in the authentication polynomial.
NIST SP 800-38D states the uniqueness requirement directly: for GCM, IVs must not repeat with the same key except with negligible allowed probability, and compliance is crucial to security.1 Appendix A explains the reason: if IVs repeat, an adversary may determine the hash subkey and construct forgeries.2 RFC 5116 says the same thing in operational language: GCM nonce reuse undermines confidentiality and all authenticity/integrity for that key; observing two distinct plaintext encryptions with the same nonce lets an attacker reconstruct the xor of the plaintexts and recover the internal hash key used for integrity.3
That is stronger than:
do not repeat nonces because it is untidy
It is:
do not repeat nonces because the construction has one-time parts
Lab: A Tiny GCM-Shaped Failure
The lab below is not AES-GCM. It uses:
- byte-sized finite-field arithmetic instead of \(\mathrm{GF}(2^{128})\);
- a deterministic pseudo-keystream instead of AES;
- a one-byte GHASH recovery example so the algebra fits on the page.
The toy is intentionally small enough to audit. It demonstrates the two separate failures under one repeated nonce:
C1 xor C2 = P1 xor P2.- A repeated tag mask gives equations that recover the toy GHASH key and allow a valid forged tag.
With the default settings:
P1: pay=1000;to=alice;ok=yes
P2: pay=9000;to=admin;ok=yes
nonce: 7
seed: 41
the lab measures:
| Quantity | Value |
|---|---|
| message bytes | 24 |
C1 xor C2 |
00 00 00 00 08 00 00 00 00 00 00 00 00 08 04 0a 0b 00 00 00 00 00 00 00 |
recovered P2 from P1 |
pay=9000;to=admin;ok=yes |
secret toy H |
0x2b |
recovered toy H |
0x2b |
secret toy mask S |
0xb3 |
recovered toy mask S |
0xb3 |
| forged plaintext byte | ! |
| forged tag | 0x0f |
The recovered H is not an implementation accident. In the one-byte toy
GHASH:
For two chosen one-byte encryptions under the same nonce:
\[T_a \oplus T_b = (C_a \oplus C_b) \cdot H.\]So:
\[H = (T_a \oplus T_b) / (C_a \oplus C_b).\]Real GCM uses a much larger field and a fuller polynomial, but RFC 5116 and NIST’s appendix are warning about the same kind of break: repeated nonces expose authentication structure that was meant to be one-time.
What the Audit Checks
The lab exports the same checks to Node. It verifies:
- the repeated nonce reuses the toy keystream prefix;
- a different nonce changes the toy keystream;
C1 xor C2equalsP1 xor P2;- knowing
P1recoversP2; - the toy GHASH recovery finds the secret
H; - the toy mask recovery finds the secret
S; - the forged one-byte ciphertext verifies under the real toy decryptor;
- tampering with an old ciphertext while reusing its old tag fails.
The deterministic audit grid is:
| Scenario | Seed | Nonce | Bytes | Secret H |
Recovered H |
Forged byte |
|---|---|---|---|---|---|---|
| payment | 41 | 7 | 24 | 43 | 43 | 33 |
| role | 12 | 3 | 29 | 136 | 136 | 65 |
| sensor | 99 | 201 | 28 | 111 | 111 | 90 |
| payment | 123 | 0 | 24 | 244 | 244 | 36 |
| role | 777 | 255 | 29 | 60 | 60 | 126 |
Reproduce it from the repository root:
node - <<'NODE'
const lab = require("./assets/js/gcm-nonce-lab.js");
const EXPECTED_CASE_SHAPE = [
"1:payment:seed=41:nonce=7:len=24:xor=24:H=43/43:S=179/179:forge=33:tag=15:9/9",
"2:role:seed=12:nonce=3:len=29:xor=29:H=136/136:S=105/105:forge=65:tag=87:9/9",
"3:sensor:seed=99:nonce=201:len=28:xor=28:H=111/111:S=71/71:forge=90:tag=165:9/9",
"4:payment:seed=123:nonce=0:len=24:xor=24:H=244/244:S=9/9:forge=36:tag=40:9/9",
"5:role:seed=777:nonce=255:len=29:xor=29:H=60/60:S=233/233:forge=126:tag=25:9/9"
];
const EXPECTED_SCENARIO_SHAPE = [
"payment:cases=2:maxBytes=24:passed=2:18/18",
"role:cases=2:maxBytes=29:passed=2:18/18",
"sensor:cases=1:maxBytes=28:passed=1:9/9"
];
const EXPECTED_CRITICAL_SHAPE = [
"ciphertext xor equals plaintext xor:5/5:ok=true",
"different nonce changes the toy keystream:5/5:ok=true",
"forged one-byte tag verifies:5/5:ok=true",
"known first plaintext recovers the second:5/5:ok=true",
"same nonce reuses the toy keystream prefix:5/5:ok=true",
"secret hash key is nonzero:5/5:ok=true",
"tampering without a new tag fails:5/5:ok=true",
"toy GHASH recovery finds H:5/5:ok=true",
"toy mask recovery finds S:5/5:ok=true"
];
const EXPECTED_CASES = EXPECTED_CASE_SHAPE.length;
const EXPECTED_CHECKS = EXPECTED_CASES * EXPECTED_CRITICAL_SHAPE.length;
function sameList(actual, expected) {
return actual.length === expected.length &&
actual.every((value, index) => value === expected[index]);
}
function caseShape(row) {
return `${row.caseId}:${row.scenario}:seed=${row.seed}:nonce=${row.nonce}:` +
`len=${row.length}:xor=${row.xorBytes}:` +
`H=${row.hashKey}/${row.recoveredHashKey}:S=${row.mask}/${row.recoveredMask}:` +
`forge=${row.forgedByte}:tag=${row.forgedTag}:` +
`${row.passedChecks}/${row.totalChecks}`;
}
function scenarioShape(row) {
return `${row.scenario}:cases=${row.cases}:maxBytes=${row.maxBytes}:` +
`passed=${row.passed}:${row.passedChecks}/${row.totalChecks}`;
}
function criticalShape(row) {
return `${row.check}:${row.passed}/${row.total}:ok=${row.ok}`;
}
const audit = lab.auditGrid();
const failed = audit.cases.filter(
(row) => !row.passed || row.passedChecks !== row.totalChecks
);
const shape = {
byScenario: audit.byScenario.map(scenarioShape),
cases: audit.cases.map(caseShape),
criticalChecks: audit.criticalChecks.map(criticalShape)
};
const shapeErrors = [];
if (!sameList(shape.cases, EXPECTED_CASE_SHAPE)) {
shapeErrors.push({ name: "cases", actual: shape.cases, expected: EXPECTED_CASE_SHAPE });
}
if (!sameList(shape.byScenario, EXPECTED_SCENARIO_SHAPE)) {
shapeErrors.push({
name: "byScenario",
actual: shape.byScenario,
expected: EXPECTED_SCENARIO_SHAPE
});
}
if (!sameList(shape.criticalChecks, EXPECTED_CRITICAL_SHAPE)) {
shapeErrors.push({
name: "criticalChecks",
actual: shape.criticalChecks,
expected: EXPECTED_CRITICAL_SHAPE
});
}
console.table(audit.cases.map((row) => ({
scenario: row.scenario,
seed: row.seed,
nonce: row.nonce,
bytes: row.length,
hashKey: row.hashKey,
recoveredHashKey: row.recoveredHashKey,
mask: row.mask,
recoveredMask: row.recoveredMask,
forgedByte: row.forgedByte,
forgedTag: row.forgedTag,
passed: row.passed
})));
console.table(audit.byScenario);
console.table(audit.criticalChecks);
const summary =
`${audit.passed}/${audit.total} cases and ${audit.passedChecks}/${audit.totalChecks} checks passed`;
if (
shapeErrors.length ||
failed.length ||
!audit.ok ||
audit.checked !== EXPECTED_CHECKS ||
audit.total !== EXPECTED_CASES ||
audit.totalChecks !== EXPECTED_CHECKS ||
audit.passed !== audit.total ||
audit.passedChecks !== audit.totalChecks
) {
throw new Error(JSON.stringify({
summary,
shapeErrors,
failed,
errors: audit.errors,
totals: {
checked: audit.checked,
total: audit.total,
passed: audit.passed,
passedChecks: audit.passedChecks,
totalChecks: audit.totalChecks
}
}, null, 2));
}
console.log(summary);
NODE
What This Toy Leaves Out
This lab does not implement:
- AES;
- 128-bit blocks;
- real GHASH length encoding;
- associated data;
- tag truncation;
- side-channel behavior;
- multi-block key-recovery algebra.
Those omissions are deliberate. The post is about the contract, not a production exploit. The real contract is stricter than the toy:
unique nonce per key, across all encryptions
NIST also recommends restricting GCM IV support to 96-bit IVs for efficiency and interoperability, while giving deterministic and random construction frameworks for meeting the uniqueness requirement.1 In March 2024, NIST announced that SP 800-38D will be revised, including removal of support for tags shorter than 96 bits and clarification of IV guidance.4
If unique nonces cannot be provided with assurance, use a construction designed for nonce misuse resistance. RFC 8452 specifies AES-GCM-SIV and says its goal is not to fail catastrophically if a nonce repeats.5 That does not mean nonce discipline stops mattering. It means the algorithm was designed around a different misuse story.
The practical checklist is short:
- Make nonce allocation part of the protocol state, not an optional parameter.
- Partition nonce space across devices, processes, shards, and restarts.
- Persist counters far enough ahead that power loss cannot replay one.
- Rotate keys before counter space or security limits are approached.
- Treat one observed GCM nonce collision under a key as a key-compromise event, not as a single bad packet.
The nonce is small. The state machine around it is not.
-
Morris Dworkin, “Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC”, NIST SP 800-38D, 2007. Section 8 states the IV/key uniqueness requirement; Section 5 recommends 96-bit IVs. ↩ ↩2
-
NIST SP 800-38D, Appendix A, “Importance of the Uniqueness Requirement on IVs.” The appendix summarizes the counter-mode confidentiality problem and the GHASH subkey/forgery consequence of repeated IVs. ↩
-
David McGrew, “An Interface and Algorithms for Authenticated Encryption”, RFC 5116, 2008. Section 5.1.1 describes GCM nonce-reuse consequences. ↩
-
NIST, “NIST to Revise Special Publication 800-38D”, March 5, 2024. ↩
-
Shay Gueron, Adam Langley, and Yehuda Lindell, “AES-GCM-SIV: Nonce Misuse-Resistant Authenticated Encryption”, RFC 8452, 2019. ↩