无效的散列方法
错误的方法:过短盐和重用盐
实现盐的最常见错误是在多个散列中重复使用相同的盐,或者使用过短的盐。
重用盐
一种常见的错误是在每个散列中使用相同的盐。盐要么被硬编码到程序中,要么仅随机生成一次。这种做法是无效的,因为如果两个用户有相同的密码,仍会生成相同的散列值。攻击者仍然可以使用反向查找表同时对每个散列进行字典攻击。攻击者只需要在对密码猜测进行散列处理之前附加上这个唯一的盐即可。如果盐被硬编码到了某个流行的产品中,则可以针对该盐构建查找表和彩虹表,以便更容易地破解该产品生成的散列。
每当用户创建账户或更改密码时,都必须生成一个新的随机盐。
过短盐
如果盐过短,攻击者可以为每个可能的盐都建立一个查找表。例如,如果盐只有三个 ASCII 字符,则只有 95 x 95 x 95 = 857,375 种可能的盐。虽然看起来好像很多,但如果每个查找表只包含 1MB 的最常见密码,那么所有查找表总共只有 837GB,考虑到现在 1000GB 硬盘的价格只有不到 100 美元,所以这并不算很多。
出于同样的原因,用户名不应该用作盐。用户名对于单个服务来说可能是唯一的,但它们是可预测的,而且经常被其他服务上的账户重复使用。攻击者可以为常见用户名构建查找表,用于破解用户名作为盐的散列。
为了使攻击者无法为每一种可能的盐创建查找表,盐必须很长。一个不错的经验法则是使用与散列函数输出长度相同长度的盐。例如,SHA256 的输出是 256 位(32 字节),所以盐至少应该是 32 个随机字节。
错误的方法:双重散列和古怪散列函数
本节介绍另一种常见的密码散列误解:散列算法的古怪组合。人们很容易得意忘形,然后尝试组合不同的散列函数,并期望结果更加安全。但在实践中,这样做几乎没有什么好处。这样做只会造成互操作性问题,有时甚至会降低散列的安全性。永远不要尝试发明自己的密码学算法,应始终使用由专家设计的标准。有些人可能会争辩说,使用多个散列函数会使计算散列的过程变慢,因此破解速度也会变慢,但有一种更好的方法可以减慢破解过程的速度,我们稍后会看到。
以下是我在互联网论坛中看到的一些糟糕的古怪散列函数示例。
md5(sha1(password))
md5(md5(salt) + md5(password))
sha1(sha1(password))
sha1(str_rot13(password + salt))
md5(sha1(md5(md5(password) + sha1(password)) + md5(password)))
不要使用上面的任何一种组合。
注意:这一部分已经被证明是有争议的。我收到了许多电子邮件,认为使用古怪的散列函数是有益的,因为如果攻击者不知道正在使用哪个散列函数,就不太可能为这种古怪的散列函数预先计算彩虹表,使得计算散列函数需要花费更长的时间。
攻击者在不知道算法的情况下的确无法攻击散列,但请留意柯克霍夫原则(Kerckhoffs's Principle),即攻击者通常可以访问源代码(特别是自由或开源软件),并且,给定来自目标系统的一些密码-散列对,对算法进行逆向工程并不困难。计算古怪的散列函数确实需要更长的时间,但只是一个小的常数因子而已。最好使用为极难并行化而设计的迭代算法(将在下面讨论)。此外,适当地对散列加盐就可以解决彩虹表问题了。
如果您真的想使用像 HMAC 这样的标准化「古怪」散列函数,那么也没问题。但是,如果您这样做的原因是为了降低散列计算速度,请先阅读下面有关密钥延伸的部分。
与无意实现了完全不安全的散列函数的风险以及古怪散列产生的互操作性问题相比,好处微不足道。显然,最好使用标准且经过良好测试的算法。
散列碰撞
由于散列函数将任意长度的数据映射成了固定长度的字符串,所以一定会有一些输入散列成了相同的字符串。加密散列函数被设计为极难发现这种碰撞。密码学家有时会发现散列函数的「攻击」,这会使找到碰撞变得更容易。一个最近的例子是 MD5 散列函数,实际上已经发现了碰撞。(译者注:SHA-1 的碰撞于 2017 年 2 月也已经被发现。)
发现碰撞攻击表明,除了用户密码以外的字符串也可能拥有相同的散列值。然而,即使是 MD5 这样的弱散列函数,发现碰撞也需要大量的专用计算能力,因此这些碰撞在实践中「偶然」发生的可能性很小。就所有实际目的而言,使用 MD5 和盐进行散列的密码与使用 SHA256 和盐进行散列的密码一样安全。(译者注:由于计算能力的进步和选择前缀碰撞攻击等技术的发展,MD5 已经不适合继续使用。)尽管如此,如果可能,最好使用更安全的散列函数,例如 SHA256、SHA512、RipeMD 或 WHIRLPOOL。