了解bcrypt密码哈希中盐的生成和使用

12

我正在处理一个使用FOSUserBundle进行用户认证的现有的Symfony 2.8网站应用程序项目。除了Web前端之外,用户还可以使用不同的智能手机客户端通过REST API连接到Web应用程序。因此,用户需要在直接登录Web应用程序和通过REST API连接时进行身份验证。

直到最新的FOSUserBundle更新之一,bcrypt密码哈希值和使用的salt存储在数据库中。

使用REST API连接时,盐值被传输到客户端以使用相同的盐值本地哈希密码。然后将哈希密码发送回Web应用程序进行身份验证。

我知道发送哈希密码而不是明文并没有增加(很多)额外的安全性,因为只能使用HTTPS进行通信。但是这是客户端工作的方式:他们需要盐来生成哈希密码。我可以在未来更新客户端,但现在这只是他们工作的方式。

问题:FOSUserBundle哈希密码的方式已更改:考虑到手动设置盐可能不够安全,而是让PHP自动生成盐(在PHP 7中甚至不可能手动设置盐),不再支持手动盐。

直接登录Web应用程序时没有问题,但由于REST客户端仍然需要盐,因此此更新会破坏REST连接。

是否有任何方法可以将这两种方法结合起来?让PHP自动创建盐,提取并将该盐发送给客户端?

据我所知,盐与哈希存储在同一字符串中:

enter image description here

然而,仅复制哈希字符串中的21个字符并将其发送到客户端是不起作用的。似乎这些21个字符足以测试/验证密码,但无法重新创建哈希。这是正确的吗?

那么,是否有使用PHPpassword_hash而不设置盐,并同时了解所使用的盐的解决方案?

编辑1:

回答@RiggsFolly的问题:MD5在任何时候都没有使用过。 bcrypt/password_hash将不止两次创建相同的哈希值。如果密码和盐都相同,则会这样做:

$s = 'password';
$salt = 'salt5678901234567890123456789012';

$options['salt'] = $salt;
$h1 = password_hash($s,PASSWORD_BCRYPT,$options);
$h2 = password_hash($s,PASSWORD_BCRYPT,$options);

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

结果:

$2y$10$salt56789012345678901uTWNlUnhu5K/xBrtKYTo7oDy8zMr/csu
$2y$10$salt56789012345678901uTWNlUnhu5K/xBrtKYTo7oDy8zMr/csu

password_hash函数如果不指定盐值,每次调用都会随机生成一个盐值,因此会为相同的密码创建一个新的哈希值。

编辑2:

正如编辑1中所示,使用32个字符的盐值将导致哈希字符串只包含盐值的前21个字符。然而,由于长度太短而无法用于重新创建相同的哈希值。

但是,如果前缀用0填充,似乎就能奏效:

$s = 'password';
$salt        = 'salt5678901234567890123456789012';
$salt_prefix = 'salt5678901234567890100000000000';

$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;

因此,一个解决方案可能是:

  • FOSUserBundle使用password_hash创建哈希值而不需要手动指定盐。
  • 从结果字符串中提取盐并用0填充到32个字符的长度。
  • 将此盐传递给客户端。

有人可以确认这是一个真正的解决方案,而不仅仅是一些巧合吗?


如果我没记错的话,该算法使用一些奇怪的20.5字节来计算哈希值。除非你的客户端使用完全相同的方式来修剪掉最后几位,否则你将得到不同的结果。虽然这只是我的初步想法。 - deceze
哈希创建在Web应用和客户端中完全相同,只要两者使用相同的密码、盐和成本。我可以控制密码和成本,到目前为止我也可以控制盐。问题是,现在盐是未知的。 - Andrei Herford
1
抱歉,我不理解最后一条评论。是的,用于生成哈希值的盐在结果字符串中部分存储。但并非完整的盐,而只有其中的21个字符。这部分似乎足以验证密码与哈希值匹配,但不足以重新生成哈希值/结果字符串。 - Andrei Herford
1
如果你想迁移到这个版本的SF,看起来客户端必须重新构建。 - RiggsFolly
1
但是这个前提都建立在沙子上。PHP手册指出,在password_hash()中使用自己的SALT已被弃用。 - RiggsFolly
显示剩余2条评论
3个回答

0

您好像误解了密码哈希和盐的工作原理。

盐从不发送给客户端。它仅在创建密码时生成(或手动指定)一次。其目的是随机化哈希函数的输出,以便当数据库落入错误的手中时,通过将输出与彩虹表进行比较来获取用户密码是不可能的。

当用户使用密码登录时,密码以未经哈希处理的形式从客户端发送到服务器(但通常通过https)。然后,密码比较函数从存储的哈希+密码中获取盐,将盐附加到用户输入中,计算哈希,然后将其与数据库中的哈希进行比较。

也许您所在的项目有一个糟糕的盐实现。事实上,使用手动盐的原因之一是为了防止这种情况发生。

因此,解决方案可能是:

让FOSUserBundle使用password_hash创建哈希,而无需手动指定盐。

从结果字符串中提取盐,并用0填充到32个字符的长度。

将此盐传递给客户端。

有人可以确认,这是一个真正的解决方案,而不仅仅是巧合吗?

这不是一个好的解决方案。唯一好的方法是确保密码哈希实现正确,这样您就不必多次生成盐。


0

http://us2.php.net/crypt所述,盐值为22个字符而不是21个。

<?php
$key = 'password';
$hash = password_hash($key, PASSWORD_BCRYPT);
$salt = substr($hash, 7, 22);
$rehash = password_hash($key, PASSWORD_BCRYPT, ['salt' => $salt]);
if ($hash == $rehash) {
  echo 'ok', PHP_EOL;
}

在crypt blowfish中,将salt5678901234567890123456789012的最后两个数字改为u只是一些魔法。


-1

让客户端提供任何盐。或者服务器提供一个从未使用过的随机盐。将哈希视为原始密码。重新对已经哈希的密码进行哈希和其他处理。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接