理解PHP使用的bcrypt salt password_hash

时间:2016-12-06 10:56:32

标签: php bcrypt salt php-password-hash

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

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

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

enter image description here

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

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

由于sha1散列长度为32个字符,因此生成的salt的长度也为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

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

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

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

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

为什么使用自动生成的盐代替手动盐更安全,如下所示:

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

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

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

修改

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

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

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

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

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

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

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

1 个答案:

答案 0 :(得分:7)

  

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

     

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

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

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

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

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

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

  

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

     

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

     

由于sha1散列长度为32个字符,因此生成的salt的长度也为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"

(如果使用了额外的字符,产生的散列会有所不同)

我不熟悉那个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  )

也就是说,(假设)Base64编码的盐首先被解码为原始二进制,用于创建实际哈希(再次以原始二进制形式),两者连接在一起,然后整件事是Base64编码的 由于输入salt实际上是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选项,并且您有足够的时间来重构您的应用程序 明智地使用它,不要尝试复制已弃用的功能。你应该在相反的方向工作 - 询问如何在不破坏你的申请的情况下将两者分开。