Passwords and secrets
Store passwords so a stolen database does not reveal them, and keep the keys your service uses out of code, logs, and reach.
BY THE END, YOU CAN
- Explain why passwords need a slow, salted hash instead of a fast one
- Verify a password against a stored record and upgrade its cost safely
- Decide where a secret lives and what to do when one leaks
Diagram description: Password → Slow hash; Salt → Slow hash; Slow hash → Stored record; Stored record → Login (read back); Stored record → A stolen copy
Store a password so a leak does not reveal it
A service never needs to read a password back, only to check one, so it stores a one-way hash instead of the password.
Not encryption: whoever obtains the key would recover every password at once. Not a fast hash such as SHA-256 either: it lets an attacker who copies the table test guesses very quickly. Password hashing functions are slow on purpose, with a cost you can raise as hardware gets faster.
Each password also gets its own random salt, stored next to the hash, so precomputed tables do not work and two users with the same password get different hashes. OWASP's Password Storage Cheat Sheet recommends Argon2id first (at least 19 MiB of memory, 2 iterations, parallelism 1), then scrypt (cost 2^17, block size 8, parallelism 1), bcrypt only for older systems (work factor 10 or more, input limited to 72 bytes), and PBKDF2 with HMAC-SHA-256 and at least 600,000 iterations where FIPS-140 compliance is required. Store the algorithm, its parameters, the salt, and the hash together, so you can verify the record and upgrade it later.
import hashlib, secrets
COST = 17 # n = 2**17, the minimum OWASP gives for scrypt
def hash_password(password):
salt = secrets.token_bytes(16)
digest = hashlib.scrypt(password.encode(), salt=salt, n=2**COST, r=8, p=1,
maxmem=2**28)
return f"scrypt${COST}${salt.hex()}${digest.hex()}"
Verify, compare, and upgrade
To check a login, read the stored record, recompute the hash of the submitted password with the record's own salt and parameters, and compare the two.
Compare with hmac.compare_digest, which Python documents as resistant to timing attacks: an ordinary == can stop at the first differing byte, and the time it takes leaks how much of a guess was right. Python's hashlib.scrypt needs a larger maxmem than its default for the recommended cost; without it the call fails with a memory-limit error rather than quietly using a weaker setting. When you raise the cost, old records still verify, because each carries its own parameters.
Rehash a password with the new cost after the user's next successful login, the only moment you have it in plain text. Accounts that never log in keep the old cost, so OWASP suggests expiring very old hashes and asking for a new password.
import hmac
def verify_password(password, record):
_, cost, salt, digest = record.split("$")
candidate = hashlib.scrypt(password.encode(), salt=bytes.fromhex(salt),
n=2 ** int(cost), r=8, p=1, maxmem=2**28)
return hmac.compare_digest(candidate, bytes.fromhex(digest))
Keep secrets out of the code
Relay has secrets of its own: the database password, the email provider's API key, the key that signs session tokens.
A secret committed to a repository stays in its history and in every clone, so deleting the file later does not take it back. When one leaks, revoke it at the provider and issue a new one first; cleaning the history comes after and has problems of its own. Keep secrets in a secret manager and give each service only the ones it needs.
Environment variables are common, but other processes can read them and they can end up in logs or crash dumps, so OWASP's Secrets Management Cheat Sheet treats them as a fallback. Never log a secret; mask it. Rotate secrets on a schedule, so a stolen one works only for a while, and prefer short-lived credentials issued on demand where the platform offers them.
Restarting a service does not revoke a stolen key: only the provider can.
COMMON FAILURE MODE
Encrypting passwords instead of hashing them. Anyone who gets the key gets every password back, while a password hash has no key to steal.
TRY IT YOURSELF
List every secret Relay uses. For each one, write where it lives, which services can read it, how you would rotate it, and what you would do in the first hour after it leaks.
Make the call in the laboratory.
The two-question checkpoint, your notes, and your saved progress are in the laboratory. Free, with no account.
Open in the laboratory ↗CONNECT IT TO RELAY
Add accounts to Relay: store passwords as scrypt records with a per-user salt, verify them with hmac.compare_digest, and rehash after login when the cost rises. Move Relay's database password and email key out of the code, and write down how each one is rotated.