The crypto Module
Reviewed & published by Brayan K
The crypto module is Node's built-in toolkit for cryptography — creating hashes and HMAC signatures, generating secure random values, and safely deriving and comparing password hashes.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll be able to fingerprint data with SHA-256, sign messages with HMAC, generate secure tokens and UUIDs, store passwords the right way with scrypt and a salt, and compare secrets safely with timingSafeEqual.
What You'll Learn in This Lesson
- How to fingerprint data with crypto.createHash('sha256')
- How HMAC signs a message with a secret key
- How to generate secure tokens with randomBytes and randomUUID
- Why passwords need scrypt or pbkdf2, never plain sha256
- Why every user needs a unique random salt
- How timingSafeEqual compares secrets without timing leaks
1️⃣ Hashing & HMAC
A hash turns any input into a fixed-size fingerprint. The same input always yields the same digest, the tiniest change yields a wildly different one, and you can't run it backwards. The pattern in Node is always createHash(algo).update(data).digest('hex').
HMAC is a keyed hash: it mixes in a secret so only someone holding that key can produce or verify the signature. It's how services sign webhooks and API requests so the receiver can trust the message wasn't forged or altered.
const crypto = require('node:crypto');
// A hash is a one-way fingerprint: same input -> same output, always,
// and you can't reverse it back to the original.
const hash = crypto.createHash('sha256').update('hello world').digest('hex');
console.log(hash);
// Even a tiny change produces a COMPLETELY different hash.
const hash2 = crypto.createHash('sha256').update('hello World').digest('hex');
console.log(hash2);const crypto = require('node:crypto');
// HMAC ties a hash to a SECRET key — used to prove a message wasn't
// tampered with (webhooks, signed cookies, API signatures).
const signature = crypto
.createHmac('sha256', 'my-secret-key')
.update('transfer $100')
.digest('hex');
console.log(signature);2️⃣ Secure Randomness
Never use Math.random() for anything security-related — it's predictable. The crypto module gives you cryptographically strong randomness: randomBytes(n) for raw tokens and salts, randomUUID() for standard v4 IDs, and randomInt(min, max) for a fair integer in a range.
Because these values are random by design, the example checks properties of the output (length, format, range) rather than printing the exact bytes — that way the output is reproducible while the values stay genuinely random.
const crypto = require('node:crypto');
// randomBytes: cryptographically strong random data (tokens, salts).
const token = crypto.randomBytes(16).toString('hex');
console.log('token length:', token.length, 'chars'); // 16 bytes -> 32 hex chars
// randomUUID: a standard v4 UUID string.
const id = crypto.randomUUID();
console.log('uuid format ok:', /^[0-9a-f-]{36}$/.test(id));
// randomInt: a fair random integer in [min, max).
const dice = crypto.randomInt(1, 7);
console.log('dice in 1..6:', dice >= 1 && dice <= 6);3️⃣ Passwords Done Right
This is the most important section. Never store a password as a plain SHA-256 hash — it's far too fast, so attackers can guess billions per second. Use a deliberately slow key-derivation function like scrypt (or pbkdf2), with a unique random salt per user so identical passwords never share a hash.
When you verify a login, re-derive the hash from the attempt and the stored salt, then compare with crypto.timingSafeEqual — a constant-time compare that won't leak how close a guess was. (We use a fixed salt here only so the demo output is reproducible; real code uses randomBytes.)
const crypto = require('node:crypto');
// NEVER store passwords with plain sha256 — it's far too fast to brute-force.
// Use a SLOW key-derivation function like scrypt, with a unique random salt.
const password = 'correct horse battery staple';
const salt = Buffer.from('a-fixed-salt-for-demo'); // real apps: crypto.randomBytes(16)
const derived = crypto.scryptSync(password, salt, 32).toString('hex');
console.log(derived);const crypto = require('node:crypto');
const a = crypto.createHash('sha256').update('token-A').digest();
const b = crypto.createHash('sha256').update('token-A').digest();
const c = crypto.createHash('sha256').update('token-B').digest();
// timingSafeEqual compares in CONSTANT time so attackers can't learn
// how much of a secret they guessed from how long the check took.
console.log('A === A:', crypto.timingSafeEqual(a, b));
console.log('A === B:', crypto.timingSafeEqual(a, c));Your turn. The program below works once you fill in the two blanks marked ___. Follow the 👉 hints, then run it and compare with the expected digest.
const crypto = require('node:crypto');
// 🎯 YOUR TURN — fill each ___ then run it.
// Produce the sha256 hex digest of the string "node".
const digest = crypto
.createHash(___) // 👉 'sha256'
.update('node')
.digest(___); // 👉 'hex'
console.log(digest);
// ✅ Expected output:
// 545ea538461003efdc8c81c244531b003f6f26cfccf6c0073b3239fdedf49446Mini-Challenge: A Safe Password Check
No blanks this time — just a brief and an outline. Write it yourself, run it, and check your output against the example in the comments. This ties together scryptSync, salts, and timingSafeEqual.
// 🎯 MINI-CHALLENGE: verify a password the right way
//
// 1. Write hashPassword(password, salt) using crypto.scryptSync(..., 32).
// 2. Store the hash of 'hunter2' with salt Buffer.from('user-42-salt').
// 3. Write verify(attempt) that re-hashes the attempt with the SAME salt
// and compares with crypto.timingSafeEqual.
// 4. Log verify('hunter2') and verify('password').
//
// ✅ Example output:
// right password: true
// wrong password: false
// your code hereCommon Errors (and the fix)
- ❌ Storing passwords as sha256 — Too fast to be safe. ✅ Use scrypt, pbkdf2, bcrypt, or argon2 with a salt.
- ❌ Reusing one salt for everyone — Identical passwords get identical hashes. ✅ A unique randomBytes salt per user, stored with the hash.
- ❌ Comparing secrets with === — Leaks timing information. ✅ Use crypto.timingSafeEqual on equal-length Buffers.
- ❌ Math.random() for tokens — Predictable, not secure. ✅ crypto.randomBytes or crypto.randomUUID.
- ❌ "digest already called" — A Hash object is single-use. ✅ Create a fresh createHash(...) for each value.
Pro Tips
- 💡 Prefer the async forms (crypto.scrypt, crypto.randomBytes with a callback) in servers so a slow derivation doesn't block the event loop.
- 💡 Store salt + hash together, often as a single string like salt:hash, so verification has everything it needs.
- 💡 For real auth, consider a vetted library like argon2 or bcrypt — they bake in sensible defaults on top of these primitives.
📋 Quick Reference — crypto
| Task | Call |
|---|---|
| Hash data | createHash('sha256').update(d).digest('hex') |
| Sign with a key | createHmac('sha256', key)... |
| Secure token | randomBytes(16).toString('hex') |
| UUID | randomUUID() |
| Password hash | scrypt(pw, salt, 32) |
| Compare secrets | timingSafeEqual(a, b) |
🎉 Lesson Complete!
- ✅ createHash('sha256') makes a one-way fingerprint of any data
- ✅ createHmac adds a secret key so a signature can be trusted
- ✅ randomBytes, randomUUID, and randomInt give secure randomness
- ✅ Passwords need a slow KDF (scrypt/pbkdf2) and a unique salt — never plain sha256
- ✅ Compare secrets with timingSafeEqual, not ===
Practice quiz
What is a hash?
- A reversible encryption of data
- A random number generator
- A one-way fingerprint that cannot be reversed
- A compression format
Answer: A one-way fingerprint that cannot be reversed. A hash maps input to a fixed-size fingerprint; the same input always yields the same digest and you cannot reverse it.
What is the Node pattern for a SHA-256 hex digest?
- createHash('sha256').update(data).digest('hex')
- hash('sha256', data)
- sha256(data).hex()
- crypto.encrypt(data, 'sha256')
Answer: createHash('sha256').update(data).digest('hex'). The pattern is always createHash(algo).update(data).digest('hex').
What does HMAC add compared to a plain hash?
- Reversibility
- Compression
- A timestamp
- A secret key, so only key holders can produce or verify it
Answer: A secret key, so only key holders can produce or verify it. HMAC mixes a secret key into the hash, used to sign webhooks and API requests so the receiver can trust them.
Which function generates cryptographically strong random bytes?
- Math.random()
- crypto.randomBytes(n)
- crypto.hash(n)
- crypto.salt(n)
Answer: crypto.randomBytes(n). crypto.randomBytes(n) returns strong random data for tokens and salts; never use Math.random() for security.
How many hex characters does crypto.randomBytes(16).toString('hex') produce?
- 32
- 16
- 64
- 8
Answer: 32. Each byte becomes two hex characters, so 16 bytes produce a 32-character hex string.
Why must you NOT store passwords as a plain SHA-256 hash?
- SHA-256 is reversible
- SHA-256 cannot hash text
- SHA-256 is too fast, so attackers can guess billions per second
- SHA-256 needs a network connection
Answer: SHA-256 is too fast, so attackers can guess billions per second. Fast hashes let attackers brute-force quickly; use a slow KDF like scrypt or pbkdf2 instead.
What is a salt and why is it unique per user?
- A secret shared by all users
- Random data per password so identical passwords get different hashes
- An encryption key
- The hashing algorithm name
Answer: Random data per password so identical passwords get different hashes. A unique random salt makes every stored hash different and defeats precomputed rainbow tables.
Which function derives a slow password hash in the lesson?
- crypto.createHash
- crypto.randomUUID
- crypto.createHmac
- crypto.scryptSync
Answer: crypto.scryptSync. scrypt is a deliberately slow key-derivation function; the demo uses crypto.scryptSync(password, salt, 32).
Why compare secrets with crypto.timingSafeEqual instead of ===?
- It is faster than ===
- It compares in constant time, avoiding timing side-channel leaks
- It works on strings only
- It encrypts the inputs first
Answer: It compares in constant time, avoiding timing side-channel leaks. === can stop at the first differing byte, leaking timing info; timingSafeEqual always takes the same time.
When should you use createHash versus createHmac?
- Always use createHash
- createHmac for file integrity only
- createHash for a plain fingerprint, createHmac when a shared secret is involved
- They are interchangeable
Answer: createHash for a plain fingerprint, createHmac when a shared secret is involved. Use createHash for integrity or cache keys with no secret; use createHmac to prove a message came from a key holder.
Continue this course
- Previous: Worker Threads & Cluster
- Next: Promises & async/await Deep Dive — all/allSettled/race/any, sequential vs parallel, and error handling
- Quick reference: Node.js cheat sheet
Frequently asked questions
Why can't I use sha256 to store passwords?
Plain hashes like sha256 are designed to be fast, which is exactly what you don't want for passwords. An attacker who steals your database can try billions of guesses per second against a fast hash. Password storage needs a slow, salted key-derivation function — scrypt, bcrypt, argon2, or pbkdf2 — that is deliberately expensive to compute, with a unique random salt per user so identical passwords don't share a hash and precomputed rainbow tables are useless. In Node, crypto.scrypt / crypto.scryptSync and crypto.pbkdf2 are built in for this purpose.
What is a salt and why do I need a different one per user?
A salt is random data mixed into the password before hashing. Without it, two users with the same password get the same hash, and attackers can use precomputed tables (rainbow tables) to reverse common passwords instantly. A unique random salt per user makes every stored hash different even for identical passwords and defeats those tables. Generate it with crypto.randomBytes, store it alongside the hash (it isn't secret), and use the same salt when verifying a login attempt.
When should I use createHash versus createHmac?
Use createHash for a plain fingerprint of data — checking file integrity, deduplicating, or generating a cache key — where no secret is involved and anyone can recompute the same hash. Use createHmac when you need to prove a message came from someone who holds a shared secret and wasn't altered in transit: signing webhooks, API requests, or cookies. HMAC mixes a secret key into the hash, so only parties with the key can produce or verify a valid signature.
Why use crypto.timingSafeEqual instead of ===?
Comparing secrets (tokens, signatures, password hashes) with === or a normal string compare can leak information through timing: those comparisons typically stop at the first differing byte, so a response that takes slightly longer reveals that more of the prefix matched. Over many attempts an attacker can reconstruct the secret. crypto.timingSafeEqual always takes the same amount of time regardless of where the inputs differ, closing that side channel. Use it whenever you compare two secret values of equal length.