密码哈希
目录
本文从低级到高级简单介绍一些开发中存储用户密码的方案
背景
在开发中,我们经常需要存储用户的密码,比如用户在注册时,我们需要将用户的密码存储在数据库中,以便用户在登录时进行验证
这样的用户管理方式导致了一个问题,任何人一旦获取到了用户和密码,就相当于拥有了用户的所有资源
黑客当然不会放过这样的机会
由于直接攻击服务难度太大(比如我很难攻击银行的系统往我账户上添加 50 块钱,但是我可以通过盗取你的账户往我账户里打 50 块钱),并且数据库系统一般是独立的系统(只需要能读取数据库内容就可以,读取一般比修改更容易),所以数据库便成为了重点攻击对象
黑客通常在攻击服务器时,一旦获取到数据库,就可以获取到用户的密码,从而进行下一步的攻击活动(比如 V 我 50)
总之黑客获取到用户密码是容易发生且危险的事情
目标
所以我们需要一种方式,阻断或者延缓黑客获取用户密码的行为
密码存储的方式
这里重点讨论通过更改密码在数据库的存储方式来达到目标
明文存储
最弱的方式,就是直接将用户密码以明文的方式存储在数据库中,这是最不安全的方式
比如:
用户名 | 平台 A 密码 |
---|---|
user1 | 123 |
user2 | cxxy |
user3 | 123 |
黑客一旦获取到了数据库,便可以以用户的身份登录到平台 A
更糟糕的是,黑客可以利用撞库
的方法,来登陆该用户在其他平台的帐号,造成更多的损失
由于用户常常在多个平台使用相同的密码,所以一旦黑客获取到了用户在一个平台的密码,就可以尝试在其他平台使用相同的密码进行登录,这种攻击方式叫做
撞库
密钥加密
将用户密码通过密钥加密后存储在数据库中,这种方式比明文存储要安全一些,但是对于攻击了数据库的黑客来说,拿到密钥不是特别难
因为黑客一般是先攻击服务器,然后才拿到数据库的,所以拿到密钥并不难,能拖延的时间也不是很长
比如设计一个简单的算法,将用户的密码每一位加上密钥的对应位的值,然后存储在数据库中
设定密钥为2
用户名 | 用户设定密码 | 加密后密码 |
---|---|---|
user1 | 123 | 345 |
user2 | cxxy | ezz{ |
user3 | 123 | 345 |
使用 user1
- 345
这样的密码无法登陆平台 A,但只要知道加密算法和密钥,就可以轻松解密
Hash 加密
将用户密码通过 hash 函数加密后存储在数据库中,这种方式是目前最常见的密码存储方式,这样黑客拿到数据库后,也无法直接获取到用户的密码
hash 加密函数是一种单向的不可逆的加密方式,即无法通过结果的 hash 值反推出原始密码
但是相同的字符串经过相同的 hash 函数加密后得到的结果是相同的
以 md5 加密为例
用户名 | 用户设定密码 | md5 加密后密码 |
---|---|---|
user1 | 123 | ac59075b964b0715 |
user2 | cxxy | 3997966d4f5c3d44 |
user3 | 123 | ac59075b964b0715 |
黑客就算获取了数据库,也无法登陆用户的账户,因为无法通过 md5 加密后的密码反推出原始密码
当用户登陆时,我们将用户输入的密码进行 md5 加密,然后和数据库中的 md5 值进行比对,如果相同,则验证通过
但是道高一尺,魔高一丈,黑客也有自己的方法来解决问题
黑客可以解析代表密码的 hash 值,如果有大量的相同的 hash 值,那么代表这个系统可能有一个默认密码,便可以开始尝试常用默认密码(比如123
,admin123
)
在这个案例中,默认密码是123
,所以黑客可以通过尝试123
来登陆用户的账户,虽然user2
的账户安全了,但是user1
和 user3
的账户就有风险了
一般在这样的攻击中,黑客的重点不是破解某个账户的密码,而是获取更多的账户密码,那肯定是挑软柿子捏
在此之上,还有一些更高级的攻击方式:
- 彩虹表:由于 hash 函数的特点,以及目前 hash 函数就那么几个。黑客便可以提前生成一张常见密码对于的 hash 值表,等获取到数据库之后便可以更快的破解密码
- 字典攻击:相比于无序的字母组合,用户更倾向于选择含有意思的单词作为密码或作为密码的一部分,黑客可以通过字典中的常见词组合来生成彩虹表,无序字符串的组合比有序字符串的组合要少很多很多很多,字典攻击可以增加破解密码的范围
加盐 Hash
正对彩虹表以及字典攻击,开发者们想到了加盐 hash
通过在密码原文中加入一个随机字符串,然后再进行 hash,这样即使 2 人有相同的密码,由于加入了不同的盐,hash 值也是不同的
比如我们在每个人的密码后面加上一个随机字符串,然后再进行 md5 加密,这样即使两个人的密码相同,由于加入了不同的盐,hash 值也是不同的
用户名 | 用户设定密码 | 盐 | md5 加密后密码 |
---|---|---|---|
user1 | 123 | 114514 | e07bbfb1f39f0c38 |
user2 | cxxy | 1919810 | 718ab3701df3e708 |
user3 | 123 | 810 | 2e3b58f53ab497c6 |
彩虹表和字典攻击虽然还能用,但效率相比于之前大大降低了(需要针对每个盐生成彩虹表)
但无论如何 salt(盐) 是需要存储在数据库中的,黑客拿到数据库后,也可以拿到 salt,可以通过暴力的方式来破解密码
由于传统 hash 的设计计算速度很快,加上现在的 GPU 计算能力加快,加盐 hash 能拖延的时间不是很长
BCrypt
在这样的背景下,出现了 BCrypt,它是一种慢 hash 函数,它的设计是为了拖延黑客的破解速度而非简单阻止黑客破解密码,因此它被设计为可以手动调整快慢的 hash 函数
底层原理可以简单理解为将 salt 和密码轮流作为 hash + salt 进行多次加密(用 salt 加密密码,然后用加密后的密码加密 salt,然后再用加密后的 salt 加密密码,然后再用加密后的密码加密的 salt...),并且可以通过设置 cost 参数来调整加密的次数,从而减慢加密的速度
这里加密使用的一般是 Blowfish 算法,与上面的加密算法不同的是,Blowfish 是一种对称加密算法,通过 salt 和加密后的字符串可以还原加密前的字符串
这样即使黑客拿到了数据库,也需要花费大量的时间来破解密码,收益与回报不成正比,倒逼黑客放弃这攻击方式
BCrypt 不需要要额外记录 salt,因为它会自动将 salt 保存在 hash 值中,一个典型的案例如下
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
\__/\/ \____________________/\_____________________________/
Alg Cost Salt Hash
这里的 salt 是随机生成的最初的 salt(如果是最终 salt 的话是可以解除原密码的),当用户输入密码时,bcrypt 会从 hash 中提取 salt,然后用这个 salt 和用户输入的密码进行 hash,然后再和 hash 值进行比对
这样的加密方式在包含了上述加密方式的优点情况下,还能拖延黑客的破解速度
在实际情况中,企业也不是白痴,当发现数据库被攻击后,可以选择提醒用户更改密码,或者提前对资源进行保护(比如引入多因素验证),BCrypt 拖延的时间能让企业有更多的时间来应对黑客攻击
不过慢 hash 也是一把双刃剑,因为它的计算速度慢,所以在验证密码时也会比较慢,并会对服务器造成一些压力,所以在设置 cost 时需要权衡利弊
参考资料: