关于密码,为什么加盐更安全?怎么保证密码存储安全?
首先,hash是不可逆的,正常情况下,不能通过哈希值直接反推出原始密码,因为哈希函数是单向函数,不具备可逆性。不过,攻击者总有办法。
第一,关于攻击
攻击者想攻击你的密码,需要满足这两个前提:
1)他能拿到你的密码的哈希编码
2)他知道你的加密算法
对于1),它可以攻克你的数据库或者其他方法来拿到,比如有这些:
泄露路径 | 描述 |
---|---|
数据库泄露 | 攻击者通过SQL注入 [1]、数据库权限提升、备份泄露等方式,获取整个用户表,其中可能包含哈希值和 salt |
Web日志泄露 | 登录接口或调试输出中不小心打印了哈希值(比如服务端日志或响应报错) |
Git泄露 | 开发者把测试用的哈希值写死在代码,并提交到了公开仓库 |
后端接口不当 | 某些接口返回用户信息时把密码哈希值也顺带返回了 |
缓存服务被攻破 | 比如 Redis、Memcached 中存了密码摘要或完整用户信息 |
内鬼行为 | 公司内部人员或离职员工故意导出或泄漏哈希数据 |
对于2),它可以有这些方法:
泄露路径 | 描述 |
---|---|
代码泄露 | 比如github公开仓库、apk/ipa/exe都可以反编译、内部泄露 |
从已知哈希看出 | 常用的hash算法就那几种,一看大概样子就知道是哪个了 [2] |
有了这两个前提之后,攻击者就可以攻击你的密码了,主要是这两个方法:
1)暴力破解
2)彩虹表、字典表
对于1)暴力破解,为了存储方便,一般hash算法是超级快的,比如MD5、SHA-256,每秒可以进行上百万次加密。所以利用GPU完全可以暴力破解,找到有相同hash的就是密码。
对于2)彩虹表/字典表,彩虹表就是存了对这些hash算法的已知密码和对应hash的一张表,拿到hash之后就可以在表里直接找到对应密码;字典表就是存了一些常用密码,比如123456,这种破解效率超级高。
第二,关于后端存储
设计理念应该是默认攻击者知道你所有的代码、算法逻辑,然后依然能保证密码不被反推出。
首先,把密码明文存在数据库是绝对不可取的;
其次,根据上面所说,如果对安全性有高要求,存md5这种简单hash也不行;
不过,可以有这些方法:
1)加盐(salt)
2)慢加密
3)双重验证、动态口令
对于1),就是在使用一般hash算法加密的时候,不直接给密码加密,而是先给密码加一个附加内容,然后再算hash,这样彩虹表就失效了,只能针对你的系统重新建彩虹表;而如果这个附加内容是完全随机的,那么对于完全相同的两个密码,加盐加密后的hash就完全不同了,这样彩虹表就完全失效了。他只能暴力破解,而密码+盐,破解起来就比较费时了。对于每个密码都会有一个随机盐值,跟着hash一起存在数据库里。
(图片来自阿里云开发者社区https://developer.aliyun.com/article/1618090)
对于2),最好是先加盐,然后配合慢加密,就是加密一次要比较久,这样他暴力破解就得更加耗时了。变成了很难完成的任务。这里有一些经过互联网检验的加密算法:
算法 | 特点 |
---|---|
bcrypt | 自动加盐,运行速度可配置,广泛应用 |
scrypt | 除了慢,还对内存要求高,抗GPU破解 |
Argon2 | 最先进的密码哈希算法,2020年起逐渐成为新标准 |
Argon2是内存硬(memory-hard)算法,意思是要消耗大量RAM才能运行。这让用GPU/ASIC进行大规模并行破解变得很慢、效率极低。相比之下,bcrypt/scrypt在硬件上更容易被优化并行。不过,Argon2运行一次hash都得100ms以上了,高负载高吞吐量需求的场景下可能不太ok,得权衡利弊根据实际需求选择算法。
对于3),直接不存密码更安全,你的系统可以只做验证码登录或者验证码双重验证,但是验证码可以被截获。。。也可以做TOTP(动态口令),这个更安全,可以用Google Authenticator、Authy等app生成验证码,比如Jira用的就是Google的这个动态口令。
第三,其他
1)为什么不自己设计hash函数,这样彩虹表就失效了
密码学界有一个经典名言:Don’t roll your own crypto不要自己发明加密算法。
还有一个原则:Kerckhoffs 原则:一个加密系统的安全性应该只依赖于密钥的保密性,而不是算法的保密性。就是说:安全系统必须默认攻击者迟早能拿到你的算法实现,所以我们不能靠“隐藏逻辑”来防止破解,安全性必须来自算法本身具有抗碰撞、抗反推、抗爆破能力。
一个是如上面所说源码容易泄露,攻击者很容易能看到你的加密方法;
第二个自己设计要加盐、慢计算、自动升级参数,还要满足avalanche Effect(雪崩效应,就是密码稍微改动一点点,hash结果就得有巨大变化才行,要不然很容易反推出加密逻辑),一旦有一点没有设计好,就等于白弄;
第三是,所谓自定义hash通常是:
拼接字符串 + 多次哈希(比如 SHA256(SHA1(password)))
加一些混淆的逻辑(比如插入字符、换顺序)
自己设计的哈希函数(甚至用数学公式)
但其实攻击者总能逆向出你的逻辑。逆向工程、已知明文攻击、差分分析、构造反向字典、碰撞攻击,有兴趣的可以一起讨论。
因此,还是推荐就用已经经过严格审计与学术验证的密码学方法,就比如上面提到的Argon2等。而且他们也支持定制,比如:可以调整bcrypt的cost参数,控制哈希的慢速程度;加盐的时候可以加入用户UID或时间戳,提高唯一性之类的。
.
.
.
[1]SQL注入
发生在没有对用户输入做严格校验,而且没有使用预编译/参数化查询的情况下。
也就是把用户输入直接当作sql的一部分,攻击者就可以在输入框里注入恶意sql。
比如:
对于登录功能,用户输入userName,后端直接拿它去查数据库:
*“select username, hash, salt from user_table where username=’”+userName+”’”*
如果攻击者输入是:’ or ‘1’=‘1’,这样运行到数据库的sql就是:
*select username, hash, salt from user_table where username=’’ or ‘1’=‘1’*
这样查询条件永远是真,就有可能绕过验证了,或者攻击者用更复杂的sql注入,有可能直接拿到全表。
这里说明一下java的安全写法,以mybatis为例:
<select id="login" resultType="User">
SELECT * FROM user_table
WHERE username = #{username} AND password = #{password}
</select>*
这里的SQL会先被数据库预编译成结构,然后把用户输入当作参数传进去,这样就不会当作代码执行了。也就是说,他会去数据库查username=’ or ‘1’=‘1’的用户,而不是把’ or ‘1’=‘1’浅浅地当作sql的一部分了。
[2]常见hash算法的结果样式
哈希函数 | 输出长度 | 示例 |
---|---|---|
MD5 | 32位十六进制 | 5f4dcc3b5aa765d61d8327deb882cf99 |
SHA-1 | 40位十六进制 | 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8 |
SHA-256 | 64位十六进制 | 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 |
bcrypt | 60位带 2 b 2b 2b等前缀 | $2b 12 12 12eImiTXuWVxfM37uY4JANj.QzbY8Xf1HnUZyC8Hq4cclqkGQz9f2q. |