JavaScript · Course navigation

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.

On this page
Before you start

JavaScript Buffers and the crypto module.

  • Derive a memory-hard hash from a password and a random salt, then verify a login attempt.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

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.

Code example

JavaScript
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));
Output
hash length: 32 bytes
right password: true
wrong password: false

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1const crypto = require("node:crypto");
2
3const salt = crypto.randomBytes(16);
4const stored = crypto.scryptSync("correct horse", salt, 32);
5
6const good = crypto.scryptSync("correct horse", salt, 32);
7const bad = crypto.scryptSync("wrong guess", salt, 32);
8
9console.log("hash length: " + stored.length + " bytes");
10console.log("right password: " + crypto.timingSafeEqual(stored, good));
11console.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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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

Behind the scenes

How JavaScript processes this code

A JavaScript engine parses and runs the source. A function body runs when the function is called. console.log writes to the console.

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.

Further reading