Password hashing: bcrypt, scrypt and Argon2
Why SHA-256 is the wrong tool for passwords, how work factors create cost for an attacker, and how to pick parameters for Argon2id, scrypt and bcrypt.
General-purpose hashes are too cheap
A GPU computes billions of SHA-256 hashes per second. A password has only tens of bits of real entropy, so a fast hash means an offline attack succeeds in hours. Password hashing must be deliberately slow and memory-hungry.
| Algorithm | Cost model | Tuning parameter | Status |
|---|---|---|---|
| SHA-256 (raw) | Negligible | None | Unsafe for passwords |
| PBKDF2 | CPU iterations | iterations, often 600,000+ | Acceptable; FIPS approved |
| bcrypt | CPU, small fixed memory | cost, 10-14 | Good; 72-byte input limit |
| scrypt | CPU and memory | N, r, p | Good; memory-hard |
| Argon2id | CPU, memory, parallelism | m, t, p | Recommended default |
| Argon2d / Argon2i | Variant tuning | Same | Use id; d is side-channel exposed, i is weaker against GPU |
Argon2id in practice
from argon2 import PasswordHasher
from argon2.low_level import Type
ph = PasswordHasher(
time_cost=3, # passes over memory
memory_cost=65536, # 64 MiB
parallelism=4, # lanes
hash_len=32,
salt_len=16,
type=Type.ID,
)
stored = ph.hash("correct horse battery staple")
# $argon2id$v=19$m=65536,t=3,p=4$c2FsdA$hash... parameters travel with the hash
ph.verify(stored, "correct horse battery staple") # True, or raises
# rehash on login when the parameters have been raised
if ph.check_needs_rehash(stored):
stored = ph.hash("correct horse battery staple")- The encoded hash string contains the algorithm, version, parameters and salt, so you can raise the cost later without a schema change.
- Aim for roughly 50 to 100 milliseconds per verification on your production hardware — slow enough to hurt, fast enough for login.
- Memory cost is the parameter that hurts GPU attackers most; time cost alone is cheap to parallelise.
- Cap parallelism at the CPU count you are willing to dedicate, because each login consumes those lanes.
# bcrypt: still fine, but mind the 72-byte limit
import bcrypt
h = bcrypt.hashpw(b"correct horse battery staple", bcrypt.gensalt(rounds=12))
bcrypt.checkpw(b"correct horse battery staple", h) # True
# pre-hash long inputs, but be careful: base64 avoids null bytes in bcrypt
import base64, hashlib
long = base64.b64encode(hashlib.sha256(b"a" * 200).digest())Operating a password store
- Never log or email a password, and never send it to an analytics system.
- Rate limit login attempts per account and per IP; the KDF protects the offline case, not the online one.
- Run the verification even when the user does not exist, against a dummy hash, so response time does not reveal account existence.
- Rehash on successful login when parameters are below the current target — that is the migration path.
- Consider a pepper stored outside the database to survive a database-only leak.
# constant work whether or not the account exists
DUMMY = ph.hash("dummy password for timing")
def login(username, password):
row = db.get_user(username)
stored = row.password_hash if row else DUMMY
try:
ph.verify(stored, password)
except Exception:
raise AuthError("invalid credentials") # same message either way
if not row:
raise AuthError("invalid credentials")
if ph.check_needs_rehash(stored):
db.set_password_hash(row.id, ph.hash(password))
return row⚠️
Raising the work factor is a one-way trade. Test the new parameters on production hardware under peak load first: a KDF tuned on a developer laptop can saturate the login service at the wrong moment, and rolling back mid-incident is painful.
FAQ
Which should I choose today?
Argon2id with 64 MiB of memory and a time cost tuned to about 100 milliseconds. If Argon2 is unavailable, use bcrypt at cost 12 or PBKDF2-SHA256 with a high iteration count.
Do I still need a salt if I use bcrypt?
Yes, and the library generates one. The salt prevents precomputed tables and makes two identical passwords produce different hashes.
Related
Salts, peppers and work factors Migrating off MD5 and SHA-1 safely
Last refreshed 2026-09-18.