“为什么使用可以在数据库中找到的随机盐比使用可以在脚本中找到的恒定盐增加安全性?”
因为“恒定盐”(实际上最好称为“胡椒”)对于所有密码散列都是相同的,因此不能满足盐的主要目的——确保即使两个用户具有相同的密码,他们的哈希值会有所不同。
特别是,假设您有 1000 个用户,并且您的带有密码哈希的用户数据库被攻击者窃取。很可能,攻击者还可以窃取您的脚本,因此也可以窃取其中包含的辣椒。(情况可能并非总是如此——例如,SQL 注入攻击可能会危及数据库,但不一定会危及应用程序代码——但您不应假设它不会发生。)
为了破解密码哈希,攻击者可以通过蛮力猜测随机密码(或者,对于第一遍,遍历一个常见密码列表),用你的固定胡椒对它们中的每一个进行哈希,然后在一个被盗哈希的数据库。值得注意的是,他们只需要为每个猜测的密码执行一次,就好像您根本没有使用过辣椒一样。
但是,如果您改为使用存储在数据库中的唯一盐,用于每个密码散列(有或没有胡椒),然后用其中一种盐对猜测的密码进行散列并将结果与被盗散列进行比较就可以看出攻击者是否该特定用户正在使用该特定密码。因此,破解所有密码(或破解任何一个或任何n 个密码)所需的时间增加了 1000 倍(数据库中的用户数量)。
或者,换个角度来看,不为 1000 个用户的数据库中的每个密码哈希使用唯一的盐会使攻击者的工作轻松 1000 倍。即使你使用迭代次数为 1000 的 PBKDF2 来减缓暴力攻击,好吧,猜猜看——不使用独特的盐只会吃掉你从使用 PBKDF2 中获得的任何好处。哎呀。
现在对于您问题的另一部分,您链接到的文章提出了一种相当复杂的选择盐的方法:
$salt = hash('sha256', uniqid(mt_rand(), true) . 'something random' .
strtolower($username));
当盐真正需要满足的唯一属性是唯一性时,为什么这么复杂?
好吧,盐真的不应该只是在您的数据库中是唯一的,而是全球唯一的,因此即使是拥有多个被盗数据库的攻击者也不会发现破解它们变得更容易。在某些攻击场景中,还希望盐是不可预测的,这样攻击者就无法在窃取您的数据库之前开始预先计算密码表,希望在您检测到违规行为时给您更多时间做出响应。
实现这两个目标的相对简单的方法是使盐足够长和随机(足够长意味着,例如,至少 100 位熵)。然而,生成真正的随机盐可能有点棘手,尤其是在 PHP 中,不幸的是,它仍然缺乏用于获取加密安全随机数的标准可移植方法。
因此,您链接到的示例代码似乎采用了一种强大的“腰带和吊带”方法:通过连接几条具有不同随机性和唯一性的数据并将它们提供给加密哈希函数,我们可以相当确定输出将与输入的总和一样唯一和随机(无论如何,直到哈希输出长度施加的限制,对于像 SHA-256 这样的 256 位哈希基本上是无限的)。特别是,只要至少有一个输入是唯一的(分别是随机的),我们基本上可以确定输出也是如此。
作为一般原则,这是一种非常好的和值得称道的安全方法。人们可能会对文章中建议的输入的具体选择提出质疑(特别是,甚至没有任何尝试从诸如openssl_random_pseudo_bytes()或Unix 系统之类的源获得真正的加密随机性/dev/urandom),但至少一般设计是声音。
特别要注意,哈希函数的建议输入包括以下内容:
- 用户名(尽管为什么它是小写的,我不知道),确保系统上的所有用户都有唯一的盐,
- 一个希望唯一的每个系统标识符(“随机的东西”),它应该确保同一个用户在不同的系统上会有不同的盐,
- 当前时间(通过uniqid()获得),确保用户每次更改密码时都会获得不同的盐,以及
- 通过 mt_rand() 和 uniqid() 获得的各种其他(伪)随机性,这可能会引入一些不可预测性(并且,至少在PHP 的某些强化版本中,甚至可能是一些真正的随机性),并且至少不会做任何事情伤害。