Password hashing revisited

From user comments on my recent password hashing post, I’ve learned about a better solution for password hashing – rather than using hashing algorithms designed to be fast such as SHA-1 and SHA-256, use slower, and more important future-adaptable algorithms such as bcrypt. I have to say this is one of the reasons I love this community – you always learn new things.

I won’t repeat the reasons why methods such as bcrypt are preferred (read the comments on the previous post to learn why). However, I will note that starting from PHP 5.3 bcrypt is in fact built-in to PHP – so if you do not require portability to older versions of PHP, bcrypt-hasing could be done very easily, using the useful but a bit enygmatic crypt function:

/**
 * Hash a string using bcrypt with specified complexity
 *
 * @param  string $password input string
 * @param  integer $complexity bcrypt exponential cost
 * @return string
 */
function bcrypt_hash($password, $complexity = 12)
{
  if ($complexity < 4 || $complexity > 31) {
    throw new InvalidArgumentException("BCrypt complexity must be between 4 and 31");
  }

  // CRYPT_BLOWFISH salts must be 22 alphanumeric characters long
  $random = get_random_alnum_salt(22);

  // The crypt function decides which algorithm to use (we need Blowfish) based on
  // the format of the salt parameter
  $salt = sprintf('$2a$%02d$%s', $complexity, $random);

  return crypt($password, $salt);
}

/**
 * This generates a random alphanumeric string of length $length.
 *
 * This may not be a cryptographic grade random string generation
 * function - but it is good enough for our example
 *
 * @param  integer $length random string length
 * @return string
 */
function get_random_alnum_salt($length)
{
  static $chars = null;
  if (! $chars) {
    $chars = implode('', array_merge(range('a', 'z'), range('A', 'Z'), range(0, 9)));
  }

  $salt = '';
  for ($i = 0; $i < $length; $i++) {
    $salt .= $chars[mt_rand(0, 61)];
  }
  return $salt;
}

/**
 * Check a password against a hashed string
 *
 * @param  string $password cleartext password
 * @param  string $hashed bcrypt format hashed string
 * @return boolean
 */
function bcrypt_check_hash($password, $hashed)
{
  // Do some quick validation that $hashed is indeed a bcrypt hash
  if (strlen($hashed) != 60 || ! preg_match('/^\$2a\$\d{2}\$/', $hashed)) {
    throw new InvalidArgumentException("Provided hash is not a bcrypt string hash");
  }
  return (crypt($password, $hashed) === $hashed);
}

As you can see, the code is pretty simple – hashing is done internally using the crypt function. It is interesting to note that the returned hash string is of a very particular format – it already contains the complexity and salt parts. So comparing hashed string to cleartext originals is also easy and done nicely by crypt – you give it the hashed string as the salt parameter, and it takes care to only take the complexity and salt prefix of the entire hashed string into account. If the result is the same string (after all the same complexity and salt were used), you will get the exact same hashed string back. Nice!

It’s important to note that bcrypt is intentionally slow – on my laptop it hashes a medium-sized password with cost factor 12 at about 0.3 seconds. It means this is a very good solution for password hashing (because you only do it once in a while at user registration and login but not in the critical path of any frequently repeating action), but not so useful for general purpose string hashing – for such uses, you can still use MD5, SHA-1 and friends.

Also keep in mind that increasing the cost factor increases the time it takes to hash string dramatically, so as computing cost is reduced you can simply increase the cost factor to ensure you are future-proof.

One thought on “Password hashing revisited

  1. I recommend using PHPAss (at http://www.openwall.com/phpass/). Not only this saves from reinventing the wheel but that library is way more tested and debugged than whatever I can come up with under normal development stresses (it is debugged from many aspects like design, only mere code debugging aspect).
    AFAIK its currently the de-facto leading/recommended solution for password hashing in PHP.

    Also, for PHP versions before v5.3 if you have the suhosin patch that will give you crypt capabilities as well. Like Shahar said above, in PHP v5.3 and above crypt is built in already.