正确应用密码学的关键是足够精确地定义您所追求的属性。
通常,当有人想要对密码进行哈希处理时,是在以下上下文中:服务器正在对用户进行身份验证;用户通过机密通道(HTTPS...)显示他们的密码。因此,服务器必须存储用户密码,或者至少存储可用于verify一个密码。我们不想“按原样”存储密码,因为获得服务器数据库读取访问权限的攻击者将了解所有密码。这是我们的攻击模型.
A password is something which fits in the brain of the average user, hence it cannot be fully unguessable. A few users will choose very long passwords with high entropy, but most will select passwords with an entropy no higher than, say, 32 bits. This is a way of saying that an attacker will have to "try" on average less than 231 (about 2 billions) potential passwords before finding the right one.
无论服务器存储什么,只要验证密码就足够了;因此,我们的攻击者拥有尝试密码所需的所有数据,仅受他可以聚集的计算能力的限制。这被称为离线字典攻击.
我们必须假设我们的攻击者可以破解一个密码。那时我们可能希望有两个属性:
- 破解单个密码应该很困难(只需几天或几周,而不是几秒钟);
- 开裂two密码应该是twice就像敲开一个一样难。
这两个特性需要不同的对策,这些对策可以结合起来。
1. 慢哈希
Hash functions are fast. Computing power is cheap. As a data point, with SHA-1 as hash function, and a 130$ NVidia graphic card, I can hash 160 millions passwords per second. The 231 cost is paid in about 13 seconds. SHA-1 is thus too fast for security.
另一方面,用户不会看到 1μs 内完成身份验证和 1ms 内完成身份验证之间有任何区别。所以这里的技巧是以一种使其变慢的方式扭曲哈希函数。
例如,给定一个哈希函数H,使用另一个哈希函数H'定义为:
H'(x) = H(x || x || x || ... || x)
where '||' 表示串联。简而言之,重复输入足够多次,以便计算H'函数需要一些不可忽略的时间。所以你设定了一个时间目标,例如1ms,并调整达到该目标所需的重复次数。 10ms 意味着您的服务器每秒能够验证 10 个用户,而只需花费其 10% 的计算能力。请注意,我们讨论的是存储散列密码以供其自身不可告人使用的服务器,因此这里不存在互操作性问题:每个服务器都可以使用为其功能量身定制的特定重复计数。
假设现在攻击者可以拥有你100倍的计算能力;例如攻击者是一名无聊的学生——许多安全系统的克星——并且可以使用大学校园内的数十台计算机。此外,攻击者可能会使用更彻底优化的哈希函数实现H(您正在谈论 PHP,但攻击者可以进行汇编)。而且,攻击者是patient:用户不能等待超过几分之一秒,但足够无聊的学生可能会尝试几天。然而,尝试 20 亿个密码仍然需要大约 3 天的计算时间。这并不是最终安全的,但比一台廉价 PC 上的 13 秒要好得多。
2. Salts
A salt是一段公共数据,您可以使用密码对其进行哈希处理,以防止sharing.
当攻击者可以对多个受攻击的密码重复使用其哈希值时,就会发生“共享”。当攻击者拥有多个散列密码(他读取散列密码的整个数据库)时,就会发生这种情况:每当他散列时one潜在的密码,他可以对照查找all他试图攻击的散列密码。我们称之为并行字典攻击。共享的另一个实例是攻击者可以构建一个预先计算的哈希密码表,然后重复使用他的表(通过简单的查找)。传说中的彩虹桌只是预计算表的一个特例(这只是时间与内存的权衡,允许使用比硬盘上所能容纳的大得多的预计算表;但构建表仍然需要对每个潜在密码进行哈希处理)。时空方面,并行攻击和预计算表是相同的攻击。
加盐会打败共享。盐是一种public改变散列过程的数据元素(可以说盐selects一整套不同函数中的哈希函数)。盐的要点是它对于每个密码都是唯一的。攻击者无法再分享破解工作,因为任何预先计算的表都必须使用特定的盐,并且对于使用不同盐进行哈希处理的密码毫无用处。
必须使用盐来验证密码,因此服务器必须为每个散列密码存储用于散列该密码的盐值。在数据库中,这只是一个额外的列。或者您可以将盐和哈希密码连接在一个 blob 中;这只是数据编码的问题,这取决于您。
假设S作为盐(即一些字节),密码的哈希过程p is: H'(S||p)(与H'上一节中定义的函数)。就是这样!
盐的要点是,每个散列密码尽可能是唯一的。实现这一目标的一个简单方法是使用随机盐:每当创建或更改密码时,使用随机生成器获取 16 个随机字节。 16 个字节应该足以使盐重用几乎不可能。请注意,每个盐应该是唯一的password:使用用户名作为盐是不够的(一些不同的服务器实例可能有同名的用户——那里存在多少个“bob”?——还有一些用户change他们的密码,并且新密码不应使用与以前的密码相同的盐)。
3. 哈希函数的选择
The H'哈希函数是在哈希函数的基础上构建的H。一些传统的实现使用了扭曲为散列函数的加密算法(例如 Unix 的 DES)crypt()
)。这促进了“加密密码”表达式的使用,尽管它并不恰当(密码未加密,因为没有解密过程;正确的术语是“散列密码”)。然而,使用真正的哈希函数(专为哈希目的而设计)似乎更安全。
最常用的哈希函数有:MD5、SHA-1、SHA-256、SHA-512(后两者统称为“SHA-2”)。 MD5 和 SHA-1 中发现了一些弱点。这些弱点严重影响some用法,但是not如上所述(弱点在于碰撞,而我们在这里研究原像抵抗)。不过,公共关系最好选择 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 as SHA-256(S||p||S||p||S||p||...||S||p)哪里 'S||p' 模式重复足够多次,以至于哈希过程需要 10 毫秒。存储两者S和哈希结果。要验证用户密码,请检索S,重新计算哈希值,并将其与存储的值进行比较。
你会活得更久、更快乐。