如何安全存储密码都不知道,难怪我被面试官吊打呢...学完这个一定打回去!

安全 数据安全
本文通过大家熟悉的密码交互场景作为出发点阐述了明文存储密码、单向无盐哈希存储、预计算哈希链集合、彩虹表、哈希+盐存储、专业密码算法存储等几个方面的相关知识。

1. 写在前面

网络安全是一个非常重要的领域,今天和大家一起来学习和密码相关的话题。

说到密码大家肯定都不陌生,我们每个人都有一系列的密码:邮箱密码、社交网站密码、各种app密码等等,密码就如同每个人网络领域的一把钥匙。

对于我们使用者来说,我们尽量避免使用一些烂大街的密码比如:123qaz、qazwsx等、提高密码的复杂度、避免使用生日等基础信息作为密码、另外要定期更换密码等来确保个人信息安全。

对于功能服务提供商来说,妥善存储保管密码是一个很重要的环节,一旦造成密码泄漏等事故造成的影响会很大,要严肃对待。

然而并不是所有的事情都如我们所期盼那样,网上用户敏感信息泄漏的事件比比皆是,国内外都有一些案例,可以自行搜索引擎搞起。

前面铺垫了一下于是引出本文的主题:

通过本文你将了解到以下内容:

  • 密码交互基本过程
  • 明文存储密码
  • 单向无盐哈希存储
  • 预计算哈希链集合和彩虹表原理
  • 哈希+盐存储
  • 专业密码加密算法

2. 密码交互过程

密码一般是用在用户登陆认证环节的,完整的过程包括:用户输入密码、客户端加密、网络传输、服务端验证等。

从严格意义上来说每个环节都可能造成密码的泄漏,过程越多出错的概率就会越大,其中用户登录认证的基本步骤可以归纳为:

  • 用户填写账号、密码以及验证码等信息;
  • 在之前注册时填写的密码经过加密储存在数据库中并做持久化备份,后续登录做验证;
  • 服务端从数据库获取该用户名下的加密密码,并且将用户输入使用相同加密过程计算结果,二者进行对比;
  • 二者相同则用户被授权登录,当然这里不考虑手机验证码之类的措施;
  • 如果二者不同则提示失败,并且对最大尝试次数做限制;

本文主要介绍服务端如何安全存储密码。

3. 密码的明文存储

这种明文存储密码的方法让人觉得不可思议,但是实际上真的有这样做的,就像这样把密码明文直接存储在MySQL,如图:

这种明文存储的密码数据一旦被拖库,就会造成很大的问题,对于一些私人小站可能会存在这种情况,因此我们日常一定要注意鉴别,以为这些站点的防范意识不够,后台漏洞很容易被不法分子利用造成泄漏,当然也有少数知名站点明文存储的案例,只能说有点无语了。

现在的密码太多了,很多人不借助一定规律或者工具的前提下会将1-2个常用密码用在很多站点,这样就出现了木桶效应:只要一个站点的密码被泄漏其他站点的密码也就不安全了。

大锤就是密码太多他记不住,索性就用一个密码注册登陆日常的很多app,有一天他为了玩游戏注册了一个小破站点,后来密码泄漏了,就这样他的其他常用app就面临着被盗的风险。

对于这种明文存储的案例,网上有人说这是灯下黑...,让我听着有种网剧看多了的感觉,个人觉得主要原因就是意识不够。

说起这个让我想起,有次放假回老家,坐客运车途中要加油,正在加油的时候,车上有个人接打电话,没过一会儿车上很多人不乐意了,开始嚷嚷他别打了,哈哈哈。

4. 单向无盐哈希存储

在谈论单向无盐哈希存储之前,我们先来达到一个共识问题:我们常说的md5/sha其实都是摘要算法,并不是加密算法。

(1) 摘要算法和加密算法

加密算法和摘要算法之间有很大区别,虽然都是把明文进行变形处理,但是加密算法必然对应解密算法,也就是输入值经过加密算法处理之后可以使用解密算法还原,但是摘要算法一般认为是单向不可逆的,没办法轻易还原原始输入。

如图展示了加解密可逆过程(注加密密文是我胡乱写的):

如图展示了摘要算法不可逆过程(注摘要密文是我胡乱写的):

简单了解了摘要算法和加密算法的区别与联系之后,我们可以知道摘要算法是单向的,我只知道原始输入A的摘要输出是B,但是根据B很难推出来A。

就像你用一把锁和一把钥匙,但是你把钥匙扔到了茫茫大海,在不损害箱子的前提下你只能一直暴力尝试,在试了第20200404把钥匙的时候发现打开了,恍然大悟原来是这把钥匙啊!

(2) 哈希冲突和暴力破解

前面举了用钥匙开箱子的问题,最终还是被打开了,先别高兴,原生钥匙也不一定就是这把呢,因为有哈希冲突,巧了可以打开,来看个图先:

 

图中我们用m输入进行摘要运算之后得到了相同的摘要值,也就是打开了箱子,但是真实的输入却是k。这里就引出了一个新问题:哈希冲突及其影响。

那么哈希冲突是什么?又为啥会冲突呢?

我们知道摘要算法生成的字符串的长度一般是固定的有128位 256位之类的,摘要算法的神奇之处在于可以把任意长度的数据转化为固定长度的字符串。

换句话说哈希后的字符串长度有限,那么总的集合就有限,这是个组合的问题很容易想到,但是输入是无限的呀!

极端一点你写了个摘要算法长度是4的十六进制字符串,也就只有65536个空间,实际上输入样本数量根本不需要65536就几乎必然出现冲突,因为不出现冲突的要求是非常高的,不信你看看生日悖论问题就知道了。

  • 生日悖论是指在不少于 23 个人中至少有两人生日相同的概率大于 50%。例如在一个 30 人的小学班级中,存在两人生日相同的概率为 70%。对于 60 人的大班,这种概率要大于 99%。从引起逻辑矛盾的角度来说,生日悖论并不是一种 “悖论”。但这个数学事实十分反直觉,故称之为一个悖论。
  • 生日悖论的数学理论被应用于设计密码学攻击方法——生日攻击。

想想也是如此,无限输入样本对有限摘要集合,存在冲突几乎是必然的:

还是晕菜的话,你想想北京13号线的早高峰,车厢10个人不拥挤,瞬间来了200个人,你说挤不挤,冲突不冲突,如图:

(3) 单向无盐哈希存储安全性

前面用了两个小节阐述了摘要算法和加密算法的区别,以及哈希冲突,观点都很中立,在了解这些必备知识之后,我们不禁要问:单向无盐哈希存储密码安全吗?

安全都是相对的,没有绝对的安全,作为防守方只能让攻击方的时间和机器成本都高到无法接受,我们才是比较安全的。

换句话说攻击方理论上可以破解我的密码,但是需要300年又或者攻击方要想在可接受的时间内破解那么就得找个天文数量级的存储空间,作为防守方你怕啥?

怕啊!万一有时间和空间都能接受的方案咋整!先卖个关子,后面重点讨论这种可怕的Trade-Off方案。

a. 在线加解密实验

常见的摘要算法md5和sha1的长度分别为128位和160位,这里的位是二进制的,转化为16进制之后长度分别为32位和40位,从长度上来说sha1相比md5冲突更小一些,那么我们就来试试sha1的破解速度吧!

笔者随意找了一个在线sha1加解密的网站,这个网站的简介看着还蛮厉害的,不过都是针对md5的,看来md5已经被如此嫌弃了:

本站专业针对md5等哈希算法进行在线解密,可上传文件在线批量破解,最多可支持数万个密码。单单MD5就拥有64T的密码数据库,3万多亿条,本站支持十几种常见的哈希算法在线解密,查询时间不到0.1秒。

摩拳擦掌赶紧试验一把:

加密很快完成,接着解密一下:

 

啊呀很快就出结果了,之前我跑了几个解密,现在让我登录才展示,我懒得登录,不过确实是解密成功了,0.01秒所言非虚。

这怎么可以,我再试个强密码:

速度还是很快,生成了一个长度为40位的16进制小写字符串,笔者还是很自信的,这么老长破解去吧!啊哈哈....

果然,它跪了,所以增加密码强度多么重要!

b. 国内外撞库研究

我国著名的密码学家山东大学的王小云教授先后对md5和sha1进行了深入研究并且在2004年左右取到了很大的进展,因此目前对于一些高安全要求的场合已经开始使用更高的摘要算法比如:sha224、sha256、sha384、sha512等算法,也就是长度更长了,集合空间更大撞库可能更小,感受一下这个长度的变化(左右滑动):

  1. 明文输入:1233445TGNVF 
  2. sha1密文:fc58f924f193654f6388cac13b6061e99c7dbabc 
  3. sha224密文:97cdfc11422a8311e812809711b3159c4530f5f183841f7fe111fd92 
  4. sha384密文:2de74b23f770126eb51ec328f591c2290fd3bed8756d0e4dffa32af9296006444c334288bd820b1297d8087977131f0f 
  5. sha512密文:96be48daec3779f6d83b991a4f281280884578b2b68a2cf5c481838e48c199c8795f89328a85f208d791465bad28acac440be3a1397eafb0bfefd60d6b9f8a9b 

GPU在进行密码破解领域有绝对的把控力,对于md5和sha1这些算法运算速度非常之快达到每秒x亿次,如果再串联多组GPU进行破译那么速度将更快。

但是就目前而言sha224/256/385/512算法还是安全的,在2017年谷歌宣布其对sha1撞库的最新研究,基于此呼吁全球相关机构公司进行算法升级,如图展示了谷歌使用两份不同的文件得到相同sha1的例子:

尽管获得了撞库进展,但是谷歌付出的成本也是巨大的:攻击算法分为两个阶段,其中第一个阶段如果使用单CPU需要6500年,第二阶段使用单GPU需要110年,可谓成本之高。

所以我们如果采用单向无盐哈希存储密码时要避免使用MD5/sha-1这些被大量研究过的短摘要,可以使用sha-256这种更安全的摘要算法,比特币目前就有使用sha-256作为其相关算法。

c. 算法升级的思考

更换更安全的摘要加密算法确实是有一定效果的,但是算力的进步是我们无法预测的,正如md5和sha-1刚被应用时也是当前的算力无法逾越的,但是随着并行计算和量子计算的发展,诸如sha-256/512这些现在看来更安全的算法什么时候会被证明又不安全也不得而知。

但是如果我偏偏想把单车开出摩托的感觉咋办?怎样用md5这种弱摘要算法能不能实现一个强密码存储系统呢?

[[321203]]

面对这个朴实的诉求,我们接着聊。

5. 彩虹表攻击

攻击是为了更好的防守,相克相生。

(1) 暴力破解

暴力破解一般可以分为 计算暴力破解和查表暴力破解,如图:

在说彩虹表Rainbow Table之前先说一下字典穷举,这个比较好理解:比如某家网站的密码构成是长度为12位含字母、数字、特殊符号,这样我们就可以根据这个特征生成一个所有密码的全排列集合,然后再进行比如md5摘要计算得到一个哈希值,然后把这个映射关系存储下来。

使用字典法暴力破解时就如同一个巨大的hash表可以迅速降低时间,但是随着加密算法的升级和密码复杂度的提升,字典就会变得非常非常大,理论上无法存储使用。

[[321204]]

人们通过努力找了一种时间和空间折中的方案-彩虹表,它将单独时间或者单独空间的不可接受变为可接受,可以说是个非常有用的东西,第一次听这个名字时诧异于为什么叫彩虹表。

(2) 空间存储效率问题

在探究彩虹表之前,我们先思考这样一个问题:如何用最少的存储空间表达最多的信息量呢?

  • C语言中的例子:想一下在C语言中我们在传结构体或者数组时一般只传递首地址,其他的元素可以根据这个首地址和偏移量来实现访问,所以我们用一个地址就可以遍历所有的变量,存储效率很高。
  • 二叉树的例子:二叉树的链式存储和顺序存储都可以借助于根节点,依据左右孩子节点的关系从而实现全树的检索和遍历,这样我们在对外传输二叉树时只需要给出根节点即可,不必全部给定。
  • 西游记的例子:西游记主角是师徒5人,算了白龙马的...,我们可以存储这5个人的信息,但是觉得有点浪费啊,因为我们知道他们5个之间是有关系的,存储一个就能找到另外四个了。

如图展示了全量存储5人信息的场景:

如图展示了利用师徒之间的关系部分存储:

简单说明下:只存储唐僧,唐僧为入口根据其大徒弟关系得到了孙悟空,从孙悟空依据师弟关系得到另外三人,这个很像数据库的索引了...

画外音:有时候很多元素内部是存在联系的,我们不必全量展示和存储这些信息,这样会造成空间上的浪费,如果我们借助于这些内在联系就可以用少量数据作为入口获取到所有的数据,这是一种思想吧!

理解这个存储效率问题对于认识彩虹表有很大的帮助。

试想字典穷举是把建立所有明文和密文之间的映射,这样就等价于唐僧师徒5人每个人都存储,但是如果我们找到了某些明文之间的内在联系,那么我是否可以只存储少量明文就可以表达这些具备内在联系的明文和密文的映射关系呢?

答案是肯定的,但是彩虹表对于明文的内在联系是建立在数学的基础上的,我们来继续探讨,彩虹表的H函数和R函数。

(3) H函数和R函数

各位读者坐稳扶好打起精神,H函数和R函数是理解彩虹表的关键所在。

先解释一下H函数和R函数分别是什么及其内在联系:

  • H函数:H函数也就是哈希函数,实现明文到密文的单向不可逆转换;
  • R函数:R函数理解为Reduction function,这里看到Reduction这个单词,其实在之前P和NP问题的文章中,我们有接触到这个单词,归约/约化的意思。
  • H函数和R函数的关系:一句话概括:H函数的定义域是R函数的值域,H函数的值域是R函数的定义域,但是R并不是H的反函数,因为H函数是不可逆的。

举个例子:

假如密码范围是长度在10个以内的字母数字组合,不区分大小写,假设输入明文为abcdfg,则存在如下关系:

  1. // 哈希函数H实现明文向密文的映射 
  2. H(abcdfg)=AB8GFTYG 
  3. // 归约函数R实现密文向'明文相同格式字符串'的映射 
  4. R(AB8GFTYG)=erfdtk 

特别地,R函数将H函数的输出作为输入,也就是H的值域作为R的定义域,R函数生成了erfdtk,这个新明文字符串并不是原始输入的明文,但是格式却是相同的。

这里可以隐约感受到R函数的重要性,它可以将相同格式的明文生成的密文作为输入,进而输出相同格式的新明文,从而产生一个相同格式的明文的集合链条,也就是找到了一类有内在联系的明文。

换句话说,我们可以只存储一个明文,中间把多个H-R进行组合串联起来,从而形成一个明文-密文的映射集合,也就是空间减少了但是信息量并没有减少,这么看来R函数确实很cool。

画外音:这里提到的R函数生成相同格式的新明文,"相同格式"这个词语的理解不好拿捏,需要借助数学手段来实现,我们暂且简单理解为长度和组合方式类似吧!

(4) 预计算哈希链和R冲突

在彩虹表出现之前有种预计算哈希链集合,就是多组哈希链组成的明文-密文集合,这里简单提一下。

前面提到了一组H-R函数,实际上只有多组H-R函数才有实际意义,比如有2000组H-R组合,那么我们将有2000对明文-密文的映射,但是只需要存储非常小的一部分即可,这也就是我们要说的哈希链,如图为2组H-R函数组成的哈希链:

上图展示的两组H-R函数中的R都是相同的,由于哈希冲突的存在我们并不能表示全部独立的明文,这样空间存储率就打折了,看下这个图:

上面两条哈希链EDEDED和FEDECE经过R函数计算分别得到222和333,222经过H函数得到FEDEFE,再经过R函数也得到了333,这样两条哈希链就存在重叠部分,也就是R函数出现了冲突。

  1. // 哈希链中的R函数冲突 
  2. R(FEDECE)=333 
  3. R(FEDEFE)=333 

更重要的是这种冲突是不好被发现的!

为啥这么说呢,因为实际中只存储哈希链中的首尾两个明文,对于上图中中间发生了R冲突,但是从重合起点看上面的链比下面的链位置更靠后,下面的链会继续进行余下的H-R函数最终生成尾部明文是555,然而上面的链生成尾部明文是444,在实际存储中是这样的:

  1. //哈希链1 
  2. 111->444 
  3. //哈希链2 
  4. 454->555 

也就是假如你设计的k=2,也就是每组2个H-R函数,两个哈希链可以表示8个明文,但是实际上333和444是存在重复的,从而只有6个有效不同的明文,由于尾部明文的不同,这种重叠是无法被检测的。

这么看来拥有个分布均匀的R函数是多么重要,但是在几千组H-R函数中要求完全没有冲突是非常难的,于是出现了多组R函数的新形式。

画外音:多组R函数的思路和布隆过滤器的多个hash函数的思想很相似

你可能会问为什么不考虑H函数的冲突情况,因为H函数就是我们需要破解的加密函数,它本身的冲突概率非常低要不然就不用费这么大劲搞彩虹表了。

(5) 彩虹表的基本原理

彩虹表针对哈希链集合的R函数冲突带来的重叠引入了多组不同的R函数系列,从而一个链上每个位置的R函数都是不同的,如图:

需要注意的是多个R函数的引入仍然不可避免冲突问题,但是这种情况下的冲突影响远小于哈希链集合中单个R函数的影响,如图展示了一种常见的彩虹表R函数冲突(黑色加重部分):

具体来说就是在不同位置出现了冲突:

  1. / 不同s输入 不同R函数产生x相同的明文 
  2. R1(FEDECE)=333 
  3. R2(FEDEFE)=333 

但是很快在下一个不同的R函数,R3和R2的作用下就不再重叠了,可覆盖的明文数量影响不大,也只有333出现了重叠后续是独立的。

极端情况下相同位置的冲突由于每个位置的R函数相同所以最终尾部明文仍然是相同的,可以进行纠正删除从而减少冗余数据,如图:

综上可知,彩虹表引入一组多个R函数确保了相同位置出现R冲突时的检测删除和不同位置出现R冲突的影响最小化,相比哈希链集合有一些优势。

读到这里我们对于为什么叫做彩虹表隐约有了一点感觉,大概意思就是每一组哈希链都有不同的R函数,就像这样:

(6) 彩虹表的攻击简单过程

彩虹表涉及一个复杂的建表过程,并且不同格式长度的密码和不同的哈希函数都会有不同的彩虹表,网上有一些现成的彩虹表,感兴趣的读者可以根据自己的现状下载一些彩虹表数据进行验证,一般来说在实用的彩虹表在100GB以上。

要增加破解的概率就需要完善的彩虹表数据作为支撑,彩虹表的意义就在于把计算暴力破解和空间枚举结合起来。

来简单说一下彩虹表的攻击过程吧:

假设有K组H-R函数组合,彩虹表按照[head_string,tail_string]格式存储,如图:

  • 假定给定的密文P位于最后一组H-R中,也就是第K个R函数存在R(P)=tail_string,如果tail_string存在于彩虹表中,则从head_string进行推算;
  • 如果第K个R函数生成的结果不存在,则向前继续搜索第K-1个R函数一直计算到最后获取tail_string,再判断是否存在;
  • 最坏的结果是从第1个R函数推算到最后得到的tail_srting仍然不存在,则说明这条链匹配失败;

到这里,我们基本上对彩虹表的原理和过程有了粗浅的认识,但是彩虹表也不是无敌的,因为它有个强大的对手【哈希+盐】存储。

6. 哈希+盐组合加密存储

一直在说无盐单向哈希存储,但什么是盐呢?

简单来说,盐就是在用户输入密码的基础上增加的额外部分数据,这部分数据也参与计算哈希存储密码。

  1. H(user_input_string+slat)=new_password 

和做菜一样,在存储密码中加盐也是技术活,不由得要问:为什么加盐就把单向哈希变得这么强大了呢?

其实很简单,因为盐不知道是怎么加的,也不知道加的什么!

如图展示了一个使用彩虹表破解明文之后进行登陆仍然失败的情况:

暴力破解得到的密码是bnghuopyi99,输入之后失败,因为用户输入的明文是nghuopyi,盐是b99,混合方式是取盐的第一个字符放在用户输入最前面,剩下的追加在后面从而形成了bnghuopyi99。

随机的盐和不确定的添加方式,让彩虹表不那么给力了,换句话说每个用户可能有单独的混合方式,破解成本大大增加。

到这里,仿佛哈希+盐的方式还是不错的,但是这仍然不是最优的解决方案,我们继续来看。

7. 专业的密码加密算法

前面我们学习的一些比如sha256这些算法本质上并不是为了存储密码设计的,相反这些摘要算法有其主要用途,那么不禁要问:有没有专门为密码设计的加密算法呢?

答案是肯定的。

我们都知道GPU的性能是十分恐怖的,计算速度非常快,试想我们把加密算法变慢,攻击方也必然会被拖慢,比如加密算法需要200ms完成加密存储,这个时间对于用户而言是可以接受的,但是对于攻击方来说时间成本就非常大了。

听着有种故意把对手绕晕的感觉,本质上专业的密码加密算法可以设置强度,来提高暴力破解的时间成本,比较常见的加密算法有以下几种:

  • bcrypt算法:bcrypt 是专门为密码存储而设计的算法,基于Blowfish 加密算法变形而来,由 Niels Provos 和 David Mazières 发表于 1999 年的 USENIX。bcrypt 的work factor参数, 可用于调整计算强度,而且 work factor 是包括在输出的摘要中的,随着攻击者计算能力的提高,使用者可以逐步增大 work factor
  • PBKDF2算法:PBKDF2(Password-Based Key Derivation Function) PBKDF2 就是将 salted hash 进行多次重复计算,这个次数是可以设定的,从而提高算法的加密时间。

目前这两个算法都有Python和C/C++版本,感兴趣的读者可以试一下,从专业的角度来说,采用专业的密码加密算法是最好的解决方案了。

8. 写在最后

本文通过大家熟悉的密码交互场景作为出发点阐述了明文存储密码、单向无盐哈希存储、预计算哈希链集合、彩虹表、哈希+盐存储、专业密码算法存储等几个方面的相关知识。

其中,单向无盐哈希存储、彩虹表、哈希+盐存储内容相对较多,由于其中涉及了较多的数学背景知识,篇幅以及本人水平所限并未深入展开,从实用的角度来说开发者建议采用专业的密码加密算法,作为用户也要增加密码意识。

 

责任编辑:赵宁宁 来源: 后端技术指南针
相关推荐

2020-04-27 10:34:23

HTTPDNSDNS网络协议

2020-08-10 07:58:18

异步编程调用

2023-12-25 09:03:33

MySQL索引数据库

2015-10-21 11:09:08

Xcode7调试神技

2020-12-21 09:00:04

MySQL缓存SQL

2020-07-29 07:37:20

Git 修复项目

2020-08-03 07:04:54

测试面试官应用程序

2020-12-16 08:05:54

Mybatis面试动态代理

2020-12-21 09:44:53

MySQL查询缓存数据库

2018-10-17 14:50:08

2022-10-31 18:38:24

MySQL数据订单表

2020-07-29 08:07:18

Python开发工具

2020-06-22 07:47:46

提交面试官订单

2020-05-12 11:05:54

MySQL索引数据库

2022-02-11 19:06:29

MySQL索引面试官

2024-01-11 15:17:59

Bean单例模式线程安全

2019-04-24 16:40:18

Redis数据库

2020-05-22 08:11:48

线程池JVM面试

2021-12-17 07:30:42

排序算法效率

2020-07-29 09:53:09

VSCode编码工具插件
点赞
收藏

51CTO技术栈公众号