I know this is asked a lot, but right now (April 2016), what is the preferred method of storing passwords securely for my users? My data is not super-sensitive (it's just a game with no personal or financial details, but it would still be nice to have secure passwords).
I maintain the blog post that James Patterson linked to in his answer. Despite being a mere two months old, it's already due for a massive update, but that's being held back by the slow movement of several other languages. I can provide you with an immediate updated answer for PHP.
I'm going to answer in this format:
- What you should do today (i.e. the standard response) and why.
- What you should do later this year.
- What you should do in the years to come.
How to Safely Store a Password in 2016
As the blog post mentioned in the current iteration of the PHP password storage section, you want bcrypt, facilitated through PHP's built-in password API. This API is straightforward, but there are a few gotchas you need to keep in mind:
Hashing a password with bcrypt
$hash = password_hash($userPassword, PASSWORD_DEFAULT);
Validating a password with bcrypt
if (password_verify($userPassword, $hash)) {
// Login successful.
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
// Recalculate a new password_hash() and overwrite the one we stored previously
}
}
Why bcrypt?
Bcrypt is the preferred algorithm right now for many reasons:
- Great GPU resistance (which matters a lot in resisting password cracking)
- Well-studied by experts for many years -- it's the conservative choice
- In low memory configurations, it's safer than scrypt.
Why not bcrypt?
There are two known weaknesses with bcrypt that you should be aware of:
- Bcrypt truncates after 72 characters.
- Bcrypt truncates after
\0
bytes.
Typically, people run into the second problem while trying to solve the first problem. A good stop-gap solution for right now is:
Today's Recommendation: Bcrypt-SHA-384 (with base64 encoding)
/**
* Bcrypt-SHA-384 Verification
*
* @ref http://stackoverflow.com/a/36638120/2224584
* @param string $plaintext
* @param string $Hash
* @return bool
*/
function bcrypt_sha384_verify(string $plaintext, string $hash): bool
{
$prehash = \base64_encode(
\hash('sha384', $plaintext, true)
);
return \password_verify($prehash, $hash);
}
/**
* Creates a Bcrypt-SHA-384 hash
*
* @ref http://stackoverflow.com/a/36638120/2224584
* @param string $plaintext
* @param int $cost
* @return string
*/
function bcrypt_sha384_hash(string $plaintext, int $cost = 12): string
{
$prehash = \base64_encode(
\hash('sha384', $plaintext, true)
);
return \password_hash(
$prehash,
PASSWORD_BCRYPT,
['cost' => $cost]
);
}
The specific reasoning was all covered in How to Safely Store Your Users' Passwords in 2016.
How to Safely Store a Password in the Immediate Future
Later this year, you might consider switching to Argon2, the winner of the Password Hashing Competition. (It's not mandatory; bcrypt is still quite good. But in the coming years Argon2 will be the recommended algorithm, barring any new attacks against it.)
You can, in fact, use it right now. You just need to install libsodium 1.0.9 and libsodium-php 1.0.3 (or newer).
Hashing a password with Argon2i in PHP using libsodium
$hash = \Sodium\crypto_pwhash_str(
$password,
\Sodium\CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
\Sodium\CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
);
Validating a password with Argon2i in PHP using libsodium
if (\Sodium\crypto_pwhash_str_verify($hash, $password)) {
// Login successful
}
If you're using Halite 2.0.0 or newer, the Password
API uses Argon2i then encrypts the hashes using authenticated encryption. (This is preferable to "peppering".)
How to Safely Store a Password in the Long Term
I'm going to be proposing an RFC soon to add Argon2i as a possible algorithm to the password hashing API. If it gets accepted in time for PHP 7.1's feature freeze, we might expect to see Argon2i be the default as early as PHP 7.3 (which should release in 2018).
Thus, the long-term solution might simply be:
Hashing a password with Argon2i, in the future
$hash = password_hash($userPassword, PASSWORD_DEFAULT);
Validating a password with Argon2i, in the future
if (password_verify($userPassword, $hash)) {
// Login successful.
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
// Recalculate a new password_hash() and overwrite the one we stored previously
}
}
In Sum
There are things you can do to improve the security of your password hashes right now, but in the long run the password hashing API will come full circle and the lazy answer today will once again be the best practice answer tomorrow.
TL;DR: password_hash()
and password_verify()
will outlive bcrypt, so just use those unless you have a compelling reason to do more.