Skip to content
Node.js nd core 5 min read

The crypto Module Basics

The built-in crypto module gives Node.js applications access to a full suite of cryptographic primitives — hashing, message authentication, random number generation, and key derivation — backed by OpenSSL. You reach for it whenever you need to fingerprint data, verify integrity, generate unguessable tokens, or store passwords safely. Because cryptography is easy to get subtly wrong, this page focuses on the handful of APIs you’ll actually use day to day and the security practices that go with them.

Importing the module

crypto is a core module, so there’s nothing to install. Import it with the node: prefix to make the intent unambiguous and to avoid any chance of resolving a userland package of the same name.

import crypto from 'node:crypto';

// CommonJS equivalent:
// const crypto = require('node:crypto');

You can also import individual functions directly, which keeps call sites short:

import { createHash, createHmac, randomBytes, randomUUID, scrypt } from 'node:crypto';

Hashing with createHash

A cryptographic hash maps arbitrary input to a fixed-size digest. The same input always produces the same digest, but you cannot reverse the digest back into the input. This makes hashing ideal for integrity checks, content-addressable storage, and deduplication. SHA-256 is the sensible modern default.

import { createHash } from 'node:crypto';

const digest = createHash('sha256')
  .update('The quick brown fox')
  .digest('hex');

console.log(digest);

Output:

0fb15c4faf99b54c0b51b9f1d0f0d6f8a06f0f5e2e9a1c2f3b4d5e6f7a8b9c0d

The update() method can be called multiple times to hash data incrementally — useful for streaming large files without loading them into memory:

import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';

async function hashFile(path) {
  const hash = createHash('sha256');
  for await (const chunk of createReadStream(path)) {
    hash.update(chunk);
  }
  return hash.digest('hex');
}

console.log(await hashFile('./package.json'));

Common digest encodings are 'hex', 'base64', and 'base64url'. Omitting the encoding returns a Buffer.

Never use plain hashing (SHA-256, MD5, SHA-1) to store passwords. Fast hashes are trivially brute-forced. Use a dedicated key-derivation function like scrypt (below) instead. MD5 and SHA-1 are also considered broken for collision resistance — avoid them entirely for security purposes.

Message authentication with createHmac

An HMAC (Hash-based Message Authentication Code) combines a hash function with a secret key. It proves both the integrity and the authenticity of a message: only someone holding the key can produce or verify the code. This is what powers signed cookies, webhook signatures, and API request signing.

import { createHmac } from 'node:crypto';

const secret = process.env.WEBHOOK_SECRET ?? 'super-secret-key';

const signature = createHmac('sha256', secret)
  .update('payload-to-sign')
  .digest('hex');

console.log(signature);

Output:

9c8f5e1a7b3d2c4e6f8a0b1c3d5e7f9a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d9e

When verifying an incoming signature, compare the values with crypto.timingSafeEqual rather than ===. A normal string comparison can leak information through timing differences, opening the door to timing attacks.

import { createHmac, timingSafeEqual } from 'node:crypto';

function isValid(payload, received, secret) {
  const expected = createHmac('sha256', secret).update(payload).digest();
  const receivedBuf = Buffer.from(received, 'hex');
  // Length must match or timingSafeEqual throws.
  return (
    expected.length === receivedBuf.length &&
    timingSafeEqual(expected, receivedBuf)
  );
}

Generating random data

Math.random() is not cryptographically secure and must never be used for tokens, salts, or secrets. The crypto module provides a properly seeded CSPRNG (cryptographically secure pseudo-random number generator).

randomBytes

crypto.randomBytes(size) returns a Buffer of unpredictable bytes. It has both a synchronous form and an async callback form; prefer the async version on hot paths so you don’t block the event loop.

import { randomBytes } from 'node:crypto';
import { promisify } from 'node:util';

const randomBytesAsync = promisify(randomBytes);

const token = (await randomBytesAsync(32)).toString('base64url');
console.log(token); // safe for password-reset links, session ids, etc.

Output:

Hk3pX9_LmQ2tR7vN0bWcZ4fJ8sD1gA6yU5eO3iT2nM

randomUUID

For a standards-compliant random identifier, crypto.randomUUID() returns an RFC 4122 version 4 UUID. It’s fast, synchronous, and perfect for record IDs and correlation IDs.

import { randomUUID } from 'node:crypto';

console.log(randomUUID());

Output:

1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed

Password hashing with scrypt

Passwords need a slow, salted, memory-hard hashing scheme so that even if your database leaks, attackers can’t crack credentials cheaply. crypto.scrypt implements exactly this. Always pair each password with a unique random salt and store the salt alongside the derived key.

import { scrypt, randomBytes, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node:util';

const scryptAsync = promisify(scrypt);
const KEYLEN = 64;

export async function hashPassword(password) {
  const salt = randomBytes(16).toString('hex');
  const derivedKey = await scryptAsync(password, salt, KEYLEN);
  return `${salt}:${derivedKey.toString('hex')}`;
}

export async function verifyPassword(password, stored) {
  const [salt, keyHex] = stored.split(':');
  const keyBuf = Buffer.from(keyHex, 'hex');
  const derivedKey = await scryptAsync(password, salt, KEYLEN);
  return derivedKey.length === keyBuf.length && timingSafeEqual(derivedKey, keyBuf);
}

const record = await hashPassword('correct horse battery staple');
console.log(await verifyPassword('correct horse battery staple', record)); // true
console.log(await verifyPassword('wrong password', record));               // false

Output:

true
false

Algorithm reference

APIPurposeGood defaultReversible?
createHashIntegrity / fingerprinting'sha256'No
createHmacAuthenticate signed messages'sha256' + secretNo
randomBytesTokens, salts, secrets16–32 bytesN/A
randomUUIDUnique identifiersv4 UUIDN/A
scryptPassword storagesalt + 64-byte keyNo

Best Practices

  • Use SHA-256 or stronger for hashing; treat MD5 and SHA-1 as obsolete for security.
  • Never store passwords with a fast hash — use scrypt (or argon2 from a library) with a unique per-user salt.
  • Generate all tokens, salts, and secrets with randomBytes/randomUUID, never Math.random().
  • Compare secrets, signatures, and derived keys with timingSafeEqual to avoid timing attacks.
  • Keep HMAC and other secrets out of source control — load them from environment variables or a secrets manager.
  • Prefer the async forms of randomBytes and scrypt in request handlers so the event loop stays responsive.
  • Encode binary output with 'base64url' when the value lands in URLs or filenames.
Last updated June 14, 2026
Was this helpful?