密码
加盐 hash 保存密码的正确方式
背景
- 大多数的
web
开发者都会遇到设计用户账号系统的需求。账号系统最重要的一个方面就是如何保护用户的密码。 - 一些大公司的用户数据库泄露事件也时有发生,所以我们必须采取一些措施来保护用户的密码,即使网站被攻破的情况下也不会造成较大的危害。
- 保护密码最好的的方式就是使用带盐的密码
hash
(salted password hashing
).对密码进行hash
操作是一件很简单的事情,但是很多人都犯了错。 - 接下来我希望可以详细的阐述如何恰当的对密码进行
hash
,以及为什么要这样做。
什么是 hash
1 |
|
Hash
算法是一种单向的函数。它可以把任意数量的数据转换成固定长度的“指纹”,这个过程是不可逆的。而且只要输入发生改变,哪怕只有一个bit
,输出的hash
值也会有很大不同。- 这种特性恰好合适用来用来保存密码。因为我们希望使用一种不可逆的算法来加密保存的密码,同时又需要在用户登陆的时候验证密码是否正确。
- 其实
hash
也算是一种加密算法,因为他是把明文信息转换成了”密文”(他人无法理解的信息),相较于真正意义上的加密算法而言,hash
是不可逆的罢了。
使用 hash 的账号系统
在一个使用 hash
的账号系统中,用户注册和认证的大致流程如下:
1 |
|
- 步骤
4
的时候不要告诉用户是账号还是密码错了。只需要显示一个通用的提示,比如账号或密码不正确就可以了。这样可以防止攻击者枚举有效的用户名。 - 还需要注意的是用来保护密码的 hash 函数跟数据结构课上见过的 hash 函数不完全一样。比如实现hash表的hash函数设计的目的是快速,但是不够安全。
- 只有加密
hash
函数(cryptographic hash functions
)可以用来进行密码的hash
。这样的函数有SHA256
,SHA512
,RipeMD
,WHIRLPOOL等
。 - 一个常见的观念就是密码经过
hash
之后存储就安全了。这显然是不正确的。有很多方式可以快速的从hash
恢复明文的密码。还记得那些md5破解网站吧,只需要提交一个hash
,不到一秒钟就能知道结果。 - 显然,单纯的对密码进行
hash
还是远远达不到我们的安全需求。下一部分先讨论一下破解密码hash
,获取明文常见的手段。
如何破解hash
字典和暴力破解攻击(Dictionary and Brute Force Attacks)
- 字典攻击是将常用的密码,单词,短语和其他可能用来做密码的字符串放到一个文件中,然后对文件中的每一个词进行hash,将这些hash与需要破解的密码hash比较。
- 这种方式的成功率取决于密码字典的大小以及字典的是否合适。
1
2
3
4
5
6
7
8Dictionary Attack
Trying apple : failed
Trying blueberry : failed
Trying justinbeiber : failed
...
Trying letmein : failed
Trying s3cr3t : success! - 暴力攻击就是对于给定的密码长度,尝试每一种可能的字符组合。这种方式需要花费大量的计算机时间。
- 但是理论上只要时间足够,最后密码一定能够破解出来。只是如果密码太长,破解花费的时间就会大到无法承受。
1
2
3
4
5
6
7
8Brute Force Attack
Trying aaaa : failed
Trying aaab : failed
Trying aaac : failed
...
Trying acdb : failed
Trying acdc : success!查表破解(Lookup Tables)
- 对于特定的hash类型,如果需要破解大量hash的话,查表是一种非常有效而且快速的方式。它的理念就是预先计算(pre-compute)出密码字典中每一个密码的hash。
- 然后把hash和对应的密码保存在一个表里。一个设计良好的查询表结构,即使存储了数十亿个hash,每秒钟仍然可以查询成百上千个hash。
1
2
3
4
5#!python
c11083b4b0a7743af748c85d343dfee9fbb8b2576c05f3a7f0d632b0926aadfc
08eac03b80adc33dc7d8fbe44b7c7b05d3a2c511166bdb43fcb710b03ba919e7
e4ba5cbd251c98e6cd1c23f126a3b81d8d8328abc95387229850952b3ef9f904
5206b8b8a996cf5320cb12ca91c7b790fba9f030408efe83ebb83548dc3007bd反向查表破解(Reverse Lookup Tables)
- 这种方式可以让攻击者不预先计算一个查询表的情况下同时对大量
hash
进行字典和暴力破解攻击。 - 首先,攻击者会根据获取到的数据库数据制作一个用户名和对应的
hash
表。 - 然后将常见的字典密码进行 hash 之后,跟这个表的 hash 进行对比,就可以知道用哪些用户使用了这个密码。这种攻击方式很有效果,因为通常情况下很多用户都会有使用相同的密码。
1
2
3
4
5
6#!bash
Searching for hash(apple) in users' hash list... : Matches [alice3, 0bob0, charles8]
Searching for hash(blueberry) in users' hash list... : Matches [usr10101, timmy, john91]
Searching for hash(letmein) in users' hash list... : Matches [wilson10, dragonslayerX, joe1984]
Searching for hash(s3cr3t) in users' hash list... : Matches [bruce19, knuth1337, john87]
Searching for hash(z@29hjja) in users' hash list... : No users used this password彩虹表 (Rainbow Tables)
彩虹表是一种使用空间换取时间的技术。跟查表破解很相似。只是它牺牲了一些破解时间来达到更小的存储空间的目的。因为彩虹表使用的存储空间更小,所以单位空间就可以存储更多的hash。彩虹表已经能够破解8位长度的任意md5hash。
加盐(Adding Salt)
- 查表和彩虹表的方式之所以有效是因为每一个密码的都是通过同样的方式来进行 hash 的。如果两个用户使用了同样的密码,那么一定他们的密码hash也一定相同。我们可以通过让每一个hash随机化,同一个密码hash两次,得到的不同的hash来避免这种攻击。
- 具体的操作就是给密码加一个随即的前缀或者后缀,然后再进行hash。这个随即的后缀或者前缀成为“盐”。正如上面给出的例子一样,通过加盐,相同的密码每次 hash 都是完全不一样的字符串了。检查用户输入的密码是否正确的时候,我们也还需要这个盐,所以盐一般都是跟hash一起保存在数据库里,或者作为hash字符串的一部分。
- 盐不需要保密,只要盐是随机的话,查表,彩虹表都会失效。因为攻击者无法事先知道盐是什么,也就没有办法预先计算出查询表和彩虹表。如果每个用户都是使用了不同的盐,那么反向查表攻击也没法成功。
1
2
3
4
5#!python
hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab
hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007盐的错误使用方式
短的盐
- 如果盐的位数太短的话,攻击者也可以预先制作针对所有可能的盐的查询表。比如,3位ASCII字符的盐,一共有95x95x95 = 857,375种可能性。看起来好像很多。假如每一个盐制作一个1MB的包含常见密码的查询表,857,375个盐才是837GB。现在买个1TB的硬盘都只要几百块而已。
- 基于同样的理由,千万不要用用户名做为盐。虽然对于每一个用户来说用户名可能是不同的,但是用户名是可预测的,并不是完全随机的。攻击者完全可以用常见的用户名作为盐来制作查询表和彩虹表破解hash。
- 根据一些经验得出来的规则就是盐的大小要跟 hash 函数的输出一致。比如,SHA256的输出是256bits(32bytes),盐的长度也应该是32个字节的随机数据。
盐的复用
- 不管是将盐硬编码在程序里还是随机一次生成的,在每一个密码hash里使用相同的盐会使这种防御方法失效。
- 因为相同的密码hash两次得到的结果还是相同的。攻击者就可以使用反向查表的方式进行字典和暴力攻击。只要在对字典中每一个密码进行hash之前加上这个固定的盐就可以了。
- 如果是流行的程序的使用了硬编码的盐,那么也可能出现针对这种程序的这个盐的查询表和彩虹表,从而实现快速破解hash。
- 用户每次创建或者修改密码一定要使用一个新的随机的盐
盐的正确使用方式
- 每一个用户,每一个密码都要使用不同的盐。
- 用户每次创建账户或者修改密码都要使用一个新的随机盐。永远不要重复使用盐。
- 盐的长度要足够,一个经验规则就是盐的至少要跟hash函数输出的长度一致。
- 盐应该跟hash一起存储在用户信息表里。
- 盐要使用密码学上可靠安全的伪随机数生成器(Cryptographically Secure Pseudo-Random Number Generator (CSPRNG))来产生。
- 存储一个密码:
1
2
3使用CSPRNG生成一个长的随机盐。
将密码和盐拼接在一起,使用标准的加密hash函数比如SHA256进行hash。
将盐和hash记录在用户数据库中 - 验证一个密码:
1
2
3从数据库中取出用户的盐和 hash
将用户输入的密码和盐按相同方式拼接在一起,使用相同的hash函数进行hash
比较计算出的hash跟存储的hash是否相同。如果相同则密码正确。反之则密码错误。 - 在web应用中,要在服务端进行hash
- 使用慢速hash函数让破解更加困难
1
2
3
4加盐可以让攻击者无法使用查表和彩虹表的方式对大量hash进行破解。但是依然无法避免对单个hash的字典和暴力攻击。
高端的显卡(GPUs)和一些定制的硬件每秒可以计算数十亿的hash,所以针对单个hash的攻击依然有效。为了避免字典和暴力攻击,我们可以采用一种称为key扩展(key stretching)的技术。
思路就是让hash的过程便得非常缓慢,即使使用高速GPU和特定的硬件,字典和暴力破解的速度也慢到没有实用价值。通过减慢hash的过程来防御攻击,但是hash速度依然可以保证用户使用的时候没有明显的延迟。
key扩展的实现是使用一种大量消耗cpu资源的hash函数。不要去使用自己创造的迭代hash函数,那是不够的。要使用标准算法的hash函数,比如PBKDF2或者bcrypt。 - 理论上不可能破解的hash:使用加密的key和密码hash硬件
1
2
3
4
5
6只要攻击者能够验证一个猜测的密码是正确还是错误,他们都可以使用字典或者暴力攻击破解hash。
更深度的防御方法是加入一个保密的key(secret key)进行hash,这样只有知道这个key的人才能验证密码是否正确。这个可以通过两种方式来实现。
一种是hash通过加密算法加密比如AES,或者使用基于key的hash函数(HMAC)。
这个实现起来并不容易。key一定要做到保密,即使系统被攻破也不能泄露才行。
但是如果攻击者获取了系统权限,无论key保存在哪里,都可能被获取到。
所以这个key一定要保存在一个外部系统中,比如专门用来进行密码验证的物理隔离的服务器。或是使用安装在服务器上特殊硬件。
hash 碰撞
- 因为hash函数是将任意数量的数据映射成一个固定长度的字符串,所以一定存在不同的输入经过hash之后变成相同的字符串的情况。
- 加密hash函数(Cryptographic hash function)在设计的时候希望使这种碰撞攻击实现起来成本难以置信的高。
- 但时不时的就有密码学家发现快速实现hash碰撞的方法。最近的一个例子就是MD5,它的碰撞攻击已经实现了。
- 碰撞攻击是找到另外一个跟原密码不一样,但是具有相同hash的字符串。但是,即使在相对弱的hash算法,比如MD5,要实现碰撞攻击也需要大量的算力(computing power),所以在实际使用中偶然出现hash碰撞的情况几乎不太可能。一个使用加盐MD5的密码hash在实际使用中跟使用其他算法比如SHA256一样安全。
- 不过如果可以的话,使用更安全的hash函数,比如SHA256, SHA512, RipeMD, WHIRLPOOL等是更好的选择。
经常提问的问题
当用户忘记密码的时候我应该怎样让他们重置?
- 在我个人看来现在外面广泛使用的密码重置机制都是不安全的,如果你有很高的安全需求,比如重要的加密服务,那么不要让用户重置他们的密码。
- 大多数网站使用绑定的email来进行密码找回。通过生成一个随机的只使用一次的token,这个token必须跟账户绑定,然后把密码重置的链接发送到用户邮箱中。当用户点击密码重置链接的时候,提示他们输入新的密码。需要注意token一定要绑定到用户以免攻击者使用发送给自己的token来修改别人的密码。
- token一定要设置成15分钟后或者使用一次后作废。当用户登陆或者请求了一个新的token的时候,之前发送的token都作废也是不错的主意。如果token不失效的话,那么就可以用来永久控制这个账户了。
- Email(SMTP)是明文传输的协议,而互联网上可能有很多恶意的路由器记录email流量。并且用户的email账号也可能被盗。使token尽可能快的失效可以降低上面提到的这些风险。
- 用户可能尝试去修改token,所以不要在token里存储任何账户信息。token应该是一个不能被预测的随机的二进制块(binary blob),仅仅用来进行识别的一条记录。
- 永远不要通过email发送用户的新密码。记得用户重置密码的时候要重新生成盐,不要使用之前旧密码使用的盐。
我应该使用什么hash算法?
可以使用
- 经过充分测试的加密hash函数,比如SHA256, SHA512, RipeMD, WHIRLPOOL, SHA3等
- 设计良好的key扩展hash算法,比如PBKDF2,bcrypt,scrypt
- crypt的安全版本。($2y$, $5$, $6$)
不要使用
- 过时的hash函数,比如MD5,SHA1
- crypt的不安全版本。($1$, $2$, $2x$, $3$)
- 任何自己设计的算法。
如果攻击者获取了数据库权限,他不能直接替换hash登陆任意账户么?
- 当然,不过如果他已经或得了数据库权限,很可能已经可以获得服务器上的所有信息了。所以没有什么必要去修改hash登陆别人账户。
- 进行密码hash的目的不是保护网站不被入侵,而是如果入侵发生了,可以更好的保护用户的密码。
为什么要讨论这么多关于hash的东西?
用户在你的网站上输入密码,是相信你的安全性。如果你的数据库被黑了。而用户密码又没有恰当的保护,那么恶意的攻击者就可以利用这些密码尝试登陆其他的网站和服务。进行撞库攻击。(很多用户在所有的地方都是使用相同的密码)这不仅仅是你的网站安全,是你的所有用户的安全。你要对你用户的安全负责。
密码
http://mybestcheng.site/2022/11/20/authenticate/password/