Seems 1 big catch point for making strong user password hashes is using random salts as opposed to static ones.
Random salts are the default behavior of both PHP 5.5's password_hash()
and the userland implementation, password_compat.
If im checking a user logging in, surely I have to somehow have a copy of the salt used (stored in db) to check?
The salt is included in the password hash itself. There is no need to store it separately.
The random salt changes each time, so if i stored it now, and the user re-logged in the hash would be different so the db password wouldnt match the posted one..??
That's the responsibility of the password_verify()
method. From the PHP docs:
Note that password_hash() returns the algorithm, cost and salt as part of the returned hash. Therefore, all information that's needed to verify the hash is included in it. This allows the verify function to verify the hash without needing separate storage for the salt or algorithm information.
EDIT: Password Verification and Random Salts
I think I understand where your confusion is coming from. Hopefully this will help explain password hashing, verification, and the part played by random salts.
If you look at the source of password_compat (https://github.com/ircmaxell/password_compat/blob/master/lib/password.php), you'll see that both password_hash()
and password_verify()
make use of PHP's crypt()
function. When you create a password with password_hash()
, you store it and never pass that password through password_hash()
again. The algorithm, cost, and salt are all returned with the hash. Example:
$options = array('salt' => 'ThisIsTheSaltIProvide.');
$hash = password_hash('password', PASSWORD_DEFAULT, $options);
// $hash = $2y$10$ThisIsTheSaltIProvide.EcCQwybvWB3iNxIv9FwsPJEWhR/ywZ6
We could have created the same hash by using crypt directly, which is precisely what password_hash()
does behind the scenes.
$hash = crypt('password', '$2y$10$ThisIsTheSaltIProvide.');
// $hash = $2y$10$ThisIsTheSaltIProvide.EcCQwybvWB3iNxIv9FwsPJEWhR/ywZ6
The hash consists of:
- Algo information: $2y$ (BLOWFISH)
- Cost param: 10
- An extra $
- The salt: ThisIsTheSaltIProvide.
Using that information, password_verify()
reproduces the hash and then compares it to the persisted hash, like so:
$existingHash = $2y$10$ThisIsTheSaltIProvide.EcCQwybvWB3iNxIv9FwsPJEWhR/ywZ6
$testHash = crypt('password', '$2y$10$ThisIsTheSaltIProvide.');
// if $existingHash and $testHash match, then the password is good
A new, additional salt never comes into play.
Additionally, using RANDOM salts is important. If everyone used the same salt, then users with the same password would also have the same hash. No one wants that.