bcrypt 是否有最大密码长度?

信息安全 密码 bcrypt Python
2021-08-08 01:49:20

我今天在玩 bcrypt 并注意到一些事情:

hashpw('testtdsdddddddddddddddddddddddddddddddddddddddddddddddsddddddddddddddddd', salt)
Output: '$2a$15$jQYbLa5m0PIo7eZ6MGCzr.BC17WEAHyTHiwv8oLvyYcg3guP5Zc1y'

hashpw('testtdsdddddddddddddddddddddddddddddddddddddddddddddddsdddddddddddddddddd', salt)
Output: '$2a$15$jQYbLa5m0PIo7eZ6MGCzr.BC17WEAHyTHiwv8oLvyYcg3guP5Zc1y'

bcrypt 是否有最大密码长度?

3个回答

是的,bcrypt 有一个最大密码长度。原始文章包含以下内容:

key 参数是一个秘密加密密钥,它可以是用户选择的最多 56 个字节的密码(当密钥是 ASCII 字符串时,包括一个终止的零字节)。

因此可以推断出最大输入密码长度为 55 个字符(不包括终止的零)。ASCII 字符,请注意:一个通用的 Unicode 字符,当以 UTF-8 编码时,最多可以使用四个字节;字形的视觉概念可能由无限数量的 Unicode 字符组成。如果您将密码限制为纯 ASCII,您将省去很多麻烦。

但是,对于实际限制存在相当多的混淆。有人认为“56 字节”的限制包括 4 字节的盐,导致下限为 51 个字符。其他人指出,该算法在内部将事物管理为 18 个 32 位字,总共 72 个字节,因此您可以使用 71 个字符(如果您不管理以零结尾的字符串,甚至可以使用 72 个字符)。

实际的实现会有一个限制,这取决于实现者在上述所有内容中所相信和执行的内容。所有体面的实现都将允许您至少 50 个字符。除此之外,不保证支持。如果您需要支持超过 50 个字符的密码,您可以添加一个初步的散列步骤,如本问题中所述(但是,当然,这意味着您不再计算“the” bcrypt,而是一个本地变体,因此互操作性血本无归)。

编辑:有人向我指出,虽然从密码学家的角度来看,这篇文章是最终的参考,但这不一定是设计师的想法。“原始”实现最多可以处理 72 个字节。根据您对形式主义的立场,您可能会声称实现是正确的,而文章是错误的。无论如何,这就是我的建议仍然有效的当前状态:如果你保持在 50 个字符以下,那么你在任何地方都会很好。(当然,如果算法一开始就没有长度限制会更好。)

tl;dr:BCrypt 限制为 72 个字节,而不是 56 个。


背景

BCrypt 限制为 72 个字节。原始论文还提到了空终止符的使用。这意味着您通常会限于:

  • 71 个字符 + 1 字节空终止符

但是 BCrypt 2a 修订版指定使用 UTF-8 编码(而原始白皮书指的是 ASCII)。使用 UTF-8 时,一个字符不代表一个字节,例如:

  • Noël 是四个字符,但五个字节 ( N o e ¨ l)
  • 💩 是一个字符,但四个字节 ( F0 9F 92 A9)
  • M̡̢̛̖̗̘̙̜̝̞̟̠̀́̂̃̄̅̆̇̉̊̋̌̍̎̏̐̑̒̓̔̕̚是一个字符,但74字节(包括空终止符)

因此,这会影响您允许使用多少个“字符” 。

那么 55 或 56 是从哪里来的呢?

原始白皮书提到最大密钥长度为 56 个字节:

最后,key 参数是一个秘密加密密钥,它可以是用户选择的最多 56 个字节的密码(当密钥是 ASCII 字符串时,包括一个终止的零字节)。

这是基于 Blowfish 建议的最大密钥大小为448 位的误解。(448 / 8 = 56 字节)。bcrypt 源自的 Blowfish 加密算法的最大密钥大小为 448 位。来自 Bruce Schneier 1993 年的原始论文Description of a New Variable-Length Key, 64-Bit Block Cipher (Blowfish)

块大小为 64 位,密钥可以是最多 448 位的任意长度。

另一方面,bcrypt 算法可以(并且确实)支持最多 72 个字节的密钥,例如:

  • 71× 8-bit character+ 1× 8-bit null terminator

72 字节的限制来自 Blowfish P-Box 的大小,即 18 个 DWORD(18×4 字节 = 72 字节)。来自原始的 bcrypt 白皮书:

Blowfish 是一个 64 位分组密码,结构为 16 轮 Feistel 网络 [14]。它使用从加密密钥派生的 18 个 32 位子密钥P1、...、P18。子键统称为P-Array

规范的 OpenBSD 实现将截断任何超过 72 字节的密钥。

这意味着如果您的 UTF8 字符串超过 72 个字节,它将被截断为 72 个字节。

警告

  • 此截断将删除空终止符
  • 这种截断甚至会发生在字符中间(对于多码点字符)

例如,如果您的密码以以下结尾:

“……订书机💩”

BCrypt 的 UTF-8 编码将是:

    ══╤══╤═══╤═══╤═══╤═══╤═══╤═════╤═════╤═════╗        
... 63│64│ 65│ 66│ 67│ 68│ 69│ 70  │ 71  │ 72  ║ 73   74
    s │ t│ a │ p │ l │ e │ r │ 0xF0│ 0x9F│ 0x92║ 0xA9 \0
    ══╧══╧═══╧═══╧═══╧═══╧═══╧═════╧═════╧═════╝
                                               |
                                            cutoff

这意味着在规范的 OpenBSD 实现中,字节在字符中间被截断(即使它给您留下无效的 utf-8 字节序列):

    ══╤══╤═══╤═══╤═══╤═══╤═══╤═════╤═════╤═════╗
... 63│64│ 65│ 66│ 67│ 68│ 69│ 70  │ 71  │ 72  ║
    s │ t│ a │ p │ l │ e │ r │ 0xF0│ 0x9F│ 0x92║
    ══╧══╧═══╧═══╧═══╧═══╧═══╧═════╧═════╧═════╝

摆脱最大长度

近年来,人们认为密码散列算法不应该有任何最大限​​制是一个好主意。但是允许客户端使用无限制密码存在问题:

  • 它通过提交数 GB 密码的人引入了拒绝服务攻击。

这就是为什么现在使用 SHA2-256 之类的东西对用户密码进行预散列变得很普遍的原因。生成的 base-64 编码字符串,例如:

n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=

只会是 44 个 ASCII 字符(45 个带有空终止符)。

这是Dropbox采用的方法,包含在bcrypt.net中:

BCrypt.EnhancedHashPassword("correct battery horse staple Noël 💩 M̡̢̛̖̗̘̙̜̝̞̟̠̀́̂̃̄̅̆̇̉̊̋̌̍̎̏̐̑̒̓̔̕̚");

这意味着您昂贵的散列算法不会导致您拒绝服务。

当心未加盐的预散列(又名密码脱壳)

在过去的几年里,一个…… “弱点” ……提出了预散列密码:

hash = bcrypt(sha256("Tr0ub4dor&3"));

问题是你有效地将你的代码变成了这样的东西:

hash = bcrypt("SEhuFRToQjRv9AWx5F9EBZroJhnyMG+Z0JQNyzhukfc=");

一开始你可能看不到问题。为了暴力破解最终的 bcrypt 哈希,他们仍然必须知道您的密码:

  • $2a$15$0l9xFKCmFa4rK2jp.otOKO6c2DQToijZ2IB5kvjA3hfqpT.XLK49S
    • SEhuFRToQjRv9AWx5F9EBZroJhnyMG+Z0JQNyzhukfc=
      • Tr0ub4dor&3

问题是攻击者不会尝试所有可能的字母、数字和符号组合。攻击者将使用字典攻击和以前的马裤密码。因此,他们不会尝试所有可能的密码,而是专注于尝试以前被破坏的密码。例如:

  • 猎人2
  • 正确的马电池订书钉
  • 密码1

现在想象他们开始包含一个不包含原始密码的后膛,而是用户密码的 SHA-256:

  • f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7
  • c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a
  • 0b14d501a594442a01c6859541bcb3e8164d183d32937b851835442f69d5c94e

事实证明,他们得到了匹配:

  • 48486e1514e842346ff405b1e45f44059ae82619f2306f99d0940dcb386e91f7

他们不知道密码是什么,但知道密码是用 SHA-256 进行散列的——一种非常快的散列算法。然后,他们现在将花费所有精力尝试蛮力:

  • 48486e1514e842346ff405b1e45f44059ae82619f2306f99d0940dcb386e91f7

回到

  • “Tr0ub4dor&3”

这不是弱点——而是捷径。就像所有字典攻击一样,都是捷径。

与其他一切一样,答案是使用加盐的prehash,例如:

hash = bcryt(hmac_sha256(salt, password));

并使用您为 bcrypt 生成的相同盐制作您预散列的盐。

是的,BCrypt 的上限为 72 个字符。这是 Blowfish 密码本身的限制。解决它的一种方法是先使用 SHA-256,然后使用 BCrypt 结果。在你的情况下,它会像

hashpw(sha256('pass'), salt)