82.6 Password Hashing: bcrypt, argon2-cffi, and Passlib
Right, let’s talk about password hashing. If you’re storing user passwords in plaintext, close this book, go find your database, and apologize to it. We’ve all seen the headlines, and you do not want your company’s name in that particular font. The goal isn’t to encrypt passwords; encryption implies you can decrypt them. We need a one-way street. We need to hash them.
A proper password hash takes the user’s password, mixes in a long, random value (a ‘salt’), and then feeds it through a computationally expensive function. This gives us three crucial properties: 1) the same password with a different salt gives a completely different hash, defeating pre-computed rainbow tables, 2) it’s slow by design, making brute-force attacks impractical, and 3) verifying a user’s login just means re-hashing their input with the original salt and seeing if it matches. We never store the actual password.
Why bcrypt is Your Reliable Workhorse
For years, bcrypt has been the gold standard, and for good reason. It was designed in 1999 by Niels Provos and David Mazières specifically for passwords. Its core feature is a configurable cost factor (often called ‘work factor’ or ‘rounds’) that lets you deliberately slow it down. As computers get faster, you just crank up the cost. It handles the salting for you automatically and bundles the salt, cost, and hash into one tidy string.
Here’s how you use it in Python with the excellent bcrypt library:
import bcrypt
# Hashing a password for the first time
password = b"super_secret_password"
# Generate a salt and hash the password. The cost factor (12) is the log2 number of rounds.
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
print(hashed) # Looks like: b'$2b$12$sR9UaPYdFeWEeDzlPaqM...'
# Verifying a password against a stored hash
user_input_password = b"super_secret_password"
if bcrypt.checkpw(user_input_password, hashed):
print("Password is correct!")
else:
print("Invalid password.")
Notice the b prefix for the strings? bcrypt works with bytes, not strings. It’s a minor gotcha, but a common one. The default cost factor is 12, which is a great starting point as of my writing. A cost of 12 means 2^12 (4096) rounds. The beauty is that verifying one password is fast for you, but an attacker trying billions of passwords finds it excruciatingly slow.
When to Bring Out the Big Guns: Argon2
While bcrypt is still perfectly secure, the winner of the 2015 Password Hashing Competition was Argon2. It’s designed to be even more resistant to specialized hardware attacks (like GPUs, FPGAs, and ASICs) that can brute-force bcrypt more efficiently. Argon2 has three variants, but you generally want argon2id as it provides a hybrid resistance against both side-channel and GPU-based attacks.
To use it in Python, we turn to argon2-cffi. It’s a bit more complex to configure, which is a feature, not a bug.
from argon2 import PasswordHasher, exceptions
# Configure the hasher. These are good, secure defaults as of now.
ph = PasswordHasher(
time_cost=2, # Number of iterations
memory_cost=102400, # Memory usage in KiB (102400 KiB = 100 MB)
parallelism=2, # Number of parallel threads
hash_len=16, # Length of the hash in bytes
salt_len=16 # Length of the random salt to generate
)
# Hashing a password
try:
hashed = ph.hash("super_secret_password")
print(hashed) # A long string containing all parameters and the hash
# Verifying a password
ph.verify(hashed, "super_secret_password")
print("Password is correct!")
except exceptions.VerifyMismatchError:
print("Invalid password.")
except exceptions.VerificationError:
print("Could not verify password (hash is corrupted).")
The key here is the memory_cost parameter. Argon2 requires a large block of memory to compute, which is extremely expensive for a massively parallel attacker. Tuning these parameters is crucial. The defaults in the library are sane, but you should test them on your production hardware to ensure verification takes an acceptable amount of time (e.g., between 500ms - 1 second).
The Best Tool for the Job: Passlib
Now, you might be thinking, “Do I have to choose? What if a new algorithm comes out next year?” This is where Passlib shines. It’s a fantastic library that provides a unified interface for over 30 password hashing schemes. Its real power is in its “context” system, which lets you easily manage and migrate hashes.
Want to upgrade from bcrypt to Argon2 seamlessly? Passlib handles it for you. When a user logs in with their old bcrypt hash, Passlib will verify it correctly and then automatically re-hash their password using your new, preferred scheme. It’s migration magic.
from passlib.context import CryptContext
# Set up your context. This defines your preferred hashes and policies.
pwd_context = CryptContext(
schemes=["argon2", "bcrypt"], # Preferred order: try argon2 first, then bcrypt
default="argon2", # Default scheme for new hashes
argon2__time_cost=2,
argon2__memory_cost=102400,
# ... all other parameters for your schemes
)
# Hashing a password
hash_string = pwd_context.hash("my_password")
# Verifying a password
if pwd_context.verify("my_password", hash_string):
print("Login successful!")
# Check if the hash needs upgrading (e.g., is it using an old scheme?)
if pwd_context.needs_update(hash_string):
print("Hash is outdated, upgrading...")
new_hash = pwd_context.hash("my_password")
# Store new_hash in the database, replacing the old one
The Pitfalls You Absolutely Must Avoid
Pepper is Pointless for Most of Us: A ‘pepper’ is a secret key added to the password before hashing. It’s often more trouble than it’s worth. If compromised, you have to reset every single password. If you lose it, no one can log in. Just use a properly configured modern algorithm. It’s enough.
Don’t Roll Your Own Cryptography: I mean it. Don’t try to chain algorithms together or invent your own. The math is subtle, and the odds you introduce a critical flaw are 100%. Stick to the blessed, peer-reviewed algorithms and well-audited libraries like the ones above.
Tune Your Work Factors: Don’t just accept the defaults forever. Every year or two, test how long verification takes on your hardware. If it’s too fast (sub-100ms), increase the cost factor. Security is an arms race, and your work factors are your armor.