Just use pwstore. It handles the details for you.
What this library does is this:
- Combine the user's password with a randomly-generated salt.
- Hash this slowly. By iterating SHA-256 a few thousand times, we make brute-force guessing a lot less practical.
- Store this has along with the salt.
This scheme is essentially an implementation of the PBKDF1 key derivation function (as specified in RFC 2898) with some convenience code around it to make it easy to use and really hard to mess up.
How to use it
Just follow the instructions in the documentation. You store a single string containing the hashed password, the salt, and some extra book-keeping information:
"sha256|12|Ge9pg8a/r4JW356Uux2JHg==|Fdv4jchzDlRAs6WFNUarxLngaittknbaHFFc0k8hAy0="
You can make one of these with the makePassword function:
>>> makePassword "hunter2" 12
"sha256|12|lMzlNz0XK9eiPIYPY96QCQ==|1ZJ/R3qLEF0oCBVNtvNKLwZLpXPM7bLEy/Nc6QBxWro="
To verify a password given by a user, use the verifyPassword function:
>>> verifyPassword "wrong guess" passwordHash
False
>>> verifyPassword "hunter2" passwordHash
True
There are also functions for adjusting the number of iterations of the hash function, so you can make the hashing slower as Moore's law progresses. This works in essentially the same way as bcrypt's work factor, except that the API here is a bit nicer than bcrypt's.
The theory
There are many wrong ways to store a password, most of them all too common. The most obvious way to go wrong is to store users' passwords in plain text. Then, when an attacker manages to get their hands on this file, they have the passwords for every user's account. One step up, but still wrong, is to simply hash all passwords with SHA1 or something. This is vulnerable to rainbow table and dictionary attacks. One step up from that is to hash the password along with a unique salt value. This is still vulnerable to dictionary attacks, since guessing a password is very fast.
The right thing to do is to use a slow hash function, to add some small but significant delay, that will be negligible for legitimate users but prohibitively expensive for someone trying to guess passwords by brute force. That is what this library does.
Where to get it
There are two versions of the package available from Hackage:
- pwstore-fast is the preferred version.
- pwstore-purehaskell has the same API, but only pure Haskell dependencies. It's usable, but about 25 times slower than pwstore-fast.
The source code is on GitHub. If anybody has suggestions for improvements, I'd love to hear them.
Discussions: Reddit [22 comments].
Nice addition to the Haskell libraries, thanks! I haven't looked over the code carefully, but this seems like a very useful thing to have.
ReplyDeleteOne small suggestion about the generally clear and ample documentation: where it is not clear from the types which function parameter does what, please say so in the haddocks for the function. That will save users a click to get to the source, or the chance of a mistake if they don't bother or if Hackage isn't providing a link to the source.
For example, instead of "Verify a password given by the user against a stored password hash", you could say "@verifyPassword userInput pwHash@ verifies the password @userInput@ given by the user against the stored password hash @pwHash@."
Another small suggestion - it might be nice to have versions of the API for Data.Text and/or String as well. Unicode characters in any given language have slightly less randomness than ASCII bytes, and any given encoding chosen by a library user could also lose varying amounts of randomness. These are probably not significant issues in the usual case, especially given your use of slow hashes. But it would be nice if a well-designed library could relieve the user from having to think about it at all.
ReplyDeleteThanks for the good advice about the docs; I'll update the documentation when I get another block of free time.
ReplyDeleteAs for Data.Text and String APIs, I'm pretty sure there's no issue there if people just use the functions for converting to and from ByteStrings. The only place where randomness matters here is the salt, which the user should generally not be providing in any case; usually it will come from /dev/urandom. With the salt, the thing that matters is that it be unique and have a high amount of entropy, to make precomputing anything as difficult as possible. I designed the default salt length with way more safety margin than is actually required, so it should be hard to mess up.
There is also pbkdf2 on hackage. Caveat: I'm the author, and I consider it somewhat bitrotten..
ReplyDeleteI would recommend that people use PBKDF2 if and only if they need variable-length keys. For simple password storage, SHA256-PBKDF1 is more than adequate. For more general key derivation, PBKDF2 is more flexible.
ReplyDeleteThe pbkdf2 package on Hackage should work, but the main problems with it are that it does not work natively with ByteStrings, and that it is kind of slow. If someone wants to write a new version of pbkdf2, that would be a useful little project.
You probably forgot about timing attack.
ReplyDelete