转自黄兢成 的 谈谈密码安全:服务端密码保存,通俗易懂,故转载并做重点标注以供自己随时查阅。
前导
现在有越来越多的网络服务,基本都需要注册才能使用,注册需填写账号和密码。
账号通常是邮箱或手机号,普通人基本上只有一两个邮箱和手机号,而普通人记得的密码也只是一两个。因此很多人会很自然地在不同的网络服务中使用相同的账号和密码。
这样就出现安全隐患。一旦某个服务的账号密码被泄露,其它的服务安全就受到牵连。
密码安全,应该分两部分讨论,本文只讨论第一点:
- 作为账号系统的设计者,如何对用户负责,更安全地保存密码。
- 作为普通个人,应该怎么管理自己的密码。
安全性
服务端肯定在某个地方保存了用户信息,这样才能验证用户登录。一旦信息被保存下来,就有可能被泄露。
安全性总是会被忽视,都是出事之后才会受到关注。而假如做得很好的,反而会淡化自身的存在,默默无闻。古人也说,善战者无功名,无勇功。反而是那些出问题比较多,自己搞出些意外,再拼命修改的,存在感会更强。但这种出问题再修正的,就算修好也会有损害的。
密码安全是一步步受到重视的
开发者设计好系统,之后系统受到攻击;开发者想方法去改进,之后攻击者又想出新的攻击方法;之后开发者再次改进,之后又出现新的攻击方式。就这样循环。有时需要先理解出了什么些问题,才能更好理解为什么需要这样做。回顾一下密码保存的方法。
明文密码
很久前安全意识还很淡薄,不少服务器密码是明文保存的。
用户 A 注册的时候,需要向服务器发送账号 A(a)
和密码 P(a)
,服务端就直接保存下用户的账号 A(a)
和密码 P(a)
。
当用户登录的时候,再次发送账号 A(a)
和密码 P(a)
,服务器比较各个账号,当某个账号 A(n) == A(a)
并且密码 P(n) == P(a)
,用户登录成功。
现在的观点看来,这样的密码管理实在是太脆弱了。一旦攻击者成功入侵,获取到数据库,就直接取得账号和密码。
有些人可能会说,数据库本身也会加密啊,就算攻击者获取到数据库也打不开。但假如数据库很有价值,比如有 1 亿用户。这个数据库被破解也只是时间问题。有些明文保存将账号密码写在文件上面,也不加密。攻击者连破解数据库的步骤也省了。
加密密码
随着时间发展,开发者认识到密码明文保存的脆弱性,采用加密保存密码。
账号基本上还是明文,因为需要找回密码之类的功能,不知道原始账号就做不到这一点。而密码通常会使用 MD5、SHA 之类的算法加密保存。
这里说加密算法其实是不准确的,更准确的是 哈希算法(或摘要算法)。平常说的加密会有相应的解密算法。加密从 E(P)
得到 P'
,就会有一种解密算法 D(P')
还原得到 P
。MD5、SHA 之类的哈希算法是 单向 的,从 H(P)
得到 P'
,之后没有办法还原出 P
。
本文中,在不引起歧义的情况下,将加密算法和哈希算法等同,只是为了描述方便。但大家要清楚意识到,MD5、SHA 严格来说 并非是加密算法。
用户 A 注册的时候,也向服务器发送账号 A(a)
和密码 P(a)
。服务端计算出加密后的密码 P'(a)
,之后保存下用户的账号 A(a)
和加密密码 P'(a)
,当用户登录的时候,再次发送账号 A(a)
和密码 P(a)
,服务器再次计算出 P'(a)
,之后保存的信息比较:
A (1), A (2), A (3), A (4) … A (n)
P’(1), P’(2), P’(3), P’(4) …. P’(n)
当某个账号 A(n) == A(a)
并且密码 P'(n) == P'(a)
,用户登录成功。因为不可以通过 P'
逆向还原出
P
。就算泄露了加密后的用户密码,攻击者也不知道原始密码。这种系统,粗略看来似乎很安全了。
攻击者角度
对于攻击者来说,既然不可以逆向还原密码,但还是可以可以正向计算加密密码。
攻击者将常用的密码,比如常用单词、数字组合、生日等用计算机暴力扫描计算生成一个大表。这个表叫 彩虹表。这样的表可以包含了 几十万亿、几百万亿个 密码,基本上涵盖了常用的密码了。常见流行的加密算法,比如 MD5、SHA 之类,都已经有预先计算好的彩虹表。
这样的表,原密码和加密密码的一一对应:
P(1) -> P(1)'
P(2) -> P(2)'
P(3) -> P(3)'
…..
P(n) -> P(n)'
这样当取得用户密码 P'
, 就再这表中查找,假如 P' == P(n)'
,就表示原来密码 P == P(n)
。有了彩虹表之后,对于常见的密码,一查表就可以还原出来了。
注意到,彩虹表不可能覆盖全部的密码,因为可能的密码实在太庞大了。但普通人取密码为方便容易记忆,都不可能是完全随机的。倾向于取容易记忆的单词、生日、电话号码之类,考虑到这些因素,彩虹表可以覆盖绝大部分日常密码。
安全性前提
这里先暂停一下,为更好讨论,需要承认安全性的两个大前提:
- 预计最坏的情况,攻击者已经知道 除了用户原始密码的任何信息。包括全部程序、加密算法、数据库等等。
- 世上没有绝对的安全,但是假如当破解密码需要的成本超过获得的收益,就是 相对安全 的。
前提 1 下,妄想隐藏某个信息期望攻击者不知道,只会获得虚假的安全感。一旦将信息保存在某个地方,就可能泄露,就需要假设攻击者已经知道了这信息。至于信息泄露已经是另外的话题,或者攻击者攻击服务器拿到全部信息,或者内部有人受贿出卖资料,但到底是怎么泄露的已经和这里的讨论无关。
前提 2 下,攻击是需要成本的(时间成本、机器成本等),一但攻击成本大于收益,作为一个理性的攻击者,就不值得去攻击了。
开发者角度
为避开彩虹表覆盖,开发者想出不少方法。
比如先用 MD5 加密,再用 SHA 加密,之后反复几次。或者土法炼钢,想出奇奇怪怪的加密算法。
他们的理论就是,我这样做之后,我的加密算法就是不标准的。攻击者不知道我的算法,就不能利用彩虹表来来还原密码了。
这样的有点天真。在安全性前提 1 下,攻击者可以知道任何你想隐藏起来的信息。攻击者既然已经可以拿到数据库了,那些程序自然也可以拿到,那些加密方式无论怎么不标准,也自然可以知道。使用不标准的加密方法,虽然攻击者预先准备好的彩虹表失效了,但攻击者可以 针对特定加密算法将彩虹表再次计算出来。
比如数据库有 1 亿用户,数据库很有价值。攻击者重新计算出彩虹表用了 5 天,之后情况又跟使用标准的加密算法一样了,没有本质区别。
另外的一种想法是,MD5 等算法计算速度实在太快了。攻击者可以很快重新计算出彩虹表,既然这样,我就用慢点的算法,让攻击者计算起来更慢,就可以增大攻击成本。这样的算法有 bcrypt
,比如计算一次密码用 0.3 秒。用这样的算法后,之前 5 天就可以算好彩虹表,现在需要 365 年。这样在安全性前提 2 下,成本过大,从而攻击者就放弃攻击了。
bcrypt
的精髓在于,其计算代价是 可调节。当出现更快的计算机时,可将计算代价调大,让攻击的速度仍然很慢。
单纯拖慢速度的方法可以作为很好的辅助手段,但不能完全依赖这个。下面会介绍 加密 + Salt 的想法,但先来了解攻击方式。
无差别攻击和针对性攻击
再次重申安全性前提:
- 攻击者已经知道除了用户原始密码的任何信息。
- 世上没有绝对的安全,但是假如当破解密码需要的成本超过获得的收益,就是相对安全的。
既然不能隐藏信息,我们的思考切入点就是 如何增大攻击者成本。上面说的故意拖慢计算速度,是其中的方法。
回顾上面的攻击手段:
假如数据库有 1 亿用户,攻击者就算没有预先准备好彩虹表,需要重新计算一次,一旦计算好,就可以针对于 1 亿人使用。
假如有 2 亿用户,也只重新计算一次,就可以直接针对 2 亿用户使用。
这样攻击成本是固定的,用户数量越大,分摊到每个人上面的攻击成本就越小。
假如攻击成本需要 5000 元,攻击成功之后,每个用户可以获得 0.1 元。这样用户数量一旦超过 50000,攻击就是值得的。
这种一次针对所有人的攻击,就是 无差别攻击。
攻击者的考虑角度和用户是不同的:
- 作为用户只关心自己的个人账号是否安全,至于其它人是否安全是不关心的。
- 作为攻击者,关心是否有足够多的人被破解,具体破解到什么人是不关心的。
用户是鱼,攻击者去网鱼,攻击者只关心一网下去捞到多少条鱼,但不会关心具体捞到哪一条鱼。
攻击者最喜欢这种一网下去、一窝端的无差别攻击了。假如一次只能攻击其中特定某一个人,一次只去捞特定的一条鱼,就是针对性攻击。针对性攻击的成本会很大。
加密 + Salt
来到这里,我们可以讨论一种常用的保存密码方法,就是 加密+Salt。Salt 在英文中是盐的意思,就是为原始密码加些其它信息。
加 Salt 的目的是 将一窝端的无差别攻击,转成针对性攻击。
Salt 是一个服务器随机生成的字符串。用户 A 注册的时候,需要向服务器发送账号 A(a)
和密码 P(a)
。这时服务端为用户 A 生成一个 随机字符串 S(a)
,再和用户原始密码组合起来,用前面讨论的 MD5、RSA 等方式计算出密文 P'(a)
。这里原始密码和 Salt 到底怎么组合,直接串起来、做 xor,、各个字符做减法,其实没有本质区别。
H[S(a) + P(a)] -> P'(a)
之后服务器将 S(a)
和 P'(a)
保存下来,随机字符串 S(a)
可以一直保持不变。当另一用户 B 注册的时候,再生成另外的随机字符串 S(b)
,重复上面注册过程保存下来。
这里的关键在于,每个用户的 Salt 都是不同的。
一旦加上 Salt,就算原始密码很简单,组合起来也足够随机。这样就算采用标准的算法,随机的密码也 不可能出现在彩虹表中,攻击者预先准备好的彩虹表就失效了。
这里攻击者是完全知道保存下来的信息的。这样每个用户的加密密码 P'
和 Salt
也会知道。
这样攻击者攻击用户 A 的时候,就需要针对 A,采用跟 A 相同的 Salt 和加密算法, 重新计算,产生下面对应关系:
S(a) + P(1) -> P’(1)
S(a) + P(2) -> P’(2)
S(a) + P(3) -> P’(3)
….
S(a) + P(n) -> P’(n)
一旦 P'(n) == P'
, 就表示用户密码为 P(n)
。到这里似乎跟之前的方法没有差别。
但攻击者去攻击用户 B 的时候,因为用户 B 的 Salt 跟用户 A 不同。这样 之前针对用户 A 的计算,对于用户 B 无效。就需要重新计算用户 B 的密码。每一个不同的 Salt, 对于攻击者来说,就相当于不同的加密算法,都需要重新计算一次。
这样的话,本来一次计算可以适用于全部用户,现在就失效了。一窝端的无差别攻击,只能是转成针对性攻击。
加 Salt 后,需要每个用户分别计算。假如每次计算成本不变,还是 5000 元,每个用户收益还是不变,获得 0.1 元。这样的攻击就很不值得了。
一个笑话:每个用户加相同的 Salt
对于 加密 + Salt,有个误解就是以为每个用户都加相同 Salt。
这种所谓的加相同 Salt 就是一个笑话。当每个用户都使用相同的 Salt 时,针对用户 A 的计算结果,就可以应用到用户 B 上。这样还是无差别攻击,跟上面说的想出一种非标准加密方法没有本质区别。
那些人可能会觉得 Salt 是随机生成的,嵌在代码中不保存到数据库,攻击者不可能知道。假如为每个用户加不同的 Salt,保存到数据库中。攻击者就知道每个用户的 Salt 了,这样更不安全。
这样的想法有点天真,这种想法跟那些去想各种各样不标准的算法,没有本质区别。上面都说了,攻击者既然可将数据库拿出来,那些程序代码也可以随手拿走。Salt 保存在代码还是保存到数据库当中是一样的。Salt 是随机生成的,但需要参与计算,就一定会被保存到某个地方当中,一但保存下来,参照前提 1,就需要假设攻击者就已经知道了。
再次强调,密码安全不能依赖隐藏信息来获得虚假的安全。需要假设攻击者已经取得全部信息,这种前提下自己发明各种土制算法也是个笑话。
现在服务端保持密码的一种比较好的方法是使用一种 慢哈希算法 + Salt。比如 bcrypt
就算专门为这种应用场合设的,其算法中可直接集成 Salt 原理。只要使用,就不用自己来操心了。基本想法是计算速度相对较慢。而选择 cost
,可调节计算速度。使用 Salt,将无差别攻击转化为针对性攻击。