tl;dr:BCrypt 限制为 72 个字节,而不是 56 个。
背景
BCrypt 限制为 72 个字节。原始论文还提到了空终止符的使用。这意味着您通常会限于:
但是 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║
══╧══╧═══╧═══╧═══╧═══╧═══╧═════╧═════╧═════╝
摆脱最大长度
近年来,人们认为密码散列算法不应该有任何最大限制是一个好主意。但是允许客户端使用无限制密码存在问题:
这就是为什么现在使用 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=
问题是攻击者不会尝试所有可能的字母、数字和符号组合。攻击者将使用字典攻击和以前的马裤密码。因此,他们不会尝试所有可能的密码,而是专注于尝试以前被破坏的密码。例如:
现在想象他们开始包含一个不包含原始密码的后膛,而是用户密码的 SHA-256:
- f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7
- c4bbcb1fbec99d65bf59d85c8cb62ee2db963f0fe106f483d9afa73bd4e39a8a
- 0b14d501a594442a01c6859541bcb3e8164d183d32937b851835442f69d5c94e
事实证明,他们得到了匹配:
- 48486e1514e842346ff405b1e45f44059ae82619f2306f99d0940dcb386e91f7
他们不知道密码是什么,但知道密码是用 SHA-256 进行散列的——一种非常快的散列算法。然后,他们现在将花费所有精力尝试蛮力:
- 48486e1514e842346ff405b1e45f44059ae82619f2306f99d0940dcb386e91f7
回到
这不是弱点——而是捷径。就像所有字典攻击一样,都是捷径。
与其他一切一样,答案是使用加盐的prehash,例如:
hash = bcryt(hmac_sha256(salt, password));
并使用您为 bcrypt 生成的相同盐制作您预散列的盐。