Storing passwords

Authentication and authorization Junior Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Predict the output

What does the last line print?

Python 3.13
import hashlib
import hmac
import os


def hash_password(pw: str) -> str:
    salt = os.urandom(16)
    dk = hashlib.scrypt(pw.encode(), salt=salt, n=2**14, r=8, p=1)
    return f"scrypt$16384$8$1${salt.hex()}${dk.hex()}"


def check(pw: str, stored: str) -> bool:
    _, n, r, p, salt, dk = stored.split("$")
    got = hashlib.scrypt(pw.encode(), salt=bytes.fromhex(salt),
                         n=int(n), r=int(r), p=int(p))
    return hmac.compare_digest(got.hex(), dk)


h1, h2 = hash_password("hunter2"), hash_password("hunter2")
print(h1 != h2, check("hunter2", h1), check("hunter3", h1))

Options
Hint 1 of 1

What does a random salt do to identical passwords?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A True True False

Correct. Random salts make the two hashes differ; the right password verifies and a wrong one doesn't.

Each call draws a fresh 16-byte salt, so the same password gives a different stored string. check reads that salt back out of the string, recomputes scrypt and compares, so only the right password matches.

Why not the others?
  • B False True False. Each hash gets its own random salt, so they differ.
  • C True True True. A different password gives a different derived key.
  • D True False False. check uses the salt stored in the hash.

30-second answer

Never store them, only a slow, salted hash made for passwords: Argon2id first choice, scrypt or bcrypt if that's what your stack offers. A unique random salt per password stops precomputed tables and makes equal passwords hash differently; the work factor makes each guess expensive. Compare in constant time, re-hash on login when you raise the cost, and rate-limit login attempts. Fast hashes like SHA-256 are wrong here, even salted.

Practise saying it · 30 s. No recording, just a timer

How do you store passwords?

Say your answer out loud, then check which of these points you covered:

  • Argon2id / scrypt / bcrypt
  • Per-user salt
  • Work factor
  • Constant-time compare
  • Rehash and rate-limit

Deep dive

OWASP's Password Storage Cheat Sheet gives current minimum parameters for each algorithm.

bcrypt truncates input at 72 bytes.

A pepper (secret key, stored outside the database) adds protection if only the database leaks.

Check new passwords against known breached lists rather than imposing composition rules.

scrypt from the standard library Ran on Python 3.13.5 · mypy --strict
Python 3.13
import hashlib
import hmac
import os


def hash_password(pw: str) -> str:
    salt = os.urandom(16)
    dk = hashlib.scrypt(pw.encode(), salt=salt, n=2**14, r=8, p=1)
    return f"scrypt$16384$8$1${salt.hex()}${dk.hex()}"


def check(pw: str, stored: str) -> bool:
    _, n, r, p, salt, dk = stored.split("$")
    got = hashlib.scrypt(pw.encode(), salt=bytes.fromhex(salt),
                         n=int(n), r=int(r), p=int(p))
    return hmac.compare_digest(got.hex(), dk)


h1, h2 = hash_password("hunter2"), hash_password("hunter2")
print(h1 != h2, check("hunter2", h1), check("hunter3", h1))

Common mistakes

  • SHA-256 or MD5 for passwords.
  • One global salt.
  • Encrypting passwords so they can be 'recovered'.

Senior take & follow-ups

Slow password hash, per-user salt, constant-time compare, rehash on upgrade, rate limits.

Follow-up questions

Think of your answer first, then open the hint.

  1. Why are fast hashes bad here?

    Show hint

    Attackers can try billions of guesses per second on GPUs.

  2. How do you upgrade old hashes?

    Show hint

    Re-hash with the new algorithm when the user next logs in.

Suggest a fix if something here is wrong or out of date.