正确应用密码学的关键是足够精确地定义您所追求的属性。
通常,当有人想要散列密码时,它是在以下情况下:服务器正在验证用户;用户通过机密通道(HTTPS ...)显示他们的密码。因此,服务器必须存储用户密码,或者至少存储一些可用于验证密码的东西。我们不想“按原样”存储密码,因为获得对服务器数据库的读取访问权限的攻击者随后将获悉所有密码。这是我们的攻击模型。
密码是适合普通用户大脑的东西,因此不能完全猜不透。少数用户会选择具有高熵的非常长的密码,但大多数用户会选择熵不高于 32 位的密码。这是一种说法,攻击者必须平均“尝试”不到 231(约 20 亿)个潜在密码,然后才能找到正确的密码。
无论服务器存储什么,验证密码就足够了;因此,我们的攻击者拥有尝试密码所需的所有数据,仅受他可以召集的计算能力的限制。这被称为离线字典攻击。
必须假设我们的攻击者可以破解一个密码。在这一点上,我们可能希望有两个属性:
- 破解单个密码应该很困难(几天或几周,而不是几秒钟);
- 破解两个密码应该是破解一个密码的两倍。
这两个属性需要不同的对策,可以结合使用。
1.慢散列
哈希函数很快。计算能力很便宜。作为一个数据点,使用 SHA-1 作为散列函数和 130 美元的 NVidia 显卡,我每秒可以散列 1.6 亿 个密码。 231 费用在大约 13 秒内支付。因此,SHA-1 对于安全性来说太快了。
另一方面,用户不会看到在 1µs 内进行身份验证和在 1ms 内进行身份验证之间有任何区别。所以这里的技巧是以一种使其变慢的方式扭曲散列函数。
例如,给定一个散列函数H,使用另一个散列函数H'定义为:
H'(x) = H(x || x || x || ... || x)
其中 '||' 表示串联。简单来说,重复输入足够多的时间,以便计算 H' 函数需要一些不可忽略的时间。所以你设定了一个时间目标,例如1ms,并调整达到该目标所需的重复次数。 10 毫秒意味着您的服务器将能够以仅 10% 的计算能力为代价每秒验证 10 个用户。请注意,我们讨论的是存储哈希密码的服务器以供其不可告人的用途,因此这里不存在互操作性问题:每个服务器都可以使用特定的重复计数,为其功能量身定制。
现在假设攻击者可以拥有你 100 倍的计算能力;例如攻击者是一个无聊的学生——许多安全系统的克星——并且可以在他的大学校园内使用数十台计算机。此外,攻击者可能会使用哈希函数 H 的更彻底优化的实现(您说的是 PHP,但攻击者可以进行汇编)。此外,攻击者耐心:用户不能等待超过几分之一秒,但一个足够无聊的学生可能会尝试几天。然而,尝试 20 亿个密码仍需要大约 3 天的计算时间。这最终并不安全,但比在一台廉价 PC 上运行 13 秒要好得多。
2。盐
salt 是您使用密码散列以防止共享的一段公共数据。
“共享”是指当攻击者可以对多个被攻击的密码重复使用他的哈希值时,就会发生这种情况。当攻击者有多个散列密码时会发生这种情况(他读取了散列密码的整个数据库):每当他散列 一个 潜在密码时,他可以对照 all 进行查找他试图攻击的散列密码。我们称之为并行字典攻击。共享的另一个实例是攻击者可以构建一个预先计算的哈希密码表,然后重复使用他的表(通过简单的查找)。传说中的 彩虹表 只是预计算表的一个特例(这只是时间-内存权衡,它允许使用比硬盘上的预计算表大得多的预计算表;但是构建该表仍然需要散列每个潜在的密码)。时空方面的并行攻击和预计算表是相同的攻击。
腌制胜过分享。 salt 是一个 public 数据元素,它改变了散列过程(可以说 salt 在一组不同的函数中选择散列函数)。盐的重点是每个密码都是唯一的。攻击者无法再共享破解工作,因为任何预先计算的表都必须使用特定的盐,并且对于使用不同盐散列的密码是无用的。
盐必须用于验证密码,因此服务器必须为每个散列密码存储用于散列该密码的盐值。在数据库中,这只是一个额外的列。或者您可以将盐和哈希密码连接在一个 blob 中;这只是数据编码的问题,由您决定。
假设 S 为盐(即一些字节),密码 p 的散列过程为:H'(S||p) em>(使用上一节中定义的 H' 函数)。就是这样!
盐的重点是尽可能地对每个散列密码唯一。实现此目的的一个简单方法是使用随机盐:每当创建或更改密码时,使用随机生成器获取 16 个随机字节。 16 字节应该足以使盐重用非常不可能。请注意,每个密码的salt应该是唯一的:使用用户名作为salt是不够的(一些不同的服务器实例可能有同名的用户——存在多少个“bob”那里?——还有,一些用户更改他们的密码,新密码不应该使用与以前密码相同的盐)。
3.哈希函数的选择
H' 散列函数建立在散列函数 H 之上。一些传统的实现使用加密算法扭曲成散列函数(例如 DES 用于 Unix 的 crypt())。这促进了“加密密码”表达的使用,尽管它不正确(密码没有加密,因为没有解密过程;正确的术语是“散列密码”)。然而,使用专为散列目的设计的真正散列函数似乎更安全。
最常用的哈希函数有:MD5、SHA-1、SHA-256、SHA-512(后两者统称为“SHA-2”)。在 MD5 和 SHA-1 中发现了一些弱点。这些弱点对一些的使用有严重的影响,但不是上面描述的(这些弱点是关于碰撞的,而我们在这里工作的是抗原像性)。不过,公关还是选择 SHA-256 或 SHA-512 更好:如果你使用 MD5 或 SHA-1,你可能需要为自己辩解。 SHA-256 和 SHA-512 的输出大小和性能不同(在某些系统上,SHA-256 比 SHA-512 快得多,而在其他系统上,SHA-512 比 SHA-256 快)。然而,性能在这里不是问题(不管散列函数的内在速度如何,我们通过输入重复使它慢得多),并且 SHA-256 输出的 256 位已经绰绰有余。为了节省存储成本,将散列函数输出截断到前 n 位,在密码学上是有效的,只要您保留至少 128 位 (n >= 128)。
4.结论
每当您创建或修改密码时,都会生成一个新的随机盐 S(16 个字节)。然后将密码 p 散列为 SHA-256(S||p||S||p||S||p||...||S||p) em>,其中 'S||p' 模式重复了足够多的时间,以至于散列过程需要 10 毫秒。存储 S 和哈希结果。要验证用户密码,请检索 S,重新计算哈希值,并将其与存储的值进行比较。
你会活得更久,更快乐。