Storing passwords safely

Hash passwords with a unique salt and a deliberately slow function, then verify in constant time.

Python
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))
Output
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
Guided walkthroughReady
1import hashlib
2import hmac
3import os
4
5ITERATIONS = 600_000
6
7
8def hash_password(password, salt):
9 return hashlib.pbkdf2_hmac("sha256", password.encode(), salt, ITERATIONS)
10
11
12def verify(password, salt, stored):
13 return hmac.compare_digest(hash_password(password, salt), stored)
14
15
16salt_a = os.urandom(16)
17salt_b = os.urandom(16)
18stored = hash_password("correct horse", salt_a)
19print(verify("correct horse", salt_a, stored))
20print(verify("wrong guess", salt_a, stored))
21print(hash_password("correct horse", salt_a) == hash_password("correct horse", salt_b))
22print(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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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.

Further reading