如何使用blowfish算法哈希长密码(>72个字符)

95

上周我读了很多关于密码哈希和Blowfish算法似乎是目前最好的哈希算法之一,但这不是本问题的主题!

72个字符长度限制

Blowfish只考虑输入密码的前72个字符:

<?php
$password = "Wow. This is a super secret and super, super long password. Let's add some special ch4r4ct3rs a#d everything is fine :)";
$hash = password_hash($password, PASSWORD_BCRYPT);
var_dump($password);

$input = substr($password, 0, 72);
var_dump($input);

var_dump(password_verify($input, $hash));
?>

输出结果为:

string(119) "Wow. This is a super secret and super, super long password. Let's add some special ch4r4ct3rs a#d everything is fine :)"
string(72) "Wow. This is a super secret and super, super long password. Let's add so"
bool(true)

从下面可以看出,只有前72个字符才有意义。Twitter正在使用blowfish(又名bcrypt)来存储他们的密码(https://shouldichangemypassword.com/twitter-hacked.php)。如果您更改您的Twitter密码为一个超过72个字符的长密码,您只需输入前72个字符即可登录到您的帐户。

Blowfish和Pepper

关于“加入pepper”的密码保护方法,有很多不同的观点。有些人认为这是不必要的,因为您必须假设秘密的pepper字符串也已知/公开,所以它并没有增强哈希函数的安全性。我有一台单独的数据库服务器,所以仅泄漏了数据库而未泄漏常量pepper的可能性很大。

在这种情况下(pepper未泄漏),您可以使基于字典的攻击变得更加困难(如果我理解不正确,请纠正我)。如果您的pepper字符串也被泄露:那也没什么大不了的 - 您仍然拥有salt,并且与没有pepper的哈希值一样得到保护。

因此,我认为加入pepper的密码保护至少不是一个坏选择。

建议

我建议为密码生成一个带有pepper的Blowfish哈希值,长度超过72个字符的密码,步骤如下:

<?php
$pepper = "foIwUVmkKGrGucNJMOkxkvcQ79iPNzP5OKlbIdGPCMTjJcDYnR";

// Generate Hash
$password = "Wow. This is a super secret and super, super long password. Let's add some special ch4r4ct3rs a#d everything is fine :)";
$password_peppered = hash_hmac('sha256', $password, $pepper);
$hash = password_hash($password_peppered, PASSWORD_BCRYPT);

// Check
$input = substr($password, 0, 72);
$input_peppered = hash_hmac('sha256', $input, $pepper);

var_dump(password_verify($input_peppered, $hash));
?>

基于这个问题:使用password_verify返回false

问题

哪种方法更安全?先获取一个SHA-256哈希值(返回64个字符)还是仅考虑密码的前72个字符?

优点

  • 用户不能通过只输入前72个字符来登录
  • 您可以添加pepper而不超过字符限制
  • hash_hmac的输出可能比密码本身具有更多的熵
  • 密码由两个不同的函数哈希

缺点

  • 仅使用64个字符构建blowfish哈希


编辑1:此问题仅涉及blowfish / bcrypt的PHP集成。感谢评论!


3
Blowfish并不是唯一一个截断密码的算法,这会让人误以为它比实际上更安全。以下是“8个字符限制”的有趣历史链接:http://security.stackexchange.com/questions/33470/what-technical-reasons-are-there-to-have-low-maximum-password-lengths - DOK
2
72个字符的截断对Blowfish算法是基本的,还是只是PHP实现的?如果我没记错,Blowfish也用于(至少一些)'nixes来加密用户密码。 - Douglas B. Staple
3
问题出在 Bcrypt,而不是 Blowfish。我可以仅使用 Python 和 Bcrypt 复现此问题。 - Blender
@Blender:感谢您的评论和对此的工作。我在php中找不到blowfish和bcrypt的不同函数,尽管它们是相同的。但是在php中对我有什么区别吗?我更喜欢使用标准的php函数。 - Frederik Kammer
1
另外请参阅Openwall的PHP密码哈希框架(PHPass)。它是可移植的,并针对许多常见的用户密码攻击进行了加固。编写该框架的人(SolarDesigner)也是编写John The Ripper并担任密码哈希竞赛的裁判员,所以他对密码攻击有一定的了解。 - jww
显示剩余2条评论
3个回答

145

问题基本上是熵的问题。所以我们从这里开始看:

每个字符的熵

每个字节的熵位数为:

  • 十六进制字符
    • 位数:4
    • 值:16
    • 72个字符的熵:288位
  • 字母数字
    • 位数:6
    • 值:62
    • 72个字符的熵:432位
  • “常见”的符号
    • 位数:6.5
    • 值:94
    • 72个字符的熵:468位
  • 完整的字节
    • 位数:8
    • 值:255
    • 72个字符的熵:576位

因此,我们的操作取决于我们期望的字符类型。

第一个问题

你的代码第一个问题是,“pepper”哈希步骤输出十六进制字符(因为没有设置hash_hmac()的第四个参数)。

因此,通过将pepper加入哈希中,你实际上将密码的最大熵减少了一半(从576个可能的位到288个可能的位)。

第二个问题

然而,sha256仅提供了256位的熵。因此,你实际上将可能的576位减少到256位。你的哈希步骤*立即*,根据定义,至少损失密码中可能熵的50%。

你可以通过切换到SHA512部分解决这个问题,其中你只会将可用的熵减少约12%。 但这仍然是一个相当大的差异。那个12%将排列数量减少1.8e19倍。那是一个很大的数字... 那就是它减少的倍数...

潜在问题

问题的根本在于有三种超过72个字符的密码类型。这种样式系统对它们的影响将非常不同:

注意:从现在开始,我假设我们正在与使用原始输出(而不是十六进制)的SHA512 pepper系统进行比较。

  • 高熵随机密码

    这些是使用密码生成器生成相当于大型密码的用户。它们是随机的(生成的,而不是人为选择的),并且每个字符的熵很高。这些类型使用高字节(字符> 127)和一些控制字符。

对于这个用户组,你的哈希函数将显着减少其可用熵到 bcrypt 中。让我再说一遍。对于使用高熵、长密码的用户,你的解决方案会显著降低他们密码的强度。 (62 位的熵会因为一个 72 个字符的密码而丢失,并且对于更长的密码会有更多的熵丢失) 对于使用包含常见符号但没有高字节或控制字符的密码的中等熵随机密码组,你将略微增加更多熵。当我说略微时,我的意思是略微。打破平衡的点是当您最大化 SHA512 的 512 位时。因此,顶峰位于 78 个字符处。 让我再说一遍。对于这类密码,您只能存储额外的 6 个字符,然后就没有熵了。 对于非随机低熵密码的这个组,你可以通过哈希显著解锁更多熵(不是创建它,而是允许更多的熵适合 bcrypt 密码输入)。打破平衡点大约是在 223 个字符之前。
让我们再说一遍。对于这类密码,预哈希绝对会显著提高安全性。 这些熵计算在现实世界中并不重要。重要的是猜测熵。那直接影响攻击者能做什么。这是你想最大化的。虽然没有太多的研究涉及猜测熵,但有一些我想指出的问题。随机猜测 72 个正确字符的概率极低。您更有可能赢得 Powerball 彩票 21 次,而不是发生碰撞...这就是我们所讨论的数字有多大。但统计上我们可能不会遇到它。对于短语的情况,前 72 个字符相同的机会比随机密码高得多。但是它仍然微不足道地低(基于每个字符的 2.3 位)。
实际上,这并不重要。猜测前72个字符且后面的字符可以显著影响结果的机会如此之低,以至于不值得担心。为什么呢?

假设你正在处理一个短语。如果一个人能够正确地输入前72个字符,他们要么非常幸运(不太可能),要么这是一个常见短语。 如果这是一个常见的短语,唯一的变量就是要把它做多长。

让我们举个例子。让我们引用圣经中的一句话(只是因为它是长文本的常见来源,而不是其他任何原因):

"You shall not covet your neighbor's house. You shall not covet your neighbor's wife, or his manservant or maidservant, his ox or donkey, or anything that belongs to your neighbor." 这是 180 个字符。第73个字符是第二个“邻居”的“g”。如果你猜到了这么多,你可能不会停在“nei”,但会继续看下面的内容(因为密码很可能会被这样使用)。因此,你的"哈希"没有多大作用。

总之,通过先进行哈希来帮助使用长密码的人并没有什么帮助。对一些用户有用,对另一些用户没用。但最终,这一切都不是特别重要的。我们处理的数字实在太高了。熵的差异不会太大。

最好保持bcrypt不变。你更有可能搞砸哈希(事实上,你已经这样做了,而且你不是第一个也不是最后一个犯这个错误的人)而不是预防攻击。集中精力保护网站的其余部分。并在注册时添加密码熵计量器以指示密码强度(如果密码过长,则指示用户可能需要更改密码)...

至少我的意见是如此(或者可能超出了2美分)...

对于使用"秘密"辣椒:

目前尚未有关于将一个哈希函数输入bcrypt的任何研究。因此,"加辣椒"的哈希值是否会导致未知漏洞还不清楚(我们知道执行hash1(hash2($value))会暴露与冲突抵抗和原像攻击有关的重大漏洞)。考虑到您已经考虑存储秘密密钥("辣椒"),为什么不以经过研究和理解的方式使用它呢?为什么不在存储它之前加密哈希?

基本上,在你哈希密码后,将整个哈希输出输入到一个强加密算法中。然后存储加密结果。

现在,SQL注入攻击不会泄露任何有用的内容,因为它们没有密钥。如果密钥泄漏,攻击者也不会比使用普通哈希更有优势(这是可以证明的,而“预哈希”的pepper并不能提供这种保证)。
注意:如果您选择这样做,请使用库。对于PHP,我强烈建议使用Zend Framework 2的Zend\Crypt包。实际上,在当前时间点,这是我唯一推荐的一个。它经过了严格的评审,并为您做出所有决策(这是非常好的事情)...
类似于:
use Zend\Crypt\BlockCipher;

public function createHash($password) {
    $hash = password_hash($password, PASSWORD_BCRYPT, ["cost"=>$this->cost]);

    $blockCipher = BlockCipher::factory('mcrypt', array('algo' => 'aes'));
    $blockCipher->setKey($this->key);
    return $blockCipher->encrypt($hash);
}

public function verifyHash($password, $hash) {
    $blockCipher = BlockCipher::factory('mcrypt', array('algo' => 'aes'));
    $blockCipher->setKey($this->key);
    $hash = $blockCipher->decrypt($hash);

    return password_verify($password, $hash);
}

使用所有的算法是有益的,因为这些算法的使用方式是众所周知且经过充分研究的(至少相对而言)。记住:

无论是最无知的业余爱好者还是最优秀的密码学家,都可以创造出自己无法破解的算法。


1
我对这个答案表示赞赏。不过有一个小问题,大多数用户使用非常弱的密码,这些密码是字典中包含的单词和派生词,黑客可以轻易破解。加入pepper可以保护他们,而不受熵问题的影响。为了避免失去熵值,您可以将密码和pepper连接起来。但是,您关于加密哈希值的建议可能是添加服务器端秘密的最佳解决方案。 - martinstoeckli
2
@martinstoeckli:我对“pepper”的概念的问题不在于它的价值,而是在于“pepper”的应用涉及到密码算法的未知领域。这不是一件好事。相反,我认为加密原语应该以设计时预期的方式组合在一起。基本上,“pepper”的核心概念听起来像是一些对密码学一无所知的人说:“更多的哈希更好!我们有盐,胡椒也很好!”我宁愿选择一个更简单、经过更多测试和更直接的实现方式。 - ircmaxell
@ircmaxell - 是的,我知道你的观点并且同意,只要哈希值之后会被加密。如果你不采取这个额外的步骤,即使使用好的哈希算法,字典攻击也会揭示出太多弱密码。 - martinstoeckli
1
@martinstoeckli:我不同意这个观点。保管秘密并不是一件简单的事情。相反,如果您使用bcrypt并设置一个好的成本(今天为12),除了最弱的密码类别之外,所有密码都是安全的(字典和简单密码是弱密码)。因此,我更愿意建议人们关注教育用户使用强度计,并在第一时间让他们使用更好的密码... - ircmaxell
@ircmaxell - 最弱的密码类别并不是最罕见的那一种。要判断一个密码是否能在真实的字典攻击中被破解,强度计需要有自己的字典。泄露的数据库是一个真正的威胁(SQL注入、丢弃的备份、废弃的服务器等),因此添加服务器端的秘密不是一个学术步骤,即使密钥存储在硬编码的位置也有帮助。你的解决方案通过加密哈希值还有另一个优点,它允许在必要时交换密钥,我喜欢这个想法。 - martinstoeckli
我认为你必须考虑到有人可能需要超过72个字符的情况。如果他们要将内容用作密钥文件(grandma.jpg),但只使用了文件头,该怎么办?如果某人的密码被攻击并且只更改了最后一部分,该怎么办?如果你不想支持长密码,最好抛出一个错误,而不是做出这样的静默假设。 - Daan Bakker

5

添加pepper是一件好事,但让我们看看为什么。

首先,我们应该回答什么时候pepper有用。只要它保持秘密,pepper就只能保护密码,所以如果攻击者可以访问服务器本身,它就没有用处。不过,一种更容易的攻击方式是SQL注入,它允许对数据库进行读取访问(对于我们的哈希值),我准备了一个SQL注入的演示程序(点击下一个箭头获取准备好的输入)来展示它有多容易。

那么pepper到底有什么用途?只要pepper保持秘密,它就可以保护弱密码免受字典攻击。密码1234会变成类似于1234-p*deDIUZeRweretWy+.O的东西。这个密码不仅更长,还包含特殊字符,不可能成为任何字典的一部分。

现在我们可以估计用户会使用什么样的密码,很可能大多数用户会输入弱密码,因为64-72个字符之间的密码实际上非常罕见。

另一个问题是暴力破解的范围。sha256哈希函数将返回256位输出,或1.2E77个组合,这对于暴力破解来说太多了,即使对于GPU也是如此(如果我计算正确的话,在2013年需要约2E61年的时间才能完成)。因此,应用pepper并不会使我们受到真正的劣势。由于哈希值不是系统化的,您不能使用常见模式加速暴力破解。

P.S. 据我所知,72个字符的限制是特定于BCrypt算法本身的。我找到的最佳答案是这个

P.P.S 我认为你的例子有缺陷,你不能使用完整的密码长度生成哈希值,然后使用截断的密码验证它。你可能意味着以同样的方式应用pepper来生成哈希值和验证哈希值。


关于您的P.P.S,我只能说:是的,他可以使用非截断密码的哈希值验证被截断的密码,并仍然得到“true”。这就是这个问题所涉及的。您自己看一下:http://viper-7.com/RLKFnB - Sliq
@Panique - 问题不在于计算BCrypt哈希,而是在HMAC之前。为了生成SHA哈希,OP使用完整长度的密码,并将结果用作BCrypt的输入。对于验证,他在计算SHA哈希之前截断密码,然后使用此完全不同的结果作为BCrypt的输入。HMAC接受任意长度的输入。 - martinstoeckli

2
Bcrypt使用基于昂贵的Blowfish密钥设置算法的算法。对于bcrypt建议的56字节密码限制(包括空终止字节),它与Blowfish密钥的448位限制有关。超过该限制的任何字节都不会完全混合到生成的哈希中。因此,在考虑这些字节对结果哈希的实际影响时,72字节的绝对限制在bcrypt密码方面的作用较小。如果您认为您的用户通常会选择超过55字节的密码长度,请记住可以增加密码拉伸的轮数来提高安全性,以防密码表遭到破坏(尽管与添加额外字符相比,这必须要多得多)。如果用户的访问权限如此重要,以至于用户通常需要非常长的密码,则密码过期时间也应该很短,例如2周。这意味着黑客在投入资源测试每个试验密码以查看其是否产生匹配哈希的工作因素方面进行攻击时,密码不太可能保持有效。当然,在密码表没有被破坏的情况下,我们应该只允许黑客最多尝试十次猜测用户的55字节密码,然后锁定用户的帐户;)如果您决定预先哈希长度超过55字节的密码,那么应使用SHA-384,因为它具有最大的输出而不超过限制。

2
"密码到期时间也应该很短,比如两周。对于非常长的密码,真的有必要保存吗?每次都使用密码重置不是更好吗?但这并不是正确的解决方案,建议采用带有令牌的双因素身份验证。" - zaph
谢谢@zaph。你能给我指一个例子吗?听起来很有趣。 - Phil
1
DRAFT NIST Special Publication 800-63B 数字认证指南,5.1.1.2. 记忆秘密验证器:验证器不应强制要求用户在任意时刻更改记忆秘密(例如,定期更改)。另请参阅Jim Fenton的Toward Better Password Requirements - zaph
2
问题在于,用户被要求频繁更改密码的次数越多,密码选择就会变得越糟糕,从而降低安全性。用户只有有限数量的易记密码可用,他们会用完,要么选择非常糟糕的密码,要么将它们写在贴在键盘底部的便笺上等等。 - zaph

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