了解 PHP password_hash 使用的 bcrypt salt

Understanding bcrypt salt as used by PHP password_hash

我很难理解 bcrypt 如何使用盐。我知道盐有什么用,但我不明白盐值到底是怎么用的。

问题1:正确的盐长度是多少?

我找到的所有来源都说,salt 的长度为 22,并且它与算法、成本和结果字符串中的实际哈希值一起存储。

但是,我发现的所有实现都使用长度为 32 的盐。例如 Symfony 使用的 FOSUserBundle 使用以下代码创建盐:

$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36)

由于 sha1 散列的长度为 32 个字符,因此生成的 salt 的长度也为 32。这只是一个懒惰的实现吗,将代码跳过到 trim字符串长度为 22,因为这是由 bcrypt 它自己完成的?或者出于某种原因需要 32 个字符?

问题2:盐长度22真的正确吗?

在下面的示例中,结果字符串中似乎只保存了盐的前 21 个字符。将这 21 个字符作为盐传递给 password_hash 将导致错误,但填充 0 将起作用:

$s = 'password';
$salt        = 'salt5678901234567890123456789012';
$salt_prefix = 'salt567890123456789010'; // first 21 chars of salt + 0

$h1 = password_hash($s, PASSWORD_BCRYPT, array('salt' => $salt));
$h2 = password_hash($s, PASSWORD_BCRYPT, array('salt' => $salt_prefix));

echo $h1 . PHP_EOL;
echo $h2 . PHP_EOL;

//Result
y$salt56789012345678901uTWNlUnhu5K/xBrtKYTo7oDy8zMr/csu
y$salt56789012345678901uTWNlUnhu5K/xBrtKYTo7oDy8zMr/csu

因此,需要将至少包含 22 个字符的盐传递给算法,但第 22 个字符似乎没有用。那是对的吗?如果根本不使用第 22 个字符,它有什么意义?

问题3:为什么不手动指定salt?

在 PHP 函数中 password_hash 不推荐使用手动哈希。相反,鼓励自动让 password_hash,因为这样会更安全。

我知道对所有密码使用 "weak" 加盐或相同的加盐会因彩虹表而导致风险。但为什么一般使用自动生成的盐更安全?

为什么使用自动生成的盐比手动生成的盐更安全,它是这样生成的:

$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36)

问题 4: 是否有任何 password_hash 的替代品仍然允许使用自定义盐?

由于我正在进行的项目的实施,我需要控制用于生成密码哈希的盐。这可以在将来更改,但知道有必要手动设置盐。由于此功能在 password_hash 中已弃用,我需要一些替代方法来生成哈希。如何做到这一点?

编辑:

简单解释一下为什么我需要控制盐:密码不仅用于直接登录网络应用程序,还用于通过 REST API 连接到应用程序。客户端向服务器请求盐并使用它(算法和成本已知)对用户在客户端输入的密码进行哈希处理。

散列密码然后发送回服务器进行身份验证。目的是不以明文形式发送密码。为了能够在客户端上生成与服务器上相同的散列,客户端需要知道服务器使用了哪种盐。

我知道散列密码不会增加任何真正的安全性,因为通信已经仅使用 HTTPS。然而,这是客户端当前操作的方式:如果客户端发回正确的密码哈希,则授予身份验证。

我无法在不破坏数千个现有客户端的情况下更改服务器端。客户端可以在未来的某个时候更新,但这将是一个漫长的过程。

既然这样做了,我需要遵循旧流程,这意味着我需要能够告诉客户盐。

不过我不需要自己生成盐。如果 PHP 知道最安全的方法如何做到这一点,我完全没问题。但我确实需要 get/extract 盐,以便将其发送给客户。

如果我理解正确,我可以让 password_hash 完成工作,然后从结果字符串中提取字符 7-29。这是正确的吗?

Problem 1: What is the correct salt length?

All sources I found say, that the salt has a length of 22 and that it is stored together with the algorithm, the costs and the actual hash value in the result string.

如果所有消息来源都这么说,那么您就没有理由质疑...

没有通用的 salt 大小,这取决于算法,对于 bcrypt,它是 22 ......尽管有一个问题。必要的大小实际上是 16 个字节,但实际上是 Base64 编码 (*)。

当您对 16 个字节的数据进行 Base64 编码时,将产生一个 24 个字符长度的 ASCII 字符串,最后 2 个字符无关紧要 - 当您trim那2个不相关的。

为什么它们无关紧要?你的问题已经足够广泛了......阅读 Wikipedia page for Base64.

* 实际上有一些 Base64“方言”,bcrypt 使用的与 PHP 的 base64_encode(). 不太一样

However, all implementations I found, use a salt with length 32. For example the FOSUserBundle used by Symfony used the following code to creat the salt:

$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36)

Since a sha1 hash is 32 chars long, the generated salt also has a length of 32. Is this just a lazy implementation, skipping the code to trim the string to a length of 22 because this is done by bcrypt it self? Or are 32 chars necessary for some reason?

该行将产生一个 31 个字符的字符串,而不是 32 个,但这实际上并不相关。如果您提供更长的字符串,将仅使用它的 必要的 部分 - 最后的字符将被忽略。
你可以自己测试一下:

php > var_dump(password_hash('foo', PASSWORD_DEFAULT, ['salt' => str_repeat('a', 22).'b']));
string(60) "y$aaaaaaaaaaaaaaaaaaaaaO8Q0BjhyjLkn5wwHyGGWhEnrex6ji3Qm"
php > var_dump(password_hash('foo', PASSWORD_DEFAULT, ['salt' => str_repeat('a', 22).'c']));
string(60) "y$aaaaaaaaaaaaaaaaaaaaaO8Q0BjhyjLkn5wwHyGGWhEnrex6ji3Qm"
php > var_dump(password_hash('foo', PASSWORD_DEFAULT, ['salt' => str_repeat('a', 22).'d']));
string(60) "y$aaaaaaaaaaaaaaaaaaaaaO8Q0BjhyjLkn5wwHyGGWhEnrex6ji3Qm"

(如果使用了额外的字符,生成的哈希值会有所不同)

我不熟悉那个 FOSUserBundle,但是是的 - 它看起来确实只是在做一些懒惰的事情,而且是不正确的。

Problem 2: Is a salt length of 22 really correct?

In the following example it seems, that only the first 21 chars of the salt are saved in the result string. Passing these 21 chars as salt to password_hash will result in an error, but padding a 0 will work:

$s = 'password';
$salt        = 'salt5678901234567890123456789012';
$salt_prefix = 'salt567890123456789010'; // first 21 chars of salt + 0

$h1 = password_hash($s, PASSWORD_BCRYPT, array('salt' => $salt));
$h2 = password_hash($s, PASSWORD_BCRYPT, array('salt' => $salt_prefix));

echo $h1 . PHP_EOL;
echo $h2 . PHP_EOL;

//Result
y$salt56789012345678901uTWNlUnhu5K/xBrtKYTo7oDy8zMr/csu
y$salt56789012345678901uTWNlUnhu5K/xBrtKYTo7oDy8zMr/csu

So, one needs to pass a salt with at least 22 chars to the algorithm but the 22nd chars seems to be useless. Is that correct? What is the sense of the 22nd char if it is not used at all?

这不是真的无关紧要......用例如填充它'A' 你会看到不同的结果。

老实说,我无法正确解释这一点,但这又是由 Base64 的工作方式引起的 因为在生成的哈希中,您实际上会看到类似于此的内容(伪代码):

base64_encode(  base64_decode($salt) . $actualHashInBinary  )

也就是说,(假设)Base64 编码的盐首先被解码为原始二进制文件,用于创建实际的哈希值(同样是原始二进制文件),将两者连接起来,然后 整件事都是 Base64 编码的。
由于输入盐实际上是 24 尺寸全长中的 22 相关,我们实际上在末尾有一个 incomplete 块,它在开始时完成(填充?)原始哈希 ...

连接 2 个单独的 Base64 编码值和在对它们进行 Base64 编码之前连接原始值是另一回事。

Problem 3: Why not specify the salt manually?

In the PHP function password_hash using a manual hash is deprecated. Instead one is encouraged to let password_hash automatically, since would be saver.

I understand that using a "weak" salt or the same salt for all passwords can lead to risks due to rainbow tables. But why is it saver to use the auto-generated salt in general?

简单地说——盐需要加密安全(即不可预测),并且PHP已经知道如何做到这一点,而机会是(压倒性的)你没有。

除非你有一个实际的硬件 CSPRNG(PHP 还没有配置使用),你能做的最好的事情就是让 PHP 自动生成盐。 然而,在这里,您显然想做相反的事情(无论出于何种原因)并在此过程中降低安全性 - 很多人 这样做。
这就是不推荐使用 salt 选项的原因 - 以保护您免受自己的伤害。 :)

Why is it saver to use the auto-generated salt instead of manual salt, that is generated like this:

$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36)

正如我所说,盐需要是不可预测的。在此特定示例中 - none 所使用的函数是不可预测的,甚至 mt_rand().
是的,mt_rand() 实际上并不是随机的,尽管它的名字暗示了什么。

Problem 4: Is there any replacement for password_hash that still allows the usage of a custom salt?

Due to the implementation of project I am working on, I need to control the salt, that is used to generate a password hash. This can be changed in the future, but right know it is necessary to set the salt manually. Since this feature is deprecated in password_hash, I need some alternative to generate the hash. How to do this?

你不知道。

您的项目绝对没有理由决定 password_hash() 盐的生成方式。我不知道您为什么认为有必要,但 100% 没有必要 - 这没有任何意义。

不过,最终 - 这就是为什么在删除某些内容之前先进行弃用的原因。现在您知道 salt 选项将来会被删除,并且您有足够的时间来重构您的应用程序。
明智地使用它,不要试图复制已弃用的功能。您应该朝相反的方向努力 - 询问如何在不破坏您的应用程序的情况下将两者分开。