3、MD5算法的详细步骤:填充、分组、压缩函数
MD5到底是怎么把一个任意长度的消息,变成一串固定的128位哈希值?
很多人觉得这算法很神秘,其实拆开看就三步:填充、分组、压缩。我刚开始做安全开发时,总觉得这算法底层很复杂,后来自己手写过一遍才发现,核心逻辑其实挺清晰的。
下面我把这三个步骤掰开揉碎了讲,你跟着走一遍就明白了。
第一步:填充消息
为什么需要填充?
MD5是按512位(64字节)一块来处理的。如果消息长度不是512的整数倍,它就没法算。
填充规则很简单,你记住一句话就够了:补位、补长度。
- 先补一个“1”位(二进制),然后补“0”,直到长度满足 448 mod 512。
- 为什么是448?因为最后64位要留给原始消息长度。
- 最后64位:用小端序存放原始消息的bit数。
举个例子。消息是"abc",长度24位。
填充逻辑的代码实现(我习惯用Python写原型验证):
def md5_padding(message):
original_len = len(message) * 8
message += b'\x80'
while (len(message) * 8) % 512 != 448:
message += b'\x00'
message += original_len.to_bytes(8, 'little')
return message
第二步:分组
填充完的消息,按512位切一刀,一块就是1个“分组(Block)”。
每块再切成16个32位的小份(Word),小端序读取。
| 层级 | 单元大小 | 说明 |
|---|---|---|
| 消息(Message) | 任意长度 | 原始输入 |
| 分组(Block) | 512 bit (64 byte) | 填充后的消息被分成N个Block |
| 字(Word) | 32 bit (4 byte) | 每个Block包含16个Word,小端序 |
我个人建议,在代码里直接把每个Block切成一个长度为16的列表,后面压缩函数用起来会很顺手。
核心点:分组的顺序就是消息的原始顺序,一个字节都不能乱。MD5对这个顺序非常敏感,这也是为什么文件内容变了,哪怕一个bit不一样,哈希值都会完全不同。
第三步:压缩函数主循环
这才是MD5的心脏。压缩函数对每个Block做4轮处理,每轮16步操作,总共64步。
为什么要做这么多步?说白了就是为了充分“搅拌”数据,让输出的每一位都和输入高度相关。
3.1 初始化MD缓冲区
MD5用一个4-word(共128位)的缓冲区保存中间结果和最终哈希值。我刚开始学的时候,总觉得这四个值就是随便给的,后来才知道它们的二进制表示有一定规律。
A = 0x67452301
B = 0xEFCDAB89
C = 0x98BADCFE
D = 0x10325476
嗯,这里要注意:这四个常数是固定的,不能用别的值替换,否则就不是标准MD5了。
3.2 主循环的4轮处理
每一轮用的逻辑函数不一样,我用表格给你列出来:
| 轮次 | 函数名 | 表达式 | 说明 |
|---|---|---|---|
| 第1轮 (F) | F(X,Y,Z) | (X & Y) | (~X & Z) | 与或非运算,偏向Y |
| 第2轮 (G) | G(X,Y,Z) | (X & Z) | (Y & ~Z) | 与或非运算,偏向Z |
| 第3轮 (H) | H(X,Y,Z) | X ^ Y ^ Z | 异或运算 |
| 第4轮 (I) | I(X,Y,Z) | Y ^ (X | ~Z) | 异或与或非混合 |
我记得第一次手写MD5时,在H函数上栽了个跟头——异或运算的优先级搞混了,算出来的值一直不对。后来老老实实加了括号才解决。
每轮处理除函数不同外,使用的常数和移位量也各不相同。
3.3 单步操作
每一单步的公式是这样的:
a = b + ((a + f(b,c,d) + M[i] + K[i]) <<< s[i])
其中:
a, b, c, d:缓冲区四个字的临时状态(每步循环一次)f(b,c,d):当前轮的逻辑函数M[i]:当前Block的第i个字K[i]:固定常数,由floor(2^32 * abs(sin(i+1)))生成<<< s[i]:循环左移,移位量各步不同
完整流程总结
到这里我们已经把填充、分组和压缩函数都过了一遍。下面这张图可以帮你直观理解整个过程:
压缩函数处理完所有分组后,ABCD四个寄存器的最终值拼接起来,就是你看到的那个32位十六进制字符串。
MD5校验的核心逻辑:两个文件,内容完全相同 → 分组顺序、内容、长度完全一致 → 压缩函数每次迭代的中间结果也完全一致 → 最终哈希值必然一致。反之只要有一个比特不同,最终哈希值就会天差地别。这就是我们能拿MD5做文件一致性校验的根本原因。
避坑指南
讲几个我在项目里踩过的坑:
- 填充顺序:先补1后补0,不是反着来。我有一个同事犯过这个错误,调了三个小时才发现。
- 长度用了比特还是字节?必须是比特(bit),不是字节(byte)。原始消息长度要乘以8。
- 小端序输出:最终128位哈希值,每个32位字内部是小端序,但输出的时候要按大端序写字符串。这个转换规则我每次都得查一下文档。
- 空文件的边界情况:消息长度为0,填充后只有1个Block,压缩函数照样执行。不要忽略这个情况。
压缩函数里的64步常数和移位量,我一般不手动记忆,直接查RFC1321标准文档。但自己实现一遍整个算法,对理解MD5的本质很有帮助。