Secure Password Storage
Never store passwords in plain text. Never store only an MD5 or SHA hash. The correct hash function for passwords is a key derivation function (KDF) that is slow by design.
Why MD5 and SHA Are Not Enough
MD5 and SHA-256 are fast — designed for high throughput. A modern GPU can compute billions of SHA-256 hashes per second.
GPU RTX 4090:
SHA-256: ~22 billion hashes/second
bcrypt cost 12: ~2,800 hashes/second
With MD5/SHA, an attacker who steals the database can brute-force common passwords in seconds.
bcrypt
A 1999 algorithm that is still secure. Includes automatic salt and an adjustable cost factor.
import bcrypt
# Hash a password
password = b"my_secret_password"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
# $2b$12$... (salt + hash embedded)
# Verify
bcrypt.checkpw(password, hashed) # True
The rounds=12 parameter sets the cost (2^12 iterations). Increase it as hardware improves.
argon2
Winner of the Password Hashing Competition (2015). Current OWASP recommendation. Three variants:
- Argon2d — GPU-resistant, but vulnerable to side-channel.
- Argon2i — side-channel resistant, less so against GPU.
- Argon2id — hybrid. Use this one.
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # iterations
memory_cost=65536, # 64 MB of memory
parallelism=2
)
hashed = ph.hash("my_password")
ph.verify(hashed, "my_password") # True
High memory usage makes attacks with specialized hardware (ASICs) impractical.
scrypt
Similar to argon2 in concept. Parameterized by N (CPU/memory), r (block size), and p (parallelism).
import hashlib, os
salt = os.urandom(16)
key = hashlib.scrypt(b"password", salt=salt, n=2**15, r=8, p=1)
Comparison
| Algorithm | Auto salt | GPU resistance | ASIC resistance | OWASP Recommendation |
|---|---|---|---|---|
| MD5 | No | Weak | Weak | Never |
| SHA-256 | No | Weak | Weak | Never (for passwords) |
| bcrypt | Yes | Medium | Medium | Yes |
| scrypt | No | Good | Good | Yes |
| argon2id | Yes | Excellent | Excellent | Preferred |
Golden Rule
When verifying a password, always use a constant-time comparison function (checkpw, verify). Never compare strings directly — this is vulnerable to timing attacks.