盐(Salt)的基本原则:

  1. 使用CSPRNG(Cryptographically Secure Pseudo-Random Number Generator)生成盐(java.security.secureRandom),而不是普通的随机数算法。CSPRNG跟普通的随机数生成算法,比如C语言标准库里面的rand()方法,有很大不同。正如它的名字所揭示,CSPRNG是加密安全的,这也表明了它产生的随机数更加随机,且不可预测。
  2. 盐不能太短。如果盐很短,那意味着密码+盐组成的字符串的长度和取值空间都有限。破解者完全可以为 密码+盐 的所有组合建立彩虹表。
  3. 盐不能重复使用。如果所有用户的密码都使用同一个盐进行加密。那么不管盐有多复杂、多大的长度,破解者都可以很容易的使用这个固定盐重新建立彩虹表,破解你的所有用户的密码。所以应当在每一次需要保存新的密码时,都生成一个新的盐,并跟加密后的hash值保存在一起。

注意:有些人可能会将每个用户都不同的字段(uid、手机号之类的)来作为盐。很明显这是不规范的,几乎违背了上面三条盐的生成规则。
在实际项目中,盐不一定要加在最前面或最后面,也可以插在中间,或者分开插入,还可以使用倒序,等等,进行灵活调整

随机盐生成

示例:
生成一个16位的随机盐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.security.SecureRandom;

public class MethodTest{
@Test
public void toRText() {
byte[] values = new byte[16];

System.out.println(Arrays.toString(values));

SecureRandom random = new SecureRandom();
random.nextBytes(values);

System.out.println(Arrays.toString(values));
// System.out.println(Base64.toBase64String(values));
//需要导入cn.hutool依赖
System.out.println(HexUtil.encodeHexStr(values));
}
}

加盐方式

传统的无加盐的加密方式很容易被彩虹表破解;当然,如果你盐加的不够也是一样的,从数学角度来讲,使用固定盐和没加盐几乎无异。

彩虹表就是穷举密码和对应摘要的一个表.。有了这个表,就可以通过遍历的方式破解密码

最早的MD5或SHA-1方式:

1
md5(md5(password) + salt)

现在大部分的加盐加密都将MD5或SHA-1替换为了更为安全的哈希函数:SHA-256或者SHA-512:

1
sha512(sha512(password) + salt)

上面的加盐方式都需要将盐值另外·储存,而是BCrypt则是通过加密密码得到,这样每个密码的盐值也是不同的:

1
2
3
4
5
bcrypt(sha512(password), salt)

//或者

bcrypt(sha512(password), salt, cost)

使用BCrypt加盐的方式一方面不用另外储存盐值了,另一方面可以大大拖慢破译者的破译速度;
由于BCrypt是采用慢哈希算法,一个明文映射多个密文,所以跟SHA比起来要慢的多(比如加密同一串字符,SHA可能只需要1微妙,而BCrypt可能需要0.1秒);
通过调整 cost 参数,可以调整该函数慢到什么程度。假设让 BCrypt 计算一次需要 0.5 秒,遍历 6 位的简单密码,需要的时间为:((26 * 2 + 10)^6) / 10 秒,约 900 年。
一般来说,SHA加盐的方式就已经很安全了,除非涉及绝密信息,并且可以牺牲一定性能时,才有必要考虑 BCrypt 加密

做了这么多操作主要还是为了下面两点:

  1. 用户明文密码不会被攻击者拿到(网络拦截、彩虹表、暴力破解)
  2. 攻击者无法使用摘要密码登录

BCrypt

前面做了那么多铺垫,现在正式进入正题。

先来看下BCrypt生成的密文
图片
说明:

  • BCrypt: 2a代表BCrypt加密版本号。
  • Rouds: 迭代次方数,10是默认值。可以设置范围为4-31。最终迭代次数为2的Rouds次方。
  • Salt: 22位的盐值(即上述的real_salt)。
  • Hash:明文password和Salt一起hash加密后生成的密文,长度31位。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    @Test
public void BCryptTest() {
//{加密
//原文
String password = "123456";
//BCrypt.hashpw(加密原文, BCrypt.gensalt( cost(加密强度)默认为10,推荐设为12 ));
String hashPw = BCrypt.hashpw(password, BCrypt.gensalt());
// String hashPw = BCrypt.hashpw(password, BCrypt.gensalt(12));
System.out.println(hashPw);

//{验证
//原文
String newPassword = "123456";
// String hashNPw = BCrypt.hashpw(newPassword, BCrypt.gensalt(12));
//验证
boolean checkpw = BCrypt.checkpw(newPassword, hashPw);
if (checkpw) {
System.out.println("验证成功");
} else {
System.out.println("验证失败");
}

}

BCrypt包可在多个依赖中引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--Bcrypt包依赖1 选择任一即可-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.10</version>
<scope>compile</scope>
</dependency>



<!--Bcrypt包依赖2 选择任一即可
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
-->
<!--Bcrypt包依赖3 选择任一即可
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.5.1</version>
</dependency>
-->

后记

BCrypt官网

大公司是如何使用BCrypt配合加密使用的呢?

分享一篇Dropbox公司发布的博文:
How Dropbox securely stores your passwords
总结一下:

  1. 首先使用SHA-512,将用户密码归一化为64字节hash值。
  2. 然后使用BCrypt算法。
  3. 最后使用AES加密。