盐(Salt)的基本原则:

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

注意:有些系统用一个每个用户都不同的字段,uid、手机号、或者别的什么,来作为盐加密密码。这不是一个好主意,这几乎违背了上面全部三条盐的生成规则。
在实际项目中,盐不一定要加在最前面或最后面,也可以插在中间,或者分开插入,还可以使用倒序,等等,进行灵活调整

随机盐生成

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

  1. import java.security.SecureRandom;
  2. public class MethodTest{
  3. @Test
  4. public void toRText() {
  5. byte[] values = new byte[16];
  6. System.out.println(Arrays.toString(values));
  7. SecureRandom random = new SecureRandom();
  8. random.nextBytes(values);
  9. System.out.println(Arrays.toString(values));
  10. // System.out.println(Base64.toBase64String(values));
  11. //需要导入cn.hutool依赖
  12. System.out.println(HexUtil.encodeHexStr(values));
  13. }
  14. }

加盐方式

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

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

最早的MD5或SHA-1方式:

  1. md5(md5(password) + salt)

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

  1. sha512(sha512(password) + salt)

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

  1. bcrypt(sha512(password), salt)
  2. //或者
  3. 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生成的密文
8bd62b65c9c1330be442d68f446a8e7e.jpg
说明:

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

示例:

  1. @Test
  2. public void BCryptTest() {
  3. //{加密
  4. //原文
  5. String password = "123456";
  6. //BCrypt.hashpw(加密原文, BCrypt.gensalt( cost(加密强度)默认为10,推荐设为12 ));
  7. String hashPw = BCrypt.hashpw(password, BCrypt.gensalt());
  8. // String hashPw = BCrypt.hashpw(password, BCrypt.gensalt(12));
  9. System.out.println(hashPw);
  10. //{验证
  11. //原文
  12. String newPassword = "123456";
  13. // String hashNPw = BCrypt.hashpw(newPassword, BCrypt.gensalt(12));
  14. //验证
  15. boolean checkpw = BCrypt.checkpw(newPassword, hashPw);
  16. if (checkpw) {
  17. System.out.println("验证成功");
  18. } else {
  19. System.out.println("验证失败");
  20. }
  21. }

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

  1. <!--Bcrypt包依赖1 选择任一即可-->
  2. <dependency>
  3. <groupId>cn.hutool</groupId>
  4. <artifactId>hutool-all</artifactId>
  5. <version>4.6.10</version>
  6. <scope>compile</scope>
  7. </dependency>
  8. <!--Bcrypt包依赖2 选择任一即可
  9. <dependency>
  10. <groupId>org.mindrot</groupId>
  11. <artifactId>jbcrypt</artifactId>
  12. <version>0.4</version>
  13. </dependency>
  14. -->
  15. <!--Bcrypt包依赖3 选择任一即可
  16. <dependency>
  17. <groupId>org.springframework.security</groupId>
  18. <artifactId>spring-security-web</artifactId>
  19. <version>5.5.1</version>
  20. </dependency>
  21. -->

BCrypt官网

后记

大公司是如何使用BCrypt配合加密使用的呢?分享一篇Dropbox公司发布的博文:
How Dropbox securely stores your passwords
总结一下:

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