0%

Store Passwords Safely · practice

Hash a Password with scrypt

The slow of the last lesson is in the standard library: hashlib.scrypt. Using it is one call. The call takes six , and this lesson goes through them.

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").

  • salt is the , as bytes.

  • n, r, and p set the cost. The next section explains them.

  • dklen is 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 you have written yourself. A call that passes the salt and the numbers by position raises 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 , and you should know how they compare with a real one.

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 settings it counts as equivalent, which lower 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 whose message ends 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 . Check my work counts scrypt calls in this chapter, and it can only count calls made that way.

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:

  1. new_salt() returns secrets.token_bytes(SALT_BYTES): 16 random bytes.

  2. hash_password(password, salt, *, n, r, p) returns hashlib.scrypt(...) of the password’s bytes, with the given salt, the given n, r, and p, and dklen=HASH_BYTES. Return the bytes as they are.

Then try it in the . Run this command twice:

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() returns secrets.token_bytes(SALT_BYTES).

  • hash_password(password, salt, *, n, r, p) returns hashlib.scrypt(...) of the password’s bytes with the given , the given n, r, and p, and dklen=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 is graded.