|$ curl https://forge-ai.dev/api/markdown?path=docs/nodejs/crypto
$cat docs/crypto.md
updated Today·20-28 min read·published

Crypto

Node.jsCryptoSecurityIntermediate🎯Free Tools
Introduction

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.

Hashing & HMAC

Use createHash for integrity checksums and createHmac for keyed authentication. Prefer SHA-256+; avoid MD5/SHA-1 for security.

hash.mjs
JavaScript
1import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
3export function sha256(buf) {
4 return createHash("sha256").update(buf).digest("hex");
5}
6
7export function hmac(secret, payload) {
8 return createHmac("sha256", secret).update(payload).digest("hex");
9}
10
11export 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}
Secure Random

Use randomBytes / randomUUID — never Math.random for tokens.

random.mjs
JavaScript
1import { randomBytes, randomUUID } from "node:crypto";
2const token = randomBytes(32).toString("base64url");
3const id = randomUUID();
Authenticated Encryption

Prefer AES-256-GCM. Always store iv + authTag with ciphertext. Never reuse (key, iv) pairs.

gcm.mjs
JavaScript
1import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
3export 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
11export 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}
Password Hashing

Use scrypt or argon2 (via package). Never store unsalted SHA hashes of passwords.

scrypt.mjs
JavaScript
1import { scrypt, randomBytes, timingSafeEqual } from "node:crypto";
2import { promisify } from "node:util";
3const scryptAsync = promisify(scrypt);
4
5export 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
11export 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}
Web Crypto

globalThis.crypto.subtle aligns with browser APIs — useful for isomorphic code.

webcrypto.mjs
JavaScript
1const key = await crypto.subtle.generateKey(
2 { name: "AES-GCM", length: 256 },
3 true,
4 ["encrypt", "decrypt"],
5);

best practice

Use timingSafeEqual for secret comparisons; never === on hashes.
Mental Model

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?

QuestionHealthySmell
CPU burnWorker / child / addonSync loops on request path
I/O waitAsync fs/net/httpSync fs in handlers
Crash survivalExternal storeRAM-only state
ShutdownDrain + closeHard exit mid-request
API Habits

Prefer promise APIs for crypto. Use node: import prefixes. Avoid mixing callback and async styles in one function.

habits.mjs
JavaScript
1export 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
11export 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

Pin Node LTS via engines and CI. Behavior for crypto can change across majors.
Error Handling Patterns

Operational errors are expected at runtime (network, ENOENT). Programmer errors are bugs. Do not empty-catch.

errors.mjs
JavaScript
1export 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
11export 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}
KindExampleAction
OperationalECONNRESETRetry/backoff or 502
Programmerundefined pathThrow; fix
OperationalENOSPCAlert + fail write
Handles, Timeouts & Leaks

Servers, sockets, watchers, and timers keep the process alive. Close them on shutdown. Always set timeouts on outbound work.

timeouts.mjs
JavaScript
1export 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

Uncleared intervals prevent clean exit in tests involving crypto.
Concurrency Limits

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.

limit.mjs
JavaScript
1export 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
23const limit = pLimit(8);
24export async function mapPool(items, fn) {
25 return Promise.all(items.map((item) => limit(() => fn(item))));
26}
Testing

Exercise crypto with node:test, ephemeral ports, and deterministic fixtures.

test.mjs
JavaScript
1import { describe, it } from "node:test";
2import assert from "node:assert/strict";
3
4describe("crypto", () => {
5 it("smoke", () => {
6 assert.equal(1 + 1, 2);
7 });
8});
🔥

pro tip

Always close servers/sockets in after hooks.
Observability

Emit structured logs, metrics (request rate, error rate, saturation), and traces. Correlate with a request id stored in AsyncLocalStorage.

als.mjs
JavaScript
1import { AsyncLocalStorage } from "node:async_hooks";
2
3export const requestContext = new AsyncLocalStorage();
4
5export function withRequest(ctx, fn) {
6 return requestContext.run(ctx, fn);
7}
8
9export function getRequestId() {
10 return requestContext.getStore()?.requestId ?? "unknown";
11}

info

Production Checklist
  • 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

Fail closed on missing required config at boot.
Common Mistakes
MistakeWhyFix
Sync I/O on requestsBlocks loopAsync / workers
Swallow errorsSilent lossLog + propagate
No timeoutsHung socketsAbortSignal
Unbounded fan-outOOM / FD limitp-limit
Trust raw inputCrashesZod validate
Hard exit502 on deployGraceful drain
Security Notes

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

Never log raw Authorization headers, cookies, or private keys.
Performance Notes

Measure before optimizing crypto. Use node --cpu-prof, clinic.js, or OpenTelemetry. Watch event-loop delay metrics.

el-delay.mjs
JavaScript
1import { monitorEventLoopDelay } from "node:perf_hooks";
2
3const h = monitorEventLoopDelay({ resolution: 20 });
4h.enable();
5setInterval(() => {
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();
FAQ

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.

agent.sh
Bash
1curl -s "https://forgelearn.dev/api/markdown?path=nodejs/crypto"
2curl -s https://forgelearn.dev/api/agent?curriculum=nodejs
3curl -s https://forgelearn.dev/api/agent?skill=forgelearn-nodejs
Practical Recipes

Retry with jitter

Transient failures around crypto often need bounded retries. Cap attempts and use full-jitter backoff.

retry.mjs
JavaScript
1export 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.

circuit.mjs
JavaScript
1export 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.

singleton.mjs
JavaScript
1let clientPromise;
2export function getClient(factory) {
3 if (!clientPromise) clientPromise = factory();
4 return clientPromise;
5}
Anti-Patterns Deep Dive

These anti-patterns show up repeatedly in Node services touching crypto. Agents should fail closed if generated code exhibits them.

Anti-patternSymptomReplace with
Callback hell + ignored errSilent wrong resultsasync/await + throw
Global mutable singleton raceFlaky testsDI + ALS context
Fire-and-forget promisesUnhandled rejectionvoid track(promise)
Busy spin wait100% CPUevents / condition vars
String-built shellRCEspawn args array
Unbounded cache MapOOMLRU with TTL
track.mjs
JavaScript
1export 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

In reviews, search for empty catch blocks and missing await — both are high-signal smells.
Interop & Version Notes

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
version-matrix.sh
Bash
1node -p process.versions
2npm ls --depth=0
3# CI matrix: 20.x LTS, 22.x LTS
Agent Generation Checklist

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

Do not advance curriculum stages after reading titles only — fetch full markdown and verify with runnable code.

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.