Yii 1.1: Use crypt() for password storage

22 followers

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.

Total 16 comments

#17222 report it
Hesam at 2014/05/13 03:24pm
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.

#17221 report it
fsb at 2014/05/13 03:14pm
openssl_random_pseudo_bytes

@Hesam Use CPasswordHelper

#17220 report it
Hesam at 2014/05/13 02:54pm
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

$salt= bin2hex($salt);

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?

#17037 report it
TonyBoy at 2014/04/27 09:58pm
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$".

#15205 report it
fsb at 2013/10/17 09:01pm
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.

#11632 report it
Chris83 at 2013/01/23 04:14pm
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

#10933 report it
mrs at 2012/12/04 03:47pm
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

#10911 report it
Boaz at 2012/12/02 03:18pm
@fsb

it attempts to educate the reader in correct use of crypt(), which is not what phpass does.

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.

#10908 report it
fsb at 2012/12/02 12:34pm
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 what phpass 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.

#10905 report it
Boaz at 2012/12/02 02:10am
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.

#10886 report it
fsb at 2012/11/29 10:28am
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.

#10881 report it
fsb at 2012/11/29 10:07am
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.

#10878 report it
John S at 2012/11/29 06:55am
@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

#10872 report it
yJeroen at 2012/11/29 03:40am
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! ;)

#10866 report it
fsb at 2012/11/28 04:37pm
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 without crypt()'s modular algorithms. But it does so by falling back to insecure behavior. As a result phpass 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 use crypt() 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.

#10862 report it
fsb at 2012/11/28 12:04pm
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?

Leave a comment

Please to leave your comment.

Write new article