
消息摘要算法是密码学算法中非常重要的一个分支,它通过对所有数据提取指纹信息以实现数据签名、数据完整性校验等功能,由于其不可逆性,有时候会被用做敏感信息的加密。消息摘要算法也被称为哈希(Hash)算法或......
消息摘要算法是密码学算法中非常重要的一个分支,它通过对所有数据提取指纹信息以实现数据签名、数据完整性校验等功能,由于其不可逆性,有时候会被用做敏感信息的加密。消息摘要算法也被称为哈希(Hash)算法或散列算法。
任何消息经过散列函数处理后,都会获得唯一的散列值,这一过程称为“消息摘要”,其散列值称为“数字指纹”,其算法自然就是“消息摘要算法”了。换句话说,如果其数字指纹一致,就说明其消息是一致的。

消息摘要算法的主要特征是加密过程不需要密钥,并且经过加密的数据无法被解密,目前可以解密逆向的只有CRC32算法,只有输入相同的明文数据经过相同的消息摘要算法才能得到相同的密文。消息摘要算法不存在密钥的管理与分发问题,适合于分布式网络上使用。消息摘要算法主要应用在“数字签名”领域,作为对明文的摘要算法。著名的摘要算法有RSA公司的MD5算法和SHA-1算法及其大量的变体。
1.1消息摘要算法的特点无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。例如应用MD5算法摘要的消息有128个比特位,用SHA-1算法摘要的消息最终有160个比特位的输出,SHA-1的变体可以产生192个比特位和256个比特位的消息摘要。一般认为,摘要的最终输出越长,该摘要算法就越安全。
消息摘要看起来是“随机的”。这些比特看上去是胡乱的杂凑在一起的,可以用大量的输入来检验其输出是否相同,一般,不同的输入会有不同的输出,而且输出的摘要消息可以通过随机性检验。一般地,只要输入的消息不同,对其进行摘要以后产生的摘要消息也必不相同;但相同的输入必会产生相同的输出。
消息摘要函数是单向函数,即只能进行正向的信息摘要,而无法从摘要中恢复出任何的消息,甚至根本就找不到任何与原信息相关的信息。
好的摘要算法,没有人能从中找到“碰撞”或者说极度难找到,虽然“碰撞”是肯定存在的(碰撞即不同的内容产生相同的摘要)。
二、什么是MD5算法MD5(MessageDigestAlgorithm5,消息摘要算法版本5),它由MD2、MD3、MD4发展而来,由RonRivest(RSA公司)在1992年提出,目前被广泛应用于数据完整性校验、数据(消息)摘要、数据签名等。MD2、MD4、MD5都产生16字节(128位)的校验值,一般用32位十六进制数表示。MD2的算法较慢但相对安全,MD4速度很快,但安全性下降,MD5比MD4更安全、速度更快。
随着计算机技术的发展和计算水平的不断提高,MD5算法暴露出来的漏洞也越来越多。1996年后被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法,如SHA-2。2004年,证实MD5算法无法防止碰撞(collision),因此不适用于安全性认证,如SSL公开密钥认证或是数字签名等用途。
2.1MD5特点稳定、运算速度快。
压缩性:输入任意长度的数据,输出长度固定(128比特位)。
运算不可逆:已知运算结果的情况下,无法通过通过逆运算得到原始字符串。
高度离散:输入的微小变化,可导致运算结果差异巨大。
2.2MD5散列128位的MD5散列在大多数情况下会被表示为32位十六进制数字。以下是一个43位长的仅ASCII字母列的MD5散列:
MD5("Thequickbrownfoxjumpsoverthelazydog")=9e107d9d372bb6826bd81d3542a419d6即使在原文中作一个小变化(比如把dog改为cog,只改变一个字符)其散列也会发生巨大的变化:
MD5("Thequickbrownfoxjumpsoverthelazycog")=1055d3e698d289f2af8663725127bd4b接着我们再来举几个MD5散列的例子:
MD5("")-d41d8cd98f00b204e9800998ecf8427eMD5("semlinker")-688881f1c8aa6ffd3fcec471e0391e4dMD5("kakuqo")-e18c3c4dd05aef020946e6afbf9e04ef三、MD5算法的用途3.1防止被篡改3.1.1文件分发防篡改在互联网上分发软件安装包时,出于安全性考虑,为了防止软件被篡改,比如在软件安装程序中添加木马程序。软件开发者通常会使用消息摘要算法,比如MD5算法产生一个与文件匹配的数字指纹,这样接收者在接收到文件后,就可以利用一些现成的工具来检查文件完整性。

这里我们来举一个实际的例子,下图是版本的下载页,该下载页通过MD5算法分别计算出不同软件包的数字指纹,具体如下图所示:

当用户从官网上下载到对应的安装包之后,可以利用一些MD5校验工具对已下载的文件进行校验,然后比对最终的MD5数字指纹,若结果与官网公布的数字指纹一致,则表示该安装包未经过任何修改是安全的,基本可以放心安装。
3.1.2消息传输防篡改假设在网络上你需要发送电子文档给你的朋友,在文件发送前,先对文档的内容进行MD5运算,得出该电子文档的“数字指纹”,并把该“数字指纹”随电子文档一同发送给对方。当对方接收到电子文档之后,也使用MD5算法对文档的内容进行哈希运算,在运算完成后也会得到一个对应“数字指纹”,当该指纹与你所发送文档的“数字指纹”一致时,表示文档在传输过程中未被篡改。
3.2信息保密在互联网初期很多网站在数据库中以明文的形式存储用户的密码,这存在很大的安全隐患,比如数据库被黑客入侵,从而导致网站用户信息的泄露。针对这个问题,一种解决方案是在保存用户密码时,不再使用明文,而是使用消息摘要算法,比如MD5算法对明文密码进行哈希运算,然后把运算的结果保存到数据库中。使用上述方案,避免了在数据库中以明文方式保存密码,提高了系统的安全性,不过这种方案并不安全,后面我们会详细分析。

当用户登录时,登录系统对用户输入的密码执行MD5哈希运算,然后再使用用户ID和密码对应的MD5“数字指纹”进行用户认证。若认证通过,则当前的用户可以正常登录系统。用户密码经过MD5哈希运算后存储的方案至少有两个好处:
防内部攻击:因为在数据库中不会以明文的方式保存密码,因此可以避免系统中用户的密码被具有系统管理员权限的人员知道。
防外部攻击:网站数据库被黑客入侵,黑客只能获取经过MD5运算后的密码,而不是用户的明文密码。
四、MD5算法使用示例4.1Java示例在Java中使用MD5算法很方便,可以直接使用JDK自带的MD5实现,也可以使用第三方库提供的MD5实现。下面我们将介绍JDK、BouncyCastle和Guava的MD5使用示例。为了保证以下示例的正常运行,首先我们需要在文件中添加BouncyCastle和Guava的坐标:
/groupIdartifactIdbcprov-jdk15on//version//groupIdartifactIdguava//version/depencyJDK实现
publicstaticvoidjdkMD5(Stringsrc)throwsNoSuchAlgorithmException{MessageDigestmd=("MD5");byte[]md5Bytes=(());("JDKMD5:"+src+"-"+bytesToHexString(md5Bytes));}BouncyCastle实现publicstaticvoidbcMD5(Stringsrc){MD5Digestdigest=newMD5Digest();((),0,().length);byte[]md5Bytes=newbyte[()];(md5Bytes,0);("BouncyCastleMD5:"+src+"-"+bytesToHexString(md5Bytes));}Guava实现publicstaticvoidguavaMD5(Stringsrc){HashFunctionhf=();HashCodehc=().putString(src,()).hash();("GuavaMD5:"+src+"-"+hc);}在JDK实现和BouncyCastle实现的示例中使用了bytesToHexString方法,该方法用于把字节数组转换成十六进制,它的具体实现如下:
privatestaticStringbytesToHexString(byte[]src){StringBuilderstringBuilder=newStringBuilder();if(src==null||=0){returnnull;}for(inti=0;;i++){intv=src[i]0xFF;Stringhv=(v);if(()2){(0);}(hv);}();}介绍完MD5算法不同的实现,下面我们来测试一下上述的方法:
publicstaticvoidmain(String[]args)throwsNoSuchAlgorithmException{jdkMD5("123");bcMD5("123");guavaMD5("123");}以上示例代码正常运行后,在控制台中会输出以下结果:
JDKMD5:123-202cb962ac59075b964b07152d234b70BouncyCastleMD5:123-202cb962ac59075b964b07152d234b70GuavaMD5:123-202cb962ac59075b964b07152d234b704.2示例
在环境中,我们可以使用crypto原生模块提供的md5实现,当然也可以使用主流的MD5第三方库,比如md5这个可以同时运行在服务端和客户端的第三方库。与Java示例一样,在介绍具体使用前,我们需要提前安装md5这个第三方库,具体安装方式如下:
$npminstallmd5--save实现
constcrypto=require('crypto');constmsg="123";functionmd5(data){consthash=('md5');(data).digest('hex');}(":"+msg+"-"+md5(msg));第三方库实现constmd5=require('md5');constmsg="123";("MD5LibMD5:"+msg+"-"+md5(msg));以上示例代码正常运行后,在控制台中会输出以下结果:
:123-202cb962ac59075b964b07152d234b70MD5LibMD5:123-202cb962ac59075b964b07152d234b70五、MD5算法的缺陷
哈希碰撞是指不同的输入却产生了相同的输出,好的哈希算法,应该没有人能从中找到“碰撞”或者说极度难找到,虽然“碰撞”是肯定存在的。
2005年山东大学的王小云教授发布算法可以轻易构造MD5碰撞实例,此后2007年,有国外学者在王小云教授算法的基础上,提出了更进一步的MD5前缀碰撞构造算法“chosenprefixcollision”,此后还有专家陆续提供了MD5碰撞构造的开源的库。
2009年,中国科学院的谢涛和冯登国仅用了220.96的碰撞算法复杂度,破解了MD5的碰撞抵抗,该攻击在普通计算机上运行只需要数秒钟。
MD5碰撞很容易构造,基于MD5来验证数据完整性已不可靠,考虑到近期谷歌已成功构造了SHA-1(英语:SecureHashAlgorithm1,中文名:安全散列算法1)的碰撞实例,对于数据完整性,应使用SHA256或更强的算法代替。下面我们来看个简单的MD5碰撞示例:
HEX(十六进制)样本A14dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa200a8284bf36e8e4b55b35f427593d849676da0d1555d8360fb5f07fea2HEX(十六进制)样本A2
4dc968ff0ee35c209572d4777b721587d36fa7b21bdc56b74a3dc0783e7b9518afbfa202a8284bf36e8e4b55b35f427593d849676da0d1d55d8360fb5f07fea2
两个样本之间的差异如下图所示:

下面我们来通过Java代码实际验证一下样本A1和样本A2经过MD5运算后输出的结果是否一致:
jdkMd5Hex方法publicstaticvoidjdkMd5Hex(StringhexStr)throwsNoSuchAlgorithmException{byte[]bytes=hexStringToBytes(hexStr);MessageDigestmd=("MD5");byte[]md5Bytes=(bytes);("JDKMD5:"+hexStr+"-"+bytesToHexString(md5Bytes));}hexStringToBytes方法publicstaticbyte[]hexStringToBytes(Strings){intlen=();byte[]data=newbyte[len/2];for(inti=0;ilen;i+=2){data[i/2]=(byte)((((i),16)4)+((i+1),16));}returndata;}main方法publicstaticvoidmain(String[]args)throwsNoSuchAlgorithmException{jdkMd5Hex("4dc968ff");//样本AjdkMd5Hex("4dc968ff");//样本B}以上示例代码正常运行后,在控制台中会输出以下结果:
JDKMD5:4dc968ff-008ee33a9d58b51cfeb425b0959121c9JDKMD5:4dc968ff-008ee33a9d58b51cfeb425b0959121c9
如果你对其它MD5碰撞的样本感兴趣,可以查看MD5碰撞的一些例子这篇文章。由于基于MD5来验证数据完整性已不可靠,因此很多人都熟悉的使用了SHA256算法来确保数据的完整性。

前面我们已经提到通过对用户密码进行MD5运算可以提高系统的安全性。但实际上,这样的安全性还是不高。为什么呢?因为只要输入相同就会产生相同的输出。接下来我们来举一个示例,字符串123456789是一个很常用的密码,它经过MD5运算后会生成一个对应的哈希值:
MD5("123456789")-25f9e794323b453885f5181f1b624d0b由于输入相同就会产生相同的结果,因此攻击者就可以根据哈希结果反推输入。其中一种常见的破解方式就是使用彩虹表。彩虹表是一个用于加密散列函数逆运算的预先计算好的表,常用于破解加密过的密码散列。查找表常常用于包含有限字符固定长度纯文本密码的加密。这是以空间换时间的典型实践,在每一次尝试都计算的暴力破解中使用更少的计算能力和更多的储存空间,但却比简单的每个输入一条散列的翻查表使用更少的储存空间和更多的计算性能。
目前网上某些站点,比如已经为我们提供了MD5密文的反向查询服务,我们以MD5("123456789")生成的结果,做个简单的验证,具体如下图所示:

因为123456789是很常见的密码,因此该网站能够反向得出正确结果那就不足为奇了。以下是cmd5网站的站点说明,大家可以参考一下,感兴趣的小伙伴可以亲自验证一下。
本站针对md5、sha1等全球通用公开的加密算法进行反向查询,通过穷举字符组合的方式,创建了明文密文对应查询数据库,创建的记录约90万亿条,占用硬盘超过500TB,查询成功率95%以上,很多复杂密文只有本站才可查询。已稳定运行十余年,国内外享有盛誉。
现在我们已经知道如果用户的密码相同MD5的值就会一样,通过一些MD5密文的反向查询网站,密码大概率会被解析出来,这样使用相同密码的用户就会收到影响。那么该问题如何解决呢?答案是密码加盐。
6.2密码加盐盐(Salt),在密码学中,是指在散列之前将散列内容(例如:密码)的任意固定位置插入特定的字符串。这个在散列中加入字符串的方式称为“加盐”。其作用是让加盐后的散列结果和没有加盐的结果不相同,在不同的应用情景中,这个处理可以增加额外的安全性。
在大部分情况,盐是不需要保密的。盐可以是随机产生的字符串,其插入的位置可以也是随意而定。如果这个散列结果在将来需要进行验证(例如:验证用户输入的密码),则需要将已使用的盐记录下来。为了便于理解,我们来举个简单的示例。
加盐示例constcrypto=require("crypto");functioncryptPwd(password,salt){constsaltPassword=password+":"+salt;("原始密码:%s",password);("加盐后的密码:%s",saltPassword);constmd5=("md5");constresult=(saltPassword).digest("hex");("加盐密码的md5值:%s",result);}cryptPwd("123456789","exe");cryptPwd("123456789","eft");以上示例代码正常运行后,在控制台中会输出以下结果:
原始密码:123456789加盐后的密码:123456789:exe加盐密码的md5值:3328003d9f786897e0749f349af490ca原始密码:123456789加盐后的密码:123456789:eft加盐密码的md5值:3c45dd21ba03e8216d56dce8fe5ebabf
通过观察以上结果,我们发现原始密码一致,但使用的盐值不一样,最终生成的MD5哈希值差异也比较大。此外为了提高破解的难度,我们可以随机生成盐值并且提高盐值的长度。
6.3bcrypt哈希加盐的方式确实能够增加攻击者的成本,但是今天来看还远远不够,我们需要一种更加安全的方式来存储用户的密码,这也就是今天被广泛使用的bcrypt。
bcrypt是一个由NielsProvos以及DavidMazières根据Blowfish加密算法所设计的密码散列函数,于1999年在USENIX中展示。bcrypt这一算法就是为哈希密码而专门设计的,所以它是一个执行相对较慢的算法,这也就能够减少攻击者每秒能够处理的密码数量,从而避免攻击者的字典攻击。实现中bcrypt会使用一个加盐的流程以防御彩虹表攻击,同时bcrypt还是适应性函数,它可以借由增加迭代之次数来抵御日益增进的电脑运算能力透过暴力法破解。
由bcrypt加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符,并将在内部被转化为448位的密钥。然而,所提供的所有字符都具有十分重要的意义。密码越强大,您的数据就越安全。
下面我们以平台的bcryptjs为例,介绍一下如何使用bcrypt算法来处理用户密码。首先我们需要先安装bcryptjs:
$npminstallbcryptjs--save处理密码
constbcrypt=require("bcryptjs");constpassword="123456789";constsaltRounds=10;asyncfunctionbcryptHash(str,saltRounds){lethashedResult;try{constsalt=(saltRounds);hashedResult=(str,salt);}catch(error){throwerror;}returnhashedResult;}bcryptHash(password,saltRounds).then();以上示例代码正常运行后,在控制台中会输出以下结果:
$2a$10$/e2goSwSEu4esz9i58dRm
很明显密码123456789经过bcrypt的哈希运算后,得到了一串读不懂的“乱码”。这里我们已经完成第一步,即用户登录密码的加密。下一步我们要实现登录密码的比对,即要保证用户输入正确的密码后,能正常登录系统。
密码校验asyncfunctionbcryptCompare(str,hashed){letisMatch;try{isMatc=(str,hashed);}catch(error){throwerror;}returnisMatch;}bcryptCompare("123456789","$2a$10$/e2goSwSEu4esz9i58dRm").then();bcryptCompare("123456","$2a$10$/e2goSwSEu4esz9i58dRm").then();以上示例代码正常运行后,在控制台中会输出以下结果:
truefalse
因为我们的原始密码是123456789,很明显与123456并不匹配,所以会输出以上的匹配结果。
七、总结本文首先介绍了消息摘要算法、MD5算法的相关概念和特点,然后详细介绍了MD5算法的用途和Java和平台的使用示例,最后我们还分析了MD5算法存在的缺陷和MD5密码的安全性问题。这里大家需要注意,由于MD5碰撞很容易构造,基于MD5来验证数据完整性已不可靠,考虑到近期谷歌已成功构造了SHA-1(英语:SecureHashAlgorithm1,中文名:安全散列算法1)的碰撞实例,对于数据完整性,应使用SHA256或更强的算法代替。
除了文中介绍的MD5应用场景,MD5还可以用于实现CDN(ContentDeliveryNetwork,内容分发网络)内容资源的防盗链,感兴趣的小伙伴可以阅读深入了解Token防盗链这篇文章。