网站密码存储方案比较

Posted by Elton's Blog on October 2, 2012

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

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

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

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

md5($password.$salt)

第三种算法(php代码)

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次运算得出的结果。 参考标准:rfc6070rfc2898 我们看一下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加密函数

<?php
  // PBKDF2 Implementation (described in RFC 2898)
  //
  // @param   string  p   password
  // @param   string  s   salt
  // @param   int     c   iteration count (use 1000 or higher)
  // @param   int     kl  derived key length
  // @param   string  a   hash algorithm
  // @param   int     st  start position of result
  //
  // @return  string  derived key
  function str_hash_pbkdf2($p, $s, $c, $kl, $a = 'sha256', $st=0)
  {
    $kb = $start+$kl;                        // Key blocks to compute
    $dk = '';                                    // Derived key 
 
    // Create key
    for ($block=1; $block<=$kb; $block++)
    {
      // Initial hash for this block
      $ib = $h = hash_hmac($a, $s . pack('N', $block), $p, true); 
 
      // Perform block iterations
      for ($i=1; $i<$c; $i++)
      {
        // XOR each iterate
        $ib ^= ($h = hash_hmac($a, $h, $p, true));
      } 
 
      $dk .= $ib;                                // Append iterated block
    } 
 
    // Return derived key of correct length
    return substr($dk, $start, $kl);
  }
?>

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

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