rfc:password_hash

This is an old revision of the document!


Request for Comments: Adding simple password hashing API

Introduction

This RFC describes a new API for simplified password hashing.

Why Do We Need Password Hashing?

Password Hashing is a way to convert a user-supplied password into a one-way derived token for storage. By using the derived token, it makes it impossible to reverse the stored token and get the original password used by the user. This adds a layer of defense in case an attacker gets access to the database storing the password.

Why Do We Need Strong Password Hashing?

As it turns out, just hashing a password using md5() or even sha512() isn't good enough. Cryptographic hash functions (such as those supplied by hash()) are designed to be fast. This is good for cryptographic needs such as signing. But for password hashing, that's a problem since it allows an attacker to brute force a lot of passwords very quickly. Adding a salt makes it resistent to rainbow tables, but not resistent to brute forcing where that salt is known.

By using either a stretched algorithm (Such as PBKDF2) or an algorithm designed to be slow (Such as bcrypt), a much better defense against brute forcing will be had.

Why Do We Need A Simple API

As recent attacks have shown, strong password hashing is something that the vast majority of PHP developers don't understand, or don't think is worth the effort. The current core implementations of strong password hashing using crypt() are actually fairly difficult to work with. The error states are difficult to check for (returning *0 or *1 on error). The salt format is difficult to generate as it uses a custom base64 alphabet (. instead of + and no padded =). Additionally, salts are reasonably difficult to generate randomly (not too difficult, but requires a fair bit of code). Additionally, checking the return when validating a password can expose the application to http://en.wikipedia.org/wiki/Timing_attack.

By providing a simple API that can be called, which takes care of all of those issues for you, hopefully more projects and developers will be able to use secure password hashing.

Common Misconceptions

Salts Need To Be Cryptographically Secure

Salts exist for a single reason: To make it so that any time (CPU effort) spent cracking a single password hash cannot be amortized across multiple hashes. That means that attacking a single password hash will have no impact on the time it will take attacking another hash. Based on that reason, salts only need to be unique in a system. There is no requirement for them to be cryptographically secure.

Hash(password + salt) Is Fine

No, it's not. There's plenty of information out there to dispel this myth. See the references section for some details.

Proposal and Patch

The proposal is to add a new set of password hashing APIs to the standard PHP library. These hashing APIs will initially be thin wrappers around crypt() to allow for automatic salt generation and better error checking. The APIs are designed such that they can easily be extended in the future as additional strong hashing algorithms are introduced into PHP's core (Such as scrypt).

New Functions

  • string password_hash(string $password, int $algo, array $options = array()) - The function which creates new password hashes. The second parameter algo indicates which algorithm should be used to execute the hash. You can use the default constant if you want the algorithm to automatically update itself to the strongest algorithm available as PHP is upgraded. If called with two parameters, it will auto-generate a salt. The $options array allows for passing in algorithm specific options. In the case of bcrypt, two options are supported: salt and cost. The salt parameter, if provided, will be used in place of an auto-generated salt. The cost parameter is passed to crypt() to control the amount of CPU time that should be expended creating the hash (higher is more resistent to brute forcing, lower is kinder on the servers. A balance should be achieved).
  • bool password_verify($password, $hash) - The function which verifies an existing hash. This hash can be created via password_hash(), or a normal crypt() hash. The only thing it provides on top of crypt() is resistance to timing attacks by using a constant-time comparison function.
  • string password_make_salt(int $length, int $salt_type = PASSWORD_SALT_BCRYPT) - This function will create a new random salt of the specified length using psuedo-random algorithms. It will be used by password_hash() if a salt is not provided. But it can also be used to generate salts for other crypt() algorithms that password_hash() does not support. It can also be used to generate strong salts for other algorithms, such as PBKDF2 (which exists as an RFC now), or 3pd libraries like PHPASS.
  • bool password_needs_rehash(string $hash, int $algo, array $options = array()) - This function checks to see if the supplied hash implements the algorithm and options provided. If not, it is assumed that the hash needs to be rehashed.
  • array password_get_info(string $hash) - This function gets the information used to generate a hash. The returned array has two keys, algo and options.

New Constants

Initially, several new constants are defined:

  • PASSWORD_BCRYPT = 1 - Create new password hashes using the CRYPT_BLOWFISH algorithm
  • PASSWORD_DEFAULT = PASSWORD_BCRYPT - The default algorithm to use for hashing if no algorithm is provided. This can change in future releases if a new, stronger hashing algorithm (such as scrypt is supported).
  • PASSWORD_SALT_RAW = 1 - When passed to password_make_salt(), it returns a raw output (0-255).
  • PASSWORD_SALT_BCRYPT = 2 - When passed to password_make_salt(), it returns output compatible with crypt() (base64 encoded with the alphabet a-zA-Z0-9./).

Supported Algorithms

* BCrypt - The CRYPT_BLOWFISH algorithm. The strongest algorithm currently supported by PHP.

Behavioral Semantics

password_hash()

Errors:

  • E_WARNING - When CRYPT is not included in core (was disabled compile-time, or is listed in disabled_functions declaration)
  • E_WARNING - When supplied an incorrect number of arguments.
  • E_WARNING - When supplied a non-string first parameter (password)
  • E_WARNING - If an algorithm is specified in the algo parameter that is not supported
  • E_WARNING - If a bcrypt cost parameter is outside of the range 4-31 (by ini or specified in the options array)
  • E_WARNING - If a non-string salt option is provided
  • E_WARNING - If a provided salt option is too short for the specified algorithm

If any error is raise, false is returned by the function.

Normal Operation: With BCrypt, the output of crypt() is checked for error states (output < 13 characters). If there was an error in hashing, false is returned (this shouldn't happen due to the verification of the parameters, but it's there in case something fails). Otherwise, the output of crypt() is returned directly.

It's important to note that the output of crypt() (and hence password_hash()) contains all the information that will be needed to verify the hash later. Therefore, if the default hashing algorithm changes, or the user changes their algorithm, old hashed passwords would still continue to function and will be validated properly.

If we look at the output format of a hash:

basic_usage.php
<?php
var_dump(password_hash("rasmuslerdorf", PASSWORD_BCRYPT, array("cost" => 7, "salt" => "usesomesillystringfor")));
// string(60) "$2y$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi"
?>

Note that $2y$ indicates the algorithm to use (in this case, bcrypt). The 07$ indicates the cost parameter supplied. The usesomesillystringfor is the salt we provided. And the final part, e2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi is the generated hash.

So this final hash string consists of everything that crypt() or password_verify() will need to test the hash. Therefore, there is no need to store the salt separately, it's included in the generated hash. And there is no need to store the algorithm separately, as it is also stored in the generated hash.

password_verify()

Errors:

  • E_WARNING - When CRYPT is not included in core (was disabled compile-time, or is listed in disabled_functions declaration)
  • E_WARNING - When supplied incorrect number of parameters.

On error, it will return false.

Normal Operation:

When passed a correct password and the generated hash from password_hash(), the function will return a boolean true. If there is any failure (hash is invalid, password is incorrect, hash is corrupted, etc), the function will return a boolean false.

It's important to note that this function does not take any indication of the algorithm or salt. That's because both are included in the resulting $hash return value from password_hash().

password_make_salt()

Errors:

  • E_WARNING - When supplied an incorrect number of parameters.
  • E_WARNING - If the length parameter is less than or equal to zero
  • E_WARNING - If the length parameter is greater than PHP_INT_MAX / 3 (needed to ensure safe allocations)

Additionally, an E_WARNING error can be thrown if the generated salt is too short to encode fully. This should never happen, and is just a sanity check to prevent an inconsistent state due to a failure in other parts of the system.

On error, it will return false;

Normal Operation:

When the salt_type parameter is PASSWORD_SALT_BCRYPT (default), the function will generate a string of the specified length consisting of random characters from the alphabet a-zA-Z0-9/.. When it is PASSWORD_SALT_RAW, the function will generate a string of the specified length consisting of random bytes (characters 0 - 255).

It will use non-cryptographically safe, but strong random entropy sources, if possible for the salt generation. On windows, it will use php_win32_get_random_bytes(). On other platforms, it will read from /dev/urandom. If neither can generate enough entropy for the request, it will fall back to using php_rand() to supplement the provided randomness (it xor-s the php_rand() value with the existing one).

password_get_info()

Errors:

  • E_WARNING - When supplied an incorrect number or type of of parameters.

On error, it will return NULL

Normal Operation:

When passed in a valid hash created by a supported password_hash algorithm, this function will return an array of information about that hash. The first associative element, “algo” is the algorithm that was used to generate the hash (or 0 if not found). The second element is “options”, which includes the used options by the hashing algorithm, with the exception of the salt used.

password_needs_rehash()

Errors:

  • E_WARNING - When supplied an incorrect number or type of of parameters.

On error, it will return NULL

Normal Operation:

The supplied hash parameter is tested to see if the algorithm and options supplied match. Basically, this is similar to a wrapper over password_get_info() to validate if the supplied hash matches the configuration options passed in. This can be used to determine if a hash needs to be re-hashed after modifying the options (such as increasing bcrypt cost, changing algorithms, etc).

basic_usage.php
<?php
$password = "rasmuslerdorf";
$hash = password_hash($password, PASSWORD_BCRYPT, array("cost" => 7, "salt" => "usesomesillystringfor")));
if (password_verify($password, $hash)) {
    if (password_needs_rehash($hash, PASSWORD_BCRYPT, array('cost' => 8))) {
        update_password_in_db($password);
    }
    log_user_in();
} else {
    error_wrong_password();
}
?>

It could be implemented in user-land by:

user_needs_rehash.php
<?php
function password_needs_rehash($hash, $algo, array $options = array()) {
    $info = password_get_info($hash);
    $return = $algo != $info['algo'];
    // Skip salt parameter if supplied to options
    $return |= array() != array_diff_assoc($info['options'], $options);
    return $return;
}
?>

Examples

Basic Usage:

basic_usage.php
<?php
$password = "foo";
$hash = password_hash($password, PASSWORD_DEFAULT);
// Store Hash
 
if (password_verify($password, $hash)) {
    // Password Is Correct
} else {
    // Password Is Not Correct
}
?>

Specifying Algorithm:

specify_algorithm.php
<?php
$password = "foo";
$hash = password_hash($password, PASSWORD_BCRYPT);
// Store Hash
 
if (password_verify($password, $hash)) {
    // Password Is Correct
} else {
    // Password Is Not Correct
}
?>

Specifying Cost:

specify_cost.php
<?php
$password = "foo";
$hash = password_hash($password, PASSWORD_BCRYPT, array("cost" => 14);
// Store Hash
 
if (password_verify($password, $hash)) {
    // Password Is Correct
} else {
    // Password Is Not Correct
}
?>

Specifying Salt Manually:

specify_salt.php
<?php
$password = "foo";
$salt = mcrypt_create_iv(22, MCRYPT_DEV_URANDOM); 
$hash = password_hash($password, PASSWORD_BCRYPT, array("cost" => 14, "salt" => $salt);
// Store Hash
 
if (password_verify($password, $hash)) {
    // Password Is Correct
} else {
    // Password Is Not Correct
}
?>

Generating Salts:

generate_salt.php
<?php
// 15 characters in the alphabet a-zA-Z0-9./
$salt = password_make_salt(15);
 
// 15 characters of binary data (0-255)
$raw_salt = password_make_salt(15, true);
?>

Possible Future Implementation Details

  • INI setting for default algo - Presently, the default algorithm is identified by a constant that can be updated only with a source-code change. It may be worth while implementing an INI setting to allow that to be chosen by the host. As the proposed implementation has only a single algorithm, this may be a choice to be made in the future.
  • Future PHP major releases should update the default bcrypt cost constant to increment it, providing default protection against increasing hardware performance. This will allow the default values for the function password_hash() to remain strong over time as hardware advances.

Updating PASSWORD_DEFAULT

I'd propose the following policy for updating the default hashing algorithm in future releases of PHP.

  • Any new algorithm must be in core for at least 1 full release of PHP prior to becoming default. So if scrypt is added in 5.5.5, it wouldn't be eligible for default until 5.7 (since 5.6 would be the full release). But if jcrypt (making it up) was added in 5.6.0, it would also be eligible for default at 5.7.0.
  • The default should only change on a full release (5.6.0, 6.0.0, etc) and not on a revision release. The only exception to this is in an emergency when a critical security flaw is found in the current default.
  • For a normal (non-emergency) change in default, an RFC shall be issued for the update of the default algorithm, following normal RFC rules.

Removed Concepts

  • An INI setting for the default bcrypt cost. This has been removed due to popular opinion. The default bcrypt cost is now determined solely by a C constant defined in ext/standard/php_password.h.

Patch

Currently, the proposed patch is not yet complete. The basic functionality is there, but it needs some refactoring and testing prior to official proposal. The Work-In-Progress can be seen on the hash_password branch of ircmaxell's fork.

The specific implementation is at password.c

PHP Implementation

Additionally, a compatibility version in PHP is maintained at Github. This can be used in PHP versions 5.3 and 5.4 and for testing.

Future Concerns

With the nature of cryptography, future compatibility is a significant concern. In order to be safe, this functionality would need to be able to adapt to changing requirements in the future. There are a few provisions that enable future compatibility in future versions of PHP:

  1. New algorithms can be added to the API. It's already designed to be extended with new algorithms. The existance of the `$algo` and `$options` parameters to `password_hash()` are designed to allow arbitrary algorithms to be implemented in the future.
  2. The default algorithm is specified by a constant PASSWORD_DEFAULT. As new and stronger algorithms are added, this constant can be updated to point to the strongest at the time.
  3. The default cost parameter to BCRYPT is specified in the php.ini file. This allows individual sites to tailor the cost of bcrypt for their needs. Additionally, the default value (if not set in PHP.ini) can be updated in the source from release to release to compensate for faster hardware.
  4. If an implemented algorithm is ever grossly compromised (to the point of uselessness), password_hash() can be changed to either reject creation of new hashes for that algorithm, or raise warnings to notify users about said problems… Additionally, password_needs_rehash() can be changed to always return true for the compromised algorithm.

Discussion Points

password_hash() Algo Argument as optional

There has been some discussion around the second argument of password_hash() (The algorithm argument) and whether it should have a default value or not.

Should Have A Default

The “should have a default setting” argument is that it makes the API easier to use. All you would need to do is password_hash($password) to safely hash a password. The default would be updated according to the “Updating PASSWORD_DEFAULT” guidelines above. The API would become string password_hash(string $password, int $algo = PASSWORD_DEFAULT, array $options = array())

Should Not Have A Default

By not having a default value (and hence being a mandatory argument), it forces implementing developers to understand that the default argument can change over time. This has a few benefits in that developers need to recognize that storage requirements may change over time, that portability may be affected, etc.

Current Position

The current position of this RFC sides with the “should not have a default” argument. Therefore, the function has a required second argument.

password_make_salt() Is Not Needed

There has also been discussion around whether or not password_make_salt() should be exposed to user-land.

It should not be exposed

The argument that it should not be exposed is that it's not really doing anything generic. It produces a random salt of the specified length. This can already be accomplished in user-land via combinations of functions such as mcrypt_create_iv() and base64_encode(). Therefore, its existence is not really necessary.

It should be exposed

The argument that it should be exposed is that it needs to be implemented in C because it is needed for password_hash(), so it should be exposed so that it can be used for other things in userland. One of these other uses is that the format for the string (a-zA-Z0-9./) is already correct for crypt(). Therefore, a single function call can create salts for the other crypt() algorithms. Whereas to safely create them now requires a combination of at least 3 function calls.

Current Position

The current position of this RFC sides with the “should be exposed” argument. Therefore, the function is exposed to PHP user land code.

password_make_salt()'s second parameter should be an integer flag

There has been discussion around the second parameter of password_make_salt() (the raw_output) and that it should take a flag to allow for future improvements to add new salt formats other than base64 and raw.

Should Be A Flag

The “Should be a flag” argument is based around the future compatibility argument. Future versions of algorithms implemented by password_hash() may require different alphabet salts. By taking a flag instead of a boolean, this can be handled transparently by introducing new constants.

Should Be A Boolean

Currently the output type of password_make_salt() is compatible with all crypt() methods. The chances that a new format will need to be introduced is rather slim. Additionally, by making it a boolean makes the API simpler.

Current Position

The current position of this RFC is that the parameter should be a flag to allow for future compatibility with unknown requirements.

password_needs_rehash() is not needed

The function password_needs_rehash() can be implemented in user-land with the information returned by password_get_info().

Not Needed

Since the function can be implemented in user-land, there is no need to implement it in core.

Needed

There are a few reasons to include it in core. It makes it significantly easier to implement as otherwise implementation specific changes would need to be made over time to ensure that new algorithms are correctly identified (with their options). It also provides the ability to always reject hashes made using a grossly compromised algorithm (letting password_verify work, but rejecting password_hash attempts)…

Current Position

The current position of this RFC is that the function is needed, and is implemented.

The Existence Of PASSWORD_DEFAULT

There's been some discussion around the existence of the PASSWORD_DEFAULT constant.

Should Have It

The argument for the constant is that it provides the ability for code to take advantage of the most secure algorithm for the current release of PHP. This would change over time, but over a long term period of time (would only change every major release). Therefore, it would make it easier to implement code that would stay secure over a long period of time.

Should Not Have It

By not having the constant, developers would be forced to choose a specific algorithm at author time. This would allow them to understand the different algorithms available and make an intelligent choice. Additionally, it would prevent migration issues that could be caused by a changing algorithm (storage requirements, etc).

Current Position

The current position of this RFC is that the benefits of the constant relating to long term security outweigh the bad parts for the average developer. Therefore, the constant exists.

password_verify() Returns FALSE On Error

There's been discussion about the return value on parameter parse errors.

NULL

The argument that password_verify should return NULL on a parameter parse error (invalid types, invalid numbers of types, etc) because that's the standard way PHP internal functions deal with parameter errors. For consistency it should also return NULL.

FALSE

The argument is that password_verify should always return a strict boolean type. That way, a check of if (false === password_verify(..)) would not accidentally return a false condition when the password was not verified. As such, it would become possible for password_verify() to return falsy, but non-false results when it did not successfully verify the password hash.

Current Position

The current position is that the security context of the function justifies the break of consistency with other core functions. Therefore password_verify() currently only ever returns a boolean (never NULL).

References

Recent Attacks

Hashing In General

Timing Attacks

Strong Algorithms

Changelog

  • 0.1 - Initial Draft
  • 0.2 - Add ini directive for bcrypt cost
  • 0.3 - Add section on future concerns
  • 0.4 - Add behavioral semantics for each function
  • 0.5 - Remove ini directive for bcrypt cost
  • 0.6 - Make $algo parameter to password_hash() no longer optional
  • 0.7 - Implement password_get_info() and password_needs_rehash()
  • 0.8 - Add discussion points section, change password_make_salt to take flag for second parameter
rfc/password_hash.1342060575.txt.gz · Last modified: 2017/09/22 13:28 (external edit)