MD5(Message Digest Algorithm 5)是一种哈希函数(摘要算法),不是加密算法,它的本质作用是将任意长度的数据压缩成一个128位(16字节)固定长度的散列值(hash值),广泛用于:
-
文件完整性校验
-
数字签名
-
密码摘要(如用户登录系统)
-
防篡改检测
一、MD5 的特点
特性 | 说明 |
---|---|
不可逆 | 从散列值无法还原原始数据 |
碰撞风险 | 不同数据可能产生相同的 MD5 值(称为“碰撞”) |
速度快 | 算法高效,适合大批量数据处理 |
安全性 | 已被发现存在漏洞(如彩虹表攻击、碰撞攻击),不适合用于高安全场景 |
MD5作为一种常用的摘要算法(或指纹算法),其具有以下几个重要的特点:
- 输入任意长度信息,输出长度固定:
MD5 可输入任意长度的信息
,其输出均为128位(bit)
固定长度的二进制数据
。 - 运算速度快:
MD5的运算均为32位 与、或、非、位移等位运算
,因此其运算速率快,几乎不消耗CPU时间。 - 不可逆:
根据
MD5的的散列结果
,无法计算
出原始数据
(查字典除外)。 - 碰撞性:
原始数据
与其MD5散列结果
并非一一对应
,存在多个原始数据
的MD5结果相同的可能性
。 - 不安全:
2011年,RFC 6151 禁止MD5用作密钥散列消息认证码。
二、MD5 的工作流程
-
预处理阶段(Pre-processing)
-
将原始信息补齐到长度为 512 的倍数(以 bit 为单位)
-
填充方式:加一个
1
再加若干个0
,最后加上原始消息的长度(64 bit)
-
-
初始化缓冲区(Initialize MD Buffer)
-
使用四个 32 位变量 A、B、C、D 作为寄存器,初始值为固定常数:
-
A = 0x67452301
B = 0xefcdab89
C = 0x98badcfe
D = 0x10325476
3.处理每个512位块(Main Loop)
-
每个块分为 16 个 32 位子块
X[0] ~ X[15]
-
对每个块进行 4轮操作(每轮 16 次,共 64 次迭代)
每一轮使用一种逻辑函数(F/G/H/I):
-
F(x, y, z) = (x & y) | ((~x) & z) G(x, y, z) = (x & z) | (y & ~z) H(x, y, z) = x ^ y ^ z I(x, y, z) = y ^ (x | ~z)
-
这些函数目的是增加非线性,提高散列的不可预测性。
-
每步操作形式如下(以FF为例):
FF(a, b, c, d, x, s, ac) {
a = a + F(b, c, d) + X[k] + ac;
a = 左循环移位 s 位 (ROTATE_LEFT(a, s));
a = a + b;
}
输出(Digest Output)
-
所有处理完后,将 A、B、C、D 连接成最终的 128 位 MD5 散列结果
总结一下你该记住的:
名称 | 作用 |
---|---|
F/G/H/I | 4种混合逻辑函数 |
FF/GG/HH/II | 每一步的完整计算公式(包括加法、移位) |
x | 当前的消息数据块 |
ac | 固定的魔法常数 |
s | 左移的位数,打乱顺序 |
a, b, c, d | 当前的4个变量,是 MD5 的“状态值” |
三、用途举例
✅ 文件完整性验证:
对文件计算 MD5 值,下载后再算一次,比对两个值是否一致。
✅ 数字签名系统中:
签名前先对原文做 MD5 摘要,再对摘要进行加密,减小签名长度。
✅ 密码校验:
把用户密码做 MD5,存入数据库。登录时再 MD5 比较是否一致。
四、MD5 的安全性
-
不可逆性:不能反推原文
-
抗碰撞性(已破坏):不同输入应产生不同的输出,但MD5已被证明可构造碰撞
-
攻击手段:
-
彩虹表攻击:预先计算常见密码的 MD5 值库
-
撞库攻击:如把 "123456"、"password" 等常用密码先建库,对比 MD5 值
-
✅ 建议措施:
-
不要使用简单密码
-
加“盐”(salt)值增加随机性
-
替代方案:使用更安全的散列算法如 SHA-256、bcrypt、Argon2 等
✅ 🌟 简化版 MD5(C++)教学示例
只做:
-
Padding 补齐
-
处理一个 512 位块(64 字节)
-
使用前 4 步的 F 函数 + 左移操作
(完整的 64 步我们暂时不实现)
#include <iostream>
#include <vector>
#include <cstring>
#include <cstdint>
using namespace std;
// 左循环移位
uint32_t leftRotate(uint32_t x, int s) {
return (x << s) | (x >> (32 - s));
}
// F函数(MD5 第1轮使用)
uint32_t F(uint32_t x, uint32_t y, uint32_t z) {
return (x & y) | (~x & z);
}
// 对消息进行填充(只处理英文字符)
vector<uint8_t> md5_padding(const string& input) {
vector<uint8_t> msg(input.begin(), input.end());
// 记录原始长度(单位:比特)
uint64_t bit_len = msg.size() * 8;
// 添加 0x80(10000000)
msg.push_back(0x80);
// 补零直到长度 % 512 == 448
while ((msg.size() * 8) % 512 != 448) {
msg.push_back(0x00);
}
// 追加 64 位小端的原始长度
for (int i = 0; i < 8; i++) {
msg.push_back((bit_len >> (8 * i)) & 0xFF);
}
return msg;
}
// 处理一个块(64字节)
void processBlock(uint8_t block[64], uint32_t& A, uint32_t& B, uint32_t& C, uint32_t& D) {
uint32_t M[16];
for (int i = 0; i < 16; ++i) {
// 每4字节拼成一个32位整数,小端模式
M[i] = block[i*4] | (block[i*4 + 1] << 8) | (block[i*4 + 2] << 16) | (block[i*4 + 3] << 24);
}
// 保存初始值
uint32_t a = A, b = B, c = C, d = D;
uint32_t s[] = {7, 12, 17, 22}; // 位移参数(只用前4步)
uint32_t k = 0xd76aa478; // 第一步使用的常数
// 模拟前 4 步
for (int i = 0; i < 4; ++i) {
uint32_t f = F(b, c, d);
uint32_t temp = a + f + M[i] + k;
a = d;
d = c;
c = b;
b = b + leftRotate(temp, s[i]);
}
// 更新结果
A += a;
B += b;
C += c;
D += d;
}
// 主函数
int main() {
string input = "abc";
vector<uint8_t> padded = md5_padding(input);
// 初始化变量
uint32_t A = 0x67452301;
uint32_t B = 0xefcdab89;
uint32_t C = 0x98badcfe;
uint32_t D = 0x10325476;
// 处理第一个块(简化:只处理一个)
processBlock(&padded[0], A, B, C, D);
// 输出结果
printf("简化 MD5 输出:%08x%08x%08x%08x\n", A, B, C, D);
return 0;
}