Revision #14 has been created by fsb on Oct 18, 2013, 12:46:21 AM with the memo:
Rewritten to accomodate 1.1.14
« previous (#11) next (#15) »
Changes
Title
unchanged
Use crypt() for password storage
Category
unchanged
Tutorials
Yii version
unchanged
Tags
unchanged
Authentication, security, password, hash, hashing
Content
changed
Storing passwords in web apps
----
> There is now a `CPasswordHelper` class in `system.utils` <a href="http://github.com/yiisoft/yii/blob/master/framework/utils/CPasswordHelper.php">at GitHub</a>
> that provides an API to simplify the use of `crypt()` for password storage.
> While this wiki article remains valid, it will in due course be rewritten
> to refer to the new class as well as explain how it works.
There are many tutorials and examples that show storage of passwords in a table.
Often the methods used are substandard and very easy to crack. There are many web pages and tutorials that show how to do it wrong.
You cannot rely on a user to use a (practically) unguessable password or to not
use that password in systems other than yours. And you should not assume that
your systems are so secure that an attacker cannot get hold of the password table or a backup of it. So you need to ensure that the password hashes in the database are useless to an attacker.
A very common error I see in what I read and other people's code is fast hashes.
MD5, for example, is very fast (as are all the SHA hashes). As of Nov
2011 you can check 350 million MD5 keys per second on a commodity nVidia processor.
So no matter what you do with salts, the combination of short passwords and fast
brute force checking means your system is open to intruders if you rely on a
non-iterated message digest such as MD5 or any of the SHA algos. Most
hash functions are indeed designed to be fast to compute.
The Blowfish hash algorithm, on the other hand, is designed to be computationally expensive and is currently considered pretty good for hashing passwords. The implementation in PHP's `crypt()` is easy to use. Set a cost parameter high enough to make a brute force attack really slow. I set it so that it takes about 250 ms
on the production server which is fast enough for users to tolerate but slow enough to
defeat a brute-force attack.
Each password should have a unique salt. The salt's purpose is to make the
dictionary size in a [rainbow table](http://en.wikipedia.org/wiki/Rainbow_table)
or [dictionary attack](http://en.wikipedia.org/wiki/Dictionary_attack) so large that the attack is not
feasible. Salts used with the Blowfish hash [do not need to be
cryptographically secure](http://security.stackexchange.com/questions/7193/cryptographic-security-of-dynamically-generated-non-random-salts/7195#7195)
random strings. A salt based on a decent pseudo-random number is sufficient to defeat a rainbow table.
Some people advocate re-salting every time a user logs in. I think this is only
useful if you also limit the time interval between user logins, e.g. by locking out users that have not logged in for a long time.
As computer speed increases with time, so does the attacker's chance of succeeding with brute force. So if your software will be in use for many years it should increase the Blowfish cost parameter in line with increases in computer speed and rehash passwords next time the user logs on.
Using PHP's crypt() to store passwords
--------------------------------------
> If your PHP is older than 5.3, please read the section **Availability of crypt()’s
Blowfish option** below.
People often get confused about how to use implement a password store using `crypt()`.
It is actually very simple but it helps to know that:
* It is safe to store the salt together with the password hash. An attacker cannot use
it to make a dictionary attack easier.
* The return value from `crypt()` is the string concatenation of the salt you give it and the
hash.
* `crypt()` ignores excess characters in the input salt string.
The built-in PHP `crypt()` function's signature is:
* string **crypt** (string `$str`[, string `$salt`])
in which the
salt string's format determines the hash method. For a Blowfish hash, the format is:
`"$2a$"`, a two digit cost parameter, `"$"`, and 22 characters from the alphabet
`"./0-9A-Za-z"`. The cost must be between `04` and `31`.
For example:
```php
crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5t')
>> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'
```
Notice that the first 29 characters of the returned value are the same as the salt string.
Append anything to the salt string argument and the result is unchanged:
```php
crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5t12345678901234567890')
>> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'
crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5t$2a$10$1qAz2wSx3eDc4rFv5tGb5t')
>> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'
```
And in particular, passing the value returned from `crypt()` back in as the salt argument:
```php
crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi')
>> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'
```
Thus we can use `crypt()` to authenticate a user by passing the hash value it
gave us previously back in as a salt when checking a password input. It is fiendishly simple:
**Create new hash**
```php
$hash = crypt($password, $salt)
```
**Validate password against a stored hash**
Compare the strings `$hash` and `crypt($password, $hash)`.
**NOTE:** We should use a constant-time string comparison algorithm to defeat
the possibility of
[timing attacks](http://blog.astrumfutura.com/2010/10/nanosecond-scale-remote-timing-attacks-on-php-applications-time-to-take-them-seriously/)
learning the result of validation. See below for an example.
## Generate a Blowfish salt
The following function will generate a salt suitable for use with `crypt()`.
```php
/**
* Generate a random salt in the crypt(3) standard Blowfish format.
*
* @param int $cost Cost parameter from 4 to 31.
*
* @throws Exception on invalid cost parameter.
* @return string A Blowfish hash salt for use in PHP's crypt()
*/
function blowfishSalt($cost = 13)
{
if (!is_numeric($cost) || $cost < 4 || $cost > 31) {
throw new Exception("cost parameter must be between 4 and 31");
}
$rand = array();
for ($i = 0; $i < 8; $i += 1) {
$rand[] = pack('S', mt_rand(0, 0xffff));
}
$rand[] = substr(microtime(), 2, 6);
$rand = sha1(implode('', $rand), true);
$salt = '$2a$' . str_pad((int) $cost, 2, '0', STR_PAD_RIGHT) . '$';
$salt .= strtr(substr(base64_encode($rand), 0, 22), array('+' => '.'));
return $salt;
}
```
Example
-------
Say I have a `user` table like this
~~~
[sql]
create table user (
id int not null auto_increment primary key,
username varchar(255) not null,
password_hash char(64) not null,
unique key (email)
)
~~~
First, consider processing the form to create a new user acount. I have (already sanitized) form input in `$form->email` and `$form->password`. I can generate the hash from the password and a salt from the `blowfishSalt()` function above:
```php
$passwordHash = crypt($form->password, blowfishSalt());
```
I can insert a row into `user` containing `$form->username` and `$passwordHash`.
When a user submits a login form, I have sanitized form input in `$form->username` and `$form->password`. To authenticate these inputs I select the record from the `user` table with matching username loading the password hash into `$record->passwordHash`
```php
if ($password_hash === crypt($form->password, $record->passwordHash))
// password is correct
else
// password is wrong
```
So there is no need to store the salt in a separate column from the hash value because
`crypt()` conveniently keeps it in the same string as the hash.
In a Yii webapp
------
Using `crypt()` requires very little code. Just one line in user registration and one in authentication plus the `blowfishSalt()` function from above.
### Registration
In a Yii webapp I usually have an Active Record model class `User` for the user records in the `user` DB table. In this example I have a controller action that processes the website's new account generation form. The form input is in `$form`, a `CForm` model instance with attributes `username` and `password`. Assume the model validated OK.
The controller action where you register new users might include:
```php
$user = new User;
$user->email = $form->email;
$user->password = crypt($form->password, self::blowfishSalt());
if ($user->save()) {
...
}
```
This assumes I put the `blowfishSalt()` function from above is a static method of this controller class.
### Authentication
To authenticate a user at logon, I follow the [auth topic in the Yii Guide](http://www.yiiframework.com/doc/guide/1.1/en/topics.auth) and write the `UserIdentity::authenticate()` method as follows. In a default `yiic webapp` this `authenticate()` is in `protected/components`.
```php
public function authenticate()
{
$record = User::model()->findByAttributes(array('username' => $this->username));
if ($record === null) {
$this->errorCode = self::ERROR_USERNAME_INVALID;
} else if ($record->password !== crypt($this->password, $record->password)) {
$this->errorCode = self::ERROR_PASSWORD_INVALID;
} else {
$this->_id = $record->id;
$this->setState('title', $record->title);
$this->errorCode = self::ERROR_NONE;
}
return !$this->errorCode;
}
```
## Constant-time string comparison.
This function is based on
[this](http://codereview.stackexchange.com/questions/13512/constant-time-string-comparision-in-php-to-prevent-timing-attacks)
and
[this](https://github.com/ircmaxell/password_compat/blob/master/lib/password.php).
```php
function same($a, $b) {
/**
* @see http://codereview.stackexchange.com/questions/13512
*/
if (!is_string($a) || !is_string($b)) {
return false;
}
$mb = function_exists('mb_strlen');
$length = $mb ? mb_strlen($a, '8bit') : strlen($a);
if ($length !== ($mb ? mb_strlen($b, '8bit') : strlen($b))) {
return false;
}
$check = 0;
for ($i = 0; $i < $length; $i += 1) {
$check |= (ord($a[$i]) ^ ord($b[$i]));
}
return $check === 0;
}
```
## Availability of `crypt()`'s Blowfish option
The `crypt()` function has ben part of PHP for a long time but not all PHP installations
have all its options.
I use the Blowfish hash option which is available in all PHP systems since 5.3.0.
It is also available in pre-5.3 PHPs (including PHP 4) if the operating system has the Blowfish hash option in
its standard library [`crypt(3)`](http://www.freebsd.org/cgi/man.cgi?query=crypt&sektion=3) function, as many *nix systems do. Failing this, the [Suhosin patch](http://www.hardened-php.net/suhosin/index.html) will provide the Blowfish hash in an old PHP system.
PHP's `CRYPT_BLOWFISH` constant is `true` if and only if the system has Blowfish.
It can
be tricky to implement good password hashing on PHP systems that do not have Blowfish in `crypt()`. I do not have any recommendations other than to upgrade your PHP or
move to a host with an up-to-date PHP.
Be careful of slow hash function implementations in PHP. The important thing is that the hash takes a lot of compute time *on the attackers equipment* and takes the minimum possible on yours. A hash implemented in PHP puts you at a disadvantage relative to the attacker. For example, imagine you iterate SHA-1 in in PHP for one second on a Micro instance on Amazon EC2 while the attacker has the same algorithm optimized to run on $5k's worth of modern GPUs. I don't know how many orders of magnitude faster the attacker's algorithm is than yours but I think its enough so that you should feel unsafe with such an implementation.**Update**: This wiki has been rewritten to be in line with Yii 1.1.14. Since many of the detailed complexities are now handled by Yii, the article focuses on how the `crypt()` built-in function works and why it's important to use it correctly.
# Storing passwords in php web apps
There are many tutorials and examples that show storage of passwords in a table.
Often the methods used are substandard and very easy to crack. For example, the
["Agile Web Application Development with Yii1.1 and PHP5"](http://www.yiiframework.com/doc/)
book's example stores `md5($password)` in the DB and calls it
"encryption". It is not. ["The Yii Blog Tutorial"](http://www.yiiframework.com/doc/blog/1.1/en/prototype.auth),
(prior to Yii version 1.1.13) was a little better in
that it used a salt but it still used md5 and is easy to crack. (Since 1.1.14 Yii has a
[CPasswordHelper](http://www.yiiframework.com/doc/api/1.1/CPasswordHelper) class which
the Blog Tutorial uses.)
The [yii-user](http://www.yiiframework.com/extension/yii-user)
and [yii-user-management](http://www.yiiframework.com/extension/yii-user-management) extensions
are similarly insecure.
Examples of the same errors abound and are by no means limited to webapps implemented in Yii or PHP.
You cannot rely on a user to use a (practically) unguessable password or to not
use that password in systems other than yours. And you should not assume that
your server is so secure that an attacker cannot get hold of the password file/table or a backup of it.
A very common error I see in what I read and other people's code is fast hashes.
MD5, for example, is very fast. As of Nov
2011 you can check 350 million keys per second on a commodity nVidia processor.
(Update: two years later the technology for brute force password cracking has
advanced to a frightening degree and is moving fast.)
So no matter what you do with salts, the combination of short passwords and fast
brute force checking means your system is open to intruders if you rely on a
non-iterated message digest such as MD5 or any of the SHA algos. Most
hash fuctions are indeed designed to be fast to compute.
The Blowfish hash function is currently considered pretty good. It is designed to be slow. The
implementation in PHP's `crypt()` is easy to use. Set a cost parameter high enough
to make a brute force attack really slow. I set it so that it takes about 250 ms
on the production server which is fast enough for users to tolerate but slow enough to
defeat a brute-force attack.
Each password should have its own salt. The salt's purpose is to make the
dictionary size in a [rainbow table](http://en.wikipedia.org/wiki/Rainbow_table)
or [dictionary attack](http://en.wikipedia.org/wiki/Dictionary_attack) so large that the attack is not
feasible. Salts used with the Blowfish hash [do not need to be
cryptographically secure](http://security.stackexchange.com/questions/7193/cryptographic-security-of-dynamically-generated-non-random-salts/7195#7195)
random strings but they do need to be unique. A long enough string from
an operating system's CSPRNG in
non-blocking mode (e.g. `/dev/urandom` on Linux) is pretty good.
Some people advocate resalting every time a user logs in. I think this is only
useful if you also limit the time interval between user logins, e.g. block an
account if the user hasn't logged in in more than *N* weeks.
If your software will be in use for many years then you should increase the cost
factor in line with increases in computer speed. You will need to rehash passwords
when do.
Using PHP's crypt() to store passwords
--------------------------------------
People often get confused about how to use implement a password store using `crypt()`.
It is actually very simple but it helps to know that:
* It is safe to store the salt together with the password hash. An attacker cannot use
it to make a dictionary attack easier.
* The string `crypt()` returns is the concatenation of the salt you give it and the
hash value.
* `crypt()` ignores excess characters in the input salt string.
`crypt()` has function signature `string crypt (string $str, string $salt)` and the
salt string format determines the hash method. For Blowfish hashing, the format is:
`"$2a$"`, a two digit cost parameter, `"$"`, and 22 digits from the alphabet
`"./0-9A-Za-z"`. The cost must be between `04` and `31`.
Notice how the first 29 characters are the same as the salt string:
crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5t')
>> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'
The characters from position 30 onwards are the hash.
Notice also how anthing appended to the salt string argument has no effect on the result:
crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5t12345678901234567890')
>> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'
crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5t$2a$10$1qAz2wSx3eDc4rFv5tGb5t')
>> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'
And in particular, pass the value returned from `crypt()` back in as the salt argument:
crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi')
>> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'
So we can use `crypt()` to authenticate a user by passing the hash value it
gave us previously back in as a salt when checking a password input.
##Example (simplistic)
Say we have a `user` table like this
create table user (
id int,
email varchar(255),
password_hash varchar(64)
)
From a user account generation form, assume that we have (already sanitized) user input in
`$form->email` and `$form->password`. We generate the hash:
$salt = openssl_random_pseudo_bytes(22);
$salt = '$2a$%13$' . strtr($salt, array('_' => '.', '~' => '/'));
$password_hash = crypt($form->password, $salt);
And insert a row into `user` containing `$form->email` and `$password_hash`.
At user logon assume we again have sanitized user input in `$form->email` and `$form->password`.
To authenticate these against the accounts in `user` we select the `password_hash` field from table `user` where `email` = `$form->email` and, with that value in `$password_hash`
if ($password_hash === crypt($form->password, $password_hash))
// password is correct
else
// password is wrong
So there is no need to store the salt in a separate column from the hash value because
`crypt()` conveniently keeps it in the same string as the hash.
While this example shows how `crypt()` works, it is too simplistic for practical
use. It glosses over several important
details including: how to obtain a decent salt (the example assumes OpenSSL
is available), what value to use for the cost parameter (the example arbitrarily
uses 13), and what function to use to compare the retrieved database hash value with
the computed value (`===` is simple but might be vulnerable to timing attacks).
The APIs in Yii's CSecurityManager and CPasswordHelper are intended to help the
user deal with these matters.
### In Yii
As of version 1.1.14, Yii has an API to help users with secure password storage:
[CPasswordHelper](http://www.yiiframework.com/doc/api/1.1/CPasswordHelper). The
[Blog Tutorial](http://www.yiiframework.com/doc/blog/1.1/en/prototype.auth)
shows how it can be used.
# Availability of crypt()'s Blowfish option
The `crypt()` function has ben part of PHP for a long time but not all PHP installations
have all its options. The Blowfish hash option is available in all PHP systems since 5.3.
It is also available in older PHPs if either the operating system has the option in
its standard library [`crypt(3)`](http://en.wikipedia.org/wiki/Crypt_(Unix)) function
(e.g. many Unix and Linux systems) or if
PHP has the [Suhosin patch](http://www.hardened-php.net/suhosin/index.html).
PHP's `CRYPT_BLOWFISH` constant is `true` if the system has Blowfish.
I have not found a solution that I can recommend to provide secure password storage
when `crypt()`'s Blowfish option is absent. If you want to be secure you have to make
this a requirement of your PHP runtime environemnt or take matters into your own
hands.
Some people have commented that [phpass](http://www.openwall.com/phpass/) has fallback
algorithms when `CRYPT_BLOWFISH` is false and asked what's wrong with that. They
are not sufficiently secure, in my opinion, to recommend and that's why [I don't
recommend phpass](https://github.com/yiisoft/yii/issues/2788).