前言
在系统中存储密码时,密码是不能够明文进行存储的,这样密码泄漏后,攻击者无法轻易(或者几乎不可能)还原出用户的原始密码,从而避免用户数据被破坏。
加密和哈希
- 加密:不论是对称加密还是非对称加密,只要密钥匹配,就可以将密文进行还原。比较适合双方需要进行沟通的场景,例如通话,发送方先对数据加密之后再传输,防止中间人窃听,接收方收到密文后,使用特定的密钥进行解密。
- 哈希:是单向的,相同的数据经过同一个哈希算法后,会生成相同的哈希值,这个哈希值无法被还原成原始数据。
所以对于密码的存储我们需要使用哈希算法,而非加密算法,这样即使攻击者获取到了密码的哈希值,也无法通过哈希值推导出用户的密码。
黑客破解密码的手段
虽然密码会使用哈希算法来进行转换,但是如果哈希算法选择不对的话,黑客还是可以很容易的破解出用户的密码,黑客常见的攻击手段:
- 字典攻击:使用包含大量常用密码的字典来匹配。
- 彩虹表攻击:使用提前计算好的哈希值集合来进行匹配。
如何选择哈希算法
哈希算法的特点是相同的输入会有相同的输出,如果单纯的将密码进行哈希计算,会出现黑客破解了一个密码,其他具有相同哈希值的用户也会攻破。因此在将密码进行哈希之前还需要加盐(salt),并且每个用户的盐是随机的,将密码和随机的盐拼接之后再进行哈希。通过加随机盐的方式,相同的密码由于盐不同,拼接后生成的哈希值不同,黑客每次就只能够破解一个密码,从而增加黑客破解密码的成本。
虽然加盐大大增加了黑客破解密码的成本,随着计算机算力的提升, 尤其是 GPU 的并行计算能力,简单的加盐密码被破解的几率还是很大,因此我们还需要一种计算速度相对较慢的哈希算法,这样可以让每秒尝试密码的次数大大降低,而对于普通用户而言,慢个几十毫秒是完全可以接受的。
通过上面的描述,存储密码比较理想的算法应该具备以下的特点:
- 单向的
- 带盐的
- 计算速度较慢的
通用哈希算法(MD5, SHA-1, SHA-256, SHA-512)是不能够用来处理密码的,因为它们的计算速度太快了,很容易被现代 GPU 给攻破。给密码加密应该选择下面的哈希算法:
- Argon2:当前最优的算法,2015年密码哈希竞赛 (Password Hashing Competition) 的获胜者,被公认为目前最强的密码哈希算法。它不仅可以调整时间成本(CPU消耗),还可以调整内存成本,能有效抵抗 GPU 和 ASIC(专用集成电路)的破解。推荐使用 Argon2id 版本,它结合了 Argon2d 和 Argon2i 的优点,提供了对侧信道攻击和 GPU 破解最好的防护
- Bcrypt:可靠且被广泛使用,它内置了盐并且有一个可以调整的代价因子(Cost Factor)。
- Scrypt:它是一个“内存困难型”(memory-hard)算法,需要大量内存,这使得利用 GPU 进行大规模并行破解的成本非常高。相比 Argon2,它的灵活性稍微差一点。
优先使用 Argon2,如果系统不支持再选择 Bcrypt 算法。这两个算法生成的哈希值都自带了盐,因此在做数据库设计的时候,可以省略一个字段。
Argon2 和 Bcrypt 在 Java 中的使用
在 pom.xml 文件中新增依赖:
<dependency>
<groupId>com.password4j</groupId>
<artifactId>password4j</artifactId>
<version>1.8.4</version>
</dependency>
代码示例:
public static void bcryptEncode() {
String raw = "888888";
BcryptFunction instance = BcryptFunction.getInstance(Bcrypt.B, 12);
Hash hashObj = Password.hash(raw).with(instance);
String encodedText = hashObj.getResult();
System.out.println("encodedText: " + encodedText);
}
public static void bcryptDecode() {
String raw = "888888";
String encodedText = "$2b$12$Wlm/wmfozYi5UQbMhMbqMOF2qo4RNleLIGSOiSo2BNF0yVNnRRhiO";
BcryptFunction instance = BcryptFunction.getInstanceFromHash(encodedText);
boolean result = Password.check(raw, encodedText).with(instance);
System.out.println(result);
}
public static void argon2Encode() {
String raw = "888888";
Argon2Function instance = Argon2Function.getInstance(4096, 20, 4, 32, Argon2.ID);
Hash hashObj = Password.hash(raw).addRandomSalt(64).with(instance);
String encodedText = hashObj.getResult();
System.out.println(encodedText);
}
public static void argon2Decode() {
String raw = "888888";
String encoded = "$argon2id$v=19$m=4096,t=20,p=4$OjcuB72RlQLfIlfwwUrv+6Ibh8DX1FUbK5ofDg8AqrKqYzsju+c/dBwBvntGnc0s5+rEhLO3254AlLmnDG/qrg$2e56eWHaPOtZpGKGpFyBmxxp92N+5thKPRtgmfPBU/Q";
Argon2Function instance = Argon2Function.getInstanceFromHash(encoded);
boolean result = Password.check(raw, encoded).with(instance);
System.out.println(result);
}