🔏 Password Hashing
Why passwords must never be stored in plaintext — and how to store them correctly⏱ ~3 min
If you grind a key into metal dust and store the dust, you can compare: grind a second key and see if the dust looks the same. But you can't reconstruct the original key from the dust. Password hashing works the same way — store the hash, compare hashes at login, never store the original password.
Why Plain Hashing (SHA-256, MD5) Is Not Enough
Salting — Defeating Rainbow Tables
A salt is a random value generated uniquely for each user and stored alongside their hash. The hash is computed as H(salt + password). Even if two users have the same password, their hashes differ because their salts differ. Rainbow tables (precomputed hash tables) become useless because they'd need a separate table for every possible salt.
Password Hashing Algorithms — Use These
| Algorithm | Type | Resistance | Recommended |
|---|---|---|---|
| bcrypt | Adaptive hash, work factor | GPU/ASIC resistant via Blowfish setup | ✓ Good default; work factor 12+ |
| Argon2id | Memory-hard KDF, winner of PHC 2015 | Best GPU/ASIC resistance via memory hardness | ✓✓ Best choice for new systems |
| scrypt | Memory-hard KDF | GPU resistant; more complex to tune | ✓ Good; less widely supported than Argon2 |
| PBKDF2 | Iterated HMAC | Less GPU resistant; fast on ASIC | ⚠️ Acceptable only with SHA-256 and 600k+ iterations (OWASP 2023 guidance) |
| SHA-256 (plain) | Fast hash | No GPU resistance, no salt by default | ❌ Never use for passwords |
Argon2id in Practice
# Python — Argon2id password hashing (argon2-cffi library)from argon2 import PasswordHasher ph = PasswordHasher( time_cost=3, # iterations (higher = slower = stronger) memory_cost=65536, # memory in KiB (64 MB) parallelism=1, # parallel threads hash_len=32, # output length in bytes salt_len=16, # random salt length) # Hash a password (salt is generated automatically)hash = ph.hash('my-strong-password')# $argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash> # Verify at logintry: ph.verify(hash, 'my-strong-password') # True # Check if rehash needed (parameters changed) if ph.check_needs_rehash(hash): hash = ph.hash('my-strong-password')except Exception: # Password incorrect — reject login passWhy is plain SHA-256 wrong for password storage?