A customer table that leaks
Password hashing means storing a slow, salted hash of each password instead of the password itself. If a thief copies the database, they get hashes that are very costly to turn back into passwords.
A shop's customer table leaks. What the thief can do next depends on one choice the developers made months earlier.
By the end of this lesson
Note
I can store passwords and keys safely.
Key words
| Word | What it means |
|---|---|
| Salt | a random value added to each password before hashing |
| Slow hash | a hash made costly on purpose, to slow down guessing |
| Work factor | how costly the hash is, such as the number of rounds |
| Dictionary attack | trying a list of likely passwords |
| Key rotation | replacing a key with a new one on a plan |
| Secret store | a locked place that hands keys to approved programs only |
The cells students fill in are left blank in this preview.
A plain password hash is easy to crack
Never store a password as it is typed. Storing its plain hash is little better. People choose common passwords, and one password always gives one hash.
An attacker hashes a list of likely passwords and compares. This is a dictionary attack. A fast hash like SHA-256 lets a graphics card try billions of guesses a second.
Hash the password hunter2 with no salt
printf 'hunter2' | sha256sumShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Find the password behind a stolen hash
import hashlib
# A stolen hash of an invented password. No salt was used.
stolen = "f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7"
guesses = ["password", "123456", "letmein"]
# Add the guess "hunter2" to the list, then press Run.
for guess in guesses:
if hashlib.sha256(guess.encode()).hexdigest() == stolen:
print("the password was", guess)
A salt makes every hash different
A salt is a random value stored beside the hash. It is added to the password before hashing. Two users with the same password then get different hashes.
The salt is not secret. It forces the attacker to attack each account on its own, so a ready-made list of hashes is useless.
Hash hunter2 with the salt k3q9
printf 'k3q9hunter2' | sha256sumShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Hash hunter2 with the salt z8m1
printf 'z8m1hunter2' | sha256sumShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Worked example: one password, three records
| Stored as | Hash (first 12 characters) |
|---|---|
| No salt, hunter2 | f52fbd32b2b3 |
| Salt k3q9, then hunter2 | 24f39aa1267f |
| Salt z8m1, then hunter2 | 0c458853fecc |
The cells students fill in are left blank in this preview.
A common mistake
Warning
Many people think encrypting passwords is safer than hashing them. Encrypted passwords can be decrypted by anyone who steals the key. A site never needs to read a password back, only to check one. So hash it, and never encrypt it.
Password hashes must be slow on purpose
A login can afford a tenth of a second. An attacker needs billions of guesses. A slow hash makes each guess costly, so the attack becomes too expensive.
Use Argon2id first. scrypt and bcrypt are sound too. PBKDF2 is acceptable with at least 600,000 rounds. Store the settings with the hash, so you can raise them later. Use a vetted library.
Worked example: a record that keeps its settings
| Part | Value | Why it is kept |
|---|---|---|
| Scheme | pbkdf2 | Tells you which hash to run |
| Rounds | 1000 (a demo: far too few) | Lets you raise it for new logins |
| Salt | k3q9 | Different for each account |
| Hash | 957dfc979f9a1524fe1f... | What the attempt is compared with |
The cells students fill in are left blank in this preview.
A slow salted hash, 1000 rounds (a demo)
python3 -c "import hashlib; print(hashlib.pbkdf2_hmac('sha256', b'hunter2', b'k3q9', 1000).hex())"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Keys belong in a secret store
A key that leaks is as bad as a password that leaks. Make keys with a secure random source, such as Python's secrets. Never type one up, and never use random.
Keep a key out of code and out of email. Give it to programs from a secret store. Allow only the programs that need it, rotate it on a plan, and replace it at once if it may have leaked.
Worked example: key sizes to choose today
| Key | Size to use |
|---|---|
| AES key | 128 bits, or 256 bits for long-lived data |
| HMAC key | 256 bits |
| RSA key | 2048 bits at least, 3072 for long-lived data |
| Elliptic curve key | About 256 bits (X25519, Ed25519) |
| Key from a password | Made with Argon2id or scrypt, never a plain hash |
The cells students fill in are left blank in this preview.
Make a pretend key file
printf 'not a real key' > demo.keyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Allow only you to read and write it
chmod 600 demo.keyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Check the file's mode as a number
stat -c '%a' demo.keyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Make a random key, count its bytes
python3 -c "import secrets; print(len(secrets.token_bytes(32)))"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.