User Authentication · 5.2

🔏 Password Hashing

Why passwords must never be stored in plaintext — and how to store them correctly⏱ ~3 min

🧂Hashing a password = grinding a key into dust

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

⚠ WarningMD5(password) and SHA-256(password) are cryptographically weak for password storage. Both are extremely fast — a GPU can compute billions per second. An attacker with a stolen database can precompute hashes for every word in a dictionary (rainbow tables) or brute-force millions of passwords per second. Never use plain SHA-256, MD5, or SHA-1 for password storage.

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

AlgorithmTypeResistanceRecommended
bcryptAdaptive hash, work factorGPU/ASIC resistant via Blowfish setup✓ Good default; work factor 12+
Argon2idMemory-hard KDF, winner of PHC 2015Best GPU/ASIC resistance via memory hardness✓✓ Best choice for new systems
scryptMemory-hard KDFGPU resistant; more complex to tune✓ Good; less widely supported than Argon2
PBKDF2Iterated HMACLess GPU resistant; fast on ASIC⚠️ Acceptable only with SHA-256 and 600k+ iterations (OWASP 2023 guidance)
SHA-256 (plain)Fast hashNo GPU resistance, no salt by default❌ Never use for passwords

Argon2id in Practice

python
# 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 login
try:
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
pass
🔒 SecurityIf your database is breached and passwords are stored as Argon2id hashes with memory=64MB, an attacker with 100 GPUs can only test about 50-100 passwords per second per GPU — a 10-character random password would take longer than the universe's age to crack. Compare: 100 GPUs cracking plain SHA-256 → billions of guesses per second → dictionary cracked in minutes.
🧠Quick Checkfirst try = +5 XP

Why is plain SHA-256 wrong for password storage?

0 XP🔥 0 days