什么是MD5
MD5(Message Digest Algorithm 5,消息摘要算法版本5),它由 MD2、MD3、MD4 发展而来,由 Ron Rivest(RSA 公司)在 1992 年提出,目前被广泛应用于数据完整性校验、数据(消息)摘要、数据签名等。MD2、MD4、MD5 都产生 16 字节(128 位)的校验值,一般用 32 位十六进制数表示。MD2 的算法较慢但相对安全,MD4 速度很快,但安全性下降,MD5 比 MD4 更安全、速度更快。
什么是消息摘要
它通过对所有数据提取指纹信息以实现数据签名、数据完整性校验等功能,由于其不可逆性,有时候会被用做敏感信息的加密。消息摘要算法也被称为哈希(Hash)算法或散列算法。
消息摘要算法的主要特征是加密过程不需要密钥,并且经过加密的数据无法被解密,目前可以解密逆向的只有 CRC32 算法,只有输入相同的明文数据经过相同的消息摘要算法才能得到相同的密文。
2.1 消息摘要算法的特点
- 无论输入的消息有多长,计算出来的消息摘要的长度总是固定的
- 消息摘要看起来是 “随机的”。这些比特看上去是胡乱的杂凑在一起的,可以用大量的输入来检验其输出是否相同,一般,不同的输入会有不同的输出,而且输出的摘要消息可以通过随机性检验。一般地,只要输入的消息不同,对其进行摘要以后产生的摘要消息也必不相同;但相同的输入必会产生相同的输出。
- 消息摘要函数是单向函数,即只能进行正向的信息摘要,而无法从摘要中恢复出任何的消息,甚至根本就找不到任何与原信息相关的信息
- 好的摘要算法,没有人能从中找到 “碰撞” 或者说极度难找到,虽然 “碰撞” 是肯定存在的(碰撞即不同的内容产生相同的摘要)。
MD5的特点
- 稳定、运算速度快。
- 压缩性:输入任意长度的数据,输出长度固定(128 比特位)。
- 运算不可逆:已知运算结果的情况下,无法通过通过逆运算得到原始字符串。
- 高度离散:输入的微小变化,可导致运算结果差异巨大。
MD5 散列
128 位的 MD5 散列在大多数情况下会被表示为 32 位十六进制数字。以下是一个 43 位长的仅 ASCII 字母列的MD5 散列:
MD5("The quick brown fox jumps over the lazy dog")
= 9e107d9d372bb6826bd81d3542a419d6
MD5 算法的用途
-
防止被篡改
-
- 文件分发防篡改
-
- 消息传输防篡改
-
信息保密 ,密码使用MD5运算后保存,当用户登录时,登录系统对用户输入的密码执行 MD5 哈希运算,然后再使用用户 ID 和密码对应的 MD5 “数字指纹” 进行用户认证。若认证通过,则当前的用户可以正常登录系统。用户密码经过 MD5 哈希运算后存储的方案至少有两个好处:
-
防内部攻击:因为在数据库中不会以明文的方式保存密码,因此可以避免系统中用户的密码被具有系统管理员权限的人员知道。
-
防外部攻击:网站数据库被黑客入侵,黑客只能获取经过 MD5 运算后的密码,而不是用户的明文密码。
MD5 实例
在 Node.js 环境中,我们可以使用 crypto 原生模块提供的 md5 实现,当然也可以使用主流的 MD5 第三方库,比如 md5 这个可以同时运行在服务端和客户端的第三方库。与 Java 示例一样,在介绍具体使用前,我们需要提前安装 md5 这个第三方库,具体安装方式如下:
npm install md5 --save
Node.js Crypto 实现
const crypto = require('crypto');
const msg = "123";
function md5(data){
const hash = crypto.createHash('md5');
return hash.update(data).digest('hex');
}
console.log("Node.js Crypto MD5:" + msg + " -> " + md5(msg));
Node.js MD5 第三方库实现
const md5 = require('md5');
const msg = "123";
console.log("MD5 Lib MD5:" + msg + " -> " + md5(msg));
MD5 算法的缺陷
哈希碰撞是指不同的输入却产生了相同的输出,好的哈希算法,应该没有人能从中找到 “碰撞” 或者说极度难找到,虽然 “碰撞” 是肯定存在的。
2005 年山东大学的王小云教授发布算法可以轻易构造 MD5 碰撞实例,此后 2007 年,有国外学者在王小云教授算法的基础上,提出了更进一步的 MD5 前缀碰撞构造算法 “chosen prefix collision”,此后还有专家陆续提供了MD5 碰撞构造的开源的库。
2009 年,中国科学院的谢涛和冯登国仅用了 220.96 的碰撞算法复杂度,破解了 MD5 的碰撞抵抗,该攻击在普通计算机上运行只需要数秒钟。
下面我们来看个简单的 MD5 碰撞示例:
样本1
4dc968ff0ee35c209572d4777b721587 d36fa7b21bdc56b74a3dc0783e7b9518
afbfa200a8284bf36e8e4b55b35f4275 93d849676da0d1555d8360fb5f07fea2
样本2
4dc968ff0ee35c209572d4777b721587
d36fa7b21bdc56b74a3dc0783e7b9518
afbfa202a8284bf36e8e4b55b35f4275
93d849676da0d1d55d8360fb5f07fea2
以上两者经过MD5运算的结果是一样的
MD5 密码安全性
MD5 密文反向查询
前面我们已经提到通过对用户密码进行 MD5 运算可以提高系统的安全性。但实际上,这样的安全性还是不高。为什么呢?因为只要输入相同就会产生相同的输出。接下来我们来举一个示例,字符串 123456789 是一个很常用的密码,它经过 MD5 运算后会生成一个对应的哈希值:
MD5("123456789") -> 25f9e794323b453885f5181f1b624d0b
由于输入相同就会产生相同的结果,因此攻击者就可以根据哈希结果反推输入。其中一种常见的破解方式就是使用彩虹表。彩虹表是一个用于加密散列函数逆运算的预先计算好的表,常用于破解加密过的密码散列。 查找表常常用于包含有限字符固定长度纯文本密码的加密。这是以空间换时间的典型实践,在每一次尝试都计算的暴力破解中使用更少的计算能力和更多的储存空间,但却比简单的每个输入一条散列的翻查表使用更少的储存空间和更多的计算性能。
目前网上某些站点,比如 cmd5.com 已经为我们提供了 MD5 密文的反向查询服务,我们以 MD5(“123456789”) 生成的结果.
密码加盐
**盐(Salt),在密码学中,是指在散列之前将散列内容(例如:密码)的任意固定位置插入特定的字符串。**这个在散列中加入字符串的方式称为 “加盐”。其作用是让加盐后的散列结果和没有加盐的结果不相同,在不同的应用情景中,这个处理可以增加额外的安全性。
**在大部分情况,盐是不需要保密的。盐可以是随机产生的字符串,其插入的位置可以也是随意而定。**如果这个散列结果在将来需要进行验证(例如:验证用户输入的密码),则需要将已使用的盐记录下来。为了便于理解,我们来举个简单的示例。
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");
以上示例代码正常运行后,在控制台中会输出以下结果:
原始密码:123456789
加盐后的密码:123456789:exe
加盐密码的md5值:3328003d9f786897e0749f349af490ca
原始密码:123456789
加盐后的密码:123456789:eft
加盐密码的md5值:3c45dd21ba03e8216d56dce8fe5ebabf
通过观察以上结果,我们发现原始密码一致,但使用的盐值不一样,最终生成的 MD5 哈希值差异也比较大。此外为了提高破解的难度,我们可以随机生成盐值并且提高盐值的长度。
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
Node.js 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);
以上示例代码正常运行后,在控制台中会输出以下结果:
$2a$10$O1SrEy3KsgN0NQdQjaSU6OxjxDo0jf.j/e2goSwSEu4esz9i58dRm
很明显密码 123456789 经过 bcrypt 的哈希运算后,得到了一串读不懂的 “乱码”。这里我们已经完成第一步,即用户登录密码的加密。下一步我们要实现登录密码的比对,即要保证用户输入正确的密码后,能正常登录系统。
Node.js 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)
以上示例代码正常运行后,在控制台中会输出以下结果:
true
false
因为我们的原始密码是 123456789,很明显与 123456 并不匹配,所以会输出以上的匹配结果。
七、总结
本文首先介绍了消息摘要算法、MD5 算法的相关概念和特点,然后详细介绍了 MD5 算法的用途和 Java 和 Node.js 平台的使用示例,最后我们还分析了 MD5 算法存在的缺陷和 MD5 密码的安全性问题。这里大家需要注意,由于 MD5 碰撞很容易构造,基于 MD5 来验证数据完整性已不可靠,考虑到近期谷歌已成功构造了 SHA-1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)的碰撞实例,对于数据完整性,应使用 SHA256 或更强的算法代替。
除了文中介绍的 MD5 应用场景,MD5 还可以用于实现 CDN (Content Delivery Network,内容分发网络) 内容资源的防盗链,感兴趣的小伙伴可以阅读 深入了解 Token 防盗链 这篇文章