Hashing Passwords in PHP

We're putting the record straight by starting with the don't.

  • Don't write your own password hashing algorithm
  • Don't store passwords in plaintext
  • Don't use poor hashing algorithms, such as MD5 and SHA1

And the dos:

  • Use a random salt for each password
  • Use a strong hashing algorithm
  • Enforce at least basic password requirements

PHP has a built in hashing function

password_hash ( string $password , int $algo [, array $options ] ) : string

int $algo currently supports the following:

  • PASSWORD_DEFAULT: built-in default (currently bcrypt)
  • PASSWORD_BCRYPT: bcrypt
  • PASSWORD_ARGON2I: Argon 2I (PHP 7.2)
  • PASSWORD_ARGON2ID: Argon 2ID (PHP 7.3)

Note: not all installations of PHP include Argon support, use the hash_algos() function to see what algorithms are available in your installation.

password_hash() creates a salt for you, hashes the $password and returns the hash.

password_verify ( string $password , string $hash ) : bool

password_verify() compares a hash to a plaintext password and returns true if it matches.

Notice how password_verify() doesnt need to know the hashing algorithm? That's because password_hash() includes the necessary details in the output hash!

echo password_hash('password', PASSWORD_DEFAULT);

****** Output ******
$2y$10$bHkeNSNebn.GlOc7eqNM2u5IwuJMiAPIU3F4YdoiDF3zg156vNOGW

The output may look funky, so let's split it up.

$2y$10$bHkeNSNebn.GlOc7eqNM2u5IwuJMiAPIU3F4YdoiDF3zg156vNOGW

$
2y => Standard crypt() way of identifying a BLOWFISH hash
$
10 => Cost of the hash, which can be changed using array $options['cost'] = int; Higher cost = slower but more secure
$
bHkeNSNebn.GlOc7eqNM2u5IwuJMiAPIU3F4YdoiDF3zg156vNOGW

The first 22 characters after the last $ symbol represent the random salt, and the rest is the hash.

Repeating the above function will result in different salts, so completely different outputs. password_verify() handles this because all of the settings required are stored in the output, as shown above.

echo password_hash('password', PASSWORD_DEFAULT), PHP_EOL;
echo password_hash('password', PASSWORD_DEFAULT), PHP_EOL;
echo password_hash('password', PASSWORD_DEFAULT), PHP_EOL;
echo password_hash('password', PASSWORD_DEFAULT), PHP_EOL;
echo password_hash('password', PASSWORD_DEFAULT), PHP_EOL;

***** OUTPUT *****
$2y$10$0y.07/U8JPqYiCuCkNpHyeMuNomW86zcfU1NvXTC.FQmRLmxjQXum
$2y$10$UGlRr8wtMXqRv5KYKMvKUeSLW1Tf.eReQrbRadzAXvsQSxhPbKzD.
$2y$10$xe4VFRHaeUbbgmbkfHaGYueXScSz5p89JcA3rNAVTYJn.h/mJnVK.
$2y$10$2ZaNSnm448eVWNN0N.D95edjZ84aKHc3agF0760SC7TlPPMbD1KZK
$2y$10$Uy0OulSHLQE7/4HHKhntL.1R0j5GHXURsZctr1g9s3UePZRGCNznm

Bonus: Check if a hash is using the latest settings

password_needs_rehash(string $password_hash, int $algo, [ array $options ]) : bool

Checks if the $password_hash uses the current hash settings. If it does not use the current settings, returns true so you can take the user input and rehash the password using the latest settings.

Examples

Create a hash

// Get password from user input
$password = $_POST['password'];

// Hash the password using PASSWORD_ARGON2ID and additional
// options supported by Argon.
$password_hash = password_hash($_POST['password'], PASSWORD_ARGON2ID, array(
    'memory_cost' => 2048,
    'time_cost' => 4,
    'threads' => 4
));

Check password

// $password_hash loaded from database

if(password_verify($_POST['password'], $password_hash))
{
    echo 'Password Correct!';
}
else
{
    echo 'Password Incorrect!';
}

Password needs rehash

// $password_hash loaded from database

if(password_verify($_POST['password'], $password_hash))
{
    echo 'Password Correct!';

    if(password_needs_rehash($password_hash, PASSWORD_ARGON2ID))
    {
        echo 'Password needs to be rehashed';
        $new_password_hash = password_hash($password, PASSWORD_ARGON2ID);
        // Update the hashed password with $new_password_hash
    }
}
else
{
    echo 'Password Incorrect!';
}
Written by Kieran on 2019-04-20
This website uses Cookies to enhance your experience. Close Manage