Salted, slow hashing & careful comparison
Store passwords safely
Lesson 10 of 20 · 14 min · Module 3: Passwords and sessions
Databases leak more often than anyone would like. If passwords were stored as they are, or with a fast hash, the attacker gets them all. Storing only a salted, deliberately slow hash keeps them out of reach.
Start this lesson →Free. No installation: you write and run the code in your browser, and it is checked automatically.
By the end, you’ll be able to…
- Explain why passwords are salted and hashed slowly
- Store the algorithm, settings, salt and hash together
- Verify a sign-in and compare results carefully
The idea
A hash is a one-way fingerprint: easy to compute from a password, practically impossible to turn back. A random salt for every password makes identical passwords produce different hashes, and a slow function makes every guess expensive. OWASP recommends Argon2id on servers; PBKDF2 with 600,000 iterations is also accepted and is built into every browser, so this lesson uses it. To check a sign-in, hash the attempt with the stored salt and iterations, and compare the results. In a real site this code belongs on the server.
const salt = crypto.getRandomValues(new Uint8Array(16)); // 16 random bytes
console.log(salt.length);
01Fast hashes are the wrong tool
SHA-256 is designed to be fast: a graphics card can try billions of guesses per second. Password hashing functions such as Argon2id, scrypt, bcrypt and PBKDF2 are slow on purpose, and their cost can be raised as computers get faster.
02Store everything needed to check later
Keeping the algorithm, the iterations and the salt next to the hash lets you raise the iterations for new passwords while old ones still verify, and upgrade each account the next time its owner signs in.
pbkdf2-sha256$600000$9f1c…$4ab2…
03Compare without leaking
A comparison that stops at the first different character takes slightly longer when more characters match, and that timing can leak information. Compare every character, decide at the end, and on a server use a built-in function such as crypto.timingSafeEqual in Node.js.
Your turn
newSalt() returns 16 random bytes from crypto.getRandomValues as hex; hashPassword(password, salt = newSalt(), iterations = 600000) returns pbkdf2-sha256$iterations$salt$hash using the pbkdf2 helper; sameText(a, b) compares two strings without stopping at the first difference; verifyPassword(password, stored) reads the stored value, which must match pbkdf2-sha256$digits$32 hex characters$64 hex characters, hashes the attempt with that salt and that number of iterations, and returns the result of sameText, or false for anything that does not match the format.What the checks look for
- The stored value has the expected format
- The hash is PBKDF2 with the given salt and iterations
- Every password gets its own random salt
- The right password is accepted
- Wrong passwords and damaged values are refused
- The stored number of iterations is used
- Text is compared character by character
- The demo stores a hash, never the password
Take this with you
A unique salt, a slow hash, stored settings and a careful comparison.
Go a little further
Make verifyPassword report when a stored value uses fewer than 600,000 iterations, so the account can be hashed again after a successful sign-in.