有人能解释一下BCrypt如何验证哈希吗?

39

我正在使用C#和BCrypt.Net来哈希我的密码。

例如:

string salt = BCrypt.Net.BCrypt.GenerateSalt(6);
var hashedPassword = BCrypt.Net.BCrypt.HashPassword("password", salt);

//This evaluates to True. How? I'm not telling it the salt anywhere, nor
//is it a member of a BCrypt instance because there IS NO BCRYPT INSTANCE.
Console.WriteLine(BCrypt.Net.BCrypt.Verify("password", hashedPassword));
Console.WriteLine(hashedPassword);

如果BCrypt没有保存盐,它是如何验证哈希后的密码的呢?我唯一的想法是它会在哈希值的末尾添加盐。

这个假设正确吗?

2个回答

115
BCrypt哈希字符串的样子如下:
$2a$10$Ro0CUfOqk6cXEKf3dyaM7OhSCvnwM9s4wIX9JeLapehKK5YdLxKcm
\__/\/ \____________________/\_____________________________/
 |   |        Salt                     Hash
 |  Cost
Version

哪里

  • 2a: 算法标识符(BCrypt,UTF8编码的密码,以null结尾)
  • 10: 成本因子(210 = 1,024轮次)
  • Ro0CUfOqk6cXEKf3dyaM7O: OpenBSD-Base64编码的盐(16字节 ⇒ 22个字符)
  • hSCvnwM9s4wIX9JeLapehKK5YdLxKcm: OpenBSD-Base64编码的哈希值(24字节 ⇒ 31个字符)

Edit: i just noticed these words fit exactly. i had to share:

$2a$10$TwentytwocharactersaltThirtyonecharacterspasswordhash
$==$==$======================-------------------------------

BCrypt创建一个24字节的二进制哈希值,使用16字节的盐。你可以自由地存储二进制哈希值和盐,没有规定必须将其转换为字符串进行base-64编码。
但是BCrypt是由OpenBSD的开发人员创建的。OpenBSD已经为他们的密码文件定义了一种格式: $[哈希算法标识符]$[算法特定数据]
这意味着"bcrypt规范"与OpenBSD密码文件格式密不可分。每当有人创建一个"bcrypt哈希"时,他们总是将其转换为以下ISO-8859-1格式的字符串: $2a$[成本]$[Base64盐][Base64哈希]
几个重要点:
  • 2a 是算法标识符

    • 1:MD5
    • 2:早期的bcrypt,对密码的编码方式存在混淆(已过时)
    • 2a:当前的bcrypt,指定密码为UTF-8编码
  • Cost 是计算哈希时使用的成本因素。"当前"值为10,表示内部密钥设置经过1,024轮迭代

    • 10:210 = 1,024次迭代
    • 11:211 = 2,048次迭代
    • 12:212 = 4,096次迭代
  • OpenBSD密码文件使用的base64算法与其他人使用的Base64编码不同;他们有自己的编码方式:

      常规Base64字母表:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
          BSD Base64字母表:./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
    

    因此,任何bcrypt的实现都不能使用任何内置或标准的base64库


有了这个知识,你现在可以验证一个密码correctbatteryhorsestapler与保存的哈希值进行比对:
$2a$12$mACnM5lzNigHMaf7O1py1O3vlf6.BA8k8x3IoJ.Tq3IB/2e7g61Km

BCrypt变种

关于bcrypt版本存在很多混淆。

$2$

BCrypt是由OpenBSD团队设计的。它被设计用于对密码进行哈希处理,以便存储在OpenBSD密码文件中。哈希处理后的密码会带有一个前缀来标识所使用的算法。BCrypt的前缀是$2$

这与其他算法的前缀形成了对比:

  • $1$: MD5
  • $5$: SHA-256
  • $6$: SHA-512

$2a$

最初的BCrypt规范没有定义如何处理非ASCII字符,或者如何处理空终止符。规范进行了修订,明确指定在对字符串进行哈希处理时:

  • 字符串必须采用UTF-8编码
  • 必须包含空终止符

$2x$, $2y$ (2011年6月)

crypt_blowfish中发现了一个错误,这是BCrypt的PHP实现。它没有正确处理第8位设置的字符。
他们建议系统管理员更新其现有密码数据库,将$2a$替换为$2x$,以表示这些哈希值不好(需要使用旧的破损算法)。他们还提出了让crypt_blowfish发出由修复算法生成的哈希值$2y$的想法。除了OpenBSD的官方版本之外,没有其他人采用2x/2y的想法。此版本标记仅适用于crypt_blowfish

版本 $2x$$2y$ 并不比 $2a$ "更好" 或 "更强大"。它们是 BCrypt 的一个特定错误实现的遗留物。

$2b$ (2014年二月)

在 OpenBSD 的 BCrypt 实现中发现了一个错误。他们使用一种不支持字符串的语言来实现,所以他们通过长度前缀、字符指针和使用 [] 对指针进行索引来伪装字符串操作。不幸的是,他们将字符串的长度存储在一个 unsigned char 中。如果密码超过255个字符,它会溢出并在255处截断。BCrypt 是为 OpenBSD 创建的。当他们的库中出现 bug 时,他们决定升级版本号。这意味着其他人如果想保持与"他们"规范的兼容性,也需要进行版本升级。


2a2x2y2b之间没有任何区别。如果你正确地编写了实现,它们都会输出相同的结果。

  • 如果你从一开始就做对了(将字符串存储为utf8并散列空终止符),那么:22a2x2y2b之间没有任何区别。如果你正确地编写了实现,它们都会输出相同的结果。
  • 版本$2b$并不比$2a$“更好”或“更强”。它是BCrypt的一个特定有缺陷的实现的遗留物。但由于BCrypt属于OpenBSD,他们可以将版本标记更改为任何他们想要的。
  • 版本$2x$$2y$不比其他任何东西更好,甚至也不可取。它们是有缺陷实现的遗留物,应该被彻底遗忘。
唯一需要关心2x和2y的人是那些可能在2011年使用过crypt_blowfish的人。而唯一需要关心2b的人是那些可能在运行OpenBSD的人。
所有其他正确的实现都是相同且正确的。

3
作弊者:TwentyTwoCharacter 和 ThirtyOneCharacters(复数形式)。幸运的是,Salt(盐)以s开头,所以可以说它在这两个单词之间共享。不过这仍然很棒... - TTT
@Ian Boyd 给出了不错的答案。我有一个问题,"cost" 的意思是你想要迭代生成随机值的次数吗?也就是说,如果 "cost" 是 10,那么它会进行 1,024 次迭代直到哈希值被生成吗? - Skizo-ozᴉʞS ツ
1
@Skizo-ozᴉʞS 从技术上讲,它需要2^n次迭代来生成加密的“密钥”。然后使用该密钥对文本“OrpheanBeholderScryDoubt”进行64次加密。但是,您可以将其视为在生成哈希之前必须运行多少次迭代。 - Ian Boyd

26
BCrypt如何在不保存盐的情况下使用哈希验证密码?
显然BCrypt不会这样做。盐必须被保存在某个地方。让我们在维基百科上查找密码加密方案。http://en.wikipedia.org/wiki/Crypt_(Unix)
该函数的输出不仅是哈希值:它还是一个文本字符串,其中包含编码的盐和标识所使用的哈希算法。
或者,关于此主题的您以前的问题的答案包括指向源代码的链接。源代码的相关部分是:
    StringBuilder rs = new StringBuilder();
    rs.Append("$2");
    if (minor >= 'a') {
        rs.Append(minor);
    }
    rs.Append('$');
    if (rounds < 10) {
        rs.Append('0');
    }
    rs.Append(rounds);
    rs.Append('$');
    rs.Append(EncodeBase64(saltBytes, saltBytes.Length));
    rs.Append(EncodeBase64(hashed,(bf_crypt_ciphertext.Length * 4) - 1));
    return rs.ToString();

显然返回的字符串是版本信息,后面跟着使用的轮数,然后是以base64编码的盐,最后是以base64编码的哈希值。


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