Crypto
The node:crypto module provides hashing, HMAC, ciphers, key generation, random bytes, and Web Crypto (globalThis.crypto.subtle). Prefer well-known constructions; never invent protocols.
Use createHash for integrity checksums and createHmac for keyed authentication. Prefer SHA-256+; avoid MD5/SHA-1 for security.
| 1 | import { createHash, createHmac, timingSafeEqual } from "node:crypto"; |
| 2 | |
| 3 | export function sha256(buf) { |
| 4 | return createHash("sha256").update(buf).digest("hex"); |
| 5 | } |
| 6 | |
| 7 | export function hmac(secret, payload) { |
| 8 | return createHmac("sha256", secret).update(payload).digest("hex"); |
| 9 | } |
| 10 | |
| 11 | export function safeEqualHex(a, b) { |
| 12 | const ba = Buffer.from(a, "hex"); |
| 13 | const bb = Buffer.from(b, "hex"); |
| 14 | return ba.length === bb.length && timingSafeEqual(ba, bb); |
| 15 | } |
Use randomBytes / randomUUID — never Math.random for tokens.
| 1 | import { randomBytes, randomUUID } from "node:crypto"; |
| 2 | const token = randomBytes(32).toString("base64url"); |
| 3 | const id = randomUUID(); |
Prefer AES-256-GCM. Always store iv + authTag with ciphertext. Never reuse (key, iv) pairs.
| 1 | import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"; |
| 2 | |
| 3 | export function encrypt(key, plaintext) { |
| 4 | const iv = randomBytes(12); |
| 5 | const cipher = createCipheriv("aes-256-gcm", key, iv); |
| 6 | const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); |
| 7 | const tag = cipher.getAuthTag(); |
| 8 | return Buffer.concat([iv, tag, enc]); |
| 9 | } |
| 10 | |
| 11 | export function decrypt(key, blob) { |
| 12 | const iv = blob.subarray(0, 12); |
| 13 | const tag = blob.subarray(12, 28); |
| 14 | const data = blob.subarray(28); |
| 15 | const decipher = createDecipheriv("aes-256-gcm", key, iv); |
| 16 | decipher.setAuthTag(tag); |
| 17 | return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf8"); |
| 18 | } |
Use scrypt or argon2 (via package). Never store unsalted SHA hashes of passwords.
| 1 | import { scrypt, randomBytes, timingSafeEqual } from "node:crypto"; |
| 2 | import { promisify } from "node:util"; |
| 3 | const scryptAsync = promisify(scrypt); |
| 4 | |
| 5 | export async function hashPassword(password) { |
| 6 | const salt = randomBytes(16); |
| 7 | const hash = await scryptAsync(password, salt, 64); |
| 8 | return `${salt.toString("hex")}:${hash.toString("hex")}`; |
| 9 | } |
| 10 | |
| 11 | export async function verifyPassword(password, stored) { |
| 12 | const [saltHex, hashHex] = stored.split(":"); |
| 13 | const hash = await scryptAsync(password, Buffer.from(saltHex, "hex"), 64); |
| 14 | return timingSafeEqual(hash, Buffer.from(hashHex, "hex")); |
| 15 | } |
globalThis.crypto.subtle aligns with browser APIs — useful for isomorphic code.
| 1 | const key = await crypto.subtle.generateKey( |
| 2 | { name: "AES-GCM", length: 256 }, |
| 3 | true, |
| 4 | ["encrypt", "decrypt"], |
| 5 | ); |
best practice
Treat crypto as a contract with the OS and the Node.js event loop. Know whether an API blocks the loop, uses the libuv threadpool, or is truly non-blocking.
Debug questions: Am I holding the event loop? Leaking handles? Sharing state across processes? Handling partial failure? Timing out outbound I/O?
| Question | Healthy | Smell |
|---|---|---|
| CPU burn | Worker / child / addon | Sync loops on request path |
| I/O wait | Async fs/net/http | Sync fs in handlers |
| Crash survival | External store | RAM-only state |
| Shutdown | Drain + close | Hard exit mid-request |
Prefer promise APIs for crypto. Use node: import prefixes. Avoid mixing callback and async styles in one function.
| 1 | export function requireEnv(name) { |
| 2 | const v = process.env[name]; |
| 3 | if (!v) { |
| 4 | const err = new Error(`Missing env ${name}`); |
| 5 | err.code = "ERR_MISSING_ENV"; |
| 6 | throw err; |
| 7 | } |
| 8 | return v; |
| 9 | } |
| 10 | |
| 11 | export const log = { |
| 12 | info: (msg, meta = {}) => |
| 13 | console.log(JSON.stringify({ level: "info", msg, ...meta, t: Date.now() })), |
| 14 | error: (msg, meta = {}) => |
| 15 | console.error(JSON.stringify({ level: "error", msg, ...meta, t: Date.now() })), |
| 16 | }; |
note
Operational errors are expected at runtime (network, ENOENT). Programmer errors are bugs. Do not empty-catch.
| 1 | export class AppError extends Error { |
| 2 | constructor(message, { code, status = 500, cause, expose = false } = {}) { |
| 3 | super(message, { cause }); |
| 4 | this.name = "AppError"; |
| 5 | this.code = code; |
| 6 | this.status = status; |
| 7 | this.expose = expose; |
| 8 | } |
| 9 | } |
| 10 | |
| 11 | export function mapError(err) { |
| 12 | if (err?.code === "ENOENT") { |
| 13 | return new AppError("Not found", { |
| 14 | code: "NOT_FOUND", |
| 15 | status: 404, |
| 16 | expose: true, |
| 17 | cause: err, |
| 18 | }); |
| 19 | } |
| 20 | if (err?.code === "ETIMEDOUT") { |
| 21 | return new AppError("Upstream timeout", { |
| 22 | code: "TIMEOUT", |
| 23 | status: 504, |
| 24 | expose: true, |
| 25 | cause: err, |
| 26 | }); |
| 27 | } |
| 28 | return err; |
| 29 | } |
| Kind | Example | Action |
|---|---|---|
| Operational | ECONNRESET | Retry/backoff or 502 |
| Programmer | undefined path | Throw; fix |
| Operational | ENOSPC | Alert + fail write |
Servers, sockets, watchers, and timers keep the process alive. Close them on shutdown. Always set timeouts on outbound work.
| 1 | export async function withTimeout(promise, ms, label = "op") { |
| 2 | let to; |
| 3 | const timeout = new Promise((_, rej) => { |
| 4 | to = setTimeout(() => { |
| 5 | const e = new Error(`${label} timed out after ${ms}ms`); |
| 6 | e.code = "ETIMEDOUT"; |
| 7 | rej(e); |
| 8 | }, ms); |
| 9 | }); |
| 10 | try { |
| 11 | return await Promise.race([promise, timeout]); |
| 12 | } finally { |
| 13 | clearTimeout(to); |
| 14 | } |
| 15 | } |
warning
Node can open thousands of async operations. Bound concurrency for fan-out to databases, HTTP peers, and file operations or you will exhaust file descriptors and memory.
| 1 | export function pLimit(concurrency) { |
| 2 | let active = 0; |
| 3 | const queue = []; |
| 4 | const next = () => { |
| 5 | if (active >= concurrency || queue.length === 0) return; |
| 6 | active++; |
| 7 | const { fn, resolve, reject } = queue.shift(); |
| 8 | Promise.resolve() |
| 9 | .then(fn) |
| 10 | .then(resolve, reject) |
| 11 | .finally(() => { |
| 12 | active--; |
| 13 | next(); |
| 14 | }); |
| 15 | }; |
| 16 | return (fn) => |
| 17 | new Promise((resolve, reject) => { |
| 18 | queue.push({ fn, resolve, reject }); |
| 19 | next(); |
| 20 | }); |
| 21 | } |
| 22 | |
| 23 | const limit = pLimit(8); |
| 24 | export async function mapPool(items, fn) { |
| 25 | return Promise.all(items.map((item) => limit(() => fn(item)))); |
| 26 | } |
Exercise crypto with node:test, ephemeral ports, and deterministic fixtures.
| 1 | import { describe, it } from "node:test"; |
| 2 | import assert from "node:assert/strict"; |
| 3 | |
| 4 | describe("crypto", () => { |
| 5 | it("smoke", () => { |
| 6 | assert.equal(1 + 1, 2); |
| 7 | }); |
| 8 | }); |
pro tip
Emit structured logs, metrics (request rate, error rate, saturation), and traces. Correlate with a request id stored in AsyncLocalStorage.
| 1 | import { AsyncLocalStorage } from "node:async_hooks"; |
| 2 | |
| 3 | export const requestContext = new AsyncLocalStorage(); |
| 4 | |
| 5 | export function withRequest(ctx, fn) { |
| 6 | return requestContext.run(ctx, fn); |
| 7 | } |
| 8 | |
| 9 | export function getRequestId() { |
| 10 | return requestContext.getStore()?.requestId ?? "unknown"; |
| 11 | } |
info
- Document engines.node and run CI on that version
- Structured JSON logs with request ids
- SIGTERM drain + health/readiness probes
- Timeouts on every outbound dependency
- Redact secrets from logs
- Load-test the crypto hot path
- OpenTelemetry traces for latency
best practice
| Mistake | Why | Fix |
|---|---|---|
| Sync I/O on requests | Blocks loop | Async / workers |
| Swallow errors | Silent loss | Log + propagate |
| No timeouts | Hung sockets | AbortSignal |
| Unbounded fan-out | OOM / FD limit | p-limit |
| Trust raw input | Crashes | Zod validate |
| Hard exit | 502 on deploy | Graceful drain |
Anything touching paths, URLs, child processes, crypto, or network input needs validation. Never interpolate untrusted strings into shells or file paths near crypto.
- Validate and normalize paths with path.resolve + root jail
- Prefer spawn with argument arrays over shell exec
- Use timingSafeEqual for secret compares
- Set restrictive CORS and security headers on HTTP APIs
danger
Measure before optimizing crypto. Use node --cpu-prof, clinic.js, or OpenTelemetry. Watch event-loop delay metrics.
| 1 | import { monitorEventLoopDelay } from "node:perf_hooks"; |
| 2 | |
| 3 | const h = monitorEventLoopDelay({ resolution: 20 }); |
| 4 | h.enable(); |
| 5 | setInterval(() => { |
| 6 | console.log({ |
| 7 | meanMs: (h.mean / 1e6).toFixed(2), |
| 8 | p99Ms: (h.percentile(99) / 1e6).toFixed(2), |
| 9 | }); |
| 10 | h.reset(); |
| 11 | }, 5000).unref(); |
When do I use this topic?
Reach for crypto when its responsibility matches the problem. Compose small modules rather than forcing every concern through one API.
Cross-platform?
Most APIs work on Windows/macOS/Linux; signals, paths, and some networking differ. Test where you deploy.
Agent verification
Generate a runnable example, run tests, fail closed if timeouts/errors/shutdown are missing.
| 1 | curl -s "https://forgelearn.dev/api/markdown?path=nodejs/crypto" |
| 2 | curl -s https://forgelearn.dev/api/agent?curriculum=nodejs |
| 3 | curl -s https://forgelearn.dev/api/agent?skill=forgelearn-nodejs |
Retry with jitter
Transient failures around crypto often need bounded retries. Cap attempts and use full-jitter backoff.
| 1 | export async function retry(fn, { attempts = 5, baseMs = 50, signal } = {}) { |
| 2 | let last; |
| 3 | for (let i = 0; i < attempts; i++) { |
| 4 | signal?.throwIfAborted?.(); |
| 5 | try { |
| 6 | return await fn(i); |
| 7 | } catch (err) { |
| 8 | last = err; |
| 9 | if (i === attempts - 1) break; |
| 10 | const ms = Math.random() * baseMs * 2 ** i; |
| 11 | await new Promise((r) => setTimeout(r, ms)); |
| 12 | } |
| 13 | } |
| 14 | throw last; |
| 15 | } |
Circuit breaker sketch
Trip open after consecutive failures; half-open after a cool-down to probe recovery.
| 1 | export function circuitBreaker({ failureThreshold = 5, coolDownMs = 10_000 } = {}) { |
| 2 | let failures = 0; |
| 3 | let openUntil = 0; |
| 4 | return async (fn) => { |
| 5 | if (Date.now() < openUntil) { |
| 6 | const e = new Error("circuit open"); |
| 7 | e.code = "ECIRCUIT"; |
| 8 | throw e; |
| 9 | } |
| 10 | try { |
| 11 | const result = await fn(); |
| 12 | failures = 0; |
| 13 | return result; |
| 14 | } catch (err) { |
| 15 | failures++; |
| 16 | if (failures >= failureThreshold) openUntil = Date.now() + coolDownMs; |
| 17 | throw err; |
| 18 | } |
| 19 | }; |
| 20 | } |
Idempotent boot
Initialize clients once; reuse across requests. Lazy-init with a promise singleton.
| 1 | let clientPromise; |
| 2 | export function getClient(factory) { |
| 3 | if (!clientPromise) clientPromise = factory(); |
| 4 | return clientPromise; |
| 5 | } |
These anti-patterns show up repeatedly in Node services touching crypto. Agents should fail closed if generated code exhibits them.
| Anti-pattern | Symptom | Replace with |
|---|---|---|
| Callback hell + ignored err | Silent wrong results | async/await + throw |
| Global mutable singleton race | Flaky tests | DI + ALS context |
| Fire-and-forget promises | Unhandled rejection | void track(promise) |
| Busy spin wait | 100% CPU | events / condition vars |
| String-built shell | RCE | spawn args array |
| Unbounded cache Map | OOM | LRU with TTL |
| 1 | export function track(promise, label = "bg") { |
| 2 | promise.catch((err) => { |
| 3 | console.error(JSON.stringify({ level: "error", msg: "background failure", label, err: String(err) })); |
| 4 | }); |
| 5 | } |
pro tip
APIs around crypto evolve across Node majors. Read changelogs when upgrading. Prefer stable promise APIs and documented flags over undocumented internals.
- Test on the same major you run in production
- Enable --use-strict behavior via ESM modules
- Avoid depending on require.extensions hacks
- Document any experimental flags in README and CI
| 1 | node -p process.versions |
| 2 | npm ls --depth=0 |
| 3 | # CI matrix: 20.x LTS, 22.x LTS |
Before claiming mastery of crypto, generated artifacts must pass this checklist.
- Uses node: import prefixes where applicable
- Errors have codes and are not swallowed
- Outbound I/O has timeouts / AbortSignal
- Resources closed in finally / shutdown
- No shell interpolation of user input
- Tests cover at least one failure path
- Logs are structured; secrets redacted
danger
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.