-
Notifications
You must be signed in to change notification settings - Fork 0
/
password-storage-and-python-example.html
173 lines (164 loc) · 14.7 KB
/
password-storage-and-python-example.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<!DOCTYPE html>
<!--[if IE 8]> <html class="no-js lt-ie9" lang="zh-cn"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" lang="zh-cn"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>道可叨 | 用户密码的存储与 Python 示例 </title>
<meta name="viewport" content="width=device-width">
<meta name="author" content="Qiang">
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
<link href="http://localhost:8000/feeds/atom.xml" type="application/atom+xml" rel="alternate" title="道可叨 Atom Feed" />
<link rel="stylesheet" href="https://zhuoqiang.github.io/theme/css/app.css">
<script src="https://zhuoqiang.github.io/theme/js/vendor/custom.modernizr.js"></script>
</head>
<body>
<header>
<hgroup>
<a href="https://zhuoqiang.github.io">
<h1 i18n>道可叨</h1>
<h2>Free Will</h2>
</a>
</hgroup>
</header>
<section>
<article>
<header>
<h1><a href="https://zhuoqiang.github.io/password-storage-and-python-example.html">用户密码的存储与 Python 示例</a></h1>
</header> <div>
<section>
<p>在各种线上应用中,用户名密码是用户身份认证的关键,它的重要性不言而喻。一方面,作为保护用户敏感数据的钥匙来说,一旦被破解,系统将敞开大门完全不设防。另一方面,密码这把钥匙本身就是非常敏感的数据:大多数用户会在不同应用中使用近似甚至完全相同的密码。一旦某一个应用的密码被破解,很可能坏人就此掌握了用户的“万能钥匙”,这个用户的其它应用也相当危险了。</p>
<p>这篇博文就重点讨论对于密码本身的存储的安全性考虑,而系统自身的安全性不在此文的范围之内。</p>
<p>对于如此重要的用户密码,究竟该怎样在系统中存储呢?</p>
<p>“君子不立危墙”,对于用户密码这个烫手山芋,一个极端的选择是系统完全不接触密码,用户的身份认证转交受信任的第三方来处理。例如 OpenID 这样的解决方案。系统向受信任的第三方求证用户身份的合法性,用户通过密码向第三方证明自己的身份。</p>
<p>这样一来,也就不用绞尽脑汁保证密码的安全了。这个作法对用户来说还有个额外的好处:再也不用为每个应用注册帐号了,同一个 OpenID 就可以登录所有支持 OpenID 的系统。</p>
<p>好虽好,可在今天这样一个裂土封国划地为营的网络战国时代,用户资源不但不能牢牢掌握在自己的手上,还要与别人分享,甚至要受制于人,这多少有点让人难以接受。(据称,现在全球有 27000 个 Web Site 支持 OpenID 登录,虽然还在持续增长中,但在茫茫“网海“中无疑还是属于小众)</p>
<p>既然网络大同时代还没来临,大部分应用还是要自己负责用户的认证,那密码该如何存储呢?按照安全性由低到高,有这样几种选择:</p>
<ol class="arabic">
<li><p class="first">密码明文直接存储在系统中</p>
<p>这种方法下密码的安全性比系统本身还低,管理员能查看所有用户的密码明文。除非是做恶意网站故意套取用户密码,否则不要用这种方式</p>
</li>
<li><p class="first">密码明文经过转换后再存储</p>
<p>与直接存储明文的方式没有本质区别,任何知道或破解出转换方法的人都可以逆转换得到密码明文</p>
</li>
<li><p class="first">密码经过对称加密后再存储</p>
<p>密码明文的安全性等同于加密密钥本身的安全性。对称加密的密钥可同时用于加密与解密。一般它会直接出现在加密代码中,破解的可能性相当大。而且系统管理员很可能知道密钥,进而算出密码原文</p>
</li>
<li><p class="first">密码经过非对称加密后再存储</p>
<p>密码的安全性等同于私钥的安全性。密码明文经过公钥加密。要还原密码明文,必须要相应的私钥才行。因此只要保证私钥的安全,密码明文就安全。私钥可以由某个受信任的人或机构来掌管,身份验证只需要用公钥就可以了</p>
<p>实际上,这也是 HTTPS/SSL 的理论基础。这里的关键是 <strong>私钥的安全</strong> ,如果私钥泄露,那密码明文就危险了。</p>
</li>
</ol>
<p>以上 4 种方法的共同特点是可以从存储的密码形式还原到密码明文。</p>
<p>当你忘了用户密码后,网站可以很贴心地通过你注册的 email 提醒你原来的密码是什么,那它肯定就是用了上面的某种方法了。这时候你就得小心了:既然网站能知道密码明文,那网站的工作人员就有可能知道,攻入这个网站的黑客也有了还原你密码明文的可能。</p>
<p>所以密码最好是以不可还原明文的方式来保存。通常利用哈希算法的单向性来保证明文以 <strong>不可还原的有损方式</strong> 进行存储。</p>
<p>这类方法的各个具体操作方式按安全性由低到高依次为:</p>
<ol class="arabic">
<li><p class="first">使用自己独创的哈希算法对密码进行哈希,存储哈希过的值</p>
<p>哈希算法复杂,独创对理论要求很高。一般独创的哈希算法肯定没有公开经过时间检验的算法质量高,天才另算</p>
</li>
<li><p class="first">使用 MD5 或 SHA-1 哈希算法</p>
<p>MD5 和 SHA-1 已破解。虽不能还原明文,但很容易找到能生成相同哈希值的替代明文。而且这两个算法速度较快,暴力破解相对省时,建议不要使用它们。</p>
</li>
<li><p class="first">使用更安全的 SHA-256 等成熟算法</p>
<p>更加复杂的算法增加了暴力破解的难度。但如果遇到简单密码,用彩虹字典的暴力破解法,很快就能得到密码原文</p>
</li>
<li><p class="first">加入随机 salt 的哈希算法</p>
<p>密码原文(或经过 hash 后的值)和随机生成的 salt 字符串混淆,然后再进行 hash,最后把 hash 值和 salt 值一起存储。验证密码的时候只要用 salt 再与密码原文做一次相同步骤的运算,比较结果与存储的 hash 值就可以了。这样一来哪怕是简单的密码,在进过 salt 混淆后产生的也是很不常见的字符串,根本不会出现在彩虹字典中。salt 越长暴力破解的难度越大</p>
<p>具体的 hash 过程也可以进行若干次叠代,虽然 hash 叠代会增加碰撞率,但也增加暴力破解的资源消耗。就算真被破解了,黑客掌握的也只是这个随机 salt 混淆过的密码,用户原始密码依然安全,不用担心其它使用相同密码的应用。</p>
</li>
</ol>
<p>上面这几种方法都不可能得到密码的明文,就算是系统管理员也没办法。对于那些真的忘了密码的用户,网站只能提供重置密码的功能了。</p>
<p>下面的 python 程序演示了如何使用 salt 加 hash 来单向转换密码明文</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">from</span> <span class="nn">hashlib</span> <span class="kn">import</span> <span class="n">sha256</span>
<span class="kn">from</span> <span class="nn">hmac</span> <span class="kn">import</span> <span class="n">HMAC</span>
<span class="k">def</span> <span class="nf">encrypt_password</span><span class="p">(</span><span class="n">password</span><span class="p">,</span> <span class="n">salt</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="sd">"""Hash password on the fly."""</span>
<span class="k">if</span> <span class="n">salt</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">salt</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">urandom</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span> <span class="c1"># 64 bits.</span>
<span class="k">assert</span> <span class="mi">8</span> <span class="o">==</span> <span class="nb">len</span><span class="p">(</span><span class="n">salt</span><span class="p">)</span>
<span class="k">assert</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">salt</span><span class="p">,</span> <span class="nb">str</span><span class="p">)</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">password</span><span class="p">,</span> <span class="n">unicode</span><span class="p">):</span>
<span class="n">password</span> <span class="o">=</span> <span class="n">password</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s1">'UTF-8'</span><span class="p">)</span>
<span class="k">assert</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">password</span><span class="p">,</span> <span class="nb">str</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">password</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">xrange</span><span class="p">(</span><span class="mi">10</span><span class="p">):</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">HMAC</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="n">salt</span><span class="p">,</span> <span class="n">sha256</span><span class="p">)</span><span class="o">.</span><span class="n">digest</span><span class="p">()</span>
<span class="k">return</span> <span class="n">salt</span> <span class="o">+</span> <span class="n">result</span>
</pre></div>
<p>这里先随机生成 64 bits 的 salt,再选择 SHA-256 算法使用 <tt class="docutils literal">HMAC</tt> 对密码和 salt 进行 10 次叠代混淆,最后将 salt 和 hash 结果一起返回。</p>
<p>使用的方法很简单:</p>
<div class="highlight"><pre><span></span><span class="n">hashed</span> <span class="o">=</span> <span class="n">encrypt_password</span><span class="p">(</span><span class="s1">'secret password'</span><span class="p">)</span>
</pre></div>
<p>下面是验证函数,它直接使用 encrypt_password 来对密码进行相同的单向转换并比较</p>
<div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">validate_password</span><span class="p">(</span><span class="n">hashed</span><span class="p">,</span> <span class="n">input_password</span><span class="p">):</span>
<span class="k">return</span> <span class="n">hashed</span> <span class="o">==</span> <span class="n">encrypt_password</span><span class="p">(</span><span class="n">input_password</span><span class="p">,</span> <span class="n">salt</span><span class="o">=</span><span class="n">hashed</span><span class="p">[:</span><span class="mi">8</span><span class="p">])</span>
<span class="k">assert</span> <span class="n">validate_password</span><span class="p">(</span><span class="n">hashed</span><span class="p">,</span> <span class="s1">'secret password'</span><span class="p">)</span>
</pre></div>
<p>虽然只有简短几行,但借助 python 标准库帮助,这已经是一个可用于生产环境的高安全密码加密验证算法了。</p>
<p>总结一下用户密码的存储:</p>
<ul class="simple">
<li>上善不战而屈人之兵。如果可能不要存任何密码信息 让别人(OpenID)来帮你做事,避开这个问题</li>
<li>如果非要自己认证,也只能存 <strong>不可逆的有损密码信息</strong> 。通过单向 hash 和 salt 来保证只有用户知道密码明文</li>
<li><strong>绝对不能存可还原密码原文的信息</strong> 。如果因为种种原因一定要可还原密码原文,请使用非对称加密,并保管好私钥</li>
</ul>
<p>说完了密码的存储,后面有空会接着聊聊 <a class="reference external" href="https://zhuoqiang.github.io/password-transport.html">密码的传输</a></p>
</section>
</div>
<footer>
<p>
<time datetime="2010-06-13T02:31:00+08:00" pubdate>2010-06-13</time><a href="https://zhuoqiang.github.io/category/programming.html"><span class="category" i18n>programming</span></a>
</p>
<ul>
<li><a href="https://zhuoqiang.github.io/tag/security.html"><span class="tag" i18n>security</span></i></a>
<li><a href="https://zhuoqiang.github.io/tag/python.html"><span class="tag" i18n>python</span></i></a>
<li><a href="https://zhuoqiang.github.io/tag/web.html"><span class="tag" i18n>web</span></i></a>
</ul>
</footer><link rel="stylesheet" href="https://unpkg.com/gitalk/dist/gitalk.css">
<script src="https://unpkg.com/gitalk@latest/dist/gitalk.min.js"></script>
<div id="gitalk-container"></div>
<script type="text/javascript">
var gitalk = new Gitalk({
clientID: "2f60008869581fd61d19",
clientSecret: "6b303dcf58c04cd35782bae82540921663cbfda8",
repo: "zhuoqiang.github.com",
owner: "zhuoqiang",
admin: ["zhuoqiang"],
id: "password-storage-and-python-example",
title: "用户密码的存储与 Python 示例",
perPage: 30,
distractionFreeMode: true,
pagerDirection: "last",
});
gitalk.render('gitalk-container');
</script></article>
</section>
<footer>
<p>@ <a href="mailto:zhuo.qiang@gmail.com"><span i18n>Qiang</span></a> | <span class="heart"><a href="https://github.com/getpelican/pelican">Pelican</a> & <a href="https://bitbucket.org/zhuoqiang/jing">Jing</a></span></p>
</footer>
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.3.min.js"></script>
<script>window.jQuery || document.write('<script src="https://zhuoqiang.github.io/theme/js/vendor/jquery-1.8.3.min.js"><\/script>')</script>
<script src="https://zhuoqiang.github.io/theme/js/vendor/moment.min.js"></script>
<script src="https://zhuoqiang.github.io/theme/js/vendor/moment-lang.min.js"></script>
<script src="https://zhuoqiang.github.io/theme/js/vendor/i18next-1.6.0.min.js"></script>
<script src="https://zhuoqiang.github.io/theme/js/main.js"></script>
<script type="text/javascript">
var GoSquared = {};
GoSquared.acct = "GSN-743443-H";
(function(w){
function gs(){
w._gstc_lt = +new Date;
var d = document, g = d.createElement("script");
g.type = "text/javascript";
g.src = "//d1l6p2sc9645hc.cloudfront.net/tracker.js";
var s = d.getElementsByTagName("script")[0];
s.parentNode.insertBefore(g, s);
}
w.addEventListener ?
w.addEventListener("load", gs, false) :
w.attachEvent("onload", gs);
})(window);
</script>
</body>
</html>