盐
盐(Salt)的基本原则:
- 使用CSPRNG(Cryptographically Secure Pseudo-Random Number Generator)生成盐(java.security.secureRandom),而不是普通的随机数算法。CSPRNG跟普通的随机数生成算法,比如C语言标准库里面的rand()方法,有很大不同。正如它的名字所揭示,CSPRNG是加密安全的,这意味着用它产生的随机数更加随机,且不可预测。
- 盐不能太短。想想查询表和彩虹表的原理,如果盐很短,那意味着密码+盐组成的字符串的长度和取值空间都有限。破解者完全可以为密码+盐的所有组合建立彩虹表。
- 盐不能重复使用。如果所有用户的密码都使用同一个盐进行加密。那么不管盐有多复杂、多大的长度,破解者都可以很容易的使用这个固定盐重新建立彩虹表,破解你的所有用户的密码。应当在每一次需要保存新的密码时,都生成一个新的盐,并跟加密后的hash值保存在一起。
注意:有些系统用一个每个用户都不同的字段,uid、手机号、或者别的什么,来作为盐加密密码。这不是一个好主意,这几乎违背了上面全部三条盐的生成规则。
在实际项目中,盐不一定要加在最前面或最后面,也可以插在中间,或者分开插入,还可以使用倒序,等等,进行灵活调整
随机盐生成
示例:
生成一个16位的随机盐
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方式:
md5(md5(password) + salt)
现在大部分的加盐加密都将MD5或SHA-1替换为了更为安全的哈希函数:SHA-256或者SHA-512:
sha512(sha512(password) + salt)
上面的加盐方式都需要将盐值另外·储存,而是BCrypt则是通过加密密码得到,这样每个密码的盐值也是不同的:
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 加密
做了这么多操作主要还是为了下面两点:
先来看下BCrypt生成的密文
说明:
- BCrypt: 2a代表BCrypt加密版本号。
- Rouds: 迭代次方数,10是默认值。可以设置范围为4-31。最终迭代次数为2的Rouds次方。
- Salt: 22位的盐值(即上述的real_salt)。
- Hash:明文password和Salt一起hash加密后生成的密文,长度31位。
示例:
@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包可在多个依赖中引入:
<!--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配合加密使用的呢?分享一篇Dropbox公司发布的博文:
How Dropbox securely stores your passwords
总结一下:
- 首先使用SHA-512,将用户密码归一化为64字节hash值。
- 然后使用BCrypt算法。
- 最后使用AES加密。