The code review comment I have written most often, and regretted most often, is "use a stronger algorithm."
It is regrettable because it is almost never the actual defect. It is the defect I can see from where I am standing. The team swaps SHA-1 for SHA-256, everyone agrees the review was useful, and the thing that would have mattered stays where it was.
A crypto call has to be right four separate times, and being correct at one layer makes the next one invisible. AES looks like a decision you already made. It is four.
Layer 1 — the primitive. {#layer-1} Right kind of thing? A hash where you needed a
KDF. A cipher where you needed a MAC. Math.random() where you needed a CSPRNG. Everyone
reviews this layer, because it is visible in the function name.
Layer 2 — the mode. {#layer-2} Primitive right, mode wrong. AES in ECB encrypts
identical blocks to identical ciphertext — which is why the famous penguin is still
legible after "encryption." Nothing about aes-256-ecb reads as wrong at a glance; it
contains aes and 256, and both are reassuring.
Layer 3 — the parameters. {#layer-3} Right primitive, right mode, wrong inputs. A static IV. An iteration count set in 2015 and never revisited. RSA padding left at a default that predates the attack it enables. Each is a single argument, usually a constant — and constants do not attract review attention.
Layer 4 — the usage. {#layer-4} Everything above correct, and the surrounding code
gives it away. Comparing an HMAC with === and leaking the answer through timing.
Deriving a key correctly, then logging it. This layer is not crypto code at all, which is
why crypto review misses it.
A rule list tells you what to grep for. The layer tells you where your team is, and therefore what you are not going to catch.
My working hypothesis — and it is a hypothesis, not something I have measured — is that failures cluster by how much crypto attention a team has already had. Never thought about it: layer 1. Just audited: layer 2, because the audit said "upgrade the primitives" and it did exactly that. Careful for years: layers 3 and 4.
If that holds, the uncomfortable corollary is that layer 3 and 4 defects live longest because the code passes every review asking "are we using strong algorithms." The maturity that fixes the first two layers is the same maturity that stops looking further down. That is the measurement I most want from this taxonomy.
Layers 1 and 2 live in a single call expression. createHash("sha1") and aes-256-ecb
are string literals; matching them is syntactic and any linter does it well.
Layers 3 and 4 are not. Whether an IV is static depends on where it came from — a data-flow question, the difference between matching a pattern and following a value. Whether a comparison is timing-unsafe depends on whether the value is a secret, which no analyzer knows from the expression alone.
So tool coverage is inversely correlated with how long a defect survives. The layers a tool covers cleanly are the ones your team already fixes. Read any claim that a tool "covers crypto" as "covers layers 1 and 2" until shown otherwise — and checking that needs a corpus with known answers, not a rule count.
The layers map to distinct CWEs, which is the useful part — the taxonomy already encodes the distinction most reviews collapse:
| Layer | CWE |
|---|---|
| 1 — primitive | CWE-327 risky algorithm; CWE-338 weak PRNG |
| 2 — mode | CWE-327 — ECB has no dedicated CWE |
| 3 — parameters | CWE-1204 weak IV generation; CWE-323 reusing a nonce or IV; CWE-916 insufficient computational effort |
| 4 — usage | CWE-208 observable timing discrepancy |
Filing all four as "weak crypto" is how the layer 3 and 4 instances disappear from your own bug data, and then from your priorities.
Prior art worth reading: Egele et al., An Empirical Study of Cryptographic Misuse in Android Applications (CCS 2013), which found the overwhelming majority of apps using crypto APIs got at least one layer wrong. A decade old, different platform, and the layering has not aged at all. NIST SP 800-175B is the reference for what each primitive is for, and Bernstein's cache-timing work on AES is the clearest statement of why layer 4 is not paranoia.
A model asked for "AES encryption in Node" produces something that runs. Running is the constraint it optimizes, and every one of these layers is invisible to that constraint. ECB runs. A static IV runs. An iteration count of 1000 runs. The code is correct at the layer a reader checks, because it was trained on a corpus where that is what correct looked like.
Same reason the human review misses it. The model read the same tutorials you did.
Which layer does your codebase fail at? The answer tends to say more about when the code was written than who wrote it — layer 2 usually means "audited once, years ago."
I write about measurement and static analysis at dev.to/ofri-peretz.
