加密哈希函数 BCrypt

bcrypt 是由 Niels Provos 和 David Mazières 设计的密码散列函数,基于 Blowfish 密码,于 1999 年在 USENIX 上提出。 除了加入盐来防止彩虹表攻击外,bcrypt 还是一种自适应函数:随着时间的推移,计算机硬件的计算能力会越来越高,它可以通过增加迭代计数使其计算过程变慢,以此抵消硬件性能的提升,使其仍然可以抵抗暴力搜索破解攻击。

bcrypt 函数是 OpenBSD的默认密码哈希算法,也是某些 Linux 发行版(例如 SUSE Linux)的默认算法,在绝大部分流行的编程语言中,都有具体实现。

背景

Blowfish算法 在分组密码中因其昂贵的密钥设置阶段而引人注目。它首先使用标准状态的子密钥,然后使用该状态的一部分密钥执行块加密,并使用该加密的结果(更准确地说是散列)来替换一些子密钥。然后它使用这种修改后的状态来加密密钥的另一部分,并使用结果替换更多的子密钥。它以这种方式进行,使用逐渐修改的状态来散列密钥并替换状态位,直到设置所有子密钥为止。

Provos和Mazières充分利用了这一点并更进一步。他们为Blowfish开发了一种新的密钥设置算法,并将由此产生的密码称为“Eksblowfish”(意为“昂贵的密钥调度Blowfish”)。密钥设置从修改后的标准Blowfish密钥设置开始,利用salt(盐值)和密码来设置所有子密钥。之后进行多轮迭代,在每一轮中交替使用salt和密码作为密钥应用标准的Blowfish密钥编排算法,每一轮都以上一轮的子密钥状态为初始状态。理论上,这个方法并不比标准的Blowfish密钥调度更强,但是重设密钥的轮数可以自由配置。因此,该过程可以被设置为任意缓慢,有助于阻止对哈希值或salt的暴力破解攻击。

描述

bcrypt函数的输入是密码字符串(最高72字节)、数字表示的成本和16字节(128位)的盐值。盐值通常是随机值。bcrypt函数使用这些输入来计算24字节(192位)哈希值。bcrypt函数的最终输出是以下形式的字符串:

1
$2<a/b/x/y>$[cost]$[22 character salt][31 character hash]

例如,输入密码 abc123xyz、成本 12和随机盐值,bcrypt的输出字符串为:

1
2
3
$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
\__/\/ \____________________/\_____________________________/
Alg Cost Salt Hash

解释:

$2a$ :哈希算法标识符(bcrypt)

12 :输入成本(2 12 即 4096 轮)

R9h/cIPz0gi.URNNX3kh2O :输入盐的 Base-64 编码

PST9/PgBkqquzi.Ss7KIUgO2t0jWMUW :计算出的 24 字节哈希值的前 23 字节的 Base-64 编码

bcrypt 中的 base-64 编码使用表 ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,这与 RFC 4648 Base64 编码不同。

版本历史

$2$ (1999)

最初的 bcrypt 规范定义了 $2$ 前缀。这遵循在 OpenBSD 密码文件中存储密码时使用的模块化加密格式格式

  • $1$ :基于 MD5 的 crypt (‘md5crypt’)
  • $2$ :基于 Blowfish 的 crypt(’bcrypt’)
  • $sha1$ :基于 SHA-1 的 crypt(’sha1crypt’)
  • $5$ :基于 SHA-256 的加密(’sha256crypt’)
  • $6$ :基于 SHA-512 的 crypt(’sha512crypt’)

$2a$

最初的规范中没有定义如何处理非ASCII字符,也没有定义如何处理 null 终止符。$2a$ 规范经过修订,指定在对字符串进行哈希处理时:

  • 字符串必须是 UTF-8 编码
  • 必须包含 null 终止符

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

在2011年6月,人们在crypt_blowfish(bcrypt算法的一个PHP实现)中发现了一个bug。这个bug导致对第8位(bit)被置为1的字符处理不当。为了解决这个问题,有人建议系统管理员更新现有的密码数据库,将”2a”替换为”2x”,以此来标记那些有问题的哈希值(需要使用旧的有缺陷的算法进行处理)。同时他们还建议,让crypt_blowfish对于用修复后的算法生成的哈希值,输出 $2y$

除了crypt_blowfish之外,包括Canonical和OpenBSD在内的其他机构都没有采用2x/2y这种版本标记方式。这种版本标记更改仅限于crypt_blowfish。

$2b$ (2014 年 2月)

人们在OpenBSD对于bcrypt的实现中发现了一个bug。这个bug使用了一个无符号的8位值来保存密码的长度。对于超过255个字节的密码,密码不会被截断到72个字节,而是会被截断到72或者长度对256取模后的值(两者中更小的那个)。例如,一个260字节的密码会被截断到4个字节,而不是72个字节。

bcrypt最开始是为OpenBSD开发的。当他们的库中出现bug时,他们决定增加版本号。

Java中使用

依赖

pom.xml

1
2
3
4
5
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>

主程序

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
String password = "JeremyTsai"; // 原始密码
// 使用 BCrypt 加密密码
String h1 = BCrypt.hashpw(password, BCrypt.gensalt());
String h2 = BCrypt.hashpw(password, BCrypt.gensalt());
String h3 = BCrypt.hashpw(password, BCrypt.gensalt());
System.out.println("加密后的密码:" + h1);
System.out.println("加密后的密码:" + h2);
System.out.println("加密后的密码:" + h3);
System.out.println("验证密码1: " + BCrypt.checkpw(password, h1));
System.out.println("验证密码2: " + BCrypt.checkpw(password, h2));
System.out.println("验证密码3: " + BCrypt.checkpw(password, h3));
}

输出如下:

1
2
3
4
5
6
加密后的密码:$2a$10$WbhwIb1/dnE/nDkLeEco3uH0ZlPYwaEC3sGSU68K5aQRj7POA9dsS
加密后的密码:$2a$10$Yo35pvrqN0hXQFw9Rsc9ler3vmHpvoE2XtHjEQS0NDvF6gaKnB9LO
加密后的密码:$2a$10$tp056MRqL/Bq6lF5fTfTNONgf4mj7FoRkVIbjgQ.prI2/HrfHd7/y
验证密码1: true
验证密码2: true
验证密码3: true

说明

生成随机盐:BCrypt.gensalt()

生成密码哈希:BCrypt.hashpw(password, salt)

验证密码:BCrypt.checkpw(password, passwordHash)

参考引用