Secure Password Storage and Secrets Handling in Python
Introduction
When a database leaks, the way passwords were stored decides whether attackers get working credentials or just a pile of useless hashes. This lesson follows OWASP's guidance: use a slow, salted, memory-hard password hash such as Argon2id, never reversible encryption, and use Python's secrets module for anything that must be unguessable.
Prerequisites
- Python basics
- The cryptography basics lesson
- pip install argon2-cffi
Step-by-Step Guide
Step 1: Never store or encrypt passwords reversibly
Passwords should be hashed, not encrypted, because you never need the original back; you only need to check whether a login attempt matches. Fast general-purpose hashes such as SHA-256 are unsuitable, because attackers can test billions of guesses per second. A good password hash is deliberately slow and memory-hungry, and includes a unique random salt for each password, which libraries handle for you.
Step 2: Use Argon2id
OWASP recommends Argon2id as the first choice. Its minimum configuration is 19 MiB of memory, 2 iterations, and 1 degree of parallelism, with equivalent trade-offs such as 46 MiB with 1 iteration. The argon2-cffi library's defaults are stronger than that minimum and may vary by platform, so check that hashing takes a reasonable time on your server, ideally well under a second, and watch memory use in small containers.
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher() # defaults are stronger than OWASP's minimum
stored_hash = ph.hash("correct horse battery staple")
def check_login(stored_hash: str, password: str) -> bool:
try:
ph.verify(stored_hash, password)
except VerifyMismatchError:
return False
if ph.check_needs_rehash(stored_hash):
new_hash = ph.hash(password)
# save new_hash to the database so old hashes get upgraded over time
return TrueStep 3: Know the alternatives and their pitfalls
If Argon2id is unavailable, OWASP lists scrypt with N of 2 to the 17th power, r of 8, and p of 1; bcrypt with a work factor of at least 10, for legacy systems; and PBKDF2-HMAC-SHA256 with 600,000 iterations when FIPS compliance is required. In Python, hashlib.scrypt needs a larger maxmem than its 32 MiB default for those parameters. bcrypt only considers the first 72 bytes of a password, and recent versions of the Python library raise an error for longer input instead of silently truncating, so handle that case deliberately.
import hashlib, os
salt = os.urandom(16)
key = hashlib.scrypt(
b"password-bytes",
salt=salt,
n=2**17, r=8, p=1,
maxmem=256 * 1024 * 1024, # raise above the 32 MiB default
dklen=32,
)
import bcrypt
hashed = bcrypt.hashpw(b"password-bytes", bcrypt.gensalt()) # default cost 12
ok = bcrypt.checkpw(b"password-bytes", hashed)Step 4: Generate tokens with the secrets module
Python's random module is not suitable for security. The secrets module uses the operating system's secure random source. Use it for session identifiers, password reset links, and API keys, and compare secret values with compare_digest, which takes constant time and avoids leaking information through timing differences. 32 bytes, or 256 bits, is a sensible size for tokens.
import secrets
reset_token = secrets.token_urlsafe(32)
reset_url = "https://example.com/reset?token=" + reset_token
def tokens_match(a: str, b: str) -> bool:
return secrets.compare_digest(a, b)Step 5: Add pepper and policy carefully
A pepper is a secret value kept outside the database, for example in a secrets manager, and combined with the password before hashing, which adds protection if only the database leaks. It is optional defense in depth, and changing it forces password resets, so plan for that. Pair strong hashing with a sensible password policy: allow long passwords and all characters, check against lists of breached passwords, and offer multi-factor authentication, as the identity lessons explain.
FAQs
- How do I upgrade the hashes of existing users?
- Re-hash on successful login. After verifying the password with the old scheme, hash it with the new one and replace the stored value. Libraries like argon2-cffi can tell you when a hash needs rehashing.
- Why is a unique salt important?
- It makes identical passwords produce different hashes and defeats precomputed tables, so attackers must attack each hash separately. Modern libraries generate and store the salt inside the hash string automatically.
Summary
Hash passwords with Argon2id, upgrade old hashes at login, use scrypt, bcrypt, or PBKDF2 only when needed, generate tokens with the secrets module, compare secrets in constant time, and keep any pepper outside the database.
Comments
Log in or create a free account to join the discussion.