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"
book's example stores
md5($password) in the DB and calls it
"encryption". It is not. "The Yii Blog Tutorial",
(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 class which
the Blog Tutorial uses.)
and 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
or dictionary attack so large that the attack is not
feasible. Salts used with the Blowfish hash do not need to be
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
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.
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
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->password. We generate the hash:
$salt = openssl_random_pseudo_bytes(22); $salt = '$2a$%13$' . strtr(base64_encode($salt), array('_' => '.', '~' => '/')); $password_hash = crypt($form->password, $salt);
And insert a row into
At user logon assume we again have sanitized user input in
To authenticate these against the accounts in
user we select the
password_hash field from table
$form->email and, with that value in
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 ¶
Availability of crypt()'s Blowfish option ¶
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
(e.g. many Unix and Linux systems) or if
PHP has the Suhosin patch.
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
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
Some people have commented that phpass has fallback
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