如何用河豚散列长密码(> 72个字符)
Posted
技术标签:
【中文标题】如何用河豚散列长密码(> 72个字符)【英文标题】:How to hash long passwords (>72 characters) with blowfish 【发布时间】:2013-05-11 18:28:54 【问题描述】:上周我阅读了很多关于密码散列的文章,而 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 aka bcrypt 来存储他们的密码 (https://shouldichangemypassword.com/twitter-hacked.php),猜猜看:将您的 twitter 密码更改为超过 72 个字符的长密码,您只需输入前 72 个字符即可登录您的帐户。
河豚和胡椒
关于“peppering”密码有很多不同的看法。有人说这是不必要的,因为您必须假设秘密胡椒串也是已知/已发布的,因此它不会增强散列。我有一个单独的数据库服务器,所以很有可能只有数据库被泄露,而不是不断的辣椒。
在这种情况下(胡椒没有泄露),您根据字典进行攻击会更加困难(如果这不正确,请纠正我)。如果你的胡椒串也被泄露了:还不错——你还有盐,它的保护就像没有胡椒的哈希一样好。
所以我认为输入密码至少是不错的选择。
建议
我建议为超过 72 个字符(和胡椒)的密码获取 Blowfish 哈希:
<?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));
?>
这是基于this question:password_verify
返回false
。
问题
什么是更安全的方法?首先获取 SHA-256 哈希(返回 64 个字符)还是只考虑密码的前 72 个字符?
优点
用户无法通过仅输入前 72 个字符来登录 您可以在不超过字符限制的情况下添加辣椒 hash_hmac 的输出可能比密码本身具有更多的熵 密码由两个不同的函数散列缺点
仅使用 64 个字符来构建河豚哈希编辑 1: 这个问题仅涉及河豚/bcrypt 的 PHP 集成。感谢cmets!
【问题讨论】:
Blowfish 并不是唯一一种截断密码的方法,它误导人们认为它比实际更安全。这是interesting history of the 8-character limit. 72 个字符的截断是 Blowfish 算法的基础,还是只是 PHP 实现? IIRC Blowfish 也用于(至少一些)'nixes 来加密用户密码。 问题在于 Bcrypt,而不是 Blowfish。我可以单独使用 Python 和 Bcrypt 重现这个问题。 @Blender:感谢您的评论和您的工作。我在 php 中找不到针对河豚和 bcrypt 的不同函数,尽管它们是相同的。但是在php中对我来说没有什么不同吗?我更喜欢使用标准的php函数。 另见 Openwall 的PHP password hashing framework (PHPass)。它的便携性和强化了针对用户密码的一些常见攻击。编写框架 (SolarDesigner) 的人与编写 John The Ripper 并在 Password Hashing Competition 担任评委的人是同一个人。所以他对密码攻击略知一二。 【参考方案1】:这里的问题基本上是熵问题。所以让我们开始看那里:
每个字符的熵
每个字节的熵位数为:
十六进制字符 位数:4 值:16 72 个字符的熵:288 位 字母数字 位数:6 值:62 72 个字符的熵:432 位 “常用”符号 位:6.5 值:94 72 个字符的熵:468 位 完整字节 位数:8 值:255 72 个字符的熵:576 位所以,我们如何行动取决于我们期望的角色类型。
第一个问题
您的代码的第一个问题是您的 "pepper" 哈希步骤正在输出十六进制字符(因为未设置 hash_hmac()
的第四个参数)。
因此,通过散列你的胡椒,你有效地将密码可用的最大熵减少了 2 倍(从 576 位到 288 位可能位)。
第二个问题
然而,sha256
首先只提供256
位熵。因此,您有效地将可能的 576 位减少到 256 位。你的散列步骤*立即*,根据定义丢失
至少密码中可能熵的 50%。
您可以通过切换到 SHA512
来部分解决此问题,您只需将可用熵减少约 12%。但这仍然是一个不小的差异。这 12% 将排列的数量减少了1.8e19
的因子。这是一个很大的数字......这就是它减少它的因素......
根本问题
根本问题是超过 72 个字符的密码分为三种类型。这种风格系统对他们的影响会非常不同:
注意:从这里开始,我假设我们正在与使用 SHA512
和原始输出(不是十六进制)的辣椒系统进行比较。
高熵随机密码
这些是您使用密码生成器的用户,这些生成器会生成多少大密码密钥。它们是随机的(生成的,不是人为选择的),并且每个字符具有高熵。这些类型使用高字节(字符 > 127)和一些控制字符。
对于该组,您的散列函数将显着将其可用熵减少为bcrypt
。
让我再说一遍。对于使用高熵、长密码的用户,您的解决方案显着会显着降低密码强度。 (72 个字符的密码丢失 62 位熵,长密码丢失更多)
中等熵随机密码
该组使用的密码包含常见符号,但没有高字节或控制字符。这些是您可以输入的密码。
对于这个组,您将稍微解锁更多的熵(不是创建它,而是让更多的熵适合 bcrypt 密码)。当我说轻微的时候,我的意思是轻微的。当您最大化 SHA512 拥有的 512 位时,就会出现收支平衡。因此,峰值为 78 个字符。
让我再说一遍。对于此类密码,在熵用完之前,您只能再存储 6 个字符。
低熵非随机密码
这是使用可能不是随机生成的字母数字字符的组。像圣经引用之类的东西。这些短语每个字符大约有 2.3 位熵。
对于这个组,您可以通过散列显着解锁更多熵(不是创建它,但允许更多信息适合 bcrypt 密码输入)。在熵用完之前,盈亏平衡点大约是 223 个字符。
让我们再说一遍。对于此类密码,预散列无疑会显着提高安全性。
回到现实世界
这类熵计算在现实世界中并不重要。重要的是猜测熵。这就是直接影响攻击者可以做的事情。这就是你想要最大化的。
虽然关于猜测熵的研究很少,但我想指出一些要点。
连续随机猜出 72 个正确字符的机会非常低。你更有可能赢得 21 次强力球彩票,而不是发生这种碰撞......这就是我们所说的数字。
但我们可能不会在统计上偶然发现它。在短语的情况下,前 72 个字符相同的机会比随机密码要高得多。但它仍然很低(根据每个字符 2.3 位,您更有可能赢得 5 次强力球彩票)。
实际
实际上,这并不重要。有人猜对前 72 个字符的机会,而后者产生显着差异的机会非常低,因此不值得担心。为什么?
好吧,假设您正在使用一个短语。如果该人能够正确理解前 72 个字符,那么他们要么是真的幸运(不太可能),要么是常用短语。如果这是一个常见的短语,唯一的变量是要多长时间。
让我们举个例子。让我们从圣经中引用一段话(只是因为它是长文本的常见来源,而不是出于任何其他原因):
不要贪图邻居的房子。不可贪恋邻舍的妻子、仆婢、牛驴,以及邻舍的一切。
这是 180 个字符。第 73 个字符是第二个 neighbor's
中的 g
。如果您猜得这么多,您可能不会停在nei
,而是继续阅读其余部分(因为这就是密码可能使用的方式)。因此,您的“哈希”并没有增加太多。
顺便说一句:我绝对不提倡使用圣经引用。事实上,恰恰相反。
结论
首先通过散列,你不会真正帮助那些使用长密码的人。有些团体你绝对可以提供帮助。有些你肯定会受伤。
但归根结底,这些都不是太重要。我们正在处理的数字只是 WAY 太高了。熵的差异不会太大。
您最好保留 bcrypt。你更有可能搞砸散列(从字面上看,你已经这样做了,而且你不是第一个或最后一个犯这个错误的人)而不是你试图阻止的攻击将会发生。
专注于保护网站的其余部分。并在注册时的密码框中添加密码熵计,以指示密码强度(并指示密码是否过长,用户可能希望更改它)...
这至少是我的 0.02 美元(或者可能超过 0.02 美元)...
就使用“秘密”辣椒而言:
实际上没有研究将一个哈希函数输入 bcrypt。因此,充其量还不清楚将“胡椒”哈希输入 bcrypt 是否会导致未知漏洞(我们知道,hash1(hash2($value))
可能会暴露围绕抗碰撞和原像攻击的重大漏洞)。
考虑到您已经在考虑存储密钥(“胡椒”),为什么不以经过充分研究和理解的方式使用它呢?为什么不在存储之前加密哈希?
基本上,在您对密码进行哈希处理后,将整个哈希输出输入到强大的加密算法中。然后存储加密结果。
现在,SQL 注入攻击不会泄露任何有用的信息,因为它们没有密钥。如果密钥泄露,攻击者的处境并不比使用普通哈希(这是可证明的,辣椒“预哈希”无法提供)更好。
注意:如果您选择这样做,请使用库。对于 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);
而且它是有益的,因为您以易于理解和研究的方式使用所有算法(至少相对而言)。记住:
Bruce Schneier任何人,从最无知的业余爱好者到最优秀的密码学家,都可以创建自己无法破解的算法。
【讨论】:
非常非常感谢您的详细回答。这对我很有帮助! 我对这个答案的赞美。虽然有一点点挑剔,但绝大多数用户使用非常弱的密码、字典中包含的单词和派生词来破解密码,辣椒可以保护他们不受熵问题的影响。为避免丢失熵,您可以将密码和胡椒连接起来。但是,您关于加密哈希值的建议可能是添加服务器端机密的最佳解决方案。 @martinstoeckli:我对 pepper 概念的问题不在于它的价值。就密码算法而言,“胡椒”的应用进入了未知领域。这不是一件好事。相反,我认为密码原语应该以它们被设计为一起使用的方式组合。基本上,胡椒的核心概念在我耳边听起来就像一些对密码学一无所知的人所说的“更多的哈希更好!我们有盐,胡椒也很好!”。我宁愿有一个更简单、更多测试和更直接的实现 @ircmaxell - 是的,我知道你的观点并且我同意,只要哈希值之后会被加密。如果你不采取这个额外的步骤,字典攻击只会暴露太多的弱密码,即使是一个好的哈希算法。 @martinstoeckli:我不同意。存储秘密不是一件小事。相反,如果您以较高的成本使用 bcrypt(今天为 12 个),那么除了最弱的密码类别之外,所有密码都是安全的(字典密码和普通密码都是弱密码)。所以我宁愿建议人们专注于用strength meters 教育用户并让他们首先使用更好的密码......【参考方案2】:Bcrypt 使用基于昂贵的 Blowfish 密钥设置算法的算法。
bcrypt 推荐的 56 字节密码限制(包括空终止字节)与 Blowfish 密钥的 448 位限制有关。超出该限制的任何字节都不会完全混合到生成的哈希中。因此,当您考虑这些字节对生成的哈希值的实际影响时,bcrypt 密码的 72 字节绝对限制就不那么重要了。
如果您认为您的用户通常会选择长度超过 55 个字节的密码,请记住,您始终可以增加密码拉伸次数,以在密码表泄露的情况下提高安全性(尽管这必须进行很多比较添加额外的字符)。如果用户的访问权限非常重要,以至于用户通常需要很长的密码,那么密码的有效期也应该很短,比如 2 周。这意味着当黑客投入资源来克服测试每个试用密码以查看它是否会产生匹配的哈希值时,密码保持有效的可能性要小得多。
当然,在密码表没有被破坏的情况下,我们应该只允许黑客最多尝试十次猜测用户的 55 字节密码,然后才能锁定用户的帐户;)
如果您决定对长度超过 55 个字节的密码进行预散列,那么您应该使用 SHA-384,因为它具有最大的输出而不会超出限制。
【讨论】:
“密码有效期也应该很短,比如2周”的“密码太长了”,真的,那为什么还要保存密码,每次都使用密码重置。说真的,这是错误的解决方案,使用令牌进行两因素身份验证。 谢谢@zaph。你能给我举个例子吗?听起来很有趣。 [DRAFT NIST 特别出版物 800-63B 数字认证指南](pages.nist.gov/800-63-3/sp800-63b.html),5.1.1.2。记忆秘密验证者:验证者不应要求任意更改记忆秘密(例如,定期)。另请参阅 Jim Fenton 的 Toward Better Password Requirements。 问题是,用户需要更改密码的次数越多,密码选择就越糟糕,从而降低了安全性。用户可以记住的好密码数量有限,而且它们用完了,要么选择了非常糟糕的密码,要么将它们写在贴在键盘底部的便利贴上,等等。【参考方案3】:Peppering 密码肯定是一件好事,但让我们看看为什么。
首先我们应该回答这个问题,辣椒究竟有没有帮助。辣椒只保护密码,只要它保持秘密,所以如果攻击者可以访问服务器本身,它是没有用的。一个更简单的攻击是 SQL 注入,它允许对数据库进行读取访问(对我们的哈希值),我准备了一个 demo of SQL-injection 来展示它是多么容易(单击下一个箭头以获得准备好的输入) .
那么胡椒实际上有什么帮助呢?只要辣椒保持秘密,它就可以保护弱密码免受字典攻击。然后密码1234
将变成1234-p*deDIUZeRweretWy+.O
。此密码不仅长得多,而且还包含特殊字符,并且永远不会成为任何字典的一部分。
现在我们可以估计我们的用户会使用什么密码,可能更多的用户会输入弱密码,因为有些用户的密码在 64-72 个字符之间(实际上这种情况很少见)。
另一点是暴力破解的范围。 sha256 哈希函数将返回 256 位输出或 1.2E77 组合,这对于暴力破解来说太过分了,即使对于 GPU 也是如此(如果我计算正确,这将需要大约 2E61 years 在 2013 年的 GPU 上)。所以我们在使用胡椒粉时并没有真正的劣势。因为哈希值不是系统的,所以你不能用常见的模式加速暴力破解。
附:据我所知,72 个字符的限制是特定于 BCrypt 本身的算法的。我找到的最佳答案是this。
P.P.S 我认为您的示例有缺陷,您无法生成具有完整密码长度的散列,并使用截断的散列进行验证。您可能打算以相同的方式应用辣椒来生成哈希和验证哈希。
【讨论】:
关于你的 P.P.S,我只能说:是的,他可以用非截断密码的哈希验证截断密码,仍然得到true
。这就是这个问题的全部内容。自己看看:viper-7.com/RLKFnB
@Panique - 问题不是 BCrypt 哈希的计算,而是之前的 HMAC。为了生成 SHA 哈希,OP 使用全长密码并将结果用作 BCrypt 的输入。为了验证,他在计算 SHA 哈希之前截断了密码,然后使用这个完全不同的结果作为 BCrypt 的输入。 HMAC 接受任何长度的输入。以上是关于如何用河豚散列长密码(> 72个字符)的主要内容,如果未能解决你的问题,请参考以下文章
BCryptPasswordEncoder 密码长度是不是限制超过 72 个字符?