In order to authenticate users, web applications often store user passwords. This can be tricky, because password storage mechanisms are a watering hole for bad advice: there are several solutions to this problem but very few are truly secure. If you store the passwords of your users, your goal should be to make sure that in the event of a data compromise, user passwords should remain safe. The best way to store users passwords is to use a password-based key derivation function (PBKDF) with a sufficient work factor. If your application does not leverage a PBKDF, you should migrate password storage schemes immediately. More on this later.
A note on terminology: I will use terms interchangeably (e.g. hash/derived-key, etc.) that are not technically the same but commonly used as synonyms in practice. If this troubles you, feel free to email me.
The Many Incorrect Ways To Store User Passwords
If you are not using a PBKDF, you may be using one of the following schemes:
- Cleartext password storage
- Storing encoded (e.g. base64) passwords
- Encrypting passwords
- Storing a general purpose hash function that may involve salts and/or peppers.
These schemes do not sufficiently protect your user’s passwords! Cleartext password storage or storing encoded passwords requires no effort to reverse and encrypted passwords are susceptible to compromise by anyone with the encryption key (A password is a secret, no one should know it but the individual who decided to use it). Storing unsalted/salted/peppered hashed passwords using general purpose hash functions (e.g. MD5, SHA1, SHA256, etc.) is better than most options but suffers from a serious flaw:
General purpose hash functions are designed to be performant…
This may seem counterintuitive. Generally speaking, faster is better. With password storage schemes, the opposite is true! In the event of a data compromise, if an attacker has collected your user’s password hashes, her next step is to compute the hash of millions/billions of potential passwords. If you are using a general purpose hash function to store passwords, an attacker can compute password hashes at the rate of BILLIONS a SECOND. Salting and/or peppering your hashes may make the attackers life a bit more difficult but the underlying issue remains.
Several password-based key derivation functions address the efficiency issue in a clever way: they include a key-stretching or work factor parameter that purposefully slows down the computation of the password hash. This decrease in efficiency is negligible over the Internet but a massive burden to any attacker who is computing millions/billions of password hashes. In other words, your users will not notice a decrease in response time but attackers will struggle to crack passwords. Whats not to like about that?
How Do I Get A Fancy PBKDF?
Use bcrypt! bcrypt addresses just about every issue that I mentioned above. bcrypt hashes have a built-in salt and most bcrypt APIs should allow you to select a work factor. Furthermore, bcrypt is widely available via standard libraries or can be provided by an external dependency. There are other options, but bcrypt is probably your best bet.
For the rest of this post, I will demonstrate example implementations in common backend programming languages. Please feel free to copy this code! I am using an interface (a function that takes a string password and returns a string hash) which is imperfect but fits well to most languages.
— GIST https://gist.github.com/myover/6e1a785866731eeee927238aa5daa116.js —
— GIST https://gist.github.com/myover/f86c377b95e3e7ea26cb2869fb472bad.js —
— GIST https://gist.github.com/myover/7d6a082915807d2d5f3ecf4115bb09b1.js —
— GIST https://gist.github.com/myover/553b5145f02dbfdac31da4d124f31787.js —
— GIST https://gist.github.com/myover/6f2c90cd55b327f3bd63444a0ce107b6.js —
— GIST https://gist.github.com/myover/1ee807dd51bb3d6403553171cbde69e8.js —
A Note on Password Migration
If you are not storing password hashes using a proper password storage algorithm you should update right away. If you have already stored hashed passwords, this maybe a little tricky. My recommendation is to get “legacy” password hashes out of the database right away. To do this, update your password hash storage and validation routines to store/validate the value:
bcrypt(legacy_hash(password)). Persisting legacy password hashes in any form is no good! A similar issue lead to major breakthroughs in cracking Ashley Madison passwords. This is important!