Tools · Cryptography and passwords in practice · 09 / 20
Storing passwords correctly in code: Argon2id and bcrypt with Python
Hash and verify passwords with Argon2id or bcrypt, choose parameters, handle rehashing when parameters change, and avoid fast hashes and homemade schemes.
Why special hashes
General-purpose hashes such as SHA-256 are designed to be fast, which helps attackers who steal a database and try billions of guesses. Password hashing functions are deliberately slow and, in Argon2’s case, memory-hard, and they include a random salt per password so identical passwords produce different hashes. OWASP recommends Argon2id first, then scrypt, then bcrypt, with PBKDF2 for FIPS-constrained environments.
Using argon2-cffi and bcrypt
Both libraries produce a self-describing string containing algorithm, parameters, salt, and hash, so you store one field. Verify with the library function, which compares in constant time.
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1) # OWASP minimum profile
stored = ph.hash('correct horse battery staple')
print(stored) # $argon2id$v=19$m=19456,t=2,p=1lt;salt>lt;hash>
def login(stored_hash, attempt):
try:
ph.verify(stored_hash, attempt)
except VerifyMismatchError:
return False, stored_hash
if ph.check_needs_rehash(stored_hash):
stored_hash = ph.hash(attempt) # upgrade parameters transparently
return True, stored_hash
import bcrypt
h = bcrypt.hashpw(b'correct horse battery staple', bcrypt.gensalt(rounds=12))
print(bcrypt.checkpw(b'correct horse battery staple', h))Lab: measure the cost
In a virtual environment, install argon2-cffi and bcrypt. Time 100 hashes with SHA-256, bcrypt at cost 12, and Argon2id with the profile above. Compare the per-hash times: SHA-256 takes microseconds, the others tens to hundreds of milliseconds. Then increase memory_cost and observe check_needs_rehash returning True for old hashes.
python -m venv venv && . venv/bin/activate && pip install argon2-cffi bcrypt
python - <<'EOF'
import time, hashlib, bcrypt
from argon2 import PasswordHasher
pw = b'lab-password'
for name, fn in [('sha256', lambda: hashlib.sha256(pw).digest()),
('bcrypt12', lambda: bcrypt.hashpw(pw, bcrypt.gensalt(12))),
('argon2id', lambda: PasswordHasher().hash(pw))]:
t = time.perf_counter(); [fn() for _ in range(20)]; print(name, round((time.perf_counter() - t) / 20 * 1000, 2), 'ms')
EOFPitfalls
Do not invent schemes like sha256(salt + password). bcrypt only uses the first 72 bytes of input, so very long passphrases are truncated; Argon2 has no such limit. Never log passwords or hashes. Add rate limiting and MFA, because even good hashing does not stop online guessing.
Diagram
Password hashing options
OWASP order of preference.
- Argon2id
Memory-hard; first choice. Example: m=19 MiB, t=2, p=1.
- scrypt
Memory-hard; good alternative.
- bcrypt
Widely supported; cost 10 or higher; 72-byte input limit.
- PBKDF2-HMAC-SHA256
For FIPS needs; high iteration count (hundreds of thousands).
- Plain SHA-256 or MD5
Never for passwords.
Check yourself
1. Which algorithm does OWASP recommend first for password storage?
2. How do you upgrade old hashes to new parameters without forcing resets?
3. What input length limit does bcrypt have?