引言
为了系统高安全性的保障,对密码加密方式的演进进行了学习。
为什么数据库中的密码要加密呢?
往轻了说是义务,往严重了说是违法。
根据中华人民共和国2016
年11
月颁布的《中华人民共和国网络安全法》第三章第一节第二十一条规定:网络运营者应当按照网络安全等级保护制度的要求,采取数据分类、重要数据备份和加密等措施。
密码明文存储,没发生数据库泄露等意外没问题,谁都不知道;数据泄露之后,既不符合法律,也造成了用户信息的泄露。
因为现在的应用是在是太多了,用户根本没法去记忆这么多平台的密码,所以要么是所有平台共用密码,要么是根据不同平台有规律的密码。这样用户的密码一旦泄露,可能造成不可估计的损失。
安全方案
AES
最常见的方式就是AES
加密方式。
高级加密标准(英语:Advanced Encryption Standard
,缩写:AES
),是美国联邦政府采用的一种区块加密标准。
AES
加密算法(使用128
,192
,和256
比特密钥的版本)的安全性,在设计结构及密钥的长度上俱已到达保护机密信息的标准。最高机密信息的传递,则至少需要192
或256
比特的密钥长度。用以传递国家安全信息的AES
实现产品,必须先由国家安全局审核认证,方能被发放使用。
AES
属于对称加密算法,加解密需要密钥,而密钥想要百分之百不泄露,根本不可能做到。
AES
不适合密码加密场景。
SHA-1/MD5
既然加密算法存在密码可解密的安全性问题,直接从加密算法改用hash
算法,使得密码不可解密。最常用的hash
算法就属SHA-1
/MD5
了。
2009
年,中国科学院的谢涛和冯登国仅用了220.96
的碰撞算法复杂度,破解了MD5
的碰撞抵抗,该攻击在普通计算机上运行只需要数秒钟。
2017
年2
月23
日,Google
公司公告宣称他们与CWI Amsterdam
合作共同创建了两个有着相同的SHA-1
值但内容不同的PDF
文件,这代表SHA-1
算法已被正式攻破。
为什么发生碰撞就意味着算法被攻破了呢?
这两个破解的标志性事件都是能快速找到SHA-1
/MD5
的hash
碰撞。
假设用户的密码是Password-A
。
数据库中存储的是密码的hash
摘要,假设Password-A
的hash
摘要是Secret
,数据库中存储的密文是Secret
。
假设数据库泄露,通过hash
碰撞的方式,可以快速碰撞出明文Password-B
的hash
摘要也是Secret
。
这样虽然不知道当前碰撞出来的密码是否就是用户的密码,但hash
摘要的相同,意味着只要是使用hash
摘要存储密码的应用,都可以通过碰撞出来的密码Password-B
进行登录。
SHA-2
SHA-1
算法被攻破,美国国家安全局研发了第二代hash
算法标准SHA-2
,我们听说过的SHA-256
、SHA-512
都属于SHA-2
标准。
目前还没有任何事实证明SHA-2
被攻破。
这种方案看起来很安全,但是随着彩虹表的出现,这种方式也逐渐被废弃。
彩虹表是一个用于加密散列函数逆运算的预先计算好的表,常用于破解加密过的密码散列。
为了方便,用户的密码不会设置得很长,所以可以在有限的可能内及暴力枚举,彩虹表中记录着各种可能的密码哈希后的结果,直接根据密文去彩虹表中查询,就能查询出这个密文是由哪个明文hash
出来的。
加盐
类似TP
教程中的这种方式,加上一个字符串,再进行hash
,这种方式被称为加盐。
用户密码位数不足,短密码的散列结果很容易被彩虹表破解。
故生成一个长度很长能足够保证安全的盐值,与用户密码一起hash
,使得生成的hash
摘要无法在彩虹表中逆向查询。
如果真想要破解,需要获取到盐值,构造新的彩虹表。
这种盐的方式是固定了的,一整套系统中都是同一个盐的加盐逻辑,破解了该盐的彩虹表,整个系统全部变得不安全。
有没有可能,每个用户的密码加密所用的盐是随机的呢?
BCryptPasswordEncoder
为了绝对的安全性,Spring Security
内置的加密主角:BCryptPasswordEncoder
出现了。
先看这样一段代码:
public static void main(String[] args) {
PasswordEncoder encoder = new BCryptPasswordEncoder();
final String password = "123456";
System.out.println("password first encode result is : " + encoder.encode(password));
System.out.println("password second encode result is : " + encoder.encode(password));
}
令人震惊的是同一密码的两次hash
结果竟然不一样。
> Task :auth:AuthApplication.main()
password first encode result is : $2a$10$4rCU6e8nULcP.2a0DA.hZ..pL5in6vSzkpTAF8H1La9MYhRUsrPj.
password second encode result is : $2a$10$kyCJ64TQH9c4d5rIrijpbuWk8bxCgQvtUStCGd9ZzLQ2Gkh5o6LjO
这里的原理就是每次hash
时生成一个随机的盐值,这样保证每次的散列结果都不同。
从Bcrypt加密之新认识 - 简书引的一张图:
最终生成的密码是有规范的,盐值是存储在加密的密码中的。真正做到了每个用户密码hash
时使用不同的盐值。
但是如果真的想去跑彩虹表,还是能跑出来,只是需要破解每一个用户的密码都需要一张彩虹表,难度加大了而已。
既然号称绝对安全,那BCryptPasswordEncoder
是如何防止暴力枚举的呢?
其内部采用的BCrypt
算法是一种慢哈希算法,可以通过配置多次hash
来使得每次计算特别耗时,从而使暴力枚举的总计时间需要上百年,保障了安全性。
这种方案保障密码绝对安全,只是速度上有些慢:
PasswordEncoder encoder = new BCryptPasswordEncoder();
final String password = "123456";
long startTime = System.currentTimeMillis();
encoder.encode(password);
long endTime = System.currentTimeMillis();
System.out.println("BCrypt 运行时间 : " + (endTime - startTime) + " ms");
经测试,使用默认配置hash
明文为123456
的密码需要841 ms
,确实有些慢了。
BCrypt 运行时间 : 841 ms
总结
普通的hash
算法安全性有待提升,加盐可以提高安全性,BCrypt
算法最安全,但牺牲的是性能。