The crypto Module
node:crypto gives you cryptographic primitives: secure random bytes, hashing, HMACs, password hashing, symmetric and asymmetric encryption, and digital signatures. It wraps OpenSSL, so it is fast and battle-tested. The danger is not the API — it's misusing it, so this page leans heavily on what is safe versus what quietly is not.
Secure random values
For anything security-sensitive — tokens, salts, IDs — use crypto's generators, never Math.random():
import { randomBytes, randomUUID, randomInt } from 'node:crypto'
randomUUID() // 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
randomBytes(32).toString('hex') // 64-char hex token (256 bits)
randomInt(1, 100) // a uniform integer in [1, 100)Hashing
A hash is a one-way fingerprint of data — used for integrity checks, caching keys, and deduplication (not for passwords, see below):
import { createHash } from 'node:crypto'
const hash = createHash('sha256')
.update('hello world')
.digest('hex')
// → 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'Algorithm | Use for | Avoid for |
|---|---|---|
| Integrity, checksums, ETags | — |
| Legacy non-security checksums only | Anything security — it is broken |
| Legacy compatibility only | Security — collisions are practical |
HMAC — keyed hashing
An HMAC proves a message came from someone holding a shared secret — used for webhook signatures and API request signing:
import { createHmac, timingSafeEqual } from 'node:crypto'
const sign = (body, secret) =>
createHmac('sha256', secret).update(body).digest('hex')
// Verifying an incoming webhook signature — SAFELY:
const expected = sign(requestBody, process.env.WEBHOOK_SECRET)
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(receivedSig))Passwords: never plain hashing
Password hashing with built-in scrypt
import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto'
import { promisify } from 'node:util'
const scryptP = promisify(scrypt)
async function hashPassword(password) {
const salt = randomBytes(16)
const key = await scryptP(password, salt, 64)
return `${salt.toString('hex')}:${key.toString('hex')}`
}
async function verify(password, stored) {
const [saltHex, keyHex] = stored.split(':')
const key = await scryptP(password, Buffer.from(saltHex, 'hex'), 64)
return timingSafeEqual(key, Buffer.from(keyHex, 'hex'))
}Symmetric encryption (AES-GCM)
To encrypt data with a shared key, prefer an authenticated cipher like AES-256-GCM, which both encrypts and detects tampering via an auth tag. Each message needs a fresh random IV:
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'
const key = randomBytes(32) // 256-bit key (store securely!)
const iv = randomBytes(12) // fresh per message
const cipher = createCipheriv('aes-256-gcm', key, iv)
const enc = Buffer.concat([cipher.update('secret', 'utf8'), cipher.final()])
const tag = cipher.getAuthTag() // MUST keep this to decryptThe Web Crypto API
Golden rules
Random secrets →
crypto.randomBytes/randomUUID, neverMath.random().Passwords → slow + salted (
scrypt,argon2,bcrypt), neversha256.Comparing secrets →
timingSafeEqual, never===.Encryption → authenticated cipher (AES-GCM), fresh IV every time.
Don't invent crypto — use vetted primitives and high-level libraries; subtle mistakes are silent and total.