JavaScript: storing passwords with scrypt and a salt
Derive a memory-hard hash from a password and a random salt, then verify a login attempt.
const crypto = require("node:crypto");
const salt = crypto.randomBytes(16);
const stored = crypto.scryptSync("correct horse", salt, 32);
const good = crypto.scryptSync("correct horse", salt, 32);
const bad = crypto.scryptSync("wrong guess", salt, 32);
console.log("hash length: " + stored.length + " bytes");
console.log("right password: " + crypto.timingSafeEqual(stored, good));
console.log("wrong password: " + crypto.timingSafeEqual(stored, bad));hash length: 32 bytes right password: true wrong password: false
Explanation
Passwords must be stored so that a leaked database does not reveal them. Use a deliberately slow, salted password hashing function. The salt is random and stored beside the hash, so equal passwords get different hashes and precomputed tables are useless.
scrypt is memory-hard, which makes large-scale guessing on GPUs and special hardware expensive. crypto.scryptSync uses cost parameters that you can raise. Node's defaults are modest, and the OWASP Password Storage Cheat Sheet lists stronger settings, so tune the cost to what your server can afford. To verify a login, derive the hash again with the stored salt and compare in constant time. The Sync version blocks the event loop, so servers should use the asynchronous scrypt.
Explore the dry run
const crypto = require("node:crypto"); const salt = crypto.randomBytes(16);const stored = crypto.scryptSync("correct horse", salt, 32); const good = crypto.scryptSync("correct horse", salt, 32);const bad = crypto.scryptSync("wrong guess", salt, 32); console.log("hash length: " + stored.length + " bytes");console.log("right password: " + crypto.timingSafeEqual(stored, good));console.log("wrong password: " + crypto.timingSafeEqual(stored, bad));Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| A cryptographically secure random salt, unique for each user. | salt = 16 random bytes | — | |
| Store the salt, the cost parameters, and this 32-byte hash. Never store the password. | stored = 32 bytes | — | |
| At login, derive again with the stored salt. | stored = 32 bytes | — | |
| Equal hashes, so the login succeeds. | stored = 32 bytes | hash length: 32 bytes right password: true | |
| A different password gives an unrelated hash. | stored = 32 bytes | hash length: 32 bytes right password: true wrong password: false |
Cost and common mistakes
Time and memory both grow with the cost parameter, on purpose.
- Use crypto.randomBytes, not Math.random, for salts and tokens. Math.random is not secure.
- scryptSync blocks the whole event loop while it runs. In a server use crypto.scrypt with a callback or a promise.
- Store the parameters with the hash so that you can increase the cost later and re-hash on the next successful login.
Try it yourself
Why is a unique salt per user important?
Reveal answer and reasoning
With a shared salt, identical passwords would give identical hashes, and one guess could crack many accounts at once.