哈希算法是什么,它有什么作用

本文的目的是为了让没什么计算机基础的人也能看懂哈希算法的作用,如果觉得有看不懂或写得不对/不好的地方欢迎联系本人,我会修改到尽量让0基础的人也能看懂

什么是哈希算法

Hash算法 有很多的译名,一般被翻译成哈希、散列、杂凑等(本文就称其为哈希),有时也被叫做信息摘要啥的,本意是指“把…弄糟,搞砸”
它的作用也和它的名字类似,给定一个任意长度输入,它能根据输入计算出一个长度确定的值

以一种哈希算法md5举例,如果你输入的内容是114514,那得到的结果(哈希值)就是c4d038b4bed09fdb1471ef51ec3a32cd

如果只是这样的话那这个算法也没啥用,但哈希算法还有几个特性:

  1. 对于所有计算出来的哈希值,只要两个哈希值不同,那原输入就一定不同
  2. 哈希算法具有雪崩效应,原输入哪怕只改变一点点,那计算出的哈希值都会完全不同
  3. 无法直接从算出来的哈希值反推出原先的输入(因为计算的时候会丢失很多信息)

举个例子,以下是几个不同的数的md5值

1
2
3
4
114514  => c4d038b4bed09fdb1471ef51ec3a32cd
114513 => 77666beaaa7fd86de9b9373ded05db44
11451 => f0eb6568ea114ba6e293f903c34d7488
1145141 => 9e39382c84a52a4b8fe2e594955901c8

发现没有?不管你是改一位数、增加一位数还是删除一位数,最后得到的md5值都完全不同
如果不相信的话可以找个在线计算md5(别的哈希算法也行)的网站试试

正是因为这些特性,哈希算法才能被用于各种用途

哈希算法的各种用途

由于以上的特性,哈希算法可以用来验证原数据是否是某段数据,就像你识别一个人可以观察TA的指纹,而不需要再观测别的信息。因此,哈希算法有着各种各样的用途

文件完整性校验

上文有提到哈希算法对于输入的数值非常敏感,哪怕你只改了一个比特,那得到的结果都会完全不同

因此,假设我们有个网站提供文件A的下载,只要我们把文件A的哈希值计算出来并放在下载按钮旁边,那用户在拿到文件后只需要在本地计算一遍文件的哈希值再和网站上放的哈希值对比,就可以知道下载下来的问题是否有被损坏

注:哈希算法只能保证文件未被修改,不能保证提供者是可信的。如果真想伪造网站的话可以连文件的哈希值也一起伪造

这种校验也被各种程序使用,假如你有个程序能自动下载并安装某模块,为了防止下载下来的模块有问题导致运行出错,可以让服务器提供一下这个模块的哈希值。然后程序就能自动计算下载下来的模块的哈希值,并和服务器提供的哈希值进行比较,如果不一致就表明下载下来的文件有问题。这时候程序就能选择重新下载这个模块,而不是直接用这个模块然后运行时疯狂报错,还打死都找不到是哪出错了

一些网盘之类的文件扫描、文件秒传也是用哈希算法实现的。秒传的原理就是在上传文件前,客户端先把文件的哈希值计算出来并告诉服务器,服务器就去查找是否有存储过对应哈希值的文件,有的话就直接告诉客户端这文件已经存储过了,然后客户端就直接告诉用户文件上传完成,没有的话再去真正地上传文件。违规文件扫描也是这样的,服务器存储违规文件的哈希值,这样客户端上传文件的时候只要文件的哈希值被识别为违规,那上传就会直接被拒绝。这也是为什么一些聊天软件的图片屏蔽也只要改原图的一个像素都可以轻易绕过,因为哈希算法对输入非常敏感,改一个字节算出的哈希值都完全不同。只用哈希算法进行的文件校验都可以通过修改文件内容(哪怕只改一个字)的方法直接绕过

安全存储用户的密码

假设有个网站要存用户的用户名和密码到数据库里,那可以先看看以下几个方案

明文存储

这是最容易想到的思路,但同时这也是最不安全的存储方式,将用户输入的密码原样存进数据库里。为什么这么做不安全?万一网站后台有内鬼,或者因为别的什么原因数据库泄露了,那就直接寄了,别人可以直接用数据库里存的密码登录你网站的任何用户

最关键的是这样不仅会导致你网站的数据全都寄了,而且还会危害使用你网站的所有用户。很多用户喜欢在所有网站都用同一套(或类似的)用户名和密码,你泄露了用户在A网站注册用的用户名和密码,那攻击者拿到后可以拿它们去B、C、D、E、F网站尝试,就算这么做的成功率不是很高,但只要尝试的数量够多,总会有人中招的,因此作为用户也千万别全网都用同一套用户名和密码

对密码进行对称加密后存储

这样做虽说会比明文存储安全(废话),但也是有挺高的风险的

对称加密基本就是指用一个密码加密数据后,你也只能用这个密码来解密数据。但别忘了一件事,进行对称加密所需的密码你肯定也是要找个地方存储的,况且每次验证用户登录你也必须要使用这个密码,因此当数据库泄露后,如果这个密码也被一起泄露了,那一样是白给

存储密码的哈希值

还记得上文所说的哈希算法的特性吗?哈希算法是不可逆的,当你得到一个哈希值的时候,你是没法反推出它的原始值的。这样看上去用密码的哈希值来验证密码就刚好了。我们不存储原始的密码,而是直接存储密码的哈希值。这样验证密码的时候我们也不拿原始密码去对比,而是先计算用户输入的密码的哈希值,再去数据库里对比存储的密码哈希值

这样,我们在不知道原始密码的情况下依然可以验证用户输入的密码,同时由于哈希算法的特性,就算有人得到了数据库里存储的哈希值,他也没法通过哈希值逆推出原先的密码。所以为什么现在的app和网站在你忘记密码的时候都没法告诉你原始密码,只能让你重新设置?因为服务器自己都不知道你原始密码到底是什么,只有密码的哈希值,因此只能让你去重新设置了

这样做既保证了登录验证功能能正常实现,同时也能保证就算数据泄露用户的密码也不会跟着被泄露,看上去这是个完美的解决方案。

是吗?

存储密码的加盐哈希值

虽然哈希算法是不可逆的,但有个攻击方法叫彩虹表

我虽然知道114514的md5值是c4d038b4bed09fdb1471ef51ec3a32cd,但没法用c4d038b4bed09fdb1471ef51ec3a32cd反推出114514,那怎么办?简单,那就自己搞个数据库,在里面存入数据c4d038b4bed09fdb1471ef51ec3a32cd => 114514。既然没法逆推出原始数据,那就生成一堆常见的数据,并把它们的哈希值算出来,再存进我自己这个数据库里,这样下次遇到对应的哈希值就去查自己的数据库就好了。当然,你不可能穷举所有的排列组合并把它们的哈希值都存储起来,因此这个方法也只能破解常见密码的哈希值

那你总不能说我数据库泄露后只能保证密码复杂的人的密码才不会破解吧 (当然你要觉得用简单密码的人都是傻逼,活该密码被破解的话当我没说) ,因此我们可以使用加盐哈希来存储密码

什么是加盐哈希?简单来说就是我们在计算哈希值之前,先对原先的数据进行处理,加上一点“盐”,再去计算加盐后的数据的哈希值

举个例子,p@ssword是个常见密码,它的md5值是90f2c9c53f66540e67349e0ab83d8cd0,你可以在一些md5破解网站(这些网站的原理就是刚刚说的彩虹表)试试看这段md5值,都能直接查询出来

但如果我给p@ssword后面加上TURaWg==再计算md5值呢?那p@sswordTURaWg==的md5值就是17e7c834da0551e8872d77f741b58bbc,和刚刚的值完全不同(别忘了上文说的哈希算法具有雪崩效应,差一个字结果都完全不同),你再去这些破解网站就查不出来了。这样就算泄露了密码哈希值和盐的值,攻击者也必须得重新跑一遍彩虹表才有可能得知用户的密码是否是某些常见密码,要付出极高的时间成本。但假如我给每个用户都加上随机的的盐呢?那攻击者就真要疯了,要是还想暴力破解的话得针对每个用户都去跑个彩虹表,时间成本高到根本无法接受(当然这样依然是有可能跑出简单的密码的,所以别使用常见或简单的密码

所以,存储用户密码的时候可以这么设计,往数据库里存用户id + 密码的加盐哈希值 + 随机生成的盐,验证用户登录的时候就先读取盐,再把用户输入的密码加上盐之后计算哈希值,最后再和数据库里的加盐哈希值进行比较,这样可以有效避免密码被暴力破解。为了尽量防止密码被暴力破解,还可以换用计算时间所需更久的哈希算法

思考题:盐的内容对暴力破解的难度有影响吗?盐的长度呢?如果我不在密码后面加盐,而是在密码的前面加盐呢?如果我再换别的加盐方式,比如每隔一个字把盐的一个字符穿插在密码中间呢?

数字签名技术

前面说过哈希算法可以用来整文件完整性校验,但不能确保文件的提供者是可信的。不过配合上非对称加密就可以做到这一点,这种技术被称作数字签名。有关数字签名的内容我不打算在这篇文章介绍,涉及的内容比较多,感兴趣的话可以看看其他人的讲解

【软件科普】如何保证发出去的微信和QQ消息不被篡改?详解RSA加密算法

哈希算法的漏洞

哈希算法也并不是无敌的,也存在着一些漏洞

哈希碰撞

正如你可以通过假指纹的方式骗过指纹验证,哈希算法也有办法碰撞(即用不同的输入得到相同的哈希值)

以md5为例,它输出的结果是个32位的16进制数,那就一共有 16^32^ = 340282366920938463463374607431768211456 种可能的排列组合。别的哈希算法的组合可能更多,但本质还是一样的。虽然这个数非常大,但跟哈希算法可能遇到的各种输入,这个数是远远不够的。计算哈希值的时候都会丢失很多的信息。由于鸽笼原理可知,当我们把10个鸽子存进5个笼子的时候,必定有至少一个笼子里有一只以上的鸽子。因此,当数据的排列组合够多的时候,必然也会产生哈希碰撞,即两个不一样的数据能计算出相同的哈希值。举个最简单的例子,md5的输出是32位的16进制数,那你给他输入100位的16进制数的所有排列组合,它一定会生成很多一样的哈希值

其实可以反过来想,如果哈希值不会碰撞,那就表明一个哈希值能对应一个唯一的原数据,那岂不是说我可以直接用个md5值来代替一个1G的电影,然后存储的时候直接存储那个md5,要播放的时候直接根据这个md5计算出电影的原始数据然后直接播放,这样的话你还买硬盘干什么(

哈希碰撞并不是危言耸听,像md5就被爆出了能快速构造出两个md5相同的不同文件的漏洞

结合上文提到的哈希算法的用途,那就有以下的攻击手段

比如文件下载的时候,正常的文件A哈希值是a,但如果攻击者能想办法构造出一个病毒文件B,并让它的哈希值也为a,那就可以欺骗过简单的文件校验,让别人或别的程序以为A和B是同一个文件。因此,在检验哈希值的时候还可以顺便校验下文件的大小,这样可以大幅降低被假的文件欺骗的概率

不过虽然存在哈希碰撞这种攻击手段,但这并不是说哈希算法就一无是处,就能随便生成相同的哈希值了。虽然理论上是一定可以生成哈希值相同的数据的,但实际上要找到这样的数据是极其困难的,哪怕是已经被证明有缺陷的md5也只能生成两个哈希值相同的文件。对于一个给定的md5值,还是没法逆推出原数据或者可以和原数据哈希碰撞的值。而且现在有不少算法至今仍未被找出碰撞案例,例如sha-256、sha-384和sha-512算法。即使是已被证明为不安全的哈希算法,我们仍然可以在对安全性没什么要求的地方继续使用它们进行简单的验证。但在安全性要求高的地方,还是得用安全的哈希算法来进行各种操作

文章作者: Light_Quanta
文章链接: https://lq0.tech/2022/07/23/hash/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY 4.0 许可协议。转载请注明来自 Light_Quanta's Site