关于存储用户密码的思考,bcrypt 和 PBKDF2算法

来源:互联网 发布:excel导入mysql 编辑:程序博客网 时间:2024/05/01 08:36

转载来自:http://outofmemory.cn/wr/?u=https%3A%2F%2Fwww.akii.org%2Fsave-user-password-use-bcrypt-or-pbkdf2.html


首先,像国内一些网站,比如sina这样的网站,zf要求要掌握用户密码,虽然也是加密的,但是是可逆加密。只要有需要,反解你的密码只是点一下鼠标的事儿。所以建议你的密码分级,对sina这种网站,不要使用高等级的密码。

但如果我们为了对用户负责,用户密码采用不可逆算法的时候,我们就要考虑一下如何对用户密码进行加密。那么仅仅是使用不可逆算法就行了吗?还不是,在硬件飞速发展的今天,尤其是GPU运算能力超CPU 10-20倍甚至更多,使得暴力破解的时间大大缩短。那么为了使得暴力破解变得几乎不可能,我们就要使用一些不支持GPU加速破解的算法。这里所说的算法,实际上也是各种加密的hash方式。

目前常见的不可逆加密算法有以下几种:

  • 一次MD5(使用率很高)
  • 将密码与一个随机串进行一次MD5
  • 两次MD5,使用一个随机字符串与密码的md5值再进行一次md5,使用很广泛
  • PBKDF2算法
  • bcrypt
  • 其它加密算法

现在,通常推荐使用 bcrypt 或 PBKDF2 这两种算法来对密码进行加密。下面对以上几种加密算法进行一下简单的分析。
第一种就不解释了,我们看下第二种加密算法(php代码)$salt是一个随机字符串,每个用户都不一样,并且要存储下来用于验证

1
md5($password.$salt)

第三种算法(php代码)

1
md5(md5($password).$salt)

第一种和第二种都是一次md5,尤其是第一种,假设原始字符串很短,当然,我们的密码通常都不会很长,所以暴力破解还是不会耗时太久的。尤其是采用GPU运算。
下面这个网址中,作者针对cpu、gup和各种单一的加密算法破解进行了一些描述,有兴趣的可以看看:
http://www.codinghorror.com/blog/2012/04/speed-hashing.html

下面的网址还介绍了我国山东大学的王晓云和余洪波关于md5碰撞的文章,可以生成两个一样的md5值的文件。http://www.mscs.dal.ca/~selinger/md5collision/

上面链接的内容同样证明了一次MD5并不可靠。那么第二种和第三种是否可靠呢?第二种,要看你随机字符串有多长了,再加上原始密码,字符串越长,暴力破解的时间就越长,第三种就要暴力破解32位字符串的MD5,耗时嘛,以目前的硬件来看,估计单台机器普通人是等不到它破解出来了。不过如果涉及到国防级别,像美国使用超级计算机集群来破解的话,或许,用不了多长时间。

下面介绍第四种,是django 1.4默认采用的密码加密算法。点击上面PBKDF2的链接,在维基百科上已经有很详细的介绍,它使得暴力破解的希望更加渺茫。这也是django1.4安全性提升的一个亮点,在此之前它使用sha1来加密。PBKDF2实际上默认采用并推荐sha256,然后再配合10000次运算得出的结果。
参考标准:rfc6070,rfc2898
我们看一下django中关于PBKDF2的代码:utils/crypto.py

def pbkdf2(password, salt, iterations, dklen=0, digest=None):    """    Implements PBKDF2 as defined in RFC 2898, section 5.2     HMAC+SHA256 is used as the default pseudo random function.     Right now 10,000 iterations is the recommended default which takes    100ms on a 2.2Ghz Core 2 Duo.  This is probably the bare minimum    for security given 1000 iterations was recommended in 2001. This    code is very well optimized for CPython and is only four times    slower than openssl's implementation.    """    assert iterations > 0    if not digest:        digest = hashlib.sha256    hlen = digest().digest_size    if not dklen:        dklen = hlen    if dklen > (2 ** 32 - 1) * hlen:        raise OverflowError('dklen too big')    l = -(-dklen // hlen)    r = dklen - (l - 1) * hlen     hex_format_string = "%%0%ix" % (hlen * 2)     def F(i):        def U():            u = salt + struct.pack('>I', i)            for j in xrange(int(iterations)):                u = _fast_hmac(password, u, digest).digest()                yield _bin_to_long(u)        return _long_to_bin(reduce(operator.xor, U()), hex_format_string)     T = [F(x) for x in range(1, l + 1)]    return ''.join(T[:-1]) + T[-1][:r]

然后在django.contrib.auth.hashers里使用,密码以“algorithm$number of iterations$salt$password hash”的格式返回,并存储在同一个字段中。当然,如果你自己编写PBKDF2函数,你可以将salt存储在任意字段。只要让每个用户都不一样就行了。

class PBKDF2PasswordHasher(BasePasswordHasher):    """    Secure password hashing using the PBKDF2 algorithm (recommended)     Configured to use PBKDF2 + HMAC + SHA256 with 10000 iterations.    The result is a 64 byte binary string.  Iterations may be changed    safely but you must rename the algorithm if you change SHA256.    """    algorithm = "pbkdf2_sha256"    iterations = 10000    digest = hashlib.sha256     def encode(self, password, salt, iterations=None):        assert password        assert salt and '$' not in salt        if not iterations:            iterations = self.iterations        hash = pbkdf2(password, salt, iterations, digest=self.digest)        hash = hash.encode('base64').strip()        return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)     def verify(self, password, encoded):        algorithm, iterations, salt, hash = encoded.split('$', 3)        assert algorithm == self.algorithm        encoded_2 = self.encode(password, salt, int(iterations))        return constant_time_compare(encoded, encoded_2)     def safe_summary(self, encoded):        algorithm, iterations, salt, hash = encoded.split('$', 3)        assert algorithm == self.algorithm        return SortedDict([            (_('algorithm'), algorithm),            (_('iterations'), iterations),            (_('salt'), mask_hash(salt)),            (_('hash'), mask_hash(hash)),        ])

再贴一个php的PBKDF2加密函数

<?phpclass Utils{    /**     * PBKDF2 加密函数     * 参考标准     * @link https://www.ietf.org/rfc/rfc2898.txt     *     * php官方函数将在php5.5发布     * @see http://php.net/manual/en/function.hash-pbkdf2.php     * example: pbkdf2("sha256", 'password', 'salt', 1, 20);     * result:120fb6cffcf8b32c43e7 (与php5.5内置的pbkdf2函数输出一至)     *     * @param string $algo          The hash algorithm to use. Recommended: SHA256     * @param string $password      The password to use for the derivation.     * @param string $salt          The salt to use for the derivation.     * @param int    $iterations    The number of internal iterations to perform for the derivation.     * @param int    $length        The length of the derived key to output. If 0, the length of the supplied algorithm is used.     * @param boolean $raw_output   When set to TRUE, outputs raw binary data. FALSE outputs lowercase hexits.     * @return string     */    public static function pbkdf2($algo, $password, $salt, $iterations, $length= 0, $raw_output = false)    {        $algo = strtolower($algo);        if(!in_array($algo, hash_algos(), true))            throw new Exception('PBKDF2 ERROR: Invalid hash algorithm.');        if($iterations <= 0)            throw new Exception('PBKDF2 ERROR: Invalid parameters.');        // hash方式默认长度,对一个空值进行hash计算,得出        // length in octets of pseudorandom function output, a positive integer        $hlen = strlen(hash($algo, null, true));        if ($length <=0) $length = $hlen;        if ($length > (pow(2,32)-1)*$hlen)            throw new Exception('PBKDF2 ERROR: derived key too long.');         // 如果生成的加密值长度低于取值长度,就将加密再执行N遍以补足数据长度        // N=取值长度除以加密方式对应的值长度加一法取整        $block_count = ceil($length / $hlen);        $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($algo, $last, $password, true);            // perform the other $count - 1 iterations            for ($j = 1; $j < $iterations; $j++) {                $xorsum ^= ($last = hash_hmac($algo, $last, $password, true));            }            $output .= $xorsum;        }        if($raw_output)            return substr($output, 0, $length);        else            return substr(bin2hex($output), 0, $length);    }}

bcrypt加密在使用上则简单很多。不过多数语言要针对它安装扩展。如php,python都要安装扩展。

使如django中使用bcrypt加密的代码:

1
2
bcrypt=self._load_library()
        data=bcrypt.hashpw(password, salt)

所以这里就不多介绍bcrypt了。字符串的长度,影响它生成hash值的时间。当然,这似乎在任何一种hash算法上都是成正比的。

实际上,无论是bcrypt还是PBKDF2都有各自的忠实拥护者。另外bcrypt不支持超过55个字符的密码短语。到底那一个好,没有标准答案,取决于你问那一方的粉丝。我个人偏向于使用PBKDF2,下面的参考资料中,或许也会给你答案。

其它参考资料:
password-encryption-pbkdf2-using-sha512-x-1000-vs-bcrypt
sha512-vs-blowfish-and-bcrypt


0 0
原创粉丝点击