如何安全存储口令?了解下Hash加盐的原理
最近要开发一个项目,其中涉及到了用户口令存储(大家习惯称之为密码),毫不夸张的说,如果方案设计的不合格,未来再想补救就会困难重重。
记得在写《深入浅出HTTPS:原理到实战》这本书的时候,也研究了很多密码学算法,和口令加密有关的算法也有很多,参考了很多资料,最近又温习了这些资料,感觉理解的更透彻了,为了把口令加密的事情说清楚,打算写4-5篇文章。
首先,口令加密是非常系统化的一个工程,涉及到多方面,比如代码安全性,系统安全性,数据存储安全性,任何一个方面出现漏洞,可能就会导致口令的泄漏。严格的说,谁也不能保证自己系统的用户口令不被外泄,我们要做的就是尽可能的减少这种风险。
其次我写的文章主要描述口令存储方案的设计,至于 Web 程序安全,服务器安全等方面不会涉及,比如手机短信验证、图形验证码、前台登录限制这些策略对保护口令安全也非常重要,但不是描述的重点。
什么是口令呢?就是用户自行设定的一段字符串,用户要对自己的口令安全性负责,要认识到口令的重要性,因为一旦口令因为各种原因泄漏,那么攻击者就能以用户的身份进行任何操作。
从这个角度来看,用户应该尽量设定强口令(比如多种字符的组合、定期修改);系统也有义务强制要求用户使用强口令。
从破解的角度看,不管系统采用什么加密算法,强口令也更有安全性,破解的难度也会加大,从历史上出现的口令泄漏事件来看,很多是因为用户的口令太弱了。
从系统设计的角度看,口令存储到磁盘(比如 Mysql)的时候,不能是明文的,道理很简单,如果 Mysql 出现安全漏洞,比如被攻击者拖库了,那么攻击者不费吹灰之力就能获取用户的明文口令。
通过密码学算法可以将明文口令转换为无规则的密文,即使攻击者知道了口令的密文,也不能破解出口令明文,这样相当于口令没有泄漏(当然事情没有那么理想,所以才需要相对完美的口令保护方案)。
那么使用什么密码学算法呢?比较流行的解决方案就是密码学 Hash 算法(比如 sha1),很多开发者喜欢对口令进行 Hash 运算,然后将运算值存储到数据库中。用户登录的时候,程序用同样的 Hash 运算口令后,将运算值和数据库中的 Hash 值进行比较,如果相同,表示登录成功。
密码学 Hash 算法有什么特点?大家都用它保护口令呢?根本的原因在于密码学 Hash 算法具备单向性,也就是说 Hash 值不能被反解,同时不同的口令其 Hash 值是不一样的,严格的说密码学 Hash 算法很难发生碰撞。
为什么我一直强调密码学 Hash算法,普通的 Hash 算法能运算口令吗?不能,普通的 Hash 算法主要用于数据结构中(比如 Hash 表),它很容易发生 Hash 碰撞,想想看,不同的口令如果 Hash 值一样,那么我用自己的口令也许能登录别人的账户了。
所以说只有安全的密码学 Hash 算法才能用于保护口令,另外也要注意的是 Hash 运算口令不代表加密,因为它不是加密算法(对称加密算法,非对称加密算法才是)。
那么对于口令来说,应该使用 md5 还是 sha1 算法?本质上并没有太大的差别,虽然这二种算法理论上已经发生了碰撞,但在实际使用过程中很难找到两个不同值具备相同的 Hash 值。尤其对 Hash 进行加盐后(后面会说),选用 md5 还是 sha1 没有太大的差别。
如果单纯用密码学 Hash 算法运算口令,很容易被破解,接下去说说几种攻击方法。
(1)字典攻击和暴力攻击
比如攻击者拖库了,得到了所有的口令 Hash 值,现在的目的就是找出口令明文。字典攻击和暴力攻击并没有太大的区别,字典攻击可以认为是暴力攻击的一种,它将常用的字符(暴力攻击是将各种可能的字符)组合起来进行 Hash 运算,然后和数据库中的 Hash 值进行比较,如果相同,表示用户的口令被成功破解。
密码学 Hash 算法如果用于运算口令,有个最致命的弱点就是它的运算速度太快了,如果仅仅对口令进行 Hash 运算,那么很容易被破解;而且如果口令本身就是弱口令,那么破解的速度就更快了。
不管采取何种解决方案,都有可能被暴力破解,只是时间的问题。但如果采用相对完善的 Hash 解决方案(比如 Hash 加盐,后面会讲),那么被暴力破解的几率会小很多。
有些同学们会问,如果没有被拖库,暴力攻击还能进行吗?也可以,攻击者可以组合各种字符,然后调用登录接口,不断进行攻击,如果接口返回成功,表示口令被破解了。
为了避免这样的攻击,可以采取验证码和其它策略保护登录接口,但这是另外一个层面的问题了。
(2)查找表
这是比字典攻击更有效的一种方式,攻击同种 Hash 算法非常有效率。基本的做法就是预运算字典中的口令,然后将口令和口令对应的密文存储到一个数据结构中(比如 Hash 表或者 Memcached),然后可根据口令明文和密文进行查找,查找速度非常快。
(3)彩虹表
彩虹表和查找表很类似,查找表使用的存储较多,运算速度较快;而彩虹表存储较小,运算较慢,相当于用空间换时间。
原理我并没有研究,最重要的就是 R 运算(reduce function),它不是 Hash 函数的反函数,它能将一个 Hash 值转换为一个明文(不是明文)。
构建彩虹表的过程:对一个口令组合进行不断的 Hash 运算、R 运算,持续 k 次后得到一条链,然后将链条的头部和尾部存储起来(中间的不要)。
那么如何破解呢?同学们还是直接看 wiki 或找一些专门的文章,在此文中,只要记住,彩虹表能够破解口令明文,而且很有效。
那么如何防止彩虹表和查找表攻击呢?最好的方法就是 Hash 加盐(Hash+salt)。或者这么说,如果将来你要设计口令存储方案,Hash 加盐虽然不是最好的方法,但如果实施得当,也是比较安全的。
简单 Hash 就是对口令进行单纯 Hash 运算,Hash 加盐是在口令后面加一段随机值(salt),然后再进行 Hash 运算。
不管是彩虹表还是查找表,如果两个用户的口令相同,他们的密文也必定相同,攻击就相对容易,构建的口令组合相对减少,一个用户的口令密文被破解后,那么具有相同口令密文的用户其口令也被破解了。
而加盐就是保证同样的口令其密文值不一样,这样攻击者构建的彩虹表和查找表的将非常巨大,破解的速度和成功率将极低,换句话说,优秀的口令存储方案就在于即使被拖库了,攻击者也没有啥办法。
现在重点就是 salt 值如何设置了,salt 一定要不可预测,且高度随机,如果可预测的话,攻击者就能猜测出 salt,这样即使 Hash 加盐了,本质上和纯 Hash 加密没有区别。
那么如何生成 salt 呢?请使用密码学中的伪随机函数(CSPRNG),比如 PHP 语言中可以使用 openssl_random_pseudo_bytes() 函数,千万不要使用非安全的 rand() 函数。安全的伪随机函数的关键在于种子(seed),此处就不多描述了,记住一点,获取 salt 需用标准的密码学算法,不要想当然自行设计。
salt 值获取还有几个误区,比如使用和用户有关的属性(比如 email,手机号)充当用户的 salt,这样很容易被猜测。
也要避免使用全局 salt,如果将全局 salt 值硬编码,那么一旦代码泄漏或服务器被攻击,攻击者在构建字典的时候,在各种字符组合后面加上 slat,然后再 hash,想想看,是不是很容易攻击?
全局 salt 另外一个弱点在于,攻击者很容易找出规律,因为相同口令其密文值也是相同的。
salt 值也不能太短,理论上 salt 长度应该和 Hash 值长度一致。
一个设计良好的 salt,攻击基本很难成功,想象下,一个口令再加上 salt,攻击者要构建多少个 Hash 算法的输入值?而且有了 salt,即使用户的口令相对弱一点,也能有效保障用户的安全性。
Hash 加盐的根本目标是:即使被拖库了,攻击者也很难破解出密文。虽然它不是标准的口令安全存储解决方案,但从结果上来看,也是非常不错的一种设计。
最后非常非常重要的一点就是,salt 不能和口令密文存储在一起,不然就和没有 salt 一样,因为攻击者拖库后,构建字典非常简单,因为它能够直接看到 salt,所以一定要分开存储(比如在不同的机器上),这样即使口令密文表被拖库了,而 salt 表没有被拖库,也是相对安全的。
salt 看上去虽然是毫无规则的,但本质上是明文的,所以如果想进一步安全,可以可以引入一个 pepper(一个公共密钥),那么如何加密保护 salt 或密文口令呢?有两种办法。
(1)在计算密文口令的时候,采用 HMAC 方法(内部还是一个 Hash 算法):
口令密文=hash(口令+salt+pepper)
(2)或者对密文口令进行加密(对称加密算法或非对称加密算法)
口令密文=rsa(hash(口令+salt),pepper)
不管采用那种方案,pepper 的安全性非常重要,因为这个 pepper 一旦泄露,对于保护口令没有太大的作用。
但不管怎么说,使用 pepper 还是很有积极意义的,毕竟增加了一层保护。
解决方案永远没有绝对安全的,只有相对的,至于 pepper 采用那种处理方法,后一篇博文我会描述。
这篇博文是原理性的,下一篇我将实际描述一个解决方案,敬请期待~
欢迎大家关注我的公众号(ID:yudadanwx,虞大胆的叽叽喳喳),所有文章都是原创,主要来源于平时工作中遇到的问题和学习中的一些想法;也可以了解我的书《深入浅出HTTPS:从原理的实战》
本文转载自异步社区。
原文链接:https://www.epubit.com/articleDetails?id=N4457e03a-2b1a-4b36-9b49-5d402db7fe24
- 点赞
- 收藏
- 关注作者
评论(0)