"Cryptographic Failures" is the second item in the OWASP Top 10, and it is rarely about broken mathematics. It is about choosing the wrong primitive, reusing a nonce, hard-coding a key, or forgetting to encrypt at all. The previous lesson explained how encryption works; this one shows how to apply it correctly.
After this lesson you will be able to classify data that needs protection, encrypt it with an authenticated cipher in Node.js, generate secure random values, and manage keys so one leak does not expose everything.
OWASP groups several distinct mistakes under this heading:
Math.random() for tokens, predictable IVs, reused nonces.Encryption has a cost, so decide what actually needs it. A simple three-tier classification is enough for most applications:
| Tier | Examples | Requirement | |---|---|---| | Public | Product catalogue, blog posts | TLS in transit only | | Internal | User IDs, order history | TLS in transit, access control at rest | | Sensitive | Passwords, card data, government IDs, health data | TLS in transit, encrypted or hashed at rest, audit logged |
Passwords are a category of their own: hashed with Argon2id or bcrypt, never encrypted, because you must never be able to recover them.
For data you must read back later (a third-party API key, a stored document), use AES-256-GCM. GCM produces an authentication tag, so tampering is detected on decryption. The sample code above shows the full pattern; the important details are:
decipher.final() throws if the tag does not match, so tampering fails loudly.Never use aes-256-ecb (identical blocks give identical output) or CBC without a separate MAC.
Session IDs, password reset tokens and CSRF tokens must be unpredictable. Math.random() is seeded from the clock and can be reconstructed by an observer.
const crypto = require("node:crypto");
const resetToken = crypto.randomBytes(32).toString("hex"); // 256 bits of entropy
const id = crypto.randomUUID(); // v4 UUID from a CSPRNG
const pin = crypto.randomInt(0, 1_000_000).toString().padStart(6, "0");
console.log(resetToken.length, id, pin); // 64 chars, a UUID, a 6-digit PINStore the hash of a reset token and send the original to the user; a leaked database then yields nothing usable.
A normal === comparison returns as soon as the first byte differs. Over many requests that timing difference reveals a secret byte by byte.
function safeEqual(a, b) {
const bufA = Buffer.from(a), bufB = Buffer.from(b);
if (bufA.length !== bufB.length) return false;
return crypto.timingSafeEqual(bufA, bufB);
}Use this whenever you verify webhook signatures, API keys or one-time codes.
The strongest cipher is useless if the key sits in the repository. Load keys from an environment variable injected by a secrets manager or a cloud KMS, use different keys per environment, and plan rotation from day one by storing a key ID with each ciphertext:
{ "kid": "2026-03", "iv": "…", "tag": "…", "data": "…" }On rotation, new records use the new key while old ones stay readable until a job re-encrypts them.
Why must the IV be unique for every message when using AES-GCM?
crypto.randomBytes, randomUUID and randomInt for anything secret; never Math.random().timingSafeEqual, keep keys out of the repo, and store a key ID for rotation.Next lesson: Network Security Basics: Firewalls, VPNs and Ports — how traffic is controlled at the network layer before it reaches your code.