Secure password hash storage and a Yii helper extension

18 followers

NOTE

This article was rewritten as the README of the Randomness GitHub repo

Randomness

The static class Randomness is a collection of helper methods for web apps that need random data for security purposes.

Cryptographically-secure random data

Applications may require Cryptographically Secure (CS) random data Wikipedia CSPRNG to be used in forming, for example, encryption keys, random passwords, session keys, stream initialization vectors, nonces, secure unique IDs, and some kinds of salts.

PHP's mt_rand() is a simple pseudo-random number gnerator designed for use in Monte Carlo simulations, not in security systems. It is not cryptographically secure. You can determine the next random number from previous ones or from knowing the internal state of the generator.

Most operating systems on which PHP typically runs provide a CSPRNG as a service to applications. On Windows it is called CryptGenRandom. On Linux, OS X, FreeBSD etc. applications may read the /dev/random pseudo-device. Each of these OSs also offers a way for the user to query the status of the CSPRNG. But in PHP, accessing the CSPRNG can be problematic.

Randomness::randomBytes uses several different approaces to read from the operating system's CSPRNG. It is possible that all of them may fail. In this case it has an option to get data from the http://www.random.org service and another option to fall back on its own non-crypto-secure generator.

Storing passwords in 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" is a little better in that it uses a salt but it still uses md5 and is easy to crack. Examples of the same errors abound. The yii-user and yii-user-management are similarly insecure.

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 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. 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, any of the SHA algos and most of the rest.

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 (a completely arbitrary choice:-).

Each password should have its own random 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 so Randomness's salt generator by default uses the cass's own pseudo-random generator.

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.

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.

crypt('EgzamplPassword', '$2a$10$1qAz2wSx3eDc4rFv5tGb5t')
    >> '$2a$10$1qAz2wSx3eDc4rFv5tGb5e4jVuld5/KF2Kpy.B8D2XoC031sReFGi'

The first 29 characters are the same as the salt string. 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

Say we have a user table like this

create table user (
    id int not null auto_increment primary key,
    email varchar(255) not null,
    password_hash char(64) not null,
    unique key (email)
)

From a user account generation form assume that we have (already sanitized) user input in $form->email and $form->password. We generate the hash:

$password_hash = crypt($form->password, Randomness::blowfishSalt());

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.

In Yii

Randomness::blowfishSalt() generates a salt to use with crypt(), for example somewhere in the controller action where you register new users:

$user = new User;
$user->email = $form->email;
$user->bf_hash = crypt($form->password, Randomness::blowfishSalt());
if ($user->save())
    ...

To authenticate (refer to the authenticate method in protected/components/UserIdentity.php of a fresh yiic webapp):

public function authenticate() {
    $user = User::model()->findByAttributes(array(
        'email' => $this->username,
    ));
    if ($user === null
        || crypt($this->password, $user->bf_hash) !== $user->bf_hash
    ) {
        $this->errorCode = self::ERROR_UNKNOWN_IDENTITY;
        ...
    }
    ...
}

Total 5 comments

#6140 report it
marcovtwout at 2011/12/15 08:44am
Re:

Re: General passord handling policies You are right, using weak encryption in tutorials without a proper explanation is misleading and should be corrected where possible.

Re: OWASP You are right, that OWASP article you found is outdated. With the power of modern day GPU's, requirements on password strength have changed: password length is the most important (min. 12 chars), but mixed case and adding numbers/symbols still adds significant complexity. In the end, you want passwords that are easy to remember, but hard to crack.

For more guidelines as to password strength, this serves me well: http://en.wikipedia.org/wiki/Password_strength#Guidelines_for_strong_passwords

#6041 report it
fsb at 2011/12/07 10:31am
OWASP

@rtfm: Agreed about that OWASP article.

…it Checks If The Password Contains Characters From Each Of The Following Character Sets: CHAR_LOWERS, CHAR_UPPERS, CHAR_DIGITS, CHAR_SPECIALS. Finally, it calculates the password strength by multiplying the length of the new password by the number of character sets it is comprised of. A value of less than 16 is considered weak…

This is a good example of why I don't use such password strength meters, they give dangerously stupid answers. "H@ckMe1" by this algorithm has a reassuringly high strength of 28 but its entropy, assuming the user's native language is English, is very low. A dictionary attack will get it.

#6026 report it
fsb at 2011/12/06 10:42am
General passord handling policies

The comments of rtfm and marcovtwout are welcome. Such matters are beyond the scope of what I wanted to address.

I wanted to focus on the chronic problem of insecure password hashing. I am unhappy that influential tutorials, guides and books routinely demonstrate poor methods. If they are going to punt on the problem it would be better if these guides saved password in plaintext forcing users to take responsibility for researching appropriate methods.

#6024 report it
hofrob at 2011/12/06 09:35am
@marcovtwout

"put requirements on things like minimum password length, using uppercase and lowercase characters, etc."

Minimum length should be the only requirement. Upper/lower case and numbers are not making your passwords any more secure (the improvement is negligible). And maybe a hint that people should use passphrases instead of passwords.

And judging from this article: Password length & complexity I wouldn't use OWASP as a reference.

#6023 report it
marcovtwout at 2011/12/06 08:24am
For completeness

You touched all the important points in this article for storing passwords: using salt, a slow hashing algoritm, real randomness.

For completeness, don't forget to define a password policy for your application and users as well, for example:

  • when sending out passwords (over email), send users a one-time password that must be changed on login.
  • put requirements on things like minimum password length, using uppercase and lowercase characters, etc.
  • make sure your application doesn't give out information like existing usernames or emailadresses, through feedback on the login form or a forgot password form.

The OWASP is your friend here: https://www.owasp.org/index.php/Main_Page

Leave a comment

Please to leave your comment.

Write new article