Security fundamentals · Beginner

Why passwords need slow hashes

4 min read

Why it matters

Security fundamentals appeared in 70% of the Cybersecurity Engineer job descriptions we read. Password databases leak regularly, and what decides the damage is how the passwords were stored. This lesson shows the difference in numbers.

The concept

The CIA triad. Security protects confidentiality (only the right people see data), integrity (data is not changed without detection) and availability (systems work when needed). Every control you add supports one or more of these.

Hashing is not encryption. A hash is one-way: you cannot get the input back. For passwords you store a hash and compare hashes at login.

Fast hashes are wrong for passwords. SHA-256 is designed to be fast, which helps attackers: with a leaked hash they can try millions of guesses per second from lists of real leaked passwords.

Password hashing functions (Argon2id, scrypt, bcrypt) are deliberately slow and memory-hungry, and add a random salt per password so identical passwords get different hashes and precomputed tables are useless. OWASP recommends Argon2id first.

Worked example: guessing against two hashes

passwords.py
import hashlib
import time

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

leaked_wordlist = ["123456", "password", "qwerty", "iloveyou", "india@123", "welcome1", "admin@123"]
wordlist = leaked_wordlist + [f"guess{i}" for i in range(200_000)]  # stand-in for a big leaked list

# What a careless app stores: unsalted SHA-256 of the password.
stored_sha256 = hashlib.sha256(b"india@123").hexdigest()

t0 = time.perf_counter()
cracked = next(w for w in wordlist if hashlib.sha256(w.encode()).hexdigest() == stored_sha256)
print(f"SHA-256: cracked '{cracked}' in {time.perf_counter() - t0:.4f} s")

t0 = time.perf_counter()
for w in wordlist:
    hashlib.sha256(w.encode()).hexdigest()
rate = len(wordlist) / (time.perf_counter() - t0)
print(f"SHA-256 guesses per second on one CPU core: about {rate:,.0f}")

ph = PasswordHasher()  # Argon2id with the library's default cost settings
stored_argon2 = ph.hash("india@123")
print("Argon2id hash:", stored_argon2[:32] + "...")

t0 = time.perf_counter()
tried = 0
for w in wordlist[:20]:
    tried += 1
    try:
        ph.verify(stored_argon2, w)
        break
    except VerifyMismatchError:
        pass
per_guess = (time.perf_counter() - t0) / tried
print(f"Argon2id: {tried} guesses took {per_guess * 1000:.0f} ms each, about {1 / per_guess:,.0f} guesses per second")
print("same password, different salt:", ph.hash("india@123") != stored_argon2)

One run on our test machine (pip install argon2-cffi, version 25.1):

shellsession
$ python3 passwords.py
SHA-256: cracked 'india@123' in 0.0000 s
SHA-256 guesses per second on one CPU core: about 2,543,156
Argon2id hash: $argon2id$v=19$m=65536,t=3,p=4$y...
Argon2id: 5 guesses took 62 ms each, about 16 guesses per second
same password, different salt: True

A common password falls instantly against unsalted SHA-256, and one CPU core tries about 2.5 million guesses per second (attackers use GPUs, which are far faster). Against Argon2id with the library defaults (64 MB of memory, 3 passes), the same core manages about 16 guesses per second, around 160,000 times fewer. The hash string records the algorithm and its cost parameters, so you can raise the cost later and rehash users when they next log in.

Slow hashing does not save a password like india@123 forever: it is in every guessing list. That is why length requirements, checks against known breached passwords and multi-factor authentication matter too.

Common mistakes

  • Storing passwords with MD5, SHA-1 or SHA-256, even with a salt.

  • Encrypting passwords instead of hashing them. If the key leaks, every password leaks.

  • Writing your own scheme. Use a maintained library and its verify function.

  • Logging passwords in request logs or error messages.

Interview angle

"How should an application store passwords?" (Argon2id or bcrypt through a library, unique salts, tuned cost, plus MFA and breached-password checks). "Hashing versus encryption?" "What is a salt and what does it protect against?" (identical passwords and precomputed tables).

Practice task

  1. Measure Argon2id with time_cost 1, 3 and 6 on your machine and choose settings that keep a login under about 100 ms.

  2. Add a check that rejects passwords appearing in a list of known breached passwords.

Further reading