crypto is Node.js's toolkit for hashing, encryption, and generating secure random values — the building blocks behind password storage, signed tokens, and data protection generally. It's built on OpenSSL, a widely-used, well-audited cryptography library.
Hashing — one-way, used for things like password storage
A hash takes input of any size and produces a fixed-size output; the same input always produces the same hash, but you can't reverse a hash back into the original input.
const crypto = require('crypto');
const hash = crypto.createHash('sha256').update('my-secret-value').digest('hex');
console.log(hash); // a fixed-length hex string
A critical point about passwords specifically: never hash them plainly like this
A plain hash like the one above is fast to compute — which is exactly the wrong property for password storage, since it lets an attacker try billions of guesses per second against a stolen hash. Passwords need a slow, purpose-built algorithm with built-in salting, like bcrypt or crypto.scrypt:
const crypto = require('crypto');
function hashPassword(password) {
return new Promise((resolve, reject) => {
const salt = crypto.randomBytes(16).toString('hex');
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
if (err) return reject(err);
resolve(`${salt}:${derivedKey.toString('hex')}`);
});
});
}
function verifyPassword(password, stored) {
return new Promise((resolve, reject) => {
const [salt, key] = stored.split(':');
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
if (err) return reject(err);
resolve(key === derivedKey.toString('hex'));
});
});
}
The salt (a random value unique to each password) prevents two users with the same password from producing the same stored hash, which defeats precomputed "rainbow table" attacks.
Generating secure random values
const token = crypto.randomBytes(32).toString('hex');
console.log(token); // suitable for session tokens, password reset links, API keys
Math.random() is not cryptographically secure and should never be used for anything security-sensitive — crypto.randomBytes() is the correct tool whenever unpredictability actually matters.
HMAC — verifying a message hasn't been tampered with
const secret = 'shared-secret-key';
const message = 'transaction-id:1029';
const signature = crypto.createHmac('sha256', secret).update(message).digest('hex');
console.log(signature);
An HMAC combines a message with a secret key to produce a signature; anyone with the same secret can verify the signature matches, but nobody without it can forge a valid one — commonly used to verify webhook payloads actually came from the service that claims to have sent them.
Symmetric encryption — reversible, when you need to read the data back later
const algorithm = 'aes-256-cbc';
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16);
function encrypt(text) {
const cipher = crypto.createCipheriv(algorithm, key, iv);
return cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
}
function decrypt(encrypted) {
const decipher = crypto.createDecipheriv(algorithm, key, iv);
return decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
}
const encrypted = encrypt('sensitive data');
console.log(decrypt(encrypted)); // 'sensitive data'
Unlike hashing, encryption is meant to be reversed — used when you need the original data back later (versus hashing, which is one-way and used for verification, not retrieval).
The single most important habit
Cryptography is one area where writing your own algorithm from scratch is almost always a mistake — use the well-tested, standard algorithms crypto provides (SHA-256, AES, scrypt/bcrypt) rather than inventing something custom, and reach for an established, actively-maintained library (like bcrypt) for password hashing specifically, since the details of doing it safely are easy to get subtly wrong.