Yii密码加密与验证(源码分析)

        yii权威指南上,关于处理密码,提供了两种帮助函数,那么其工作原理是什么呢?

public function generatePasswordHash($password, $cost = null)
{
    if ($cost === null) {
        $cost = $this->passwordHashCost;
    }

    if (function_exists('password_hash')) {//如果存在password_hash()方法,就使用该方法返回加密后的结果
        /** @noinspection PhpUndefinedConstantInspection */
        return password_hash($password, PASSWORD_DEFAULT, ['cost' => $cost]);
    }

    $salt = $this->generateSalt($cost);//如果不存在password_hash()方法,就使用cypt+salt的形式返回哈希化的结果
    $hash = crypt($password, $salt);
    // strlen() is safe since crypt() returns only ascii
    if (!is_string($hash) || strlen($hash) !== 60) {
        throw new Exception('Unknown error occurred while generating hash.');
    }

    return $hash;
}

        关于上述代码,需要讲解的是
        password_hash(),在(PHP 5 >= 5.5.0, PHP 7)中,提供的新函数,用于创建哈希密码:
         string   password_hash  (   string $password  ,   integer $algo  [,   array $options  ] )
        第一个参数为所要加密的用户密码,第二个参数为密码算法常数(PASSWORD_DEFAULT, PASSWORD_BCRYPT)第三个参数为选项
        具体用法详见 http://php.net/manual/en/function.password-hash.php 这里不在做过多解释
        
        哈希值创建完毕,我们可以用password_verify()来校验密码是否和哈希值匹配:
        boolean password_verify ( string $password , string $hash ),它接收2个参数:密码和哈希值,并返回布尔值,可以用于检查之前生成的哈希值是否和密码匹配。

        接下来咱们重点分析一下使用crypt()函数加盐值的方式:
protected function generateSalt($cost = 13)
{
    $cost = (int) $cost;
    if ($cost < 4 || $cost > 31) {
        throw new InvalidParamException('Cost must be between 4 and 31.');
    }

    // Get a 20-byte random string
    $rand = $this->generateRandomKey(20);
    // Form the prefix that specifies Blowfish (bcrypt) algorithm and cost parameter.
    $salt = sprintf("$2y$%02d$", $cost);
    // Append the random salt data in the required base64 format.
    $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22));

    return $salt;
}

    salt的生成:
    sprintf()函数:把百分号(%)符号替换成一个作为参数进行传递的变量
    $salt = sprintf("$2y$%02d$",$cost); = $2y$13$
    generateRandomKey()方法为yii生成了一个20字节长度的随机数
    然后最后将$rand使用base64_encode加密取前0-22个,将+换为.,然后前面在拼接上$salt
    yii使用的是如下方法

        就此加密过程就结束了
        关于验证密码:
public function validatePassword($password, $hash)
{
    if (!is_string($password) || $password === '') {
        throw new InvalidParamException('Password must be a string and cannot be empty.');
    }

    if (!preg_match('/^\$2[axy]\$(\d\d)\$[\.\/0-9A-Za-z]{22}/', $hash, $matches)
        || $matches[1] < 4
        || $matches[1] > 30
    ) {
        throw new InvalidParamException('Hash is invalid.');
    }

    if (function_exists('password_verify')) {//如果存在passwo_verify则直接使用其验证密码,和password_hash()配套
        return password_verify($password, $hash);
    }

    $test = crypt($password, $hash); //将用户输入的密码和哈希密码进行crypt()加密
    $n = strlen($test);//获取字节长度
    if ($n !== 60) {
        return false;
    }

    return $this->compareString($test, $hash);//compareString慢比较,两个密码,并返回Boolean类型
}

public function compareString($expected, $actual)
{
    $expected .= "\0";
    $actual .= "\0";
    $expectedLength = StringHelper::byteLength($expected);//mb_strlen($var,8bit)计算长度的,以防编码不一造成长度计算不一致
    $actualLength = StringHelper::byteLength($actual);
    $diff = $expectedLength - $actualLength;//两个长度差
    for ($i = 0; $i < $actualLength; $i++) {//逐一比较
        // $diff|= () 等价于 $diff = $diff | ()  取或  此处,一旦为结果为1,则不可能置零
        // ^ 为按位异或,相同为0,不同非0
        $diff |= (ord($actual[$i]) ^ ord($expected[$i % $expectedLength]));
    }
    //如果按位取或之后,$diff仍然为0,则说明两密码绝对相等
    return $diff === 0;
}

    为什么要用这种比较方法:
    让比较过程耗费固定的时间(全部比较完再返回结果)可以保证攻击者无法对一个在线系统使用计时攻击,以此获取
密码的哈希值,然后进行本地破解工作。  

比较两个字节序列(字符串)的标准做法是,从第一字节开始,每个字节逐一顺序比较。只要发现某字节不相同了,

就可以立即返回“假”的结果。如果遍历整个字符串也没有找到不同的字节,那么两个字符串就是相同的,并且返回“真”。

这意味着比较字符串的耗时决定于两个字符串到底有多大的不同。

举个例子,使用标准的方法比较“xyzabc”和“abcxyz”,由于第一个字符就不同,不需要检查后面的内容就可以马上

返回结果。相反,如果比较“aaaaaaaaaaB”和“aaaaaaaaaaZ”,比较算法就需要遍历最后一位前所有的“a”,然后才能知

道它们是不相同的。

攻击方式:首先攻击者准备256个字符串,它们的哈希值的第一字节包含了所有可能的情况。然后用它们去系统中尝

试登录,并记录系统返回结果所消耗的时间,耗时最长的那个就是第一字节猜对的那个。接下来用同样的方式猜测第二

字节、第三字节等等。直到攻击者获取了最够长的哈希值片段,最后只需在自己的机器上破解即可,完全不受在线系统的

限制。

     上述方法可以简化为如下方式:

private static boolean slowEquals(byte[] a, byte[] b)
{
    int diff = a.length ^ b.length;
    for(int i = 0; i < a.length && i < b.length; i++)
    diff |= a[i] ^ b[i];
    return diff === 0;
}





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值