如何安全地存储密码
无论是开发App还是网站,只要有用户登录环节,就会牵涉到如何存储用户的密码的问题。如果采用的存储密码的技术不够安全,一旦黑客闯入存储密码的数据库,他就能获取用户的密码从而可能给用户带来重大损失。这种情形任何公司都不希望发生在自己身上,因此选择安全地存储密码的策略显得十分必要。
不一定非要自己存储用户的密码
最简单的存储密码的方式就是自己并不存储,而是委托给信任的第三方存储。这就是OpenID技术,它的理念是用第三方来完成用户验证的操作。目前国外的网站如谷歌、雅虎等,国内的如腾讯等都已经提供OpenID的服务。如果我们开发一个网站并选择谷歌的OpenID服务,那么用户就可以用Gmail的账号和密码登录,接下来用户认证的事情将由谷歌完成。
采用OpenID技术无论是对网站的开发者还是用户,就具备明显的优点。由于用户的登录认证是由第三方的OpenID服务提供商完成的,我们自己没有必要存储用户名和密码,也就没有必要考虑存储密码的安全性问题,从而减少开发的成本。同时,用户不用在我们的网站上注册新的用户名和密码。这样既免去了用户在注册用户名和密码时填写资料的麻烦,也减去了用户需要记住一对新的用户名和密码的负担。
千万不要用明文存储密码
有很多公司由于种种原因不愿意把自己客户的登录信息保存在其他公司,于是不得不自己存储用户的密码。既然决定自己存储,就要考虑存储的安全性。密码存储的最低要求是不能用明文(没有经过加密)存储密码。如果用明文存储密码,一旦数据库泄露出去,所有用户的密码就毫无保留地暴露在黑客的面前,这可能给用户以及公司带来巨大的损失。
虽然不能用明文存储密码的道理显而易见,但实际上仍然有不少公司在采用这种极度不安全的方式。这一点从时不时爆出的各种网站密码泄露事件可以看出。对那些仍然在用明文存储密码的公司,我们只能奉劝他们尽早用哈希算法给密码加密之后再存储,别等到密码泄露之后造成重大损失才幡然大悟。
用哈希算法加密密码
常用的给密码加密的算法是几种单向的哈希算法。所谓的单向的算法是指我们只能从明文生成一个对应的哈希值,却不能反过来根据哈希值得到对应的明文。经常被大家用来加密的算法有MD5和SHA系列(如SHA1、SHA256、SHA384、SHA512等)。值得注意的是,MD5算法已经被中国数学家王小云破解,因此这种算法已经不建议在产品中使用。
虽然用哈希算法能提高密码存储的安全性,但还是不够安全。通常黑客在侵入保存密码的数据库之后,他会随机猜测一个密码,用哈希算法生成一个哈希值。如果该哈希值在数据库中存在,那么他就猜对了一个用户的密码。如果没有猜中也没有关系,他可以再次随机猜测下一个密码进行尝试。事实上黑客为了提高破解密码的效率,他们会事先计算大量密码对应的各种哈希算法的哈希值,并把密码及对应的哈希值存入一个表格中(这种表格通常被称为彩虹表)。在破解密码时只需要到事先准备的彩虹表里匹配即可。因此现在黑客们破解仅仅只用哈希算法加密过的密码事实上已是不费吹灰之力。
加盐提高安全性
为了应对黑客们用彩虹表破解密码,我们可以先往明文密码加盐,然后再对加盐之后的密码用哈希算法加密。所谓的盐是一个随机的字符串,往明文密码里加盐就是把明文密码和一个随机的字符串拼接在一起。由于盐在密码校验的时候还要用到,因此通常盐和密码的哈希值是存储在一起的。
采用加盐的哈希算法对密码加密,有一点值得注意。我们要确保要往每个密码里添加随机的唯一的盐,而不是让所有密码共享一样的盐。如果所有密码共享统一的盐,当黑客猜出了这个盐之后,他就可以针对这个盐生成一个彩虹表,再将我们加盐之后的哈希值到他的新彩虹表里去匹配就可以破解密码了。
虽然加盐的算法能有效应对彩虹表的破解法,但它的安全级别并不高,这是由于哈希算法的特性造成的。哈希算法最初是用来确保网络传输数据时的数据完整性。当我们通过网络传输一个数据包时,我们在发送时会在数据包的末尾附上这个数据包对应的哈希值。在接收数据时,我们再次根据接收到的数据包用同样的算法生成一个哈希值。如果这个哈希值和从网络上接收到的哈希值一样,那就证明了数据在传输时没有出现问题。为了减少网络传输的延时,我们希望哈希算法尽量的快,尽可能地减少数据校验的时间。因此在设计哈希算法的时候,快速高效是一个非常重要的指标。目前在普通配置的电脑上,主流的哈希算法的耗时在微秒的级别,这意味着我们可以在一秒之类计算哈希值近百万次。
快速高效的哈希算法给加密算法带来了不少的挑战,因为安全的加密算法应该是黑客极难破解的算法,而计算哈希值的耗时非常短,黑客们就可以用暴力法去破解加盐之后的用哈希算法加密的密码。前面提到,盐通常和哈希值存储在一起。于是黑客针对每一个盐可以采用两种简单的暴力法破解密码。
一是采用穷举法。黑客生成一个密码,和盐拼接在一起再计算哈希值。如果哈希值和数据库中的哈希值一致,那么这个密码就被破解。如果不一致,在进行下一次尝试。这种方法对低级别的密码非常有效。比如6位全是数字的密码总共只有一百万中可能。这意味着任何6位的纯数字密码即使加盐之后也能在数秒之内破解。虽然用暴力法破解高级别的密码(比如同时包含数字、大小写字母和特殊符号的密码)目前还需要大量的时间,但计算能力遵循着摩尔定律持续地提高,今天看来非常耗时的操作在今后可能非常快速地完成。另外,近年来云计算的快速发展,也使得黑客们能够用非常便宜的价格租到大量的计算机以方便他们并行地破解密码,从而黑客们低成本并且高效地破解高级别密码成为了可能。
二是黑客们从历次密码泄露事件中收集了大量的常用密码。针对每一个盐,黑客可以循环地从这些重用密码里挑选一个,加盐再计算哈希值。因此这些常用的密码即使加盐也很容易破解。
用BCrypt或者PBKDF2增加破解的难度
为了应对暴力破解法,我们需要非常耗时的而不是非常高效的哈希算法。BCrypt算法应运而生。我们可以用BCrypt算法加盐之后给密码生成一个哈希值。Bcrypt最大的特点是我们可以通过参数设置重复计算的次数。显而易见重复计算的次数越多耗时越长。如果计算一个哈希值需要耗时1秒甚至更多,那么黑客们采用暴利法破解密码将不再可能。以前面提到的6位纯数字密码为例,破解一个密码需要耗时11.5天,更不要说高安全级别的密码了。
目前已有开源项目( http://bcrypt.sourceforge.net/)实现了BCrypt算法并被业界广泛采用。如果你是一个.NET程序员,你可能会发现目前的.NETFramework中还没有包含BCrypt的实现。此时有两个选择。一是已经有一些开源项目用C#实现了BCrypt算法,我们可以直接使用。如果对这些实现的安全性存在担忧,我们也可以选择和BCrypt类似的PBKDF2。PBKDF2同样也可以通过参数设定重复计算的次数从而延长计算时间。在.NETFramework中,类型Rfc2898DeriveBytes实现了PBKDF2的功能。
小结
安全地存储密码不是一件容易的事情。虽然目前有很多公司采用加盐的哈希算法应对彩虹表破解法,但这种方法并不是足够安全的。由于哈希算法非常高效,计算哈希值耗时在微秒级别,因此黑客可以通过暴力法破解密码。一个推荐的办法用BCrypt或者PBKDF2延长计算哈希值的时间,从而提高破解密码的难度。另外,并不是每个公司、项目都需要自己存储密码。我们的另一个选择是用OpenID把用户校验工作委托给可以信赖的第三方公司。