Password Hashing — bcrypt, scrypt, Argon2
Password Hashing — bcrypt, scrypt, Argon2
Storing passwords in plaintext is obviously wrong, but the choice of hash function and how to use it has changed across eras. The MD5 → SHA-1 → bcrypt → scrypt → Argon2 progression is not fashion but the result of an arms race among attack cost, hardware, and memory. This article covers what password storage means, the rise of memory-hard functions, libraries, and OWASP guidance.
1. The danger of plaintext storage
Plaintext storage means every password leaks the moment the DB is compromised, and those passwords carry over to other services (credential stuffing). No matter how careful our own service's permission model is, plaintext storage damages the security of users on other services too.
2. What a one-way hash means
A hash function maps input to a fixed-length output. The same input produces the same hash. Not being able to recover the input from the output is the one-way property.
The starting model is to store only the hash and compare hashes on verification. Plain hashing is vulnerable in two ways:
- Rainbow table — a precomputed table of hashes for common passwords. Without salt, one table can crack many users.
- Fast GPU / ASIC computation — generic hashes like SHA-1 or SHA-256 run billions of times per second → brute force is realistic.
To block both attacks, password hashing adds two properties:
- Salt — a per-user random value mixed into input so the same password yields a different hash.
- Slow / Memory-hard — deliberately slow or memory-heavy to raise brute-force cost.
3. The progression of functions
MD5 / SHA-1 — inappropriate. Designed for fast integrity checks, not passwords. MD5 collisions (2004), SHA-1 collisions (2017 SHAttered). Plain SHA-256 is also unsuitable — the core problem is "not slow."
bcrypt (1999) — Niels Provos and David Mazières published it as OpenBSD's password hash (USENIX 1999). A modification of Blowfish's key schedule:
- Cost factor (work factor) — 2^cost rounds. Raise cost as hardware improves.
- Output format —
$2b$<cost>$<22-char salt><31-char hash>. Stores the salt alongside. - Input length limit — 72 bytes. Anything longer is truncated.
A de facto standard for a long time. The limitation is small memory footprint, leaving it weak against GPU / ASIC parallelism.
scrypt (2009) — Colin Percival's function (USENIX 2009, RFC 7914). Deliberately uses a lot of memory (memory-hard). Raises GPU / ASIC cost on the memory side. Parameters — N (CPU / memory cost), r (block size), p (parallelization). Balancing the parameters is often noted as tricky.
Argon2 (2015) — winner of the Password Hashing Competition (PHC, 2013–2015). Designed by Alex Biryukov, Daniel Dinu, and Dmitry Khovratovich. RFC 9106 (2021).
Three variants:
- Argon2d — data-dependent memory access. Side-channel risk.
- Argon2i — data-independent memory access. Side-channel safe.
- Argon2id — a compromise between the two. The RFC's recommended default.
Parameters — m (memory in KB), t (iterations), p (parallelism). Currently the strongest OWASP recommendation.
PBKDF2 — a key-derivation function based on PKCS#5 (RFC 8018). HMAC + iteration. Memory requirements are low so it is relatively weak against GPU attacks, but it remains recommended in regulated environments such as FIPS 140.
4. Salt and Pepper
Salt — a per-user random value. Usually 16 bytes. Stored alongside the hash in the DB (encoded with bcrypt or Argon2). The same password produces different hashes per user → rainbow tables become useless.
Pepper — a secret value shared across all users. Stored in environment variables or a secret manager, separated from the DB. If only the DB leaks, brute force is much harder without the pepper.
Two common implementations:
- HMAC: feed
HMAC(pepper, password)into the password hash function. - Apply HMAC to the function's result.
Changing the pepper requires re-hashing every password (or applying it only to new passwords). An operational policy decision.
5. Libraries
Node:
bcrypt(node-bcrypt) — native binding.bcryptjs— pure JS, no native build.argon2(node-argon2) — native binding.
Python:
passlib— unified interface for bcrypt, scrypt, Argon2, etc.argon2-cffi— Argon2 directly.bcrypt— bcrypt directly.
JVM:
- Spring Security
PasswordEncoder—BCryptPasswordEncoder,Argon2PasswordEncoder,Pbkdf2PasswordEncoder.
Others:
- Go —
golang.org/x/crypto/bcrypt,argon2. - Rust —
argon2,bcryptcrates.
The choice of algorithm and parameters matters more than the choice of library.
6. Parameter recommendations
Parameters change with time and hardware. The OWASP Password Storage Cheat Sheet is the de facto reference.
Recent general recommendations:
- Argon2id — m=19456 (19MB), t=2, p=1, or higher.
- bcrypt — cost=10 to 12.
- scrypt — values like N=2^17, r=8, p=1.
Target verification time around 0.5 to 1 second for the user response. Too long affects UX and availability; too short leaves attack cost low.
7. Periodic re-hashing
On successful login, if current parameters fall short of policy, re-hash with the same password and save:
if password_verify(stored, input):
if needs_rehash(stored, current_policy):
stored = hash(input, current_policy)
db.update(stored)
allow_login()
passlib, Spring Security DelegatingPasswordEncoder, and others standardize this.
8. Additional policies
- Length requirements — minimum 8 characters, recommended 12+. Allow a very long max (200) — pre-hash with SHA-256 in some patterns to address bcrypt's 72-byte truncation.
- Blocklist — block leaked passwords (Have I Been Pwned API, in-house dictionary). NIST SP 800-63B recommendation.
- Brute-force throttling — login-attempt limits (rate limiting, 03-rate-limit-redis).
- 2FA — additional factors beyond passwords. Can ease password policy strictness.
- Breach notification — on a DB incident, notify users immediately and force rotation.
9. Common pitfalls
Rolling your own — many incidents come from custom hash combinations. Use a vetted library.
bcrypt's 72-byte truncation — the tail of very long passwords is ignored. Mitigate with appropriate pre-hashing (SHA-256) or move to Argon2.
Null bytes — some bcrypt implementations treat null bytes as terminators and truncate. Sanitize input.
Timing attacks — password comparison should be constant-time. Libraries usually handle it; avoid hand-rolling.
Short or reused salt — short salts or using the user ID as salt are weak. Use 16 bytes random.
Storing pepper next to DB — defeats the point. Use a different store or secret manager.
Frozen parameters — a cost from 5 years ago is weak today. Review periodically.
Logging passwords — debug logs and error messages leaking plaintext is a frequent incident. Hash and discard input immediately.
Closing thoughts
Password storage is a place of responsibility where a DB leak can also break the security of users on other services. Argon2id (or bcrypt cost 10–12) + per-user salt + pepper + periodic re-hashing + rate limiting + leaked-password blocking — this combination is the practical set behind OWASP guidance. Do not roll your own.
Next
- headers-and-cors
- (end of security)
We refer to the OWASP Password Storage Cheat Sheet, RFC 9106 Argon2, RFC 7914 scrypt, the original bcrypt paper (Provos and Mazières 1999), PHC (Password Hashing Competition), NIST SP 800-63B, Have I Been Pwned API, and Spring Security PasswordEncoder.