0x00 前言 天下武功,唯快不破。但密码加密不同。算法越快,越容易破。 0x01 暴力破解密码破解,就是把加密后的密码还原成明文密码。似乎有不少方法,但最终都得走一条路:暴力穷举。 也许你会说还可以查表,瞬间就出结果。虽然查表不用穷举,但表的制造过程仍然需要。查表只是将穷举提前了而已。 密码加密,用的都是单向散列计算。既然单向,那就是不可逆,那只能穷举。 穷举的原理很简单。只要知道密文是用什么算法加密的,我们也用相同的算法,把常用的词组跑一遍。若有结果和密文一样,那就猜中了。 穷举的速度有多快?这和加密算法有关。加密一次有多快,猜一次也这么快。 例如 MD5 加密是非常快的。加密一次耗费 1 微秒,那破解时随便猜一个词组,也只需 1 微秒(假设机器性能一样,词组长度也差不多)。攻击者一秒钟就可以猜 100 万个,而且这还只是单线程的速度。 所以,加密算法越快,破解起来就越容易。 0x02 慢加密如果能提高加密时间,显然也能增加破解时间。 如果加密一次提高到 10 毫秒,那么攻击者每秒只能猜 100 个,破解速度就慢了一万倍。 怎样才能让加密变慢?最简单的,就是对加密后的结果再加密,重复多次。 例如,原本 1 微秒的加密,重复一万次,就慢一万倍了: function slow_md5(x)
for i = 0 ~ 10000
x = md5(x)
return x
end
攻击者破解时,也必须用这套算法跑字典。于是,破解时间就大幅增加了。 事实上,这样的「慢加密」算法早已存在,例如 bcrypt、PBKDF2 等等。它们都有一个难度系数因子,可以控制加密时间,想多慢就多慢。 加密越慢,破解时间越长。 0x03 慢加密应用最需要慢加密的场合,就是网站数据库里的密码。 近几年,经常能听到网站被「拖库」的新闻。用户资料都是明文存储,泄露了也无法挽回。唯独密码,还可以和攻击者对抗一下。 然而不少网站,使用的都是快速加密算法,因此轻易就能破解出一堆弱口令账号。 当然,有时只想破解某个特定人物的账号。只要不是特别复杂的词汇,跑上几天,很可能就破出来。 但网站用了慢加密,结果可能就不一样了。如果把加密时间提高 100 倍,破解时间就得长达数月,变得难以接受。 即使数据泄露,也能保障「密码」这最后一道隐私。 0x04 慢加密缺点不过,慢加密也有明显的缺点:消耗大量计算资源。 使用慢加密的网站,如果同时来了多个用户,服务器 CPU 可能就不够用了。要是遇到恶意用户,发起大量的登录请求,甚至造成资源被耗尽。 性能和安全总是难以兼得。所以,一般也不会使用太高的强度。 一些大型网站,甚至为此投入集群,用来处理大量的加密计算。但这需要不少的成本。 有没有什么方法,可以让我们使用算力强劲、同时又免费的计算资源? 0x05 前端加密在过去,个人电脑和服务器的速度,还是有较大差距的。但如今,随着硬件发展进入瓶颈,这个差距正缩小。在单线任务处理上,甚至不相上下。 客户端拥有强大的算力,能不能分担一些服务器的工作? 尤其像「慢加密」这种算法开源、但计算沉重的任务,为何不交给客户端来完成? 过去,提交的是明文密码;现在,提交的则是明文密码的「慢加密结果」。无论是注册,还是登陆。 而服务端,无需任何改动。将收到的「慢加密结果」,当做原来的明文密码 就行。以前是怎么保存的,现在还是怎么保存。 这样就算被拖库,攻击者破解出来的也只是「慢加密结果」,还需再破解一次,才能还原出「明文密码」。 事实上,「慢加密结果」这个中间值,是不可能破解出来的! 因为它是一个散列值 毫无规律的随机串,例如 32 位十六进制字符串,而字典都是有意义的词组,几乎不可能跑到它! 除非字节逐个穷举。但这有 16^32 种组合,是个天文数字。 所以「慢加密结果」是无法通过数据库里泄露的密文「逆推」出来的。
当然,不能逆推,但可以顺推。把字典里的词组,用前后端的算法依次执行一次: back_fast_hash( front_slow_hash(password) )
然后对比密文,即可判断有没有猜中。这样就可以用跑字典来破解。 但是有 front_slow_hash 这个障碍,破解速度就大幅降低了。 0x06 对抗预先计算不过,前端的一切都是公开的。所以 front_slow_hash 的算法大家都知道。 攻击者可以用这套算法,把常用词组的「慢加密结果」提前算出来,制作成一个「新字典」。将来拖库后,就可以直接跑这个新字典了。 对抗这种方法,还得用经典的手段:加盐。最简单的,将用户名作为盐值: front_slow_hash(password username)
这样,即使相同的密码,对于不同的用户,「慢加密结果」也不一样了。 也许你会说,这个盐值不合理,因为用户名是公开的。攻击者可以对某个重要人物的账号,单独为他建立一个字典。 那么,是否可以提供一个隐蔽的盐值?答案是:不可以。 因为这是在前端。用户还没登录,那返回谁的盐值?登陆前就能获得账号的盐值,这不还是公开的吗。 所以,前端加密的盐值无法隐藏,只能公开。 当然,即使公开,单独提供一个盐值参数,也比用户名要好。因为用户名永远不变,而独立的盐值可以定期更换。 盐值可以由前端生成。例如注册时: # 前端生成盐值
salt = rand()
password = front_slow_hash(password salt)
# 提交时带上盐值
submit(..., password, salt)
后端将用户的盐值也储存起来。 登录时,输完用户名,就可以开始查询用户对应的盐值: 盐值的更换,也非常简单,甚至可以自动完成: 前端在加密当前密码时,同时开启一个新线程,计算新盐值和新密码。提交时,将它们全都带上。 如果「当前密码」验证成功,则用「新密码」和「新盐值」覆盖旧的。 这样更换盐值,还是只用到前端的算力。 这一切都是自动的,相当于 在用户无感知的情况下,定期帮他更换密码! 密文变了,针对「特定盐值」制作的字典,也就失效了。攻击者得重新制作一次。 0x07 强度策略密码学上的问题到此结束,下面讨论实现上的问题。 现实中,用户的算力是不均衡的。有人用的是神级配置,也有的是古董机。这样,加密强度就很难设定。 如果古董机用户登录会卡上几十秒,那肯定是不行的。对于这种情况,只有以下选择:
1.强度固定根据大众的配置,制定一个适中的强度,绝大多数用户都可接受。 但如果超过规定时间还没完成,就把算到一半的 Hash 和步数提交上来,剩余部分让服务器来完成。 [前端] 完成 70% ---- |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|