Hashes, passwords, and MACs
Understand salts and work factors
Use unique salts generated by the password library and tune cost parameters so identical passwords do not share a verifier and guesses remain expensive.
A salt is a random value mixed into each password hash. It is unique per password and does not need to be secret. Its job is to prevent precomputed and shared work.
Why that matters: two users choose the same password. With a shared or missing salt, their stored verifiers match and one cracked password reveals the other immediately. Worse, the attacker can crack the whole table at once, testing each guess against every row, or use precomputed tables built years before your breach.
With unique salts, every row becomes a separate cracking job. Same password, different verifier:
import argon2 from 'argon2'
await argon2.hash('hunter2')
// $argon2id$v=19$m=65536,t=3,p=4$Rk9Qb2ZYc1E$...
await argon2.hash('hunter2')
// $argon2id$v=19$m=65536,t=3,p=4$x1VtZ2JkQnc$... <- different salt, different result
Let the library handle the salt
Let the library generate and store the salt inside its encoded result. Look at that encoded string: it carries the algorithm (argon2id), the version, the cost parameters (m, t, p), the salt, and the hash, separated by $. Verification reads everything it needs from the string itself.
This is why you should never build a salt column by hand or invent your own format. Hand-rolled salting is where bugs live: reused salts, truncated salts, salts applied to the wrong input.
Work factors age
The work factor is the cost knob: memory and iterations for Argon2id, rounds for bcrypt. Tune memory and time cost to your environment, then increase them as hardware changes. A cost that felt expensive five years ago is cheap on today’s GPUs.
Unique salts stop shared precomputation but do not make weak passwords strong. A user whose password is 123456 falls in the first seconds of any attack regardless of parameters.
Old verifiers do not upgrade themselves. Rehash after a successful login when a stored verifier uses an older policy: at that moment you hold the plaintext legitimately, so you can write a new verifier at current cost and let the old one retire naturally.
if (await argon2.verify(stored, password) && argon2.needsRehash(stored, currentOptions)) {
await saveVerifier(userId, await argon2.hash(password, currentOptions))
}
Practice
Hash the same password twice through the password library and save the two different encoded results. Verify both, then inspect where the algorithm, salt, and work factors are stored. Lower the policy cost for one test verifier and prove a successful login upgrades it without knowing or storing the plaintext later.
Lesson completed