Store Passwords Safely · practice
Hash a Password with scrypt
The slow hashlib.scrypt. Using it is one call. The call takes six
The call
hashlib.scrypt(password_bytes, salt=salt, n=n, r=r, p=p, dklen=32)
The first argument is the password as
bytes. A password arrives as text, so encode it:password.encode("utf-8").saltis the, as bytes.n,r, andpset the cost. The next section explains them.dklenis how many bytes you want back. The default is 64. This course asks for 32, so that every stored hash has one length.
Everything after the password must be passed by name. The function is defined that way, like the keyword-only TypeError: scrypt() takes exactly 1 positional argument (5 given).
What does hashlib.scrypt(b"x", salt, 16, 8, 1) do?
What n, r, and p cost
n is the work factor. It must be a power of two. Doubling n doubles the time one call takes and the memory it needs. That second part is what sets scrypt apart from a function that is only slow: memory is expensive to multiply, so a thief cannot cheaply run a huge number of guesses side by side.
r scales the size of the memory blocks. The memory one call needs is about 128 * r * n bytes. With r=8 and n=2**14 that is 128 * 8 * 16384 bytes, which is 16
p repeats the work. This course leaves it at 1.
A teaching value
This course uses n=2**14, r=8, p=1. passwords.py holds the three numbers as COST_N, COST_R, and COST_P. They were chosen because the sandbox is small and a check should not keep you waiting. They are a teaching
The OWASP Password Storage Cheat Sheet is a widely used reference for these settings. Its default minimum for scrypt in a real service is N=2**17, r=8, p=1. Compare the whole setting and not n alone: with the same r and p, this course’s n is an eighth of that, so one call does an eighth of the work, in time and in memory. The sheet also N and raise p. One of them is N=2**14, r=8, p=5. So 2**14 does appear there, with five times this course’s p. Where it is available, the sheet prefers a newer function, Argon2id, which Python’s standard library does not have.
If you try a higher n here, you meet a wall first. At n=2**15 the call raises a memory limit exceeded. scrypt needs a little more than 32 MiB at that setting, and 32 MiB is the default limit. The maxmem argument raises the limit, and a real service sets it.
Why is this course's setting, n=2**14, r=8, p=1, called a teaching value?
Cost as a parameter
hash_password(password, salt, *, n, r, p) takes the cost from its caller and fixes nothing inside. That buys two things. A test can pass a tiny cost such as n=16 and run in no time. And a service can raise its cost next year by changing three constants, without touching this function.
Write the call as hashlib.scrypt(...), through the
Two facts about the input, for honesty. scrypt accepts a password of any length, even an empty one, so nothing here limits what a user may choose. And two passwords that look the same on screen can be different bytes, because some characters can be written in more than one way. Chapter 7 comes back to that. Nothing in this lesson depends on it.
Your task
In passwords.py, write two functions:
new_salt()returnssecrets.token_bytes(SALT_BYTES): 16 random bytes.hash_password(password, salt, *, n, r, p)returnshashlib.scrypt(...)of the password’sbytes, with the given salt, the given n,r, andp, anddklen=HASH_BYTES. Return the bytes as they are.
Then try it in the
python -c "import passwords; print(passwords.hash_password('correct horse', passwords.new_salt(), n=2**14, r=8, p=1).hex())"
Each run prints 64 hex characters, and the two runs print different ones. The password was the same both times. The salt was new.
That is correct, and it is also a problem. To check ana’s password next week, the server has to compute the same bytes again, and for that it needs the same salt and the same n, r, and p. Right now they are gone as soon as the function returns. The next lesson gives them a place to live.
Press Check my work when both functions are written.
Task
In passwords.py, write two
new_salt()returnssecrets.token_bytes(SALT_BYTES).hash_password(password, salt, *, n, r, p)returnshashlib.scrypt(...)of the password’sbytes with the given , the given n,r, andp, anddklen=HASH_BYTES. Do not fix a cost inside the function, and return the bytes unchanged.
Write both calls through their secrets.token_bytes(...) and hashlib.scrypt(...).
Then press Check my work. It calls new_salt() with a stand-in in place of secrets.token_bytes that records the size you ask for, and checks that two real salts differ. It calls hash_password() four times with passwords, a salt, and costs you have not seen; one of the four is at n=2**14. Each time it counts the calls of hashlib.scrypt: there must be exactly one, with the salt and the n, r, and p that were passed in, and the result must be the 32 bytes scrypt gives for the password’s UTF-8 bytes. Passing maxmem as well is fine. Nothing is timed, and nothing from your