Level 2 · 25 min
Password Hashing: bcrypt/argon2/scrypt
Password hashing is the seatbelt of authentication: ineffective until the DB leaks, and then it''s the only thing standing between attacker-acquired hashes and millions of plaintext passwords. Use a memory-hard KDF (argon2id, scrypt) or a tunable slow hash (bcrypt). Never use MD5, SHA-1, SHA-256, or any unsalted fast hash for passwords.
Why Fast Hashes Are Catastrophic
MD5, SHA-1, SHA-256 are designed to be fast — billions of hashes per second on a modern GPU. A consumer RTX 4090 computes ~25 GH/s of SHA-256 (25 billion per second). With a leaked DB of unsalted SHA-256 password hashes, every 8-character lowercase password is brute-forced in under 10 minutes; every 10-character mixed-case password in under a day; rockyou.txt''s 14M-entry wordlist is dispatched in seconds. Salts (per-user random bytes appended to the password before hashing) defeat rainbow tables but do not slow down brute force against a single user — the attacker simply hashes wordlist + salt for each user. The fix is making the hash function expensive: bcrypt''s cost factor 12 produces ~250ms per hash, reducing GPU throughput from billions/sec to ~3/sec — a 10-billion-fold slowdown. Argon2id additionally requires memory (64 MB+), which GPUs and ASICs handle poorly; scrypt is similar.
Concrete Cost Parameters
bcrypt: cost factor 12 in 2026 (raise as hardware improves; OWASP recommends checking the cost annually and bumping by 1 every 18-24 months). 12 = 2^12 = 4096 iterations. Bcrypt''s 72-byte input limit is a known limitation — pre-hash long inputs with HMAC-SHA-256 if you need to. Argon2id (preferred for new systems): m=64MB, t=3 iterations, p=4 parallelism. The OWASP Password Storage Cheat Sheet is the authoritative reference. scrypt: N=2^17, r=8, p=1 — heavier than Argon2id but supported by older platforms. PBKDF2: only acceptable in FIPS-constrained environments, with iterations in the millions for SHA-256 (FIPS 140-3 requires 1M+ in 2026). Aim for ~250-500ms per verification on production hardware — slower than that and login latency degrades; faster and the attacker''s job gets too easy. Time per hash should be benchmarked on the actual auth server hardware, not estimated from blog posts.
Salt, Pepper, Verification, and Rehash on Login
Salt: per-user random 16+ bytes, embedded in the hash output (bcrypt and argon2 do this automatically). Pepper: an optional server-side secret added to every password before hashing — kept outside the DB (in a vault or app config) so a DB-only leak still does not enable offline cracking. Pepper is defense in depth, not a substitute for a slow hash. Verification: use the library''s constant-time compare (bcrypt.compare, argon2.verify) — never strcmp the hashes (timing leak, though minor). Rehash on login: when verifying against a hash with a stale cost factor, transparently rehash the plaintext (which you have at login time) with the current parameters and update the DB. This lets you raise the cost factor over years without forcing password resets. Algorithm migration (e.g., bcrypt to argon2): same trick — store the algorithm prefix in the hash ($2b$ for bcrypt, $argon2id$ for argon2), branch on it at verify, and migrate users on next login.
Code example
// Java with Spring Security 6+
// Configure: BCryptPasswordEncoder with cost 12
@Bean
PasswordEncoder passwordEncoder() {
return new DelegatingPasswordEncoder("argon2", Map.of(
"argon2", new Argon2PasswordEncoder(16, 32, 4, 65536, 3), // saltLen, hashLen, p=4, m=64MB, t=3
"bcrypt", new BCryptPasswordEncoder(12)
));
}
// Registration
String hash = encoder.encode(rawPassword);
// stored: $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>
// Login — constant-time verify + rehash if stale
boolean ok = encoder.matches(rawPassword, storedHash);
if (ok) {
if (encoder.upgradeEncoding(storedHash)) {
// Cost factor or algorithm changed — rehash with current parameters
String newHash = encoder.encode(rawPassword);
userRepo.updatePasswordHash(userId, newHash);
}
// proceed with login
} else {
// log failure (rate limit / lockout)
}
// Pepper (defense in depth) — HMAC the password with a server-side secret first
byte[] peppered = Mac.getInstance("HmacSHA256")
.doFinal((PEPPER + rawPassword).getBytes(UTF_8));
String hash = encoder.encode(Base64.getEncoder().encodeToString(peppered));