Use crypt() for password storage

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.

« previous (#3)next (#11) »

  1. Storing passwords in web apps
  2. Using PHP's crypt() to store passwords
  3. Generate a Blowfish salt
  4. Example
  5. In a Yii webapp
  6. Constant-time string comparison.
  7. Availability of crypt()'s Blowfish option

Storing passwords in web apps

There is now a CPasswordHelper class in system.utils at GitHub that provides an API to simplify the use of crypt() 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.

15 0
20 followers
Viewed: 179 388 times
Version: Unknown (update)
Category: Tutorials
Written by: fsb
Last updated by: fsb
Created on: Nov 27, 2012
Last updated: 10 years ago
Update Article

Revisions

View all history

Related Articles