· 3 min read · 694 words
Bcrypt vs Argon2 vs Scrypt vs PBKDF2
What separates the four password hashing functions in practice, which one to pick for a new project, and why switching an existing one is rarely the best use of your time.
- bcrypt
- argon2
- scrypt
- pbkdf2
All four are deliberately slow functions for storing passwords. They differ in what they make expensive, and that difference is the whole story.
The one line version
- New project with a maintained Argon2 library available: Argon2id.
- Existing bcrypt database that works: leave it, raise the cost factor.
- Environment where you only get what the platform ships: PBKDF2, with a high iteration count.
- Scrypt: fine, well studied, and rarely the answer to a question you actually have.
What each one makes expensive
PBKDF2
PBKDF2 repeats a fast hash such as SHA-256 many times. That is the entire design. It costs CPU time and almost no memory, which is exactly the shape of work a GPU is built for. An attacker with a graphics card gets a very large speedup over your server, so PBKDF2 needs a genuinely high iteration count to hold the line. OWASP currently points at 600,000 iterations for SHA-256.
Its advantage is availability. It is in the standard library of nearly everything, it is what FIPS validated environments allow, and WebCrypto gives it to you in the browser with no dependency at all.
Bcrypt
Bcrypt runs a modified Blowfish key schedule in a loop. It needs about 4 KB of memory that it accesses in an unpredictable pattern, which is small but awkward, and it happens to be a poor fit for how GPUs are wired. That accident of design is why a function from 1999 is still standing.
It has one tuning knob, the cost factor, and the 72 byte input limit. Both are easy to live with once you know about them.
Scrypt
Scrypt was the first widely used design to make memory the cost rather than time. You set how much memory a single hash needs, and an attacker has to provide that memory for every guess running in parallel. Memory is the expensive part of custom cracking hardware, so this raises their cost in a way that iterations alone do not.
It has three interacting parameters, N, r and p, and getting them wrong is easier than it should be. Setting N high with r left at 1 gives you far less memory use than the number suggests.
Argon2
Argon2 won the Password Hashing Competition in 2015 and is what the current guidance recommends. It separates memory, time and parallelism into three parameters you set independently, which makes it possible to say what you actually want rather than deducing it from an interaction.
Use the id variant. Argon2i resists side channel observation but is weaker against time and memory tradeoff attacks, Argon2d is the reverse, and Argon2id combines the two approaches. It is the variant every current recommendation names.
Side by side
| PBKDF2 | Bcrypt | Scrypt | Argon2id | |
|---|---|---|---|---|
| Year | 2000 | 1999 | 2009 | 2015 |
| Cost is | CPU time | CPU time | Memory | Memory and time |
| Memory per hash | negligible | about 4 KB | tunable | tunable |
| GPU resistance | weak | decent | good | good |
| Tuning knobs | 1 | 1 | 3 | 3 |
| Input limit | none | 72 bytes | none | none |
| In standard libraries | almost always | often | sometimes | sometimes |
Reasonable starting parameters
These follow the current OWASP cheat sheet. Tune them until a single hash takes somewhere in the range of 250 to 500 milliseconds on your own hardware, then stop.
Argon2id m = 19 MiB, t = 2, p = 1
or m = 47 MiB, t = 1, p = 1
bcrypt cost = 12
scrypt N = 2^17, r = 8, p = 1
PBKDF2 600,000 iterations with SHA-256Should you migrate?
If you already run bcrypt at a sensible cost factor, migrating to Argon2 is a small improvement for a real amount of work. Raising your cost factor is cheaper and buys you more. Spend the time on rate limiting your login endpoint and checking passwords against a breach list instead, because both of those stop attacks that no hash function can.
If you are on unsalted MD5 or SHA-1, that is a different conversation and you should move now. Wrap the old hash inside the new one so you can migrate without waiting for logins, and drop the wrapper as accounts come through.
The migration write up covers doing this without forcing a password reset.