前段时间爆出 Facebook 明文存储用户密码,多达 6 亿用户,而它的用户总数是 27 亿,占比 22 % 。
看到这个消息,是不是很震惊?
无独有偶,之前有听过很多银行系统的密码也是明文(真假没有验证)。
在读书时,忘记学校网站密码后,直接打电话给 IT 人员,IT 人员让我说出可能的密码,然后他会告诉我是否正确。那时候我怀疑密码是明文存储的,虽然也没能验证。为什么怀疑是明文,怎么可能要求每个 IT 人员都知道怎么对密码进行一定的变换,然后和存储的密码进行比较呢?(如果有相应的工具那么另说)
明文存储密码存在泄露的风险,也存在撞库的风险。什么是撞库?撞库就是你在好几家网站上共用一个密码,一家的数据库泄露,你在其他网站的密码信息也就被泄露了。关于这种情况,你会说,每个网站密码用不一样的不就好了?可是,有那么多网站,那么多密码你记得过来吗?
我们知道明文存储密码是不安全的,加密存储密码是安全的。
但是,加密存储密码也不一定安全,随着技术的进步,之前的加密算法的安全性大大降低,很容易被破解,于是新的加密算法不断被提出。在这个过程中,有如下几种密码的存储方式。
方法 1:一次 HASH 操作
很早的方法是对密码进行 HASH 操作(如 MD5, SHA - 1,SHA - 256 算法)。HASH(哈希)也即散列:把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要。
以 MD5 为例,讲述密码存储过程。服务端将密码进行 MD5 操作后得到的值存储到数据库中。客户端将明文密码进行 MD5 操作,然后将值发送给服务端,服务端将收到的值和数据库中存储的值相比较就可以知道传入的密码是否正确。这种情况下,服务端也不知道真实的密码,所以密码是找不回的,只能重置。
由于 MD5 是单向的、不可逆的,所以这种方式流行了很长时间。
MD5 的原理是对于特定的输入产生特定的输出。这既是它的工作原理,也可以用于进行破解:可以进行数字、字母的组合来确定真实的密码。最简单的是暴力破解,就是一直尝试,直至成功。但是服务端一般会限制尝试的次数,也即失败一定次数后,锁定 IP 一段时间。有些网站并不会告诉你是用户名错误还是密码错误,防止你进行有针对性的穷举,只会告诉你密码或者用户名错误。
暴力破解的升级版是查表破解,也即先生成大量的 HASH 值并进行存储。需要破解的时候直接进行查表匹配,只要匹配上,就可以获得明文密码。这种方式是用空间换时间,也规避了服务器限制登录失败次数的这一限制。如果将生成的 HASH 值存储到文件中,可以被网络用户共享,可以被更多人使用。
更先进的方法是 Rainbow table(彩虹表),原理和查表破解有些相似,只不过以牺牲时间来达到更小的存储空间,也即以时间换空间。进行匹配时耗时比较久,但是 HASH 值存储所占的空间比较小。
Rainbow table 对于简单密码的破解很有效,1.4 G 的彩虹表可以在 13.6 s 内破解 99.9% 的数字字母混合型的 Windows 密码。如果是 6 位纯数字密码,只有 10^6 种组合(譬如银行卡密码),只有 1 百万种可能性。如果是 6 位的数字和大小写字母的组合,共有(10 + 26 * 2) ^ 6 ,大约是 568 亿种可能性。如果是 8 位的数字和大小写字母的组合,共有 218.34 万亿种可能性。如果将标点符号也包含进来,那么可能性就更多了。所以,现在很多网站要求用户设置的密码长度不低于 8 位,需要包含数字、字母(区分大小写)、标点符号等,将破解难度增大。
虽然密码的强度增加了,但是面对日益增长的硬盘大小及计算能力(超核、GPU、特定 HASH 硬件),此种方式也不那么可靠了。
一次 MD5 的方式虽然可以被破解,但这并不是 MD5 最大的问题,其致命的问题是 MD5 算法并不可靠:之前人们以为对于不同的输入,输出不同;后来被证明,对于不同的输入,输出可能相同。
针对一次 HASH 的不足,人们提出了 HASH + Salt(盐值)的操作。
方法 2:HASH + Salt
HASH + Salt 的操作也称为两次 HASH,其原理是:第一次 HASH 操作进行身份的初次鉴权,网站会返回一个随机数(Salt,盐值),客户端通过服务器返回的随机数以及协商好的规则进行第二次 HASH 操作,将操作结果发送给服务器,服务器也通过相同的方法进行操作,如果两次结果相同,那么就鉴权成功。
此种方式下,服务器也不存储明文密码,存储的是密码第一次 HASH 后的结果。即使该值被泄露,也不会泄露明文密码。
为什么两次 HASH 会比较安全呢?一是其引入了随机数;二是第二次 HASH 的规则可以自定义。随机数使得暴力破解及 Rainbow table 方式破解的难度增大很多,但也并不是不可能(想想内存及计算能力的飞速发展)。第二次 HASH 的规则可以自定义,也增大了破解的难度。
仔细想一下,盐值的产生及维护也有一定的技巧。
盐值应该满足如下几个特点:
-
盐值的随机性要比较好,不能使用 rand 类的函数,应该使用CSPRNG(Windows 下的 CryptGenRandom 函数及 Linux 下读取 /dev/urandom 文件)
-
盐值不应该太短
-
盐值应该唯一:不同用户不同盐值
-
盐值不应该是一些特殊字符串,譬如用户名、手机号等
稍微想一想,盐值得存储也有很多学问,不能和密码 HASH 后的值存储在一起。
方法 3:对称加密算法
采用 AES 等对称加密算法对密码进行加密,然后对加密后的密文进行存储。
此种方式下的密钥存储及管理也是不小的难题。是为每个用户产生一个密钥,还是为一定数量的用户产生一个密钥?如果每个用户一个密钥,那么管理难度较大。如果一定数量用户一个密钥,那么安全性又不高。
对称加密算法加密后的密文可以进行解密得到明文,如果密钥泄露,那么明文密码也存在泄露的可能性。
此种方式下,客户端需要对明文密码进行加密(对于 HTTPS,不需要考虑此种情况)。那么,就会涉及密钥传输的问题。密钥的传输主要有两种方法:
-
公钥加密方法:如 RSA 算法 + AES 算法
-
密钥交换算法:如 DH 算法
公钥加密传输密钥的步骤如下:
-
客户端获取服务端的公钥(RSA 算法产生),用服务端的公钥加密对称加密算法(AES 算法)的密钥(该密钥用于加密用户密码),然后将结果发送给服务端
-
服务端用自己的私钥(RSA 算法产生)解密出对称加密算法的密钥,然后用该密钥解密出用户密码
-
服务端用保存的密钥加密上一步得到的明文密码,然后和数据库中存储的密文密码进行比较(或者解密出数据库中的明文密码,和解密出的用户明文密码进行比较),如果相同,那么验证通过;否则,验证失败
上述过程中,服务端需要维护密文密码、密钥、公钥文件及私钥文件,对于大量用户的场景,难度也不小。
当然,也可以通过密钥交换算法传输加密明文密码时需要的密钥,如 DH 算法,感兴趣的可以去研究一下。
方法 4:密码 HASH 算法
常用的密码 HASH 算法有 PBKDF2、 bcrypt(blowfish)、scrrpt。虽然这里称之为密码 HASH,但是不同于之前说的 MD5 等算法。密码 HASH 算法的基本思想是提高暴力破解的时间成本或者空间成本。
PBKDF2(Password-Based Key Derivation Function 2)简单而言就是将 Salted hash 进行多次重复计算,这个次数是可选择的,可以明确指定 HASH 次数,指定的次数越多,暴力破解的成本也就越多。
Bcrypt 是专门为密码存储而设计的算法,基于 Blowfish 加密算法变形而来。Bcrypt 最大的好处是有一个参数(work factor),可用于调整计算强度,而且 work factor 是包括在输出的摘要中的。随着攻击者计算能力的提高,使用者可以逐步增大 work factor,而且不会影响已有用户的登陆。
Scrypt 是由著名的 FreeBSD 黑客 Colin Percival 为他的备份服务 Tarsnap 开发的。Scrypt 不仅计算所需时间长,而且占用的内存也多,使得并行计算多个摘要异常困难,因此利用 Rainbow table 进行暴力攻击更加困难。
在实际的使用场景中,上述方案都有其应用场景。对安全的认知、资源、技术等因素往往会影响对方案的选择,上述方案也可以相互结合,以减小密码泄露的可能性。
对用户密码进行加密存储是一个难度不小的挑战,所以,一些网站明文存储密码也就不难理解了。在设置密码时,最好增大密码强度,多个网站尽量不要使用相同的密码,定期更换密码,以减小密码泄露的可能性。