Store Passwords Safely
A Salt and a Slow Function
The thief of Lesson 1 had two advantages. One guess served every user: 300 digests were computed once and compared with the whole table. And each guess cost almost nothing, because
Those are two separate problems, and they have two separate repairs. This lesson keeps them apart, because each repair leaves the other problem untouched.
A salt
A
The first two lines are the table of Lesson 1: the same password gives the same digest, a353b0deccba3f4e, both times. The last two lines are dana and gus again, with the same password and a salt each. Their digests, 335dd0afc19d7c76 and 52e28c68e4587e5e, have nothing in common.
A salt is not a secret, and it is not a key. It sits in the clear beside the hash, because the server needs it again to check a password. It only has to be different for every record and impossible to predict, so it comes from secrets, for the reason Chapter 2 gave.
This block is here to show what a salt does. It is not how passwords are stored: the
Where does a record's salt live?
What a salt changes for the thief
Think back to the letmein depends on which record it is tried against, so there is no single digest to store for it.
The thief has to start over for every record: take that record’s salt, hash all 300 words with it, compare. For 8 users that is 8 times 300, which is 2,400 digests. For 8 million users it is 2,400 million. The work now grows with the number of accounts.
The second leak is closed too. Two users with the same password get different salts and so different digests. Nobody can see from the table that dana and gus chose alike.
Every record now has its own salt. 8 users, 300 words: how many digests to try every word against every user?
What a salt does not change
Now take the thief who does not want everybody. They want ana.
Ana’s salt is in the table, next to her hash. The thief takes it, hashes 300 words with it, and is done. The salt did take one thing away: digests that somebody prepared before the leak are useless against her record. But fresh guesses against one record cost what they did before. Each guess is still one fast SHA-256 call, and a graphics card still makes an enormous number of those every second.
So a salt answers the first advantage, one guess for all, and leaves the second one alone.
A slow function
The second repair is to make each guess expensive. A password hash is a function built so that one call costs a noticeable amount of time and memory, on purpose.
The server pays that cost once for every login, where a few hundredths of a second do not matter. The thief pays it once for every guess, against every record. Millions of guesses at a few hundredths of a second each is a different business from millions of guesses at next to nothing.
Python’s standard library has such a function, hashlib.scrypt. This is one call, as a fragment, because it does not run in this page:
digest = hashlib.scrypt(
b"sunshine2024", salt=dana_salt, n=2**14, r=8, p=1, dklen=32
)
print(digest.hex())
It prints 3c0f35b38363436da5b163be671680e2337f6a9ac47bdd4b21504e6c2c0be613. The salt is an n, r, and p set how much work one call does. With these settings a call takes about two hundredths of a second on this site and needs about 16
The two repairs, side by side:
| The thief’s advantage | The repair | What it changes |
|---|---|---|
| One guess is tested against every record at once, and equal passwords show | A random salt for every record | Every record has to be attacked on its own |
| Each guess costs almost nothing | A slow function | Every guess costs time and memory |
A password is stored safely only with both.
A thief wants one particular user's password and has that user's salted SHA-256 record. Which repair makes this harder?
In the next lesson you call the slow function yourself.