Hash Passwords With Scrypt
Hash passwords with scrypt, a random salt and explicit cost parameters, then verify them with a timing-safe comparison.
Code
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 } as const;
function deriveKey(password: string, salt: Buffer, length: number): Promise<Buffer> {
return new Promise((resolve, reject) => {
scrypt(password.normalize("NFKC"), salt, length, { ...SCRYPT_PARAMS }, (error, key) => {
if (error) reject(error);
else resolve(key);
});
});
}
export async function hashPassword(password: string): Promise<string> {
if (typeof password !== "string" || password.length < 8) {
throw new RangeError("password must be at least 8 characters");
}
const salt = randomBytes(16);
const key = await deriveKey(password, salt, 64);
return [
"scrypt",
SCRYPT_PARAMS.N,
SCRYPT_PARAMS.r,
SCRYPT_PARAMS.p,
salt.toString("base64"),
key.toString("base64"),
].join("$");
}
export async function verifyPassword(password: string, stored: string): Promise<boolean> {
if (typeof stored !== "string") return false;
const parts = stored.split("$");
if (parts.length !== 6 || parts[0] !== "scrypt") return false;
const [, rawN, rawR, rawP, saltText, keyText] = parts;
const expected = Buffer.from(keyText!, "base64");
const salt = Buffer.from(saltText!, "base64");
if (expected.length === 0 || salt.length === 0) return false;
let actual: Buffer;
try {
actual = await new Promise<Buffer>((resolve, reject) => {
scrypt(
password.normalize("NFKC"),
salt,
expected.length,
{ N: Number(rawN), r: Number(rawR), p: Number(rawP), maxmem: 64 * 1024 * 1024 },
(error, key) => error ? reject(error) : resolve(key),
);
});
} catch {
return false;
}
return actual.length === expected.length && timingSafeEqual(actual, expected);
}Report a problem with this exampleOpens a pre-filled issue on GitHub.