← thecodex.expert · The Codex Family of Knowledge
The Codex / JavaScript / crypto — Cryptography

crypto — Cryptography

Secure hashing, HMACs, random data, UUIDs, and encryption in Node.js — the crypto built-in module.

What it is, in plain English: The crypto module provides cryptographic functionality in Node.js. Use crypto.randomBytes() for cryptographically secure random data. Use crypto.randomUUID() for UUIDs. createHash() creates MD5, SHA-1, SHA-256 hashes. createHmac() creates signed hashes for API authentication. For passwords specifically, use scrypt or bcrypt (from npm) — never plain SHA hashes.
Where this bites you

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 signed

Secure random data and hashing

Never use Math.random() for security. It's not cryptographically secure. Always use 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));  // true

The 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: push
Try It Yourself

Official Sources

The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.