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.
Four layers, each hiding the next
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 where someone passed
RSA_PKCS1_PADDING explicitly — Node defaults to OAEP, so this one takes a deliberate
argument to get wrong, which is exactly why it survives review. Each is a single value,
usually a constant, and constants do not attract 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.
Why layers beat a rule list
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.
Detection: the easy layers are the trap
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.
Where this lands in the standards
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.
Why generated code lands on layers 2 and 3
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.
Top comments (0)