0%

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 is fast.

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 is a handful of random bytes, made new for every password record. It goes into the hash together with the password, and it is stored next to the result.

Try it

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 in it is still fast SHA-256. The second half of this lesson deals with that.

Where does a record's salt live?

What a salt changes for the thief

Think back to the of Lesson 1, from each word’s digest to the word. With salts it no longer works. The digest of 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 of its own, and 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 of memory while it runs. The next lesson explains each argument.

The two repairs, side by side:

The thief’s advantageThe repairWhat it changes
One guess is tested against every record at once, and equal passwords showA random salt for every recordEvery record has to be attacked on its own
Each guess costs almost nothingA slow functionEvery 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.