在各种线上应用中,用户名密码是用户身份认证的关键,它的重要性不言而喻。一方面,作为保护用户敏感数据的钥匙来说,一旦被破解,系统将敞开大门完全不设防。另一方面,密码这把钥匙本身就是非常敏感的数据:大多数用户会在不同应用中使用近似甚至完全相同的密码。一旦某一个应用的密码被破解,很可能坏人就此掌握了用户的“万能钥匙”,这个用户的其它应用也相当危险了。
这篇博文就重点讨论对于密码本身的存储的安全性考虑,而系统自身的安全性不在此文的范围之内。
对于如此重要的用户密码,究竟该怎样在系统中存储呢?
“君子不立危墙”,对于用户密码这个烫手山芋,一个极端的选择是系统完全不接触密码,用户的身份认证转交受信任的第三方来处理。例如 OpenID 这样的解决方案。系统向受信任的第三方求证用户身份的合法性,用户通过密码向第三方证明自己的身份。
这样一来,也就不用绞尽脑汁保证密码的安全了。这个作法对用户来说还有个额外的好处:再也不用为每个应用注册帐号了,同一个 OpenID 就可以登录所有支持 OpenID 的系统。
好虽好,可在今天这样一个裂土封国划地为营的网络战国时代,用户资源不但不能牢牢掌握在自己的手上,还要与别人分享,甚至要受制于人,这多少有点让人难以接受。(据称,现在全球有 27000 个 Web Site 支持 OpenID 登录,虽然还在持续增长中,但在茫茫“网海“中无疑还是属于小众)
既然网络大同时代还没来临,大部分应用还是要自己负责用户的认证,那密码该如何存储呢?按照安全性由低到高,有这样几种选择:
-
密码明文直接存储在系统中
这种方法下密码的安全性比系统本身还低,管理员能查看所有用户的密码明文。除非是做恶意网站故意套取用户密码,否则不要用这种方式
-
密码明文经过转换后再存储
与直接存储明文的方式没有本质区别,任何知道或破解出转换方法的人都可以逆转换得到密码明文
-
密码经过对称加密后再存储
密码明文的安全性等同于加密密钥本身的安全性。对称加密的密钥可同时用于加密与解密。一般它会直接出现在加密代码中,破解的可能性相当大。而且系统管理员很可能知道密钥,进而算出密码原文
-
密码经过非对称加密后再存储
密码的安全性等同于私钥的安全性。密码明文经过公钥加密。要还原密码明文,必须要相应的私钥才行。因此只要保证私钥的安全,密码明文就安全。私钥可以由某个受信任的人或机构来掌管,身份验证只需要用公钥就可以了
实际上,这也是 HTTPS/SSL 的理论基础。这里的关键是 私钥的安全 ,如果私钥泄露,那密码明文就危险了。
以上 4 种方法的共同特点是可以从存储的密码形式还原到密码明文。
当你忘了用户密码后,网站可以很贴心地通过你注册的 email 提醒你原来的密码是什么,那它肯定就是用了上面的某种方法了。这时候你就得小心了:既然网站能知道密码明文,那网站的工作人员就有可能知道,攻入这个网站的黑客也有了还原你密码明文的可能。
所以密码最好是以不可还原明文的方式来保存。通常利用哈希算法的单向性来保证明文以 不可还原的有损方式 进行存储。
这类方法的各个具体操作方式按安全性由低到高依次为:
-
使用自己独创的哈希算法对密码进行哈希,存储哈希过的值
哈希算法复杂,独创对理论要求很高。一般独创的哈希算法肯定没有公开经过时间检验的算法质量高,天才另算
-
使用 MD5 或 SHA-1 哈希算法
MD5 和 SHA-1 已破解。虽不能还原明文,但很容易找到能生成相同哈希值的替代明文。而且这两个算法速度较快,暴力破解相对省时,建议不要使用它们。
-
使用更安全的 SHA-256 等成熟算法
更加复杂的算法增加了暴力破解的难度。但如果遇到简单密码,用彩虹字典的暴力破解法,很快就能得到密码原文
-
加入随机 salt 的哈希算法
密码原文(或经过 hash 后的值)和随机生成的 salt 字符串混淆,然后再进行 hash,最后把 hash 值和 salt 值一起存储。验证密码的时候只要用 salt 再与密码原文做一次相同步骤的运算,比较结果与存储的 hash 值就可以了。这样一来哪怕是简单的密码,在进过 salt 混淆后产生的也是很不常见的字符串,根本不会出现在彩虹字典中。salt 越长暴力破解的难度越大
具体的 hash 过程也可以进行若干次叠代,虽然 hash 叠代会增加碰撞率,但也增加暴力破解的资源消耗。就算真被破解了,黑客掌握的也只是这个随机 salt 混淆过的密码,用户原始密码依然安全,不用担心其它使用相同密码的应用。
上面这几种方法都不可能得到密码的明文,就算是系统管理员也没办法。对于那些真的忘了密码的用户,网站只能提供重置密码的功能了。
下面的 python 程序演示了如何使用 salt 加 hash 来单向转换密码明文
1 import os 2 from hashlib import sha256 3 from hmac import HMAC 4 5 def encrypt_password(password, salt=None): 6 """Hash password on the fly.""" 7 if salt is None: 8 salt = os.urandom(8) # 64 bits. 9 10 assert 8 == len(salt) 11 assert isinstance(salt, str) 12 13 if isinstance(password, unicode): 14 password = password.encode('UTF-8') 15 16 assert isinstance(password, str) 17 18 result = password 19 for i in xrange(10): 20 result = HMAC(result, salt, sha256).digest() 21 22 return salt + result
这里先随机生成 64 bits 的 salt,再选择 SHA-256 算法使用 HMAC 对密码和 salt 进行 10 次叠代混淆,最后将 salt 和 hash 结果一起返回。
使用的方法很简单:
1 hashed = encrypt_password('secret password')
下面是验证函数,它直接使用 encrypt_password 来对密码进行相同的单向转换并比较
1 def validate_password(hashed, input_password): 2 return hashed == encrypt_password(input_password, salt=hashed[:8]) 3 4 assert validate_password(hashed, 'secret password')
虽然只有简短几行,但借助 python 标准库帮助,这已经是一个可用于生产环境的高安全密码加密验证算法了。
总结一下用户密码的存储:
- 上善不战而屈人之兵。如果可能不要存任何密码信息 让别人(OpenID)来帮你做事,避开这个问题
- 如果非要自己认证,也只能存 不可逆的有损密码信息 。通过单向 hash 和 salt 来保证只有用户知道密码明文
- 绝对不能存可还原密码原文的信息 。如果因为种种原因一定要可还原密码原文,请使用非对称加密,并保管好私钥