Passwords need to be strong and password database storage requires encryption. Many security leaks involve database theft which exposes the passwords. Better to have them encrypted.

Simplest use case, the md5() function. Not recommended. This function is very fast making it worthwhile to guess passwords by brute force.

$str = 'secret';
$crypted = md5($str);
if ($crypted == md5($str)) {
    echo 'logged in';
}

With a weak password it is easy to look up the plaintext password corresponding to the encrypted password in a rainbow table. The md5 value of ‘secret’ can even be googled. A weak password can be fortified by using a simple hash:

$hash = 'se&sW4$88**';
$str = 'secret';
$crypted = md5($str . $hash);
if ($crypted == md5($str . $hash)) {
    echo 'logged in';
}

With the hash stored in the application code, it is not available to the hacker having just a copy of the database. The password is still weak when tried directly in a dictionary attack but a lookup of the md5 value requires a significantly larger rainbow table.

Another md5 disadvantage is that if two users share the same password that then the md5 values are also the same. The solution is then to create a unique hash value for every password. The database record then contains both a password field and a hash field. The md5 algorithm itself is now considered unsafe and should not be used in production environments. Its successor, the sha1() function, suffers all the same disadvantages.

An alternative is crypt(). For PHP < 5.3 it is based on the Unix crypt function. This function stores a hash in the encrypted value itself. Each result of crypt('secret') is again a unique string.

$str = 'secret';
$crypted = crypt($str);
if ($crypted == crypt($str)) {
    echo 'logged in';
}

Manually adding a hash is recommended otherwise the hash is too weak. The phrase “hash” is misleading. It is more like an options string with the dollar sign as divider. The first option is the type of algorithm to use. The prefix “$2y$” (> 5.3.7) tells crypt to use the Blowfish algorithm, “$1$” signals md5. The actual hash string then follows. With Blowfish a second option is the “cost” and the actual hash then must be a 22 character random string. The default cost value is 7. A higher cost (meaning more rounds of encryption) makes crypting and decrypting slower and that is exactly what you want if you want to annoy the hacker. The cost is the base-2 logarithm of the iteration count and must be in range of 04 to 31.

First check if Blowfish is available:

if (defined("CRYPT_BLOWFISH") && CRYPT_BLOWFISH) {
    echo "CRYPT_BLOWFISH is enabled";
}

For the creation of a random string many methods exist, like the one described here:

$str = 'secret';
$length = 22;
$pool = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
$randomString = substr(str_shuffle($pool), 0, $length);
$cost = '07';
// example output: $2y$07$c258Zzsm4pPIXZzUAYVBgA$
$crypted = crypt($str, "$2a$" . $cost . "$" . $randomString . "$");

if ($crypted == crypt($str, $crypted)) {
    echo 'logged in';
}

These methods have in common that they are not truly random, a notion that hackers can exploit. An alternative is the mcrypt_create_iv() function from the mcrypt library:

$size = mcrypt_get_iv_size(MCRYPT_CAST_256, MCRYPT_MODE_CFB);
$iv = mcrypt_create_iv($size, MCRYPT_DEV_RANDOM);
// example output: $@¬º‹DY†´;Ý&ÏS 

In mcrypt jargon the random string is now called an initialization vector (IV) but in the end it is still a random string and a truly random string. Final problem to solve: how to convert the iv into the 22-character string required for Blowfish?

$randomString = substr(str_replace('+', '.', base64_encode($iv)), 0, 22);

The MySQL equivalent of the PHP crypt() function is ENCRYPT()

update `user` set password = ENCRYPT('secret') WHERE user_id = 3;

In MySQL ENCRYPT() is also based on the Unix crypt() function which may not be available on systems such as Windows.

To summarize, what is an acceptable way to store a password? The answer is bcrypt:

$size = mcrypt_get_iv_size(MCRYPT_CAST_256, MCRYPT_MODE_CFB);
$iv = mcrypt_create_iv($size, MCRYPT_DEV_RANDOM);

$randomString = substr(str_replace('+', '.', base64_encode($iv)), 0, 22);
$str = 'secret';

$cost = '07';
// Blowfish PHP > 5.3.7
$algo = "2y";
$hash = sprintf("$%s$%s$%s$", $algo, $cost, $randomString);
$crypted = crypt($str, $hash);

if ($crypted == crypt($str, $crypted)) {
    echo 'logged in';
}

As of PHP 5.5 the password_hash function will make things easier. Now the defaults are Blowfish (bcrypt) and NOT setting a hash:

$str = 'secret';
$crypted = password_hash($str, PASSWORD_DEFAULT);
if (password_verify($str, $crypted)) {
    echo 'logged in';
}

Note added: Sorry, forget about crypt, bcrypt or mcrypt. Instead go for scrypt.

Sources

* https://crackstation.net/hashing-security.htm
* https://dev.mysql.com/doc/refman/4.1/en/encryption-functions.html#function_encrypt
* https://en.wikipedia.org/wiki/MD5
* https://en.wikipedia.org/wiki/Data_Encryption_Standard
* http://www.the-art-of-web.com/php/blowfish-crypt/
* http://programmers.stackexchange.com/questions/253094/difference-between-reverse-lookup-tables-and-rainbow-tables
* http://stackoverflow.com/questions/14798275/best-way-to-store-passwords-in-mysql-database
* http://mlo.io/blog/2012/06/26/securing-your-passwords.html
* http://blog.codinghorror.com/your-password-is-too-damn-short/
* https://scott.arciszewski.me/blog/2013/10/php-scrypt-setup

Advertisements