基于:密码散列PBKDF2的用户密码加密
为什么需要把应用程序中用户的密码进行散列化?
当设计一个需要接受用户密码的应用时, 对密码进行散列是最基本的,也是必需的安全考虑。 如果不对密码进行散列处理,那么一旦应用的数据库受到攻击, 那么用户的密码将被窃取。 同时,窃取者也可以使用用户账号和密码去尝试其他的应用, 如果用户没有为每个应用单独设置密码,那么将面临风险。
通过对密码进行散列处理,然后再保存到数据库中, 这样就使得攻击者无法直接获取原始密码, 同时还可以保证你的应用可以对原始密码进行相同的散列处理, 然后比对散列结果。
需要着重提醒的是,密码散列只能保护密码 不会被从数据库中直接窃取, 但是无法保证注入到应用中的 恶意代码拦截到原始密码。
为何诸如 md5() 和 sha1() 这样的常见散列函数不适合用在密码保护场景?
MD5,SHA1 以及 SHA256 这样的散列算法是面向快速、高效 进行散列处理而设计的。随着技术进步和计算机硬件的提升, 破解者可以使用“暴力”方式来寻找散列码 所对应的原始数据。
因为现代化计算机可以快速的“反转”上述散列算法的散列值, 所以很多安全专家都强烈建议 不要在密码散列中使用这些散列算法。
如果不建议使用常用散列函数保护密码, 那么我应该如何对密码进行散列处理?
当进行密码散列处理的时候,有两个必须考虑的因素: 计算量以及“盐”。 散列算法的计算量越大, 暴力破解所需的时间就越长。
PHP 5.5 提供了 一个原生密码散列 API, 它提供一种安全的方式来完成密码 散列和 验证。 PHP 5.3.7 及后续版本中都提供了一个 » 纯 PHP 的兼容库。
PHP 5.3 及后续版本中,还可以使用 crypt() 函数, 它支持多种散列算法。 针对每种受支持的散列算法,PHP 都提供了对应的原生实现, 所以在使用此函数的时候, 你需要保证所选的散列算法是你的系统所能够支持的。
当对密码进行散列处理的时候,建议采用 Blowfish 算法, 这是密码散列 API 的默认算法。 相比 MD5 或者 SHA1,这个算法提供了更高的计算量, 同时还有具有良好的伸缩性。
如果使用 crypt() 函数来进行密码验证, 那么你需要选择一种耗时恒定的字符串比较算法来避免时序攻击。 (译注:就是说,字符串比较所消耗的时间恒定, 不随输入数据的多少变化而变化) PHP 中的 == 和 === 操作符 和 strcmp() 函数都不是耗时恒定的字符串比较, 但是 password_verify() 可以帮你完成这项工作。 我们鼓励你尽可能的使用 原生密码散列 API。
“盐”是什么?
加解密领域中的“盐”是指在进行散列处理的过程中 加入的一些数据,用来避免从已计算的散列值表 (被称作“彩虹表”)中 对比输出数据从而获取明文密码的风险。
简单而言,“盐”就是为了提高散列值被破解的难度 而加入的少量数据。 现在有很多在线服务都能够提供 计算后的散列值以及其对应的原始输入的清单, 并且数据量极其庞大。 通过加“盐”就可以避免直接从清单中查找到对应明文的风险。
如果不提供“盐”,password_hash() 函数会随机生成“盐”。 非常简单,行之有效。
我应该如何保存“盐”?
当使用 password_hash() 或者 crypt() 函数时, “盐”会被作为生成的散列值的一部分返回。 你可以直接把完整的返回值存储到数据库中, 因为这个返回值中已经包含了足够的信息, 可以直接用在 password_verify() 或crypt() 函数来进行密码验证。
下图展示了
crypt() 或 password_hash() 函数返回值的结构。 如你所见,算法的信息以及“盐”都已经包含在返回值中, 在后续的密码验证中将会用到这些信息。
以下贴出一个用户注册时密码的加密代码 此代码中用了一个封装好的是静态加密类
/**
* 注册一个用户
* @return \User
*/
public function register()
{
//调用类获取盐
$salt = YumEncrypt::generateSalt();
//设置密码 将返回一个加密后的
$this->setPassword($this->password, $salt);
$this->createTime = time();
$this->userName = empty($this->userName) ? $this->mobile : $this->userName;
$this->updateTime = time();
$this->lastVisit = time();
$this->lastPasswordChange = time();
$this->status = User::STATUS_ACTIVE;
$this->regIp = Tools::getIpToInt();
if ($this->validate()) {
$this->save(false, array('trueName', 'mobile', 'password', 'userName', 'email','companyName',
'salt',
'createTime', 'lastPasswordChange', 'status', 'regIp',
));
$this->afterRegister();
}
return $this;
}
/**
* 设置密码
* @param type $password
* @param type $salt
* @return \User
*/
public function setPassword($password, $salt = null)
{
if ($password != '') {
if (!$salt)
$salt = YumEncrypt::generateSalt();
$this->password = YumEncrypt::encrypt($password, $salt);
$this->lastPasswordChange = time();
$this->salt = $salt;
if (!$this->isNewRecord)
return $this->save(false, array('password', 'lastPasswordChange', 'salt'));
else
return $this;
}
}
封装过的密码加密类
<?php
/**
* This class file holds static encryption functions used by Yum.
*
* The password encryption system is based on:
*
* Password hashing with PBKDF2.
* Author: havoc AT defuse.ca
* www: https://defuse.ca/php-pbkdf2.htm
**/
class YumEncrypt {
/**
* This function is used for password encryption.
* @return hex encoded hash string.
*/
public static function encrypt($string, $salt = null)
{
if(!$salt)
$salt = YumEncrypt::generateSalt();
return YumEncrypt::pbkdf2($string, $salt);
}
/**
* This function is used for generating the salt.
* @return base64_encoded hash string.
*/
public static function generateSalt()
{
if (function_exists('mcrypt_create_iv'))
{
$sHash = base64_encode(mcrypt_create_iv(64, MCRYPT_DEV_URANDOM));
}
else
{
$sHash = hash('sha256', mt_rand() . uniqid());
}
return $sHash;
}
/**
* This function is used for generating the salt.
* @return hash string.
*/
public static function validate_password($password, $good_hash, $salt)
{
$enc_pwd = YumEncrypt::encrypt($password, $salt);
return YumEncrypt::slow_equals($enc_pwd, $good_hash);
}
// Compares two strings $a and $b in length-constant time.
private static function slow_equals($a, $b)
{
$diff = strlen($a) ^ strlen($b);
for($i = 0; $i < strlen($a) && $i < strlen($b); $i++) {
$diff |= ord($a[$i]) ^ ord($b[$i]);
}
return $diff === 0;
}
/*
* PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
* $algorithm - The hash algorithm to use. Recommended: SHA256
* $password - The password.
* $salt - A salt that is unique to the password.
* $count - Iteration count. Higher is better, but slower. Recommended: At least 1000.
* $key_length - The length of the derived key in bytes.
* $raw_output - If true, the key is returned in raw binary format. Hex encoded otherwise.
* Returns: A $key_length-byte key derived from the password and salt.
*
* Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
*
* This implementation of PBKDF2 was originally created by https://defuse.ca
* With improvements by http://www.variations-of-shadow.com
*/
private static function pbkdf2($password, $salt, $algorithm = 'sha256', $count = 1000, $key_length = 64, $raw_output = false)
{
$algorithm = strtolower($algorithm);
if(!in_array($algorithm, hash_algos(), true))
die('PBKDF2 ERROR: Invalid hash algorithm.');
if($count <= 0 || $key_length <= 0)
die('PBKDF2 ERROR: Invalid parameters.');
$hash_length = strlen(hash($algorithm, "", true));
$block_count = ceil($key_length / $hash_length);
$output = "";
for($i = 1; $i <= $block_count; $i++) {
// $i encoded as 4 bytes, big endian.
$last = $salt . pack("N", $i);
// first iteration
$last = $xorsum = hash_hmac($algorithm, $last, $password, true);
// perform the other $count - 1 iterations
for ($j = 1; $j < $count; $j++) {
$xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
}
$output .= $xorsum;
}
if($raw_output)
return substr($output, 0, $key_length);
else
return bin2hex(substr($output, 0, $key_length));
}
}
?>