了解PHP password_hash使用的bcrypt salt

And*_*ord 6 php salt bcrypt php-password-hash

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

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

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

在此输入图像描述

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

$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36)
Run Code Online (Sandbox Code Playgroud)

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

问题 2:22的盐长度是否正确?

在下面的例子中,似乎只有盐的前21个字符保存在结果字符串中.将这21个字符作为salt传递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
$2y$10$salt56789012345678901uTWNlUnhu5K/xBrtKYTo7oDy8zMr/csu
$2y$10$salt56789012345678901uTWNlUnhu5K/xBrtKYTo7oDy8zMr/csu
Run Code Online (Sandbox Code Playgroud)

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

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

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

据我所知,对所有密码使用"弱"盐或相同的盐会导致彩虹表的风险.但为什么一般使用自动生成的盐会更安全?

为什么使用自动生成的盐而不是手动盐更安全,生成如下:

$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36)
Run Code Online (Sandbox Code Playgroud)

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

由于我正在进行的项目的实现,我需要控制salt,用于生成密码哈希.这可以在将来改变,但是知道有必要手动设置盐.由于不推荐使用此功能password_hash,我需要一些替代方法来生成哈希.这该怎么做?

编辑:

简单解释为什么我需要控制盐:密码不仅用于直接登录Web应用程序,还用于通过REST API连接到应用程序.客户端从服务器请求salt并使用它(算法和成本已知)来散列密码,用户在客户端输入密码.

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

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

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

由于这样做,我需要遵循旧的过程,这意味着我需要能够告诉客户盐.

不过,我并不需要生成盐自己.如果PHP知道如何做到这一点最安全的方式,我完全没问题.但我确实需要获取/提取盐,将其发送给客户.

如果我理解了所有内容,我可以让它password_hash完成工作,然后从结果字符串中提取字符7-29.它是否正确?

Nar*_*arf 8

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

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

如果所有消息来源都说出来,那么你不应该质疑......

没有通用的盐大小,它取决于算法和bcrypt,它是22 ...虽然有一个问题.必要的大小实际上是16个字节,但实际上是Base64编码的(*).

当您对16字节的数据进行Base64编码时,将产生24个字符长度的ASCII字符串,最后2个字符无关紧要 - 当您修剪这2个不相关的字符时,它变为22.

为什么他们无关紧要?你的问题已经足够广泛了......阅读Base64维基百科页面.

*实际上有一些Base64"方言",而bcrypt使用的方言与PHP的方言并不完全相同base64_encode().

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

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

由于sha1散列是32个字符长,生成的盐也有32个长度.这只是一个懒惰的实现,跳过代码将字符串修剪为22的长度,因为这是由bcrypt自己完成的吗?或者因某种原因需要32个字符?

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

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

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

我不熟悉那个FOSUserBundle,但是是的 - 看起来它只是在做一些懒惰和错误的事情.

问题 2:22的盐长度是否正确?

在下面的例子中,似乎只有盐的前21个字符保存在结果字符串中.将这21个字符作为salt传递给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
$2y$10$salt56789012345678901uTWNlUnhu5K/xBrtKYTo7oDy8zMr/csu
$2y$10$salt56789012345678901uTWNlUnhu5K/xBrtKYTo7oDy8zMr/csu

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

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

我无法正确地解释这一点,但它又是由Base64如何工作引起的,因为在生成的哈希中,你实际上看到类似于此的东西(伪代码):

base64_encode(  base64_decode($salt) . $actualHashInBinary  )
Run Code Online (Sandbox Code Playgroud)

也就是说,(假设)Base64编码的盐首先被解码为原始二进制,用于创建实际的散列(再次以原始二进制形式),两者被连接,然后整个事物被Base64编码.
由于输入盐实际上是24个全长的22个相关的,我们实际上在末尾有一个不完整的块,它在原始哈希的开头完成(填充?)...

连接2个单独的Base64编码值并在Base64编码之前连接原始值是另一回事.

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

在PHP函数中,不推荐使用手动哈希的password_hash.相反,我们鼓励人们自动设置password_hash,因为它会更安心.

据我所知,对所有密码使用"弱"盐或相同的盐会导致彩虹表的风险.但为什么一般使用自动生成的盐更安全呢?

简单地说 - 盐需要加密安全(即不可预测),并且PHP已经知道如何做到这一点,而机会(绝大多数)你不知道.

除非你有一个实际的硬件CSPRNG(PHP尚未配置使用),你可以做的最好的事情就是让PHP自动生成盐.然而,在这里,我们显然想要反过来(无论出于何种原因)并在此过程中降低其安全性 - 很多人都这样做.
这就是为什么不推荐使用salt选项 - 为了保护您自己.:)

为什么要使用自动生成的盐代替手动盐,这是生成如下:

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

正如我所说,盐必须是不可预测的.在这个具体的例子中 - 使用的所有功能都不可预测,甚至mt_rand().
是的,mt_rand()尽管它的名字暗示,实际上并不是随机的.

问题4:是否有替换password_hash仍然允许使用自定义盐?

由于我正在进行的项目的实现,我需要控制salt,用于生成密码哈希.这可以在将来改变,但是知道有必要手动设置盐.由于不推荐使用此功能password_hash,我需要一些替代方法来生成哈希.这该怎么做?

你没有.

你的项目决定password_hash()盐是如何产生的绝对没有理由.我不知道为什么你认为这是必要的,但它100%不是 - 这没有任何意义.

虽然,最终 - 这就是为什么在删除某些东西之前实施折旧的原因.现在您知道将来会删除salt选项,并且您有足够的时间来重构您的应用程序.
明智地使用它,不要尝试复制已弃用的功能.你应该在相反的方向工作 - 询问如何在不破坏你的申请的情况下将两者分开.