NodeJSThe crypto Module

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():

JS
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)
`Math.random()` is NOT cryptographically secure
`Math.random()` is a fast, *predictable* pseudo-random generator — its output can be reverse-engineered. Never use it for session tokens, password-reset links, API keys, or anything an attacker shouldn't guess. Use `crypto.randomBytes`, `crypto.randomUUID`, or `crypto.randomInt`, which draw from the OS's cryptographically-secure entropy source.
Hashing

A hash is a one-way fingerprint of data — used for integrity checks, caching keys, and deduplication (not for passwords, see below):

JS
import { createHash } from 'node:crypto'

const hash = createHash('sha256')
  .update('hello world')
  .digest('hex')
// → 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'

Algorithm

Use for

Avoid for

sha256 / sha512

Integrity, checksums, ETags

md5

Legacy non-security checksums only

Anything security — it is broken

sha1

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:

JS
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))
Compare secrets with `timingSafeEqual`, not `===`
`a === b` on strings short-circuits at the first differing character, so the *time it takes* leaks how many leading characters matched — a timing attack that can recover a secret. `crypto.timingSafeEqual` compares in constant time regardless of where they differ. Always use it for signatures, tokens, and MAC verification.
Passwords: never plain hashing
Do NOT store passwords with sha256/md5
Fast hashes are the *wrong* tool for passwords — their speed lets attackers try billions of guesses per second against a stolen database. Use a deliberately *slow*, salted, memory-hard algorithm: `crypto.scrypt` (built in) or the `argon2`/`bcrypt` packages. Each password gets a unique random salt, stored alongside the hash.

Password hashing with built-in scrypt

JS
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:

JS
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 decrypt
Never reuse an IV, never hardcode a key
Reusing an IV with the same key under GCM catastrophically breaks the cipher — generate a fresh random IV for *every* message. And keys belong in a secrets manager or environment variable, never in source code. Key handling is covered in [Dependency & Secret Security](/nodejs/dependency-security).
The Web Crypto API
`crypto.subtle` is the cross-platform alternative
Node also implements the standard **Web Crypto API** at `globalThis.crypto.subtle` — the same promise-based interface browsers and Deno expose. For isomorphic code that must run in multiple runtimes, prefer it over the Node-specific `createHash`/`createCipheriv` API.
Golden rules
  • Random secretscrypto.randomBytes/randomUUID, never Math.random().

  • Passwords → slow + salted (scrypt, argon2, bcrypt), never sha256.

  • Comparing secretstimingSafeEqual, 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.

Next
The pattern that powers Node's whole async architecture — emitters and listeners: [The events Module](/nodejs/events-module).