10

引言

为了系统高安全性的保障,对密码加密方式的演进进行了学习。

为什么数据库中的密码要加密呢?

往轻了说是义务,往严重了说是违法。

根据中华人民共和国201611月颁布的《中华人民共和国网络安全法》第三章第一节第二十一条规定:网络运营者应当按照网络安全等级保护制度的要求,采取数据分类、重要数据备份和加密等措施。

image.png

密码明文存储,没发生数据库泄露等意外没问题,谁都不知道;数据泄露之后,既不符合法律,也造成了用户信息的泄露。

因为现在的应用是在是太多了,用户根本没法去记忆这么多平台的密码,所以要么是所有平台共用密码,要么是根据不同平台有规律的密码。这样用户的密码一旦泄露,可能造成不可估计的损失。

安全方案

AES

最常见的方式就是AES加密方式。

高级加密标准(英语:Advanced Encryption Standard,缩写:AES),是美国联邦政府采用的一种区块加密标准。

AES加密算法(使用128192,和256比特密钥的版本)的安全性,在设计结构及密钥的长度上俱已到达保护机密信息的标准。最高机密信息的传递,则至少需要192256比特的密钥长度。用以传递国家安全信息的AES实现产品,必须先由国家安全局审核认证,方能被发放使用。

AES属于对称加密算法,加解密需要密钥,而密钥想要百分之百不泄露,根本不可能做到。

AES不适合密码加密场景。

SHA-1/MD5

既然加密算法存在密码可解密的安全性问题,直接从加密算法改用hash算法,使得密码不可解密。最常用的hash算法就属SHA-1/MD5了。

2009年,中国科学院的谢涛和冯登国仅用了220.96的碰撞算法复杂度,破解了MD5的碰撞抵抗,该攻击在普通计算机上运行只需要数秒钟。

2017223日,Google公司公告宣称他们与CWI Amsterdam合作共同创建了两个有着相同的SHA-1值但内容不同的PDF文件,这代表SHA-1算法已被正式攻破。

为什么发生碰撞就意味着算法被攻破了呢?

这两个破解的标志性事件都是能快速找到SHA-1/MD5hash碰撞。

假设用户的密码是Password-A

数据库中存储的是密码的hash摘要,假设Password-Ahash摘要是Secret,数据库中存储的密文是Secret

假设数据库泄露,通过hash碰撞的方式,可以快速碰撞出明文Password-Bhash摘要也是Secret

这样虽然不知道当前碰撞出来的密码是否就是用户的密码,但hash摘要的相同,意味着只要是使用hash摘要存储密码的应用,都可以通过碰撞出来的密码Password-B进行登录。

SHA-2

SHA-1算法被攻破,美国国家安全局研发了第二代hash算法标准SHA-2,我们听说过的SHA-256SHA-512都属于SHA-2标准。

目前还没有任何事实证明SHA-2被攻破。

这种方案看起来很安全,但是随着彩虹表的出现,这种方式也逐渐被废弃。

彩虹表是一个用于加密散列函数逆运算的预先计算好的表,常用于破解加密过的密码散列。

为了方便,用户的密码不会设置得很长,所以可以在有限的可能内及暴力枚举,彩虹表中记录着各种可能的密码哈希后的结果,直接根据密文去彩虹表中查询,就能查询出这个密文是由哪个明文hash出来的。

加盐

类似TP教程中的这种方式,加上一个字符串,再进行hash,这种方式被称为加盐。

image.png

用户密码位数不足,短密码的散列结果很容易被彩虹表破解。

故生成一个长度很长能足够保证安全的盐值,与用户密码一起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时生成一个随机的盐值,这样保证每次的散列结果都不同。

image.png

Bcrypt加密之新认识 - 简书引的一张图:

image.png

最终生成的密码是有规范的,盐值是存储在加密的密码中的。真正做到了每个用户密码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算法最安全,但牺牲的是性能。


张喜硕
2.1k 声望423 粉丝

浅梦辄止,书墨未浓。