密码保存里的学问

对于用户密码保存,最佳实践是加盐和慢哈希。那为什么要用这种方式?

一般的Web服务器都会在数据库中保存用户名密码,但如果网站存在漏洞而导致脱库,那数据库中的密码就会直接暴露,如果密码以明文的形式保存,那只能说太惨了(参见CSDN密码泄露事件)。

因为哈希函数具有很好的单向性,即已知消息,通过哈希函数能快速计算出对应的摘要,而反过来则困难得多得多,这就是哈希函数的不可逆性。所以可以用哈希函数对密码计算摘要,然后保存在数据库中,这样就算数据库被攻击者拿到了,密码也不会直接暴露。

需要补充的是,服务器拿到前台用户输入的明文密码(HTTPS加密传输),通过计算哈希的方式,与数据库中此用户的摘要比对,如果一致,则验证通过,否则拒绝登录。这里用到了哈希函数的抗碰撞性,即任意两个不同的数据,其摘要相同的可能性极低,或者换一种表述,对于一个数据P1,很难构造与P1不同的数据P2,使得Hash(P2)=Hash(P1)。所以只要匹配到了摘要相等,就可以认为密码是正确的。

但因为对于选定的哈希函数(如常用的MD5、SHA-1、SHA-512),数据和摘要是一一对应的,所以攻击者想到了针对所有数据,依次计算出相应的摘要,然后把摘要作为key,数据作为value,存储起来,这就是彩虹表(Rainbow Table)。彩虹表是一种以空间换时间的方法,同时使用了更多的技术,稍后补充。

攻击者利用现有的彩虹表,一般可以获得对常用用户密码快速的破解,因此需要寻求对抗的方法。需要注意,这里的破解是针对整个用户表,也即所有用户的破解,是一种无差别攻击。按照成本收益考虑,攻击的成本就是下载或构造彩虹表,以及匹配的成本,这种成本在当今的算力和存储成本下是较少的,同时,收益是破解所有用户密码带来的收益,如果用户够多,则单个用户的成本收益比会降低很多。

因为在安全领域,如果攻击一个东西的成本收益不匹配,那么攻击者就是自找没趣。所以问题在于,我们能不能提高单个用户的成本收益比?既然之前是对所有用户都无差别地计算哈希,那我们能不能加点东西,把无差别攻击降级为针对性攻击呢?

这就是加盐:即对每个用户,都生成一个随机字符串(salt)。把salt拼接到密码上,再使用MD5等算法计算哈希,即saltHash=MD5(passwd+salt)。另一种方式是,saltHash=MD5(MD5(passwd),salt)。

这样,就算构造彩虹表,也需要针对每个用户的salt构造,单个用户的成本收益比显著提高了,这就是针对性攻击。

通过加盐,攻击成本大大提高了,但针对短密码,攻击者仍然可以暴力破解,即枚举所有的密码组合,加盐后计算哈希。为了对抗这种攻击,一种慢哈希算法被发明,这就是bcrypt和pdkdf2,拿bcrypt说明,bcrypt有一个cost参数,通过调节cost,可以提高或降低计算哈希的时间,目前bcrypt的时间大概在0.3s,如果未来算力提高,我们可以提高cost,来维持计算哈希的时间不变。对比MD5,暴力破解bcrypt的时间成本非常大,如果是8位字母数字组成的密码,则需要(262 + 10) ^ 8 * 0.3s / (365246060) = 51万年,攻击者早就不知道在哪了。

另外,使用bcrypt后,每次计算都需要0.3s,如果放在后台服务器处理,则需要的计算量是很大的,参考知乎CoderZh的设计,bcrypt可以放在客户端,从而分担服务器的压力,不得不说很巧妙。

这种加盐和慢哈希的方法被用在了很多地方,如Linux的/etc/shadow文件,其中用户密码保存为:ididsalt$encrypted,id表示具体的哈希函数,encrypted是摘要,我在Ubuntu 18中看到,默认的id=6,对应SHA-512。

在OpenBSD中,则默认使用bcrypt作为密码哈希函数,这里有bcrypt的介绍,有空了看看论文实现。

参考:

1.黄兢成,谈谈密码安全:服务端密码保存

2.CoderZh,加盐密码保存的最通用方法是?

3.密码保存的最佳实践讨论

4.Smallay,什么是彩虹表?

5.Wikipedia: bcrypt