本文转载自微信公众号「 全栈修仙之路」,作者 阿宝哥 。转载本文请联系 全栈修仙之路公众号。
在大文件上传的场景中,为了提高大文件上传的用户体验,我们会支持断点续传。在上传过程中,我们会对大文件进行分片处理,然后使用 md5 算法计算分片的哈希值,再把该分片的内容和其对应的哈希值一起提交到服务器。
当服务器接收到分片对应的哈希值时,会先查询该哈希值是否已经存在。如果存在,则表示该分片已经上传过。这时可以返回大文件已上传的字节数,从而让客户端可以继续上传剩余的内容。
其实分片对应的哈希值也可以被称为分片的 “数字指纹”,那么什么是 “数字指纹” 呢?要理解 “数字指纹”,我们需要先来了解一下什么是消息摘要算法。
一、什么是消息摘要算法
消息摘要算法是密码学算法中非常重要的一个分支,它通过对所有数据提取指纹信息以实现数据签名、数据完整性校验等功能,由于其不可逆性,有时候会被用做敏感信息的加密。消息摘要算法也被称为哈希(Hash)算法或散列算法。
任何消息经过散列函数处理后,都会获得唯一的散列值,这一过程称为 “消息摘要”,其散列值称为 “数字指纹”,其算法自然就是 “消息摘要算法” 了。换句话说,如果其数字指纹一致,就说明其消息是一致的。
(图片来源 —— https://zh.wikipedia.org/wiki/散列函數)
消息摘要算法不存在密钥的管理与分发问题,适合于分布式网络上使用。消息摘要算法主要应用在 “数字签名” 领域,作为对明文的摘要算法。著名的摘要算法有 RSA 公司的 MD5 算法和 SHA-1 算法及其大量的变体。
1.1 消息摘要算法的特点
- 无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。例如应用 MD5 算法摘要的消息有 128 个比特位,用 SHA-1 算法摘要的消息最终有 160 个比特位的输出,SHA-1的变体可以产生 192 个比特位和 256 个比特位的消息摘要。一般认为,摘要的最终输出越长,该摘要算法就越安全。
- 消息摘要看起来是 “随机的”。这些比特看上去是胡乱的杂凑在一起的,可以用大量的输入来检验其输出是否相同,一般,不同的输入会有不同的输出,而且输出的摘要消息可以通过随机性检验。一般地,只要输入的消息不同,对其进行摘要以后产生的摘要消息也必不相同;但相同的输入必会产生相同的输出。
- 消息摘要函数是单向函数,即只能进行正向的信息摘要,而无法从摘要中恢复出任何的消息,甚至根本就找不到任何与原信息相关的信息。
- 好的摘要算法,没有人能从中找到 “碰撞” 或者说极度难找到,虽然 “碰撞” 是肯定存在的(碰撞即不同的内容产生相同的摘要)。
1.2 消息摘要算法的家谱
消息摘要算法主要分为三大类:MD(Message Digest,消息摘要算法)、SHA-1(Secure Hash Algorithm,安全散列算法)和 MAC(Message Authentication Code,消息认证码算法)。
MD 系列算法包括 MD2、MD4 和 MD5 共 3 种算法;SHA 算法主要包括其代表算法 SHA-1 和 SHA-1 算法的变种 SHA-2 系列算法(包含 SHA-224、SHA-256、SHA-384 和 SHA-512);MAC 算法综合了上述两种算法,主要包括 HmacMD5、HmacSHA1、HmacSHA256、HmacSHA384 和 HmacSHA512 算法。
尽管上述内容列举了各种消息摘要算法,但仍不能满足应用需要。基于这些消息摘要算法,又衍生出了 RipeMD 系列(包含 RipeMD128、RipeMD160、RipeMD256、RipeMD320)、Tiger、GOST3411 和 Whirlpool 算法。
对于大多数前端开发者来说,接触得比较多的应该是 MD5 算法。所以,接下来阿宝哥将重点介绍 MD5 算法。
二、什么是 MD5 算法
MD5(Message Digest Algorithm 5,消息摘要算法版本5),它由 MD2、MD3、MD4 发展而来,由 Ron Rivest(RSA 公司)在 1992 年提出,目前被广泛应用于数据完整性校验、数据(消息)摘要、数据签名等。MD2、MD4、MD5 都产生 16 字节(128 位)的校验值,一般用 32 位十六进制数表示。MD2 的算法较慢但相对安全,MD4 速度很快,但安全性下降,MD5 比 MD4 更安全、速度更快。
随着计算机技术的发展和计算水平的不断提高,MD5 算法暴露出来的漏洞也越来越多。1996 年后被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法,如 SHA-2。2004 年,证实 MD5 算法无法防止碰撞(collision),因此不适用于安全性认证,如 SSL 公开密钥认证或是数字签名等用途。
2.1 MD5 特点
- 稳定、运算速度快。
- 压缩性:输入任意长度的数据,输出长度固定(128 比特位)。
- 运算不可逆:已知运算结果的情况下,无法通过通过逆运算得到原始字符串。
- 高度离散:输入的微小变化,可导致运算结果差异巨大。
2.2 MD5 散列
128 位的 MD5 散列在大多数情况下会被表示为 32 位十六进制数字。以下是一个 43 位长的仅 ASCII 字母列的 MD5 散列:
MD5("The quick brown fox jumps over the lazy dog")
= 9e107d9d372bb6826bd81d3542a419d6
- 1.
- 2.
即使在原文中作一个小变化(比如把 dog 改为 cog,只改变一个字符)其散列也会发生巨大的变化:
MD5("The quick brown fox jumps over the lazy cog")
= 1055d3e698d289f2af8663725127bd4b
- 1.
- 2.
接着我们再来举几个 MD5 散列的例子:
MD5("") -> d41d8cd98f00b204e9800998ecf8427e
MD5("semlinker") -> 688881f1c8aa6ffd3fcec471e0391e4d
MD5("kakuqo") -> e18c3c4dd05aef020946e6afbf9e04ef
- 1.
- 2.
- 3.
三、MD5 算法的用途
3.1 防止被篡改
3.1.1 文件分发防篡改
在互联网上分发软件安装包时,出于安全性考虑,为了防止软件被篡改,比如在软件安装程序中添加木马程序。软件开发者通常会使用消息摘要算法,比如 MD5 算法产生一个与文件匹配的数字指纹,这样接收者在接收到文件后,就可以利用一些现成的工具来检查文件完整性。
(图片来源 —— https://en.wikipedia.org/wiki/MD5)
这里我们来举一个实际的例子,下图是 MySQL Community Server 8.0.19 版本的下载页,该下载页通过 MD5 算法分别计算出不同软件包的数字指纹,具体如下图所示:
(图片来源 —— https://dev.mysql.com/downloads/mysql/)
当用户从官网上下载到对应的安装包之后,可以利用一些 MD5 校验工具对已下载的文件进行校验,然后比对最终的 MD5 数字指纹。若结果与官网公布的数字指纹一致,则表示该安装包未经过任何修改是安全的,基本可以放心安装。
3.1.2 消息传输防篡改
假设在网络上你需要发送电子文档给你的朋友,在文件发送前,先对文档的内容进行 MD5 运算,得出该电子文档的 “数字指纹”,并把该 “数字指纹” 随电子文档一同发送给对方。当对方接收到电子文档之后,也使用 MD5 算法对文档的内容进行哈希运算,在运算完成后也会得到一个对应 “数字指纹”,当该指纹与你所发送文档的 “数字指纹” 一致时,表示文档在传输过程中未被篡改。
3.2 信息保密
在互联网初期很多网站在数据库中以明文的形式存储用户的密码,这存在很大的安全隐患,比如数据库被黑客入侵,从而导致网站用户信息的泄露。针对这个问题,一种解决方案是在保存用户密码时,不再使用明文,而是使用消息摘要算法,比如 MD5 算法对明文密码进行哈希运算,然后把运算的结果保存到数据库中。使用上述方案,避免了在数据库中以明文方式保存密码,提高了系统的安全性,不过这种方案并不安全,后面我们会详细分析。
当用户登录时,登录系统对用户输入的密码执行 MD5 哈希运算,然后再使用用户 ID 和密码对应的 MD5 “数字指纹” 进行用户认证。若认证通过,则当前的用户可以正常登录系统。用户密码经过 MD5 哈希运算后存储的方案至少有两个好处:
防内部攻击:因为在数据库中不会以明文的方式保存密码,因此可以避免系统中用户的密码被具有系统管理员权限的人员知道。
防外部攻击:网站数据库被黑客入侵,黑客只能获取经过 MD5 运算后的密码,而不是用户的明文密码。
四、MD5 算法使用示例
在 Node.js 环境中,我们可以使用 crypto 原生模块提供的 md5 实现,当然也可以使用主流的 MD5 第三方库,比如 md5 这个可以同时运行在服务端和客户端的第三方库。在介绍具体使用前,我们需要提前安装 md5 这个第三方库,具体安装方式如下:
$ npm install md5 --save
- 1.
4.1 crypto 模块使用示例
const crypto = require("crypto");
const msg = "阿宝哥";
function md5(data) {
const hash = crypto.createHash("md5");
return hash.update(data).digest("hex");
}
console.log("Node.js Crypto MD5: " + msg + " -> " + md5(msg));
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
4.2 MD5 库使用示例
const md5 = require('md5');
const msg = "阿宝哥";
console.log("MD5 Lib MD5: " + msg + " -> " + md5(msg));
- 1.
- 2.
- 3.
- 4.
以上示例代码正常运行后,在控制台中会输出以下结果:
Node.js Crypto MD5: 阿宝哥 -> 8eec7fcf817f7340b791b32ecdbed570
MD5 Lib MD5: 阿宝哥 -> 8eec7fcf817f7340b791b32ecdbed570
- 1.
- 2.
五、MD5 算法的缺陷
哈希碰撞是指不同的输入却产生了相同的输出,好的哈希算法,应该没有人能从中找到 “碰撞” 或者说极度难找到,虽然 “碰撞” 是肯定存在的。
2005 年山东大学的王小云教授发布算法可以轻易构造 MD5 碰撞实例,此后 2007 年,有国外学者在王小云教授算法的基础上,提出了更进一步的 MD5 前缀碰撞构造算法 “chosen prefix collision”,此后还有专家陆续提供了MD5 碰撞构造的开源的库。
2009 年,中国科学院的谢涛和冯登国仅用了 220.96 的碰撞算法复杂度,破解了 MD5 的碰撞抵抗,该攻击在普通计算机上运行只需要数秒钟。
MD5 碰撞很容易构造,基于 MD5 来验证数据完整性已不可靠,考虑到近期谷歌已成功构造了 SHA-1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)的碰撞实例,对于数据完整性,应使用 SHA256 或更强的算法代替。
5.1 MD5 碰撞样本
下面我们先来看一下 MD5 碰撞的样本:
5.1.1 HEX(十六进制)样本 A1
4dc968ff0ee35c209572d4777b721587
d36fa7b21bdc56b74a3dc0783e7b9518
afbfa200a8284bf36e8e4b55b35f4275
93d849676da0d1555d8360fb5f07fea2
- 1.
- 2.
- 3.
- 4.
5.1.2 HEX(十六进制)样本 A2
4dc968ff0ee35c209572d4777b721587
d36fa7b21bdc56b74a3dc0783e7b9518
afbfa202a8284bf36e8e4b55b35f4275
93d849676da0d1d55d8360fb5f07fea2
- 1.
- 2.
- 3.
- 4.
两个样本之间的差异如下图所示:
5.2 验证 MD5 碰撞
下面我们来使用 Node.js 实际验证一下样本 A1 和样本 A2 经过 MD5 运算后输出的结果是否一致:
5.2.1 设置样本数据
let sample1 = `
4dc968ff0ee35c209572d4777b721587
d36fa7b21bdc56b74a3dc0783e7b9518
afbfa200a8284bf36e8e4b55b35f4275
93d849676da0d1555d8360fb5f07fea2`;
let sample2 = `
4dc968ff0ee35c209572d4777b721587
d36fa7b21bdc56b74a3dc0783e7b9518
afbfa202a8284bf36e8e4b55b35f4275
93d849676da0d1d55d8360fb5f07fea2
`;
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
5.2.2 定义 getHashResult 方法
function getHashResult(hexString) {
const hash = crypto.createHash("md5");
const buffer = Buffer.from(hexString.replace(/\s/g, ""), "hex");
return hash.update(buffer).digest("hex");
}
- 1.
- 2.
- 3.
- 4.
- 5.
5.2.3 执行碰撞检测
let sample1Md5 = getHashResult(sample1);
let sample2Md5 = getHashResult(sample2);
if (sample1Md5 === sample2Md5) {
console.log(`出现 MD5 碰撞: ${sample1Md5}`);
} else {
console.log(`未出现 MD5 碰撞`);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
以上代码成功运行后,在控制台中会输出以下结果:
出现 MD5 碰撞: 008ee33a9d58b51cfeb425b0959121c9
- 1.
如果你对其它 MD5 碰撞的样本感兴趣,可以查看 MD5碰撞的一些例子 这篇文章。由于基于 MD5 来验证数据完整性不太可靠,所以 Node.js 使用了 SHA256 算法来确保数据的完整性。
(图片来源 —— https://nodejs.org/download/release/v15.6.0/SHASUMS256.txt.asc)
六、MD5 密码安全性
6.1 MD5 密文反向查询
前面我们已经提到通过对用户密码进行 MD5 运算可以提高系统的安全性。但实际上,这样的安全性还是不高。为什么呢?因为只要输入相同就会产生相同的输出。接下来我们来举一个示例,字符串 123456789 是一个很常用的密码,它经过 MD5 运算后会生成一个对应的哈希值:
MD5("123456789") -> 25f9e794323b453885f5181f1b624d0b
- 1.
由于输入相同就会产生相同的结果,因此攻击者就可以根据哈希结果反推输入。其中一种常见的破解方式就是使用彩虹表。彩虹表是一个用于加密散列函数逆运算的预先计算好的表,常用于破解加密过的密码散列。 查找表常常用于包含有限字符固定长度纯文本密码的加密。这是以空间换时间的典型实践,在每一次尝试都计算的暴力破解中使用更少的计算能力和更多的储存空间,但却比简单的每个输入一条散列的翻查表使用更少的储存空间和更多的计算性能。
目前网上某些站点,比如 cmd5.com 已经为我们提供了 MD5 密文的反向查询服务,我们以 MD5("123456789") 生成的结果,做个简单的验证,具体如下图所示:
因为 123456789 是很常见的密码,因此该网站能够反向得出正确结果那就不足为奇了。以下是 cmd5 网站的站点说明,大家可以参考一下,感兴趣的小伙伴可以亲自验证一下。
现在我们已经知道如果用户的密码相同 MD5 的值就会一样,通过一些 MD5 密文的反向查询网站,密码大概率会被解析出来,这样使用相同密码的用户就会受到影响。那么该问题如何解决呢?答案是密码加盐。
6.2 密码加盐
盐(Salt),在密码学中,是指在散列之前将散列内容(例如:密码)的任意固定位置插入特定的字符串。这个在散列中加入字符串的方式称为 “加盐”。其作用是让加盐后的散列结果和没有加盐的结果不相同,在不同的应用情景中,这个处理可以增加额外的安全性。
在大部分情况,盐是不需要保密的。盐可以是随机产生的字符串,其插入的位置可以也是随意而定。如果这个散列结果在将来需要进行验证(例如:验证用户输入的密码),则需要将已使用的盐记录下来。为了便于理解,我们来举个简单的示例。
6.2.1 Node.js MD5 加盐示例
const crypto = require("crypto");
function cryptPwd(password, salt) {
const saltPassword = password + ":" + salt;
console.log("原始密码:%s", password);
console.log("加盐后的密码:%s", saltPassword);
const md5 = crypto.createHash("md5");
const result = md5.update(saltPassword).digest("hex");
console.log("加盐密码的md5值:%s", result);
}
cryptPwd("123456789","exe");
cryptPwd("123456789","eft");
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
以上示例代码正常运行后,在控制台中会输出以下结果:
原始密码:123456789
加盐后的密码:123456789:exe
加盐密码的md5值:3328003d9f786897e0749f349af490ca
原始密码:123456789
加盐后的密码:123456789:eft
加盐密码的md5值:3c45dd21ba03e8216d56dce8fe5ebabf
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
通过观察以上结果,我们发现原始密码一致,但使用的盐值不一样,最终生成的 MD5 哈希值差异也比较大。此外为了提高破解的难度,我们可以随机生成盐值并且提高盐值的长度。
6.3 bcrypt
哈希加盐的方式确实能够增加攻击者的成本,但是今天来看还远远不够,我们需要一种更加安全的方式来存储用户的密码,这也就是今天被广泛使用的 bcrypt。
bcrypt 是一个由 Niels Provos 以及 David Mazières 根据 Blowfish 加密算法所设计的密码散列函数,于 1999 年在 USENIX 中展示。bcrypt 这一算法就是为哈希密码而专门设计的,所以它是一个执行相对较慢的算法,这也就能够减少攻击者每秒能够处理的密码数量,从而避免攻击者的字典攻击。实现中 bcrypt 会使用一个加盐的流程以防御彩虹表攻击,同时 bcrypt 还是适应性函数,它可以借由增加迭代之次数来抵御日益增进的电脑运算能力透过暴力法破解。
由 bcrypt 加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是 8 至 56 个字符,并将在内部被转化为 448 位的密钥。然而,所提供的所有字符都具有十分重要的意义。密码越强大,您的数据就越安全。
下面我们以 Node.js 平台的 bcryptjs 为例,介绍一下如何使用 bcrypt 算法来处理用户的密码。首先我们需要先安装 bcryptjs:
$ npm install bcryptjs --save
- 1.
6.3.1 使用 bcryptjs 处理密码
const bcrypt = require("bcryptjs");
const password = "123456789";
const saltRounds = 10;
async function bcryptHash(str, saltRounds) {
let hashedResult;
try {
const salt = await bcrypt.genSalt(saltRounds);
hashedResult = await bcrypt.hash(str, salt);
} catch (error) {
throw error;
}
return hashedResult;
}
bcryptHash(password, saltRounds).then(console.log);
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
以上示例代码正常运行后,在控制台中会输出以下结果:
$2a$10$O1SrEy3KsgN0NQdQjaSU6OxjxDo0jf.j/e2goSwSEu4esz9i58dRm
- 1.
很明显密码 123456789 经过 bcrypt 的哈希运算后,得到了一串读不懂的 “乱码”。这里我们已经完成第一步,即用户登录密码的加密。下一步我们要实现登录密码的比对,即要保证用户输入正确的密码后,能正常登录系统。
6.3.2 使用 bcryptjs 校验密码
async function bcryptCompare(str, hashed) {
let isMatch;
try {
isMatc = await bcrypt.compare(str, hashed);
} catch (error) {
throw error;
}
return isMatch;
}
bcryptCompare(
"123456789",
"$2a$10$O1SrEy3KsgN0NQdQjaSU6OxjxDo0jf.j/e2goSwSEu4esz9i58dRm"
).then(console.log);
bcryptCompare(
"123456",
"$2a$10$O1SrEy3KsgN0NQdQjaSU6OxjxDo0jf.j/e2goSwSEu4esz9i58dRm"
).then(console.log);
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
以上示例代码正常运行后,在控制台中会输出以下结果:
true
false
- 1.
- 2.
因为我们的原始密码是 123456789,很明显与 123456 并不匹配,所以会输出以上的匹配结果。
七、总结
本文首先介绍了消息摘要算法、MD5 算法的相关概念和特点,然后详细介绍了 MD5 算法的用途和 Node.js 平台的使用示例,最后我们还分析了 MD5 算法存在的缺陷和 MD5 密码的安全性问题。这里需要大家注意的是,基于 MD5 来验证数据完整性已不可靠,考虑到近期谷歌已成功构造了 SHA-1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)的碰撞实例,对于数据完整性校验,应使用 SHA256 或更强的算法代替。
除了文中介绍的 MD5 应用场景,MD5 还可以用于实现 CDN(Content Delivery Network,内容分发网络)内容资源的防盗链,感兴趣的小伙伴可以阅读 “深入了解 Token 防盗链” 这篇文章。
近期阿宝哥突然对 JS 逆向 很感兴趣,因此打算系统学习一下相关的知识,目前已有初步的学习计划,想一起学习的小伙伴可以私聊阿宝哥哈。
八、参考资源
- 维基百科 - MD5
- 维基百科 - 彩虹表
- 维基百科 - 盐_(密码学)
- 加密基础知识一 MD5 SHA-1 CRC 加盐
- MD、SHA、MAC消息摘要算法实现与应用
- MD5 碰撞的一些例子