node:crypto basics
The node:crypto module handles cryptography: hashes, random values, encryption. The blog needs three things from it: unique ids, random tokens and safe storage of the admin password.
A random id
randomUUID() creates a UUID — a 36-character string that, for all practical purposes, never repeats:
import { randomUUID } from 'node:crypto';
const id = randomUUID();
console.log(id); // for example 3b241101-e2bb-4255-8caf-4136c566a962
console.log(id.length); // 36A UUID is handy as a comment id: it does not have to be computed from existing records.
A random token
randomBytes() returns the given number of random bytes. Turned into a string, they make a token — for example, for a password-reset link:
import { randomBytes } from 'node:crypto';
const token = randomBytes(32).toString('hex');
console.log(token.length); // 64Each byte is written as two hexadecimal characters, so 32 bytes give 64 characters. Math.random() is not suitable for this: its values can be predicted.
A hash
A hash is a fixed-length "fingerprint" of data. The same data always gives the same hash, and the original data cannot be recovered from the hash:
import { createHash } from 'node:crypto';
const hash = (text) => createHash('sha256').update(text).digest('hex');
console.log(hash('Hello, Node.js').length); // 64
console.log(hash('Hello, Node.js') === hash('Hello, Node.js')); // true
console.log(hash('Hello, Node.js') === hash('Hello, Node.JS')); // falseHashes are used to check that a file has not changed, or to get a short cache key.
Passwords
For passwords plain sha256 is too fast: trying millions of guesses is easy. You need a slow function with a salt — a random addition unique to each password. The built-in scrypt function fits:
import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
import { promisify } from 'node: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')}`;
}
async function verifyPassword(password, stored) {
const [salt, keyHex] = stored.split(':');
const key = await scryptAsync(password, salt, 64);
return timingSafeEqual(key, Buffer.from(keyHex, 'hex'));
}
const stored = await hashPassword('blog-admin-2025');
console.log(await verifyPassword('blog-admin-2025', stored)); // true
console.log(await verifyPassword('wrong', stored)); // falseThe database stores the string salt:hash. At login the password is hashed with the same salt and compared. timingSafeEqual() compares in constant time, so the response speed does not reveal how many characters matched.
Common mistake
Storing passwords as plain text or as a simple unsalted hash is not allowed: if the database leaks, they are known at once. Even unsalted sha256 is cracked with ready-made tables of popular passwords. Use scrypt with a salt as above, or a proven library such as bcrypt.