I want to store secure user passwords in a MySQL database with PHP.
How can I make it better?
My Class:
private static $algo = '$2a';
private static $cost = '$10';
private static $pepper = 'eMI8MHpEByw/M4c9o7sN3d';
public static function generateSalt($length) {
$randomBinaryString = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
$randomEncodedString = str_replace('+', '.', base64_encode($randomBinaryString));
return substr($randomEncodedString, 0, $length);
}
public static function generateHash($password) {
if (!defined('CRYPT_BLOWFISH'))
die('The CRYPT_BLOWFISH algorithm is required (PHP 5.3).');
$password = hash_hmac('sha256', $password, self::$pepper, false);
return crypt($password, self::$algo . self::$cost . '$' . self::generateSalt(22));
}
public static function checkPassword($hash, $password) {
$salt = substr($hash, 0, 29);
$password = hash_hmac('sha256', $password, self::$pepper, false);
$new_hash = crypt($password, $salt);
return ($hash == $new_hash);
}
Either use this answer's suggestions (for PHP >= 5.5), or the following class. Thanks to martinstoeckli for pointing out the password_hash
functions. I read the code over, and the only different thing in password_hash
that I can see is error-checking and DEV_URANDOM
usage from the OS to generate a more random salt.
class PassHash {
public static function rand_str($length) {
$chars = "0123456789./qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM";
//only allowed chars in the blowfish salt.
$size = strlen($chars);
$str = "";
for ($i = 0; $i < $length; $i++)
$str .= $chars[rand(0, $size - 1)]; // hello zend and C.
return $str;
}
public static function hash($input) {
return crypt($input, "$2y$13$" . self::rand_str(22));
// 2y is an exploit fix, and an improvement over 2a. Only available in 5.4.0+
}
public static function hash_weak($input) {
return crypt($input, "$2a$13$" . self::rand_str(22)); }
// legacy support, Add exception handling and fall back to <= 5.3.0
public static function compare($input, $hash) {
return (crypt($input, $hash) === $hash);
}
}
It's what I've always used. A suggestion is also PHPass. It's tried and tested.
The only downfall in this script is that I generate random numbers from
rand()
, and not the source from the OS, but that's easily changed.
Also, there is no real reason to be using SHA256
hashing on top of bcrypt. SHA256
is weak, and can be broken with relatively little effort3.
In addition, hashing passwords is essential practice, but for true security, run all input through at least John the Ripper's wordlist1 to remove the most common passwords and inform a user to use a different password. Wordlists are used far more effectively than any bruteforce due to terribly weak passwords.
And as a final note,
do not force your users to use symbols, uppercase and numbers, force them to use a long password2.
Length is everything (no humour intended) when it comes to bruteforcing passwords. Pretty much any preset cracker will be set to not go over 12 characters unless a config is edited. If you ever see a site with a "maximum length" on passwords, make sure to never re-use a password there, because they have no security whatsoever4.
1. Arbitrary choice of cracker; pick what you find to work best
2. http://xkcd.com/936/
3. Comparatively (it's several orders of magnitude faster and is technically security through obscurity)
4. I have even seen banks do this. Having a maximum length on their passwords made me switch banks.