更安全的密码存储与验证方案

现在比较流行的密码存储是对密码明文做 HASH运算,把得到HASH值存储到数据库中。验证过程就是再次对用户发过来的明文密码做 HASH,然后把得到HASH值与数据库中的存储的HASH做比较。当然在做HASH时加盐可以提高密码的安全性。

密码的攻击手段

只要了解了密码存储和验证的原理,就可以想出“破解”之法;如果想要得到一个更安全的密码存储和验证方案就需要去研究一下针对密码都有哪些攻击手段。以下内容来自于网络。

字典和暴力破解攻击(Dictionary and Brute Force Attacks)

最常见的破解hash手段就是猜测密码。然后对每一个可能的密码进行hash,对比需要破解的hash和猜测的密码hash值,如果两个值一样,那么之前猜测的密码就是正确的密码明文。猜测密码攻击常用的方式就是字典攻击和暴力攻击。

字典攻击是将常用的密码,单词,短语和其他可能用来做密码的字符串放到一个文件中,然后对文件中的每一个词进行hash,将这些hash与需要破解的密码hash比较。这种方式的成功率取决于密码字典的大小以及字典的是否合适。

这种攻击方式,对于随机且复杂密码无效。

查表破解(Lookup Tables)

对于特定的hash类型,如果需要破解大量hash的话,查表是一种非常有效而且快速的方式。它的理念就是预先计算(pre-compute)出密码字典中每一个密码的hash。然后把hash和对应的密码保存在一个表里。

这种攻击方式,对于随机且复杂密码无效。

反向查表破解(Reverse Lookup Tables)

这种方式可以让攻击者不预先计算一个查询表的情况下同时对大量hash进行字典和暴力破解攻击。

首先,攻击者会根据获取到的数据库数据制作一个用户名和对应的hash表。然后将常见的字典密码进行hash之后,跟这个表的hash进行对比,就可以知道用哪些用户使用了这个密码。这种攻击方式很有效果,因为通常情况下很多用户都会有使用相同的密码。

这种攻击方式,对于随机且复杂密码无效。

彩虹表 (Rainbow Tables)

彩虹表中存储的是一个个“散列链”。

假设我们有一个密文散列函数H和密码P。传统的做法是把H(X)的所有输出穷举,查找H(X[y])==H(P),得出P==X[y]。

而彩虹表中的“散列链”是为了降低传统做法对空间的要求。首先需要定义一个衰减函数 R 把散列值变换成另一字符串。通过交替运算H函数和R函数,形成交替的密码和散列值链条。 例如:假设密码是6个小写字母,散列值为32位长,链条看起来可能是这样的:

 1 aaaaaa ---> 281DAF40 ---> sgfnyd ---> 920ECF10 ---> kiebgt
 2 _______H()___________R()_________H()___________R()________

要生成一个表,我们选择一组随机的初始密码,每一个密码计算一个固定长度K的链,并只存储每一个链的第一个和最后一个密码。第一密码被称为始点,最后一个被称为末点。在上面例举的链中,“aaaaaa”就是始点,“kiebgt”就是末点,其他密码(或散列值)并不被保存。

假如给定一个散列值h ,我们要反运算(找到对应的密码),计算出一个链,以对h应用R函数开始,然后H函数,然后R函数,一直继续。如果在该运算过程中的任何点(每次应用R后),我们发现该点的值匹配我们生成的表中的一个末点,那么我们就得到了相应的始点,用这个始点来重新计算链。这条链会有很高的几率包含值h,而如果确实包含,链中h前面紧接的值就是我们所寻求的密码p。

例如,如果我们给出的散列值920ECF10,我们将以对其应用R开始计算链:

 1 920ECF10 ---> kiebgt
 2 _________R()________

由于kiebgt是我们的末点之一,我们找到始点aaaaaa并开始跟这个链条,直到发现920ECF10:

 1 aaaaaa ---> 281DAF40 ---> sgfnyd ---> 920ECF10
 2 _______H()___________R()_________H()__________

因此,密码是“sgfnyd”。

需要注意的是,这条链并不一定包含散列值h。因为以h开始的链与某一个始点的链条可能会合并。例如,一个散列值FB107E70,我们往下计算它的链条,会得到kiebgt:

 1 FB107E70 ---> bvtdll ---> 0EE80890 ---> kiebgt
 2 _________R()_________H()___________R()________

网络监听

这不是一种针对密码储存的攻击手段,但却是得到用户密码最有效的手段。

我把网卡状态设置为“混杂”模式,就可以收到局域内所有主机收发的报文。或者当我有路由器的控制权时,我就可以通过路由器得到所有经过此路由的数据。

如果用户的密码以明文方式发送到服务器端做验证,我就可以很轻松的得到用户的真实口令。就算是把用户口令求哈希后再发送到服务器端做验证,我依然可以得到用户口令的哈希值;然后我只需要模仿客户端的行为向服务器发送请求,就能够得到用户的所有权限。

最初的密码存储和验证方案

此方案就是对用户的密码做一次 MD5 摘要,然后将得到的“摘要信息”存储到数据库中。每次用户登录时会把用户名和密码明文发送到服务器端,服务器端通过用户名从数据库中查询得到密码的“摘要信息”。然后对密码明文做 MD5 后与数据库中存储的密码的“摘要信息”做比较;如果一致则登录成功,如果不一致则提示密码错误。

听说最早一批的互联网产品使用的就是这种“密码方案”。我在最初做用户登录功能时(大学里的课程设计中)也是用的这种方案。

缺点

  1. 利用彩虹表,很容易被攻破。
  2. 网络监听的攻击方式也能够得到密码明文。

所以不建议使用。

加盐提高安全性

为了应对黑客们用彩虹表破解密码,我们可以先往明文密码加盐,然后再对加盐之后的密码用哈希算法加密。所谓的盐是一个随机的字符串,往明文密码里加盐就是把明文密码和一个随机的字符串拼接在一起。由于盐在密码校验的时候还要用到,因此通常盐和密码的哈希值是存储在一起的。

采用加盐的哈希算法对密码加密,有一点值得注意。我们要确保要往每个密码里添加随机的唯一的盐,而不是让所有密码共享一样的盐。如果所有密码共享统一的盐,当黑客猜出了这个盐之后,他就可以针对这个盐生成一个彩虹表,再将我们加盐之后的哈希值到他的新彩虹表里去匹配就可以破解密码了。

缺点

对于“网络监听”的攻击方法没有安全性可言。

更安全的密码存储和验证策略

因为更安全,所以更复杂。

密码存储

密码加盐后计算得到 SHA256 哈希值并存储到数据库中,同时盐也要存储到数据库。盐是变化的,修改密码的时候会生成新的盐。

密码验证

用户登录前,服务器需要生成一个会话唯一的 Token 并与 用户的“当前盐” 一起发送到客户端。客户端拿到Token和盐之后需要进行以下步骤的操作:

  1. 对用户输入的密码加盐后计算得到一个 HASH 值 p_hash;(如果用户输入的密码正确的话,这个值与数据库中存储的密码是一样的)
  2. 把上一步得到的 HASH 值 p_hash 与 用户名拼接后,以 Token 为 key 使用 hmac_sha256_hex 计算机得到一个新的哈希值 passwd。
  3. 把用户名和passwd 发送到服务器端进行密码验证

服务器端接收到客户端验证密码的请求后需要进行以下步骤:

  1. 使用用户名从数据库中查询得到用户密码的 HASH 值 passwd_hash
  2. 把上一步得到的 passwd_hash 与用户名拼接后,以会话中存储的 Token(与之前发给客户端的Token值相同)为 key 使用 hmac_sha256_hex 计算得到哈希值 password;
  3. 把前端发过来的 passwd 与上一步得到的 password 做比较,如果一致则登录成功,否则提示密码错误。
  4. 为了防止有人对网络进行监听,抓取用户向服务器提交的登录认证请求,然后模拟用户对抓取到的数据包进行提交,从而获得服务器的授权;每次授权成功后,服务器需要把用户登录时使用的Token立马设置失效。

优点

以上所有的攻击手段对此方案的安全性影响都不大。当然了,前提是你的密码够复杂。

总结

  1. 所有简单且有规律的常用密码,即使加了盐,对于“字典和暴力破解攻击”也是不安全的。
  2. 所有简单且有规律的常用密码,在没有加盐或有大量盐重复的情况下,对于“查表破解”,“反向查表破解”和“彩虹表”攻击方式是不安全的。
  3. 所有没有加盐就存储的密码,无论复杂与否,对于“彩虹表”攻击方式都是不安全的。
  4. 所有没有在前端使用动态盐对密码做哈希的密码验证方式,对于“网络监听”攻击方式都是不安全的。

推荐阅读更多精彩内容