You are viewing revision #10 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version or see the changes made in this revision.
- Storing passwords in web apps
- Using PHP's crypt() to store passwords
- Generate a Blowfish salt
- Example
- In a Yii webapp
- Constant-time string comparison.
- Availability of crypt()'s Blowfish option
Storing passwords in web apps ¶
There is now a
CPasswordHelper
class insystem.utils
at GitHub that provides an API to simplify the use ofcrypt()
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 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. 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:
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:
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:
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
$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 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()
.
/**
* 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:
$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
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:
$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 and write the UserIdentity::authenticate()
method as follows. In a default yiic webapp
this authenticate()
is in protected/components
.
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 and this.
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)
function, as many *nix systems do. Failing this, the Suhosin patch 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.
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.