crypto module
The crypto module is a built-in Node.js module that provides cryptographic functionality. It supports hashes (MD5, SHA-1, SHA-256), ciphers (AES, DES), signing (RSA, ECDSA), key derivation (PBKDF2, scrypt), and secure random generation. This module is the foundation for implementing security features in Node.js applications.
Syntax
const crypto = require('crypto'); // CommonJS
import crypto from 'crypto'; // ES modules (Node.js 19+)
import { createHash } from 'crypto'; // named imports
Key functions and classes
The crypto module provides several categories of functionality. At its core are hashing operations — one-way transformations that turn arbitrary input into a fixed-size digest. Hashes are deterministic (same input always produces the same output) and irreversible, making them useful for data integrity checks, cache keys, and password storage when combined with appropriate key derivation.
Hashing
Create hash digests with createHash:
const crypto = require('crypto');
const hash = crypto.createHash('sha256');
hash.update('plaintext-password');
const digest = hash.digest('hex');
console.log(digest);
// 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
Supported algorithms: 'md5', 'sha1', 'sha256', 'sha512', 'sha3-256', etc. Call crypto.getHashes() to list all algorithms available in your Node.js version.
Output encodings: 'hex', 'base64', 'latin1', 'buffer'. The 'hex' encoding produces a lowercase hexadecimal string and is the most common choice for displaying hashes.
HMAC (hash-based message authentication code)
Plain hashing alone cannot verify that a message was sent by someone who knows a shared secret. HMAC solves this by combining the hash function with a secret key, producing a signature that is both tamper-evident and authenticated. The receiver who holds the same key can recompute the HMAC and compare it to verify the message has not been altered.
const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', 'secret-key');
hmac.update('message');
const signature = hmac.digest('base64');
console.log(signature);
Ciphers and Decipher
While hashing and HMAC are one-way operations, ciphers let you encrypt data and later decrypt it with the same key. The createCipheriv and createDecipheriv methods require an initialization vector (IV) for security — reusing an IV with the same key compromises the encryption. Always generate a fresh random IV for each encryption operation.
const crypto = require('crypto');
const algorithm = 'aes-256-cbc';
const key = crypto.randomBytes(32); // 256-bit key
const iv = crypto.randomBytes(16); // 128-bit IV
// Encrypt
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update('secret message', 'utf8', 'hex');
encrypted += cipher.final('hex');
// Decrypt
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
console.log(decrypted); // "secret message"
Key Derivation
User passwords are rarely the right length or format to use directly as encryption keys. Key derivation functions stretch a password into a cryptographically strong key of the desired length. They are intentionally slow and consume memory to resist brute-force attacks — an attacker must pay this cost for every password guess.
Derive keys from passwords using PBKDF2:
const crypto = require('crypto');
const password = 'my-password';
const salt = crypto.randomBytes(16);
crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, derivedKey) => {
console.log(derivedKey.toString('hex'));
});
Or with scrypt (memory-hard):
PBKDF2 is CPU-bound — a fast attacker with specialized hardware can still try many guesses per second. Scrypt adds a memory-hardness requirement, forcing the attacker to allocate significant RAM for each attempt. This makes large-scale parallel attacks far more expensive.
const { scrypt } = require('crypto');
const { promisify } = require('util');
const scryptAsync = promisify(scrypt);
async function hashPassword(password) {
const salt = crypto.randomBytes(16);
const derivedKey = await scryptAsync(password, salt, 64);
return salt.toString('hex') + ':' + derivedKey.toString('hex');
}
Secure random generation
Every cryptographic operation depends on good random numbers — for keys, IVs, salts, and nonces. The crypto.randomBytes() and crypto.randomInt() functions draw from the operating system’s cryptographically secure entropy source, making them suitable for security-sensitive values. Never use Math.random() for cryptographic purposes; it is a predictable PRNG.
Generate cryptographically secure random values:
const crypto = require('crypto');
// Random bytes
const bytes = crypto.randomBytes(16);
console.log(bytes.toString('hex'));
// Random integer in range
const number = crypto.randomInt(1, 100);
console.log(number);
Timing-Safe Comparison
A timing attack measures how long a comparison takes to infer information about the secret value. Standard string comparison operators like === return early on the first mismatch, leaking byte-by-byte information about how many characters matched. crypto.timingSafeEqual() compares the full length of both buffers in constant time, eliminating this side channel.
Compare values in constant time to prevent timing attacks:
const crypto = require('crypto');
const safeCompare = crypto.timingSafeEqual(
Buffer.from('expected'),
Buffer.from('received')
);
console.log(safeCompare); // false
WebCrypto API (Node.js 18+)
Node.js 18 introduced the WebCrypto API as crypto.webcrypto, providing a standards-based alternative to the legacy crypto module. The WebCrypto API uses promises and follows the same interface available in browsers, so code written against it can run in both environments without modification. The trade-off is verbosity — every operation requires await and explicit TextEncoder/TextDecoder conversions.
const { subtle } = crypto.webcrypto;
async function sha256(message) {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await subtle.digest('SHA-256', msgBuffer);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
This API follows the browser WebCrypto standard.
Never use MD5 or SHA-1 for passwords
MD5 and SHA-1 are fast hashing algorithms — which is exactly the wrong property for password storage. Their speed lets attackers try billions of guesses per second. For passwords, use bcrypt, argon2, or scrypt (available in Node’s built-in crypto via crypto.scrypt). These are designed to be slow and memory-hard.
const { scrypt, randomBytes } = require('crypto');
const { promisify } = require('util');
const scryptAsync = promisify(scrypt);
async function hashPassword(password) {
const salt = randomBytes(16).toString('hex');
const key = await scryptAsync(password, salt, 64);
return salt + ':' + key.toString('hex');
}
For non-password data like file checksums or cache keys, MD5 and SHA-1 are fine — the collision risk matters only for security-sensitive contexts.
crypto vs WebCrypto
Node.js provides two crypto APIs. The legacy require('crypto') module has been available since Node.js 0.x and covers nearly all use cases. The crypto.webcrypto property (Node 15+) exposes the standard Web Crypto API, which is also available in browsers. Using WebCrypto makes code portable between Node and the browser at the cost of a more verbose async API.
// Legacy crypto — synchronous, Node-only
const hash = require('crypto').createHash('sha256').update('data').digest('hex');
// WebCrypto — async, works in Node and browser
const { subtle } = globalThis.crypto;
const buf = await subtle.digest('SHA-256', new TextEncoder().encode('data'));
Prefer WebCrypto for new code that must run in both environments. Stick with the legacy module when you need synchronous hashing or features not yet in the Web Crypto standard.