crypto — Cryptography
Secure hashing, HMACs, random data, UUIDs, and encryption in Node.js — the crypto built-in module.
An HMAC signs exact bytes. A webhook receiver that parses the JSON body and then calls JSON.stringify() to check the signature is hashing different bytes: spacing, key order and number formatting can all change in the round trip. Every genuine request then fails verification, and the usual "fix" (turning verification off) is far worse than the bug.
import { createHmac, timingSafeEqual } from "crypto";
function verifyHMAC(received, payload, secret) {
const expected = createHmac("sha256", secret).update(payload).digest("hex");
const a = Buffer.from(received, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
const SECRET = "my-secret";
// what the sender actually sent, byte for byte (note the spaces):
const rawBody = '{"event": "push", "repo": "codex"}';
const signature = createHmac("sha256", SECRET).update(rawBody).digest("hex");
// receiver: parse first, then re-serialise to check the signature
const parsed = JSON.parse(rawBody);
console.log(verifyHMAC(signature, JSON.stringify(parsed), SECRET));
console.log(JSON.stringify(parsed));
// false
// {"event":"push","repo":"codex"} <- not the bytes that were signedSecure random data and hashing
crypto.randomBytes()
or crypto.randomUUID() for anything security-related.
Secure random data
import { randomBytes, randomUUID, randomInt } from "crypto";
// Random bytes (for tokens, salts, keys):
const token = randomBytes(32).toString("hex"); // 64-char hex string
const salt = randomBytes(16).toString("base64"); // 24-char base64 string
console.log("Token:", token); // "a3f8c2..."
console.log("Salt:", salt);
// UUID — universally unique identifier (v4):
const id = randomUUID();
console.log("UUID:", id); // "110e8400-e29b-41d4-a716-446655440000"
// Random integer in range [min, max):
const roll = randomInt(1, 7); // dice: 1-6
console.log("Dice:", roll);Hashing — one-way fingerprinting
import { createHash } from "crypto";
// SHA-256 hash of a string:
function sha256(data) {
return createHash("sha256").update(data, "utf-8").digest("hex");
}
console.log(sha256("hello")); // "2cf24dba..." (always same for same input)
console.log(sha256("hello")); // same
console.log(sha256("Hello")); // completely different — case sensitive!
// SHA-256 of a file:
import { readFileSync } from "fs";
const fileHash = createHash("sha256")
.update(readFileSync("document.pdf"))
.digest("hex");
// Use to verify file integrity (checksums)
// Common hash algorithms:
// sha256 — most common, 256-bit output ✓
// sha512 — stronger, 512-bit output ✓
// sha1 — deprecated, collision-prone ✗
// md5 — broken for security, ok for checksums ✗HMAC — signed hashes for authentication
import { createHmac, timingSafeEqual } from "crypto";
// HMAC = Hash + Secret Key → verifiable signature
const SECRET = process.env.WEBHOOK_SECRET ?? "my-secret";
const payload = JSON.stringify({ event: "push", repo: "codex" });
// Sign:
const signature = createHmac("sha256", SECRET)
.update(payload)
.digest("hex");
console.log("Signature:", signature);
// Verify (ALWAYS use timingSafeEqual — prevents timing attacks):
function verifyHMAC(received, payload, secret) {
const expected = createHmac("sha256", secret).update(payload).digest("hex");
const a = Buffer.from(received, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b); // constant-time comparison
}
console.log(verifyHMAC(signature, payload, SECRET)); // trueThe verifyHMAC() above is correct. What matters is what you pass to it: the raw request body, exactly as it arrived, before any parsing. With a framework, that means asking for the raw body on webhook routes (for example express.raw({ type: "application/json" })). Verify first, then parse:
import { createHmac, timingSafeEqual } from "crypto";
function verifyHMAC(received, payload, secret) {
const expected = createHmac("sha256", secret).update(payload).digest("hex");
const a = Buffer.from(received, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
const SECRET = "my-secret";
const rawBody = '{"event": "push", "repo": "codex"}';
const signature = createHmac("sha256", SECRET).update(rawBody).digest("hex");
// receiver: verify the RAW bytes first, parse only after they check out
if (verifyHMAC(signature, rawBody, SECRET)) {
const event = JSON.parse(rawBody);
console.log("verified:", event.event);
}
// verified: pushOfficial Sources
- Node.js Docs — crypto
- Node.js Docs — Crypto — randomBytes
- Node.js Docs — Web Crypto API
- OWASP — Password Storage Cheat Sheet
The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.