HackBlue

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.

10 min read · practice only with authorization

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.

Snippet
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=1
lt;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.

Snippet
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')
EOF

Pitfalls

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?