Password Hashing
22 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)A database leak is bad. A database full of readable passwords is a disaster. A hash lets you check a password without keeping it, and a salt plus a slow algorithm keeps leaked hashes hard to crack.
A hash is a one-way fingerprint
Hash function is a function that turns any input into a fixed-size output, always the same for the same input. It cannot be reversed, so the original data cannot be read from the hash.
A server never needs to read a password back. It only needs to ask: is this the same password? A hash gives the answer. The same input always gives the same output, and the output cannot be turned back into the input.
import hashlib
h1 = hashlib.sha256(b"Orange#2024").hexdigest()h2 = hashlib.sha256(b"Orange#2024").hexdigest()print(h1 == h2, len(h1))Example 1 · Equal passwords give equal plain hashes
import hashlib
users = {"asha": "Welcome1", "ravi": "Welcome1", "meena": "Tr0ub4dor&3"}for name, pw in users.items(): print(name, hashlib.sha256(pw.encode()).hexdigest()[:16])asha 7e19e31ae82d7490 ravi 7e19e31ae82d7490 meena 48486e1514e84234
What this code does
- asha and ravi chose the same password, so their hashes match. An attacker who cracks one row has cracked both.
- A salt removes this link.
Equal input gives an equal hash, and SHA-256 always gives 64 hex characters. A plain fast hash is still a weak choice for passwords, as the next sections show.
A salt makes every hash different
Salt is a random value, unique per password, mixed in before hashing. It is stored beside the hash. It makes equal passwords hash differently and makes pre-built tables useless.
Two users often pick the same password. With a plain hash, their rows look the same, and one pre-built table of common passwords cracks everyone at once. A random salt added before hashing breaks this.
import hashlib, secrets
salt_a = secrets.token_bytes(16)salt_b = secrets.token_bytes(16)h_a = hashlib.pbkdf2_hmac("sha256", b"Orange#2024", salt_a, 1000)h_b = hashlib.pbkdf2_hmac("sha256", b"Orange#2024", salt_b, 1000)print(h_a == h_b)Example 2 · The same password with two salts
import hashlib
for salt in (b"salt-for-asha", b"salt-for-ravi"): d = hashlib.pbkdf2_hmac("sha256", b"Welcome1", salt, 1000) print(d.hex()[:16])
same = hashlib.pbkdf2_hmac("sha256", b"Welcome1", b"salt-for-asha", 1000)print(same.hex()[:16])2ea4cce9335e8af1 2530dabf8f60c958 2ea4cce9335e8af1
What this code does
- Fixed salts are used here so the output is stable. Real salts come from
secretsand are new each time. - The third line repeats the first salt and gives the first hash again: that is how login checks work.
Same password, different salts, different hashes. The salt is saved next to the hash. It is not secret; it only stops shared cracking.
Slow on purpose
Work factor is a setting such as the iteration count or memory size that decides how slow a password hash is. Raise it over the years as computers get faster.
SHA-256 is built to be fast. A graphics card can try billions of guesses per second. Password hashes are built to be slow and use memory, so each guess costs the attacker real time. The speed is a setting called the work factor.
import hashlib
salt = b"0123456789abcdef"d = hashlib.scrypt(b"Orange#2024", salt=salt, n=2**14, r=8, p=1, dklen=32)print(len(d), d.hex()[:12])Example 3 · hash_password and verify_password
import hashlib, hmac, secrets
def hash_password(password, n=2**14): salt = secrets.token_bytes(16) digest = hashlib.scrypt(password.encode(), salt=salt, n=n, r=8, p=1, dklen=32) return f"scrypt${n}${salt.hex()}${digest.hex()}"
def verify_password(password, stored): scheme, n, salt_hex, digest_hex = stored.split("$") digest = hashlib.scrypt(password.encode(), salt=bytes.fromhex(salt_hex), n=int(n), r=8, p=1, dklen=32) return hmac.compare_digest(digest, bytes.fromhex(digest_hex))
stored = hash_password("Orange#2024", n=2**12)print(stored.split("$")[0], len(stored.split("$")))print(verify_password("Orange#2024", stored))print(verify_password("orange#2024", stored))print(stored == hash_password("Orange#2024", n=2**12))scrypt 4 True False False
What this code does
- The stored text holds the scheme, the cost, the salt and the hash, joined by
$. - Hashing the same password again gives a different stored text because the salt is new, yet
verify_passwordstill works.
scrypt needs about 16 MB of memory for each guess at these settings. Raising n doubles the cost. Real projects use the argon2-cffi or bcrypt libraries, which follow the same idea.
Compare in constant time and upgrade
A normal == stops at the first different byte. The tiny time difference can leak how much matched. hmac.compare_digest takes the same time either way. Store the settings with the hash so you can raise them later.
import hmac
stored = bytes.fromhex("ab12cd34")guess = bytes.fromhex("ab12cd35")print(hmac.compare_digest(stored, guess))Example 4 · Upgrade the hash at login
import hashlib, hmac, secrets
def hash_password(password, n=2**14): salt = secrets.token_bytes(16) digest = hashlib.scrypt(password.encode(), salt=salt, n=n, r=8, p=1, dklen=32) return f"scrypt${n}${salt.hex()}${digest.hex()}"
def verify_password(password, stored): scheme, n, salt_hex, digest_hex = stored.split("$") digest = hashlib.scrypt(password.encode(), salt=bytes.fromhex(salt_hex), n=int(n), r=8, p=1, dklen=32) return hmac.compare_digest(digest, bytes.fromhex(digest_hex))
stored = hash_password("Orange#2024", n=2**10)CURRENT_N = 2**12
def login(password, stored): if not verify_password(password, stored): return False, stored if int(stored.split("$")[1]) < CURRENT_N: stored = hash_password(password, n=CURRENT_N) print("upgraded") return True, stored
ok, stored = login("Orange#2024", stored)print(ok, stored.split("$")[1])ok, stored = login("Orange#2024", stored)print(ok, stored.split("$")[1])upgraded True 4096 True 4096
What this code does
- The cost is read from the stored text, so old and new hashes live together in one table.
- Only a correct login can upgrade, because only then is the plain password in hand.
Use this for hashes, tokens and API keys. At login you already know the plain password, so that is the moment to re-hash it with stronger settings.
Next step
Check your understanding
12 quick questions