Hashes, passwords, and MACs

Hash passwords slowly

Store password verifiers with Argon2id or another current password-hashing construction instead of fast hashes or reversible encryption.

Password storage has a special threat: an attacker can guess offline after stealing the database. No rate limit, no lockout, no logs. Just their hardware against your hashing choice.

That is why fast hashes fail here. An attacker steals a user table containing unsalted SHA-256 password hashes. They can test billions of common guesses offline without contacting the application or triggering a rate limit. SHA-256 was designed to be fast, and here speed works entirely for the attacker.

Encrypting passwords is also wrong: whoever holds the key can recover every password. You never need the original password back. You only need to check a guess against a verifier.

Use a password-hashing function

Use a maintained password library and current OWASP parameters. The function should be deliberately expensive in time and memory. Argon2id is the first recommendation today; bcrypt and scrypt remain acceptable. Memory hardness matters because it blunts GPU attacks, which is why Argon2id is preferred over bcrypt for new systems.

import argon2 from 'argon2'

const verifier = await argon2.hash('correct horse battery staple')
// $argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$hashedbytes...

await argon2.verify(verifier, 'correct horse battery staple') // true
await argon2.verify(verifier, 'wrong guess')                  // false

Store the encoded algorithm, parameters, salt, and result, never the original password. The encoded string above already carries all of that, so verification years later still knows exactly what to recompute.

OWASP currently suggests Argon2id with at least 19 MiB of memory, an iteration count of 2, and one degree of parallelism as a baseline. Treat that as a floor, not a target.

Tune the cost on real hardware

Argon2id raises the time and memory cost for every guess. Raising it too far can exhaust login servers, so measure on production-like hardware and keep request-level abuse controls.

console.time('hash')
await argon2.hash('benchmark-password', { memoryCost: 65536, timeCost: 3 })
console.timeEnd('hash')
// hash: 92ms

Somewhere between 50ms and a few hundred milliseconds per hash is the usual budget. Remember that logins happen concurrently: 20 simultaneous logins at 64 MiB each is over a gigabyte of memory.

Practice

Benchmark a maintained Argon2id implementation with current OWASP guidance on production-like hardware and save latency plus memory settings. Verify a known password and reject a wrong one. Then run several concurrent hashes and show that the chosen cost remains within the service resource budget.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →