← Programs meet the world STAGE 04 · 32 MIN

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.

Open in the laboratory ↗

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
At sign-up, the password and a new random salt go through a slow hash, and only the record is stored. At login, the record’s own salt and cost are used again and the results are compared in constant time.

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.

Python
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.

Python
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.

RETRIEVAL CHECK / APPLY THE IDEA

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.