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

首先,像国内一些网站,比如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次运算得出的结果。
参考标准:rfc6070rfc2898
我们看一下django中关于PBKDF2的代码:utils/crypto.py

  1. def pbkdf2(password, salt, iterations, dklen=0, digest=None):   
  2.     """  
  3.     Implements PBKDF2 as defined in RFC 2898, section 5.2  
  4.  
  5.     HMAC+SHA256 is used as the default pseudo random function.  
  6.  
  7.     Right now 10,000 iterations is the recommended default which takes  
  8.     100ms on a 2.2Ghz Core 2 Duo.  This is probably the bare minimum  
  9.     for security given 1000 iterations was recommended in 2001. This  
  10.     code is very well optimized for CPython and is only four times  
  11.     slower than openssl's implementation.  
  12.     """  
  13.     assert iterations > 0   
  14.     if not digest:   
  15.         digest = hashlib.sha256   
  16.     hlen = digest().digest_size   
  17.     if not dklen:   
  18.         dklen = hlen   
  19.     if dklen > (2 ** 32 - 1) * hlen:   
  20.         raise OverflowError('dklen too big')   
  21.     l = -(-dklen // hlen)   
  22.     r = dklen - (l - 1) * hlen   
  23.   
  24.     hex_format_string = "%%0%ix" % (hlen * 2)   
  25.   
  26.     def F(i):   
  27.         def U():   
  28.             u = salt + struct.pack('>I', i)   
  29.             for j in xrange(int(iterations)):   
  30.                 u = _fast_hmac(password, u, digest).digest()   
  31.                 yield _bin_to_long(u)   
  32.         return _long_to_bin(reduce(operator.xor, U()), hex_format_string)   
  33.   
  34.     T = [F(x) for x in range(1, l + 1)]   
  35.     return ''.join(T[:-1]) + T[-1][:r]  

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

  1. class PBKDF2PasswordHasher(BasePasswordHasher):   
  2.     """  
  3.     Secure password hashing using the PBKDF2 algorithm (recommended)  
  4.  
  5.     Configured to use PBKDF2 + HMAC + SHA256 with 10000 iterations.  
  6.     The result is a 64 byte binary string.  Iterations may be changed  
  7.     safely but you must rename the algorithm if you change SHA256.  
  8.     """  
  9.     algorithm = "pbkdf2_sha256"   
  10.     iterations = 10000   
  11.     digest = hashlib.sha256   
  12.   
  13.     def encode(self, password, salt, iterations=None):   
  14.         assert password   
  15.         assert salt and '$' not in salt   
  16.         if not iterations:   
  17.             iterations = self.iterations   
  18.         hash = pbkdf2(password, salt, iterations, digest=self.digest)   
  19.         hash = hash.encode('base64').strip()   
  20.         return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)   
  21.   
  22.     def verify(self, password, encoded):   
  23.         algorithm, iterations, salt, hash = encoded.split('$', 3)   
  24.         assert algorithm == self.algorithm   
  25.         encoded_2 = self.encode(password, salt, int(iterations))   
  26.         return constant_time_compare(encoded, encoded_2)   
  27.   
  28.     def safe_summary(self, encoded):   
  29.         algorithm, iterations, salt, hash = encoded.split('$', 3)   
  30.         assert algorithm == self.algorithm   
  31.         return SortedDict([   
  32.             (_('algorithm'), algorithm),   
  33.             (_('iterations'), iterations),   
  34.             (_('salt'), mask_hash(salt)),   
  35.             (_('hash'), mask_hash(hash)),   
  36.         ])  

再贴一个php的PBKDF2加密函数

  1. <?php   
  2.   // PBKDF2 Implementation (described in RFC 2898)   
  3.   //   
  4.   // @param   string  p   password   
  5.   // @param   string  s   salt   
  6.   // @param   int     c   iteration count (use 1000 or higher)   
  7.   // @param   int     kl  derived key length   
  8.   // @param   string  a   hash algorithm   
  9.   // @param   int     st  start position of result   
  10.   //   
  11.   // @return  string  derived key   
  12.   function str_hash_pbkdf2($p$s$c$kl$a = 'sha256', $st=0)   
  13.   {   
  14.     $kb = $start+$kl;                        // Key blocks to compute   
  15.     $dk = '';                                    // Derived key    
  16.   
  17.     // Create key   
  18.     for ($block=1; $block<=$kb$block++)   
  19.     {   
  20.       // Initial hash for this block   
  21.       $ib = $h = hash_hmac($a$s . pack('N', $block), $p, true);    
  22.   
  23.       // Perform block iterations   
  24.       for ($i=1; $i<$c$i++)   
  25.       {   
  26.         // XOR each iterate   
  27.         $ib ^= ($h = hash_hmac($a$h$p, true));   
  28.       }    
  29.   
  30.       $dk .= $ib;                                // Append iterated block   
  31.     }    
  32.   
  33.     // Return derived key of correct length   
  34.     return substr($dk$start$kl);   
  35.   }   
  36. ?>  

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

  1. bcrypt = self._load_library()   
  2.         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

  1. da shang
    donate-alipay
               donate-weixin weixinpay

发表评论↓↓