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.)
The yii-user
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
cryptographically secure
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(base64_encode($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. The Blog Tutorial 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)
function
(e.g. many Unix and Linux systems) or if
PHP has the Suhosin patch.
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 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.
Reply to John S
You may think whatever you want but I feel that Yii users should be properly informed so I must correct you:
Use of MD5 hash to store passwords is a very common mistake. But the fact that many people make the mistake does not make it a de facto standard or make it any less insecure.
MD5 hash is well proven to be easy to crack using brute force. Your salts do nothing to make that attack harder. (Anyone who cares to can find the fact online without difficulty.)
Your claim that your methods are "secure enough" begs two question:
secure enough for whom? for your clients? for the users of your webapps? How do they feel about your security methods?
secure enough for what? A system that stores personally-identifying information? One that could affect a person's reputation if an account is abused?
More responses to John S
I do not understand why you continue to make statements about a specialist subject in which, as you have deomonstrated, you have no expertise.
Nevertheles, I have to set the record straight.
"After reading many article on many website. I get a conclusion :"
" Best practice is using using already created library like phpass. phpass is already integrated in wordpress 2.5+, drupal7+, phpbb3+."
" See openwall.org/phpass"
This is not a best practice. It appears to be a common practice. But it is a bad practice.
I recommend against
phpass
because it implements an iterated MD5 algorithm in PHP and uses it without warning. I explicitly warned against this in the last paragraph of the wiki article.phpass
does this in order to be portable to systems withoutcrypt()
's modular algorithms. But it does so by falling back to insecure behavior. As a resultphpass
actually creates insecure webapps by giving the programmers that use it the impression that it is secure when it is, in fact, not. On servers that have Blowfish hash, it does nothing more than usecrypt()
in the way I demonstrated in the article (except that it is implemented in a way to make it hard for the user to understand)."or you can write your own library."
As I demonstrated in the wiki, the solution is so simple that a library is not needed.
"At this time the computational power required to actually crack a hashed (strong) password doesn't exist."
Wrong! And you contradict this in the following sentences...
"The only way for computers to "crack" a password is to recreate it and simulate the hashing algorithm used to secure it. The speed of the hash is linearly related to its ability to be brute-forced. Worse still, most hash algorithms can be easily parallelized to be reproduced even faster. This is why costly scheme like Bcrypt or Scrypt are so important (google it to know about it)"
I explained the importance of a computationally expensive hash in the wiki. It seems clear now that you have not read it.
"brute forcing a strong password will take long-long time to complete."
How long it takes depends on the entropy of the original password. But you can be sure that it will take an impractically long time if you use Blowfish hash as I demonstrated in the Wiki.
"Salt are make it more harder to crack. But highly still can be cracked."
Wrong again! Salts do not slow down a brute fore attack. The purpose of a salt is to defeat a dictionary (e.g. rainbow) attack.
Q
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.
Could you explain why this would be useful?
In the very unlikely case that someone would succeed in a bruteforce hack of a blowfish encrypted password, they would have the password right? That password would work with any hash based on the same password, no? Maybe I need more coffee though! ;)
@fsb again
phpass will use blowfish if available, password
hashing is a lot more
complicated than just
saying 'no' to 1 hash
function and loosely
hinting that some other
hashing algorithm could
be better, without any
further argumentation.
PHPass is widely
reviewed , created by profesionall with many year of experience (phpass author says : more than 10 year of experience) and found to be
(one of) the best libraries
available for PHP i think.
What i mean with
"At this time the computational
power required to actually crack a
hashed (strong) password doesn't
exist."
is a decode a one way encryption,
one way encrypt cant be decoded it only can be
"the only way for computers to
"crack" a password is to recreate it
and simulate the hashing algorithm used to secure it."
there are available base64 encode / decode, one way encryption/hash (like md5) can only encode, there is no decode
Using salt are make it more harder if the salt is a strong (rare / long / dificult to generate) salt
Reply to yJeroen
Good question. Much about security (and not just with computers) depends on time.
How long is the attacker willing to spend on the effort? Your security design is often to make the known attacks take so long that no competent attacker would bother. Both methods described in the wiki work this way. Use a hash that takes half a second to compute rather than half a microsecond slows down the brute-force attack by a factor of a million. Adding 128 bits of entropy to each password makes a dictionary attack 10^38 times harder!
If every password is rehashed at least once a month with a new salt then we make sure that the attacker has only one month to complete the attack, which adds even more confidence. To rehash, you need to have the password cleartext so you do it when a user logs in. But if the user doesn't log in then that starts to defeat the purpose. Hence my comment that to make the trick work, you need limit how long an account can be unused and still be accessed with the old password.
More responses to John S
"password hashing is a lot more complicated than just saying 'no' to 1 hash function and loosely hinting that some other hashing algorithm could be better, without any further argumentation."
I do not agree that password hashing is complicated in PHP. Not at all. Any decent PHP programmer can handle it.
My recocomendation is that people use
crypt()
with the Blowfish hash option and not use a server without it for secure password storage. Again, not complicated."PHPass is widely reviewed , created by profesionall with many year of experience (phpass author says : more than 10 year of experience) and found to be (one of) the best libraries available for PHP i think."
If these claims give you confidnce in the library then you must be easily persuaded. I recommend that people take the trouble to understand the security aspects of the software for which they are responsible. This is why I try to be completely transparent and why I explained in the wiki every detail of how the software I recommend works.
This is a good opportunity to point out that this is not the only Openwall recommendation that I think is dangerous. And
phpass
is not the only PHP security lib I have reviewed that is dangerously flawed.I recommend using phpass extension
The extension integrates phpass into Yii in simply no-time.
I see little reason in reinventing the wheel. Also, for those who wish NOT to have their passwords security reduced its recommended to stay with hashPortable = false in the configuration (just as stated by the author...).
In any case - its good that this subject is being discussed. For some strange reason many developers are still ignorant with regard to this basic security issue.
I do not recommend phpass
There are a number of reasons why I recommend against using
phpass
, some already discussed in these comments. Others include its complexity, the inclusion of 6 hash algorithms that are better avoided (some of which are known to be unsafe), the inclusion of unproven password strength heuristics, and the lack of care regarding timing attacks.@Boaz: This wiki does not reinvent a wheel, it attempts to educate the reader in correct use of
crypt()
, which is not whatphpass
does.I understand the desire for a simple solution that "just works" in "simply no-time". But
phpass
is not a good choice.password_compat
is the best I can identify.@fsb
And this is good. Still, my point is that I think that the reader should also consider using phpass which is IMHO a very good off-the-shelf solution for password encryption/checking, if you know how to use it (this means knowing the territory, familiarize oneself with its limitations and configuring it accordingly).
I'm not an expert on security and most programmers should not be experts on that subject as well. But nor should they refrain from investing themselves in that subject when appropriate.
The conclusion I came to when I invested myself into the issue a while back was that probably phpass is the best solution for me. A maintained library for password generation/check that is robust enough and safe enough, that someone has already thought of for me (among the rest :-) .
I also suggest that if you have feedback on phpass you should submit it to the maintainers of this library/package.
Thanks
Hi fsb,
Its looks really good. Password encryption is an issue for user management. I think we all will get great idea from your nice article.
Thanks for nice contribution.
mrs
yii-password-strategies
I'm not sure if it has already been mentioned but there's a brilliant extension that provides support for bcrypt encrypted passwords called yii-password-strategies
yii-password-strategies
yii-password-strategies has some nice features—upgrading the cost parameter at login is nice. The password entropy policy checks are a useful example but applications generally need a password policy derived from system requirements.
On the down side, yii-password-strategies incorporates 4 hashing methods, 3 of which are insecure and should not be used. And it's implementation of all 4 is potentially open to timing attacks owing to the string comparison function.
Better salt
I think the salt should look like:
$salt = '$2y$07$' . strtr(base64_encode(mcrypt_create_iv(16, MCRYPT_DEV_URANDOM)), '+', '.');
From the manual:
Versions of PHP before 5.3.7 only support "$2a$" as the salt prefix: PHP 5.3.7 introduced the new prefixes to fix a security weakness in the Blowfish implementation. Please refer to » this document for full details of the security fix, but to summarise, developers targeting only PHP 5.3.7 and later should use "$2y$" in preference to "$2a$".
openssl_random_pseudo_bytes
Hi,
On my dev machine, wamp windows7, openssl_random_pseudo_bytes(22) generates unreadable characters such as : ��I�U2Vf�, which causes the crypt function fail and outputs :*0. My workaround was using
which generates a string longer than 22 characters, but now the crypt function works. Can I use bin2hex confidently? Is it still a random number considering the fact that crypt will truncate only the first 22 characters of the salt?
openssl_random_pseudo_bytes
@Hesam Use CPasswordHelper
Re: openssl_random_pseudo_bytes
Thanks FSB for prompt response,
I didn't know the tutorial got old, I guess we should move the update from the end of document to the top.
Thanks anyways.
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.