用户密码的存储与 Python 示例
在各种线上应用中,用户名密码是用户身份认证的关键,它的重要性不言而喻。一方面,作为保护用户敏感数据的钥匙来说,一旦被破解,系统将敞开大门完全不设防。另一方面,密码这把钥匙本身就是非常敏感的数据:大多数用户会在不同应用中使用近似甚至完全相同的密码。一旦某一个应用的密码被破解,很可能坏人就此掌握了用户的“万能钥匙”,这个用户的其它应用也相当危险了。
这篇博文就重点讨论对于密码本身的存储的安全性考虑,而系统自身的安全性不在此文的范围之内。
对于如此重要的用户密码,究竟该怎样在系统中存储呢?
“君子不立危墙”,对于用户密码这个烫手山芋,一个极端的选择是系统完全不接触密码,用户的身份认证转交受信任的第三方来处理。例如 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 来单向转换密码明文
import os
from hashlib import sha256
from hmac import HMAC
def encrypt_password(password, salt=None):
"""Hash password on the fly."""
if salt is None:
salt = os.urandom(8) # 64 bits.
assert 8 == len(salt)
assert isinstance(salt, str)
if isinstance(password, unicode):
password = password.encode('UTF-8')
assert isinstance(password, str)
result = password
for i in xrange(10):
result = HMAC(result, salt, sha256).digest()
return salt + result
这里先随机生成 64 bits 的 salt,再选择 SHA-256 算法使用 HMAC 对密码和 salt 进行 10 次叠代混淆,最后将 salt 和 hash 结果一起返回。
使用的方法很简单:
hashed = encrypt_password('secret password')
下面是验证函数,它直接使用 encrypt_password 来对密码进行相同的单向转换并比较
def validate_password(hashed, input_password):
return hashed == encrypt_password(input_password, salt=hashed[:8])
assert validate_password(hashed, 'secret password')
虽然只有简短几行,但借助 python 标准库帮助,这已经是一个可用于生产环境的高安全密码加密验证算法了。
总结一下用户密码的存储:
- 上善不战而屈人之兵。如果可能不要存任何密码信息 让别人(OpenID)来帮你做事,避开这个问题
- 如果非要自己认证,也只能存 不可逆的有损密码信息 。通过单向 hash 和 salt 来保证只有用户知道密码明文
- 绝对不能存可还原密码原文的信息 。如果因为种种原因一定要可还原密码原文,请使用非对称加密,并保管好私钥
说完了密码的存储,后面有空会接着聊聊 密码的传输
密码传输问题
说了 密码的存储 问题,接下来再聊聊密码的传输问题。
对于在线系统,密码的传输要经过下面几个步骤:
- 用户在浏览器中输入原始密码:键盘 ——> 操作系统 ——> 浏览器内存
- 程序对原始密码进行转换:内存中的原始密码 ——> 内存中的转换后的密码
- 转换后的密码在线上传输:内存中转换后的密码 ——> 网络 ——> 系统
这其中每一步都可能泄露原始密码,当然也有相应的保护措施。
密码输入
千里之行始于足下,用户输入密码这第一步往往是最危险的。常用的攻击方法包括:
-
偷看输入的密码
在公共场合输入密码很容易被偷看,例如使用 ATM 机取款的时候。输入密码时密码明文用 * 代替就是为了防止偷窥。但这样正常用户也不能直接用眼睛确认输入密码是否正确,通常在设置新密码时就要输入两遍来确保输入无误。iPhone 在这点做了改进,每输入一个密码字符先显示半秒钟的明文再转成 * 显示,鉴于使用 iPhone 虚拟键盘输入时,按错键的概率还是比较高的,这个折中也是在可用性和安全性上做了妥协。还有些系统为了最大限度的防偷窥,在输入密码时屏幕没有任何输出,比如 Unix/Linux 的命令行登录界面。这样就连输入的密码长度都看不出来。
-
用木马程序记录键盘输入
现在比较流行的 QQ 或网络游戏的盗号就常用这种方式进行。安装杀毒软件来防盗号自不必说,还可以用屏幕软键盘输入密码,这样木马就记录不到键盘事件,只能通过分析鼠标点击和当时屏幕图象来破解密码。如果再进一步,软键盘的字符布局每次都随计产生,那就更加重了分析破解的难度。
-
感染应用程序或使用钓鱼手法,直接得到内存中的密码值
不管如何防范输入的过程,一旦密码到程序里,就会以明文的形式呈现在内存中,只要恶意软件模仿安全程序(或模仿网站的外观)直接套取密码就轻而易举。现在出现的假 ATM 机诈骗也是这种手法的衍生。还有一种,不是替换或模仿程序,而是用病毒感染原程序将内存中的值读到。要防范这种攻击,必须要对原程序的完整性和合法性进行验证,只有在验证通过后,才能进行正常的登录交互操作。这个验证可以用数字签名来实现。比如 Windows 7 中所有微软的可执行文件都带有微软的数字签名。在网站上则是 HTTPS 的验证。当然,这个验证过程还牵扯到人的判断,在社会工程学上,软件要配合一些强制的措施,才能保证人不会麻痹大意中招。比如浏览器在访问非信任机构签发的数字签名的 HTTPS 站点时,会警告并且阻止用户进行访问。Windows 7 现在所有的驱动程序也都必须要有微软的数字签名才能运行。
密码转换
原始密码会经过一些转换,才能在线上传输。这跟密码的存储类似。直接传输密码明文是最不安全的。而用简单的可逆变换,或者固定密钥加密也只是增加了破解难度。最好是每次服务器随机产生一个密钥,送给客户端进行加密。
如果使用 HTTPS,那所有通过 SSL 通道的信息都经过了随机密钥加密。自然也包括了密码。HTTPS 虽然安全,可它最大的问题是性能。连接初始时密钥的协商是通过非对称加密的体系进行的,这会造成连接较慢(密钥协商好后的数据加密是纯耗 CPU 的工作,在现在的硬件条件下,并不是瓶颈)。金融在线系统一般都使用 HTTPS ,但大部分在线应用出于性能的考虑,会选择使用 HTTP 交换随机密码的方式。
随机密钥由服务器生成并发送给客户端。客户端用此密钥将密码加密,送给服务器。这里不要求加密方法是可逆的。一个较安全的做法是客户端使用 MD5 或 SHA-1 算法对密码进行不可逆转换,再用密钥加密送到 Server。现在已经有很多 Javascript 的加密库可以在浏览器端进行这样的转换工作。
密码在线传输
如果只使用 HTTP 而不使用 HTTPS,那就算密码不被攻破,还是有可能发生重放攻击。当中间人截获了转换后的密码后,他不必知道密码明文就可以用转换后的密码通过服务器的认证。
现在最新的研究是利用量子力学所揭示的粒子对的超距相关性来进行量子加密传输。可以类比古代密信的火漆封口,一旦信件被拆开,火漆肯定被破坏。收信人就会知道。量子加密很耗资源,是为了军事等绝密级别信息传输准备的技术。用于量子加密传输的信息也只会是密钥。一旦双方确认了彼此的密钥,就可以使用普通通道来传输加密后的密文了。看上去量子加密传输很象终极解决方案,可最近也传出了针对量子加密的成功攻击的 案例 。
常用服务分析
这里用抓包方式分析一下常用的网络服务的密码传输,看看它们在安全性方面做的如何
网站 | 密码传输方式 | 安全性 |
---|---|---|
bitbucket.org | HTTPS 加密传输 | 高 |
微软 live.com | HTTPS 加密传输 | 高 |
google.com | HTTPS 加密传输 | 高 |
开心网 kaixin001.com | HTTP Javascript 加密传输 | 中 |
西祠 xici.com | HTTP Javascript 加密传输 | 中 |
csdn.net | HTTP Javascript 加密传输 | 中 |
javaeye.com | HTTP 明文传输 | 低 |
天涯 tianya.cn | HTTP 明文传输 | 低 |
人人网 renren.com | HTTP 明文传输 | 低 |
对那些既不支持 HTTPS 又不经过客户端加密,而是直接使用 HTTP 明文传送密码的网站,建议不要使用常用的密码来注册,避免安全隐患。
小结
密码的传输比 密码的存储 更加敏感和不安全,大致有三个层次的传输策略:
- 使用 HTTPS 加密传输,非常安全。HTTPS 对服务器性能要求高,也影响登录速度。一般用在高安全性的登录上面。Google 和微软的登录都强制使用 HTTPS 确保安全第一
- 使用随机密钥对密码进行变换后再传输,相对安全。密码明文很安全,但仍可能发生重放攻击。这种方式是性能和安全性的折中。一般的服务使用足亦,例如国内的开心网
- 不做任何修饰,直接将密码明文通过 HTTP 传输。这种方式实现起来非常简单,但却是对用户隐私和数据的不负责任。很可惜,国内几个著名网站都是采用这种简单方式。用户的应对之道就是不要在这些网站上使用常用的密码,例如你银行卡的密码。
其它
密码在传输过程中的泄露的途径很多,你很可能完全没有意识到密码正被窃听。比如最近的一个新闻,骗子用软件对电话按键音进行音频分析,进而得到用户的密码。大概我们在用电话银行时都没有想到,按键的声音居然也是我们密码传输的一种载体吧。
最近在使用浦发银行的 400 电话服务时,惊奇地发现当系统提示输入密码时,除了听到自己的按键音外,听筒里还有其它的按键音随机的响起。因为这些背景音的干扰,居然让人输入密码时有点手足无措(声音也是种 UI 界面,只是一直被忽略,其实它对用户的重要性一点也不比图形 UI 来的低)
略一思考,这不正是防止电话按键音泄露银行密码的安全措施吗。浦发连这点都想到了,真是魔高一层道高一丈!
来源:http://zhuoqiang.me/password-storage-and-python-example.html