Storing passwords safely
Hash passwords with a unique salt and a deliberately slow function, then verify in constant time.
import hashlib
import hmac
import os
ITERATIONS = 600_000
def hash_password(password, salt):
return hashlib.pbkdf2_hmac("sha256", password.encode(), salt, ITERATIONS)
def verify(password, salt, stored):
return hmac.compare_digest(hash_password(password, salt), stored)
salt_a = os.urandom(16)
salt_b = os.urandom(16)
stored = hash_password("correct horse", salt_a)
print(verify("correct horse", salt_a, stored))
print(verify("wrong guess", salt_a, stored))
print(hash_password("correct horse", salt_a) == hash_password("correct horse", salt_b))
print(len(salt_a), len(stored))True False False 16 32
Explanation
A system that stores passwords must assume its database will eventually leak. Never store passwords in plain text or encrypted with a key kept next to the data. Store only a verifier that can check a guess but is expensive to reverse.
A fast hash such as SHA-256 is the wrong tool, because an attacker can test billions of guesses per second. Use a password-hashing function designed to be slow and to use memory: Argon2id, scrypt, bcrypt, or PBKDF2. Give every password its own random salt, a unique value stored next to the hash. The salt makes two users with the same password produce different hashes and defeats precomputed rainbow tables. The work factor (iteration count) is tuned so that one hash takes a noticeable fraction of a second on your servers, and it must be raised over time as hardware improves.
This example uses PBKDF2-HMAC-SHA256 with 600,000 iterations, which is available in Python's standard library. OWASP lists Argon2id as the first choice and PBKDF2 for situations that require FIPS-approved algorithms; check the current guidance when you choose parameters. To compare the result, use hmac.compare_digest, which takes the same time however many bytes match, so timing cannot reveal how close a guess was.
Explore the dry run
import hashlibimport hmacimport os ITERATIONS = 600_000 def hash_password(password, salt): return hashlib.pbkdf2_hmac("sha256", password.encode(), salt, ITERATIONS) def verify(password, salt, stored): return hmac.compare_digest(hash_password(password, salt), stored) salt_a = os.urandom(16)salt_b = os.urandom(16)stored = hash_password("correct horse", salt_a)print(verify("correct horse", salt_a, stored))print(verify("wrong guess", salt_a, stored))print(hash_password("correct horse", salt_a) == hash_password("correct horse", salt_b))print(len(salt_a), len(stored))Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| PBKDF2 applies HMAC-SHA256 600,000 times, making each guess expensive for an attacker. | — | — | |
| A fresh random 16-byte salt from the operating system's secure generator; it is stored with the hash. | — | — | |
| Hashing the right password with the same salt reproduces the stored value. | — | True | |
| A wrong guess gives a different hash. | — | True False | |
| The same password with a different salt gives a different hash, so identical passwords are not visible in the database. | — | True False False | |
| A 16-byte salt and a 32-byte derived key. | — | True False False 16 32 |
Cost and common mistakes
Verification costs about ITERATIONS hash computations by design. The cost is intentional and tuned to your hardware.
- Never use a fast general-purpose hash (SHA-256, MD5) directly for passwords. Speed helps the attacker.
- Do not reuse one salt for all users, and never derive it from the password. Generate a random salt per password.
- Compare secrets with hmac.compare_digest. A normal == can leak information through timing differences.
Try it yourself
Why must the salt be stored next to the hash if it is not secret?
Reveal answer and reasoning
To verify a login the system must recompute the hash with the same salt. The salt's job is uniqueness, which defeats precomputed tables, not secrecy.