数据加密解读

首先,我们明确一下安全加密方案的终极目标:

即使在数据被拖库,代码被泄露,请求被劫持的情况下,也能保障用户的密码不被泄露。

说具体一些,我们理想中的绝对安全的系统大概是这样的:

  1. 首先保障数据很难被拖库。
  2. 即使数据被拖库,攻击者也无法从中破解出用户的密码。
  3. 即使数据被拖库,攻击者也无法伪造登录请求通过验证。
  4. 即使数据被拖库,攻击者劫持了用户的请求数据,也无法破解出用户的密码。

如何保障数据不被拖库,这里就不展开讲了。首先我们来说说密码加密。现在应该很少系统会直接保存用户的密码了吧,至少也是会计算密码的 md5 后保存。md5 这种不可逆的加密方法理论上已经很安全了,但是随着彩虹表的出现,使得大量长度不够的密码可以直接从彩虹表里反推出来。

所以,只对密码进行 md5 加密是肯定不够的。聪明的程序员想出了个办法,即使用户的密码很短,只要我在他的短密码后面加上一段很长的字符,再计算 md5 ,那反推出原始密码就变得非常困难了。加上的这段长字符,我们称为盐(Salt),通过这种方式加密的结果,我们称为 加盐 Hash 。比如:

imgimg

上一篇我们讲过,常用的哈希函数中,SHA-256、SHA-512 会比 md5 更安全,更难破解,出于更高安全性的考虑,我的这个方案中,会使用 SHA-512 代替 md5 。

imgimg

通过上面的加盐哈希运算,即使攻击者拿到了最终结果,也很难反推出原始的密码。不能反推,但可以正着推,假设攻击者将 salt 值也拿到了,那么他可以枚举遍历所有 6 位数的简单密码,加盐哈希,计算出一个结果对照表,从而破解出简单的密码。这就是通常所说的暴力破解。

为了应对暴力破解,我使用了加盐的慢哈希。慢哈希是指执行这个哈希函数非常慢,这样暴力破解需要枚举遍历所有可能结果时,就需要花上非常非常长的时间。比如:bcrypt 就是这样一个慢哈希函数:

imgimg

通过调整 cost 参数,可以调整该函数慢到什么程度。假设让 bcrypt 计算一次需要 0.5 秒,遍历 6 位的简单密码,需要的时间为:((26 * 2 + 10)^6) / 2 秒,约 900 年。

好了,有了上面的基础,来看看我的最终解决方案:

imgimg

上图里有很多细节,我分阶段来讲:

1. 协商密钥

基于非对称加密的密钥协商算法,可以在通信内容完全被公开的情况下,双方协商出一个只有双方才知道的密钥,然后使用该密钥进行对称加密传输数据。比如图中所用的 ECDH 密钥协商。

2. 请求 Salt

双方协商出一个密钥 SharedKey 之后,就可以使用 SharedKey 作为 AES 对称加密的密钥进行通信,客户端传给服务端自己的公钥 A ,以及加密了的用户ID(uid)。服务端从数据库中查找到该 uid 对于的 Salt1 和 Salt2 ,然后再加密返回给客户端。

注意,服务端保存的 Salt1 和 Salt2 最好和用户数据分开存储,存到其他服务器的数据库里,这样即使被 SQL 注入,想要获得 Salt1 和 Salt2 也会非常困难。

3. 验证密码

这是最重要的一步了。客户端拿到 Salt1 和 Salt2 之后,可以计算出两个加盐哈希:

SaltHash1 = bcrypt(SHA512(password), uid + salt1, 10)
SaltHash2 = SHA512(SaltHash1 + uid + salt2)

使用 SaltHash2 做为 AES 密钥,加密包括 uid,time,SaltHash1,RandKey 等内容传输给服务端:

Ticket = AES(SaltHash2, uid + time + SaltHash1 + RandKey)
AES(SharedKey, Ticket)

服务端使用 SharedKey 解密出 Ticket 之后,再从数据库中找到该 uid 对应的 SaltHash2 ,解密 Ticket ,得到 SaltHash1 ,使用 SaltHash1 重新计算 SaltHash2 看是否和数据库中的 SaltHash2 一致,从而验证密码是否正确。

校验两个哈希值是否相等时,使用时间恒定的比较函数,防止试探性攻击。

time 用于记录数据包发送的时间,用来防止录制回放攻击。

4. 加密传输

密码验证通过后,服务端生成一个随机的临时密钥 TempKey(使用安全的随机函数),并使用 RandKey 做为密钥,传输给客户端。之后双方的数据交互都通过 TempKey 作为 AES 密钥进行加密。

假设被拖库了

以上就是整个加密传输、存储的全过程。我们来假设几种攻击场景:

  1. 假设数据被拖库了,密码会泄露吗?
    数据库中的 Salt1 ,Salt2 , SaltHash2 暴露了,想从 SaltHash2 直接反解出原始密码几乎是不可能的事情。
  2. 假设数据被拖库了,攻击者能不能伪造登录请求通过验证?
    攻击者在生成 Ticket 时,需要 SaltHash1 ,但由于并不知道密码,所以无法计算出 SaltHash1 ,又无法从 SaltHash2 反推 SaltHash1 ,所以无法伪造登录请求通过验证。
  3. 假设数据被拖库了,攻击者使用中间人攻击,劫持了用户的请求,密码会被泄露吗?
    中间人拥有真实服务器所有的数据,仿冒了真实的 Server ,因此,他可以解密出 Ticket 中的 SaltHash1 ,但是 SaltHash1 是无法解密出原始密码的。所以,密码也不会被泄露。
    但是,中间人攻击可以获取到最后的 TempKey ,从而能监听后续的所有通信过程。这是很难解决的问题,因为在服务端所有东西都暴露的情况下,中间人假设可以劫持用户数据,仿冒真实 Server , 是很难和真实的 Server 区分开的。解决的方法也许只有防止被中间人攻击,保证 Server 的公钥在客户端不被篡改。
    假设攻击已经进展到了这样的程度,还有办法补救吗?有。由于攻击者只能监听用户的登录过程,并不知道真实的密码。所以,只需要在服务端对 Salt2 进行升级,即可生成新的 SaltHash2 ,从而让攻击者所有攻击失效。
    具体是这样的:用户正常的登录,服务端验证通过后,生成新的 Salt2 ,然后根据传过来的 SaltHash1 重新计算了 SaltHash2 存入数据库。下次用户再次登录时,获取到的是新的 Salt2 ,密码没有变,同样能登录,攻击者之前拖库的那份数据也失效了。

Q & A

  1. 使用 bcrypt 慢哈希函数,服务端应对大量的用户登录请求,性能承受的了吗?
    该方案中,细心一点会注意到, bcrypt 只是在客户端进行运算的,服务端是直接拿到客户端运算好的结果( SaltHash1 )后 SHA-512 计算结果进行验证的。所以,把性能压力分摊到了各个客户端。
  2. 为什么要使用两个 Salt 值?
    使用两个 Salt 值,是为了防止拖库后,劫持了用户请求后将密码破解出来。只有拥有密码的用户,才能用第一个 Salt 值计算出 SaltHash1 ,并且不能反推回原始密码。第二个 Salt 值可以加大被拖库后直接解密出 SaltHash1 的难度。
  3. 为什么要动态请求 Salt1 和 Salt2 ?
    Salt 值直接写在客户端肯定不好,而且写死了要修改还得升级客户端。动态请求 Salt 值,还可以实现不升级客户端的情况下,对密码进行动态升级:服务端可定期更换 Salt2 ,重新计算 SaltHash2 ,让攻击者即使拖了一次数据也很快处于失效状态。
  4. 数据库都已经全被拖走了,密码不泄露还有什么意义呢?
    其实是有意义的,正如刚刚提到的升级 Salt2 的补救方案,用户可以在完全不知情的情况下,不需要修改密码就升级了账号体系。同时,保护好用户的密码,不被攻击者拿去撞别家网站的库,也是一份责任。

我理解的破解方式主要有3种方式:**
暴力破解,就是把所有可能试一次
彩虹表,就是一个pre-computed table
**字典攻击,就是拿一些常见的密码去暴力破解

举个例子:**
假如你是这么存密码的,密码helloworld,md5(helloworld)得到fc5e038d38a57032085441e7fe7010b0,然后就把md5的结果存到数据库里,没有加盐。那么攻击者可以先查表,看一下md5的表里,是哪个密码对应fc5e038d38a57032085441e7fe7010b0,查到了那就完事了。
所以需要加盐,盐对于每个账号都是唯一的,而且是公开的,不是藏起来的。
加了盐之后呢,那就变成,我的密码helloworld, md5(helloword + salt),比如我的盐是nizhendehaoshuaia的一串随机字符。这种情况下,彩虹表就没用了,为什么呢?因为彩虹表就是提前计算的,那我要算你这个彩虹表,我就得 md5(“nizhendehaoshuaia + 注册密码的所有可能”),把这个整个东西算出来先。那攻击者怎么知道你的盐是多长的啊?提前算这种就根本不划算。当然你要是用个只有几位字符的盐,那可以算。
所以为什么盐要每个账号不一样?因为对于另一个不同的盐,比如nikanbudongwoyemeibanfa,他又得重新算一次md5(“nizhendehaoshuaia + 注册密码的所有可能”)。如果你的盐只有一个,那么他只要算一次,就能反查你所有的账号。
还要注意的是,比如上面加盐能保证你不直接受彩虹表的反查攻击,但人家要强行爆破一条密码,还是轻轻松松的。比如我就要爆破 md5(helloword + nizhendehaoshuaia),你这条,那我直接暴力穷举。
**所以需要那些计算得很慢的哈希函数,那这样别人穷举计算要花很长时间以至于放弃。md5,据说每秒能算10亿次哈希,还是12年的数据。那你看看你这个md5(helloword + nizhendehaoshuaia),假设密码是8~12位,28+29+210+211+2^12 = 结果才多少个组合?算出来不是分分钟的事情?sha1也是一样,算得太快了。bcrypt我看一个视频演示的都是0.x秒一个。

*最后,假如你用了安全的哈希函数比如bcrypt,再加盐,但还是不能避免字典攻击。举个例子,我的密码helloword, 盐是#JHVDNE)233,假设bcrypt(helloworld, #JHVDNE)233)算出来的哈希结果是239fhdnvkon923(随便编一个)。那字典攻击怎么做?字典里我有常见的用户密码。那我直接拿常见的密码bcrypt(常见密码, #JHVDNE)233)一个个试就行了。(不过bcrypt这样正向计算好像不能填写自定义的盐,有一个函数随机生成的,不过大概就是这个意思)

所以为了进一步防止字典攻击,可以在应用服务器(放源代码的服务器)上(假设应用服务器和数据库是分开的,数据库被黑了跟应用服务器被黑看做2件事),加一个pepper。或者让用户用更安全的密码咯。**
所以md5或者sha1这种,加盐等于没加。
加一个pepper就是,在应用服务器上有一串随机字符串,然后哈希的时候,比如说这样
secure_hashfunction(pepper + helloworld + salt),只要你服务器没被黑,攻击者不知道你的pepper,他就算不出来。因为比如穷举hashfunction(注册密码的所有可能 + salt),在瞎算啊。你都不知道我前面还有一个pepper拼接了。
即使你用md5,假如你服务器没被黑,也是算不出来,为什么呢?
例如我服务器用一个128bit长的字符串做pepper,然后md5(pepper + password + salt),这样。首先,你可能不知道我用了pepper,发现即使是md5(注册密码的所有可能 + salt)都得不到我的哈希结果。然后你觉得我应该用了pepper了,但是你怎么算呢?md5(2的128次方种可能 + 注册密码的所有可能 + salt)? 虽然md5一秒钟能算10亿个结果,但是你先搞清楚2^128次方有多大,md5你也爆破不过来。
但是这里要注意你的pepper要够长哦,假如你只用了3位或者4位字符的话,那md5(4位字符的所有可能 + 注册密码的所有可能 + salt),还是很好算的。
还要注意的是,你如果坚持要用md5 + pepper,你得保证你的应用服务器不被黑,pepper不被看到哦。假如攻击者知道了你的pepper,那就变成 md5(已知pepper + 注册密码的所有可能 + salt)了,跟没pepper一点区别没有。
所以还是要用bcrypt,即使被知道了pepper,bcrypt(已知pepper + 注册密码的所有可能 + salt),也爆破得慢(重申一遍bcrypt的函数不是这样用的我只是举例)。
**最后,还是不要用常见密码、太弱的密码。假如一个数据库只用了bcrypt和salt,那么bcrypt(弱密码 + salt),暴力破解起来感觉也不会太难,字典攻击就可能更快了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值