Keep what you keep safely
Why it matters Even a careful site keeps some data: an email to log in, a password, maybe an address for deliveries. Databases get copied, backups get lost, laptops get stolen. Done well, a stolen copy of your database tells the thief almost nothing. This lesson covers the two main tools: password hashing and encrypting fields at rest.
Passwords: never store them, store a slow fingerprint
Think of a blender. Put in a strawberry and you get a smoothie; you cannot get the strawberry back. To check a password, you blend the attempt the same way and see if the smoothie matches. You never need the original.
Password hash: a one-way fingerprint of a password, made with a function that is deliberately slow and uses a salt, so stolen hashes are very costly to guess from.
Salt: random bytes stored next to each hash and mixed in before hashing, so two people with the same password get different hashes and pre-computed guess lists are useless.
Why slow? A fast hash like plain SHA-256 can be tried billions of times a second, so short passwords fall quickly. Password hashes are tuned to take a noticeable fraction of a second each. That is nothing for one login and a huge cost for someone trying a billion guesses. The widely used choices are Argon2id (the first recommendation in OWASP's password storage guidance), scrypt, bcrypt and PBKDF2 (checked October 2026).
# Teaching code. In a real app, use your framework's password functions.
import hashlib, hmac, os
ROUNDS = 600_000 # PBKDF2-HMAC-SHA256 setting suggested by OWASP (checked October 2026)
def hash_password(password):
salt = os.urandom(16)
key = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, ROUNDS)
return salt.hex() + "$" + key.hex()
def check_password(password, stored):
salt_hex, key_hex = stored.split("$")
key = hashlib.pbkdf2_hmac("sha256", password.encode(), bytes.fromhex(salt_hex), ROUNDS)
return hmac.compare_digest(key.hex(), key_hex) # compare without leaking timingIn Django you do not write this yourself: user.set_password() and check_password() already use PBKDF2 by default, and Django can switch to Argon2 or scrypt in settings (checked October 2026).
A common misconception: "we encrypt our passwords". Encryption can be undone by whoever has the key, so one stolen key reveals every password. Passwords should be hashed, never encrypted.
Other fields: encrypt at rest, keep the key elsewhere
An email address must be readable again (to send a receipt), so a one-way hash will not do. Here you encrypt: lock the value with a key, and unlock it only when the app needs it.
Encryption at rest: storing data in the database or on disk in locked form, so a copy of the files is unreadable without the key.
- Keep the key out of the database, for example in an environment variable or a key service. A key stored in the same backup as the data protects nothing.
- Use a vetted library, never home-made cryptography. In Python, the
cryptographypackage'sFernetis a simple, safe choice. - Looking things up. Good encryption gives a different result each time, even for the same email, so you cannot search for it. Store a keyed hash next to it, an HMAC (a hash mixed with a secret key), and search by that. Without the key, the HMAC cannot be brute-forced the way a plain hash can.
# Concept sketch (needs: pip install cryptography)
from cryptography.fernet import Fernet
import hmac, hashlib
box = Fernet(ENCRYPTION_KEY) # key from the environment
email_enc = box.encrypt(email.encode()) # stored, readable only with the key
email_lookup = hmac.new(LOOKUP_KEY, email.strip().lower().encode(), hashlib.sha256).hexdigest()This is how floati.life's own browser waitlist works: the email is stored encrypted, duplicates are found with a keyed HMAC of the address, and only a hash of the IP address is kept for rate limiting. The Learn "save across devices" feature goes further and never stores the email at all, only a keyed fingerprint, with progress encrypted (checked October 2026).
What it protects, and what it does not
Encryption at rest protects against a stolen backup, a copied database or a lost disk. It does not protect against an attacker who runs code inside your app, because your app holds the key. That is why collecting less (lesson 76) and deleting on time (next lesson) still matter most.
In other languages
// JavaScript (Node): password hashing with built-in scrypt
const salt = crypto.randomBytes(16);
const key = crypto.scryptSync(password, salt, 64);// Go
hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)// PHP (Argon2id where available)
$hash = password_hash($password, PASSWORD_ARGON2ID);
$ok = password_verify($attempt, $hash);// C# (ASP.NET Core Identity)
var hash = new PasswordHasher<User>().HashPassword(user, password);Recap
- Hash passwords with a slow, salted function (Argon2id, scrypt, bcrypt or PBKDF2); never encrypt them.
- Encrypt fields you must read back, keep the key outside the database, and look them up with a keyed HMAC.
- Encryption at rest stops stolen copies, not an attacker inside your running app.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.