Revision #14 was created by fsb on Oct 18, 2013, 12:46:21 AM.
Rewritten to accomodate 1.1.14
Content
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
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:
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:
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:
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`.
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
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