3、MD5算法的详细步骤:填充、分组、压缩函数

MD5到底是怎么把一个任意长度的消息,变成一串固定的128位哈希值?

很多人觉得这算法很神秘,其实拆开看就三步:填充、分组、压缩。我刚开始做安全开发时,总觉得这算法底层很复杂,后来自己手写过一遍才发现,核心逻辑其实挺清晰的。

下面我把这三个步骤掰开揉碎了讲,你跟着走一遍就明白了。

第一步:填充消息

为什么需要填充?

MD5是按512位(64字节)一块来处理的。如果消息长度不是512的整数倍,它就没法算。

填充规则很简单,你记住一句话就够了:补位、补长度。

  • 先补一个“1”位(二进制),然后补“0”,直到长度满足 448 mod 512。
  • 为什么是448?因为最后64位要留给原始消息长度。
  • 最后64位:用小端序存放原始消息的bit数。

举个例子。消息是"abc",长度24位。

这里有个坑:填充后的块数 = ( (原始长度bit + 64 + 511) / 512 ) 向上取整。这个公式我每次写代码都要再核对一遍,容易写错。

填充逻辑的代码实现(我习惯用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]:循环左移,移位量各步不同
重要提示:MD5使用小端序处理所有多字节数据。如果你用的平台是大端序(比如某些嵌入式系统),必须在读写32位字时做字节序转换。我在一个嵌入式项目里吃过这个亏,一次编译出的哈希值和标准MD5工具对不上,排查了大半天才发现是字节序问题。

完整流程总结

到这里我们已经把填充、分组和压缩函数都过了一遍。下面这张图可以帮你直观理解整个过程:

MD5算法核心流程 MD5算法核心流程 原始消息 填充 + 长度 512位分组 压缩函数主循环(4轮 × 16步 = 64步) F/B/D/A + 循环左移 + 主常数 每一步更新ABCD寄存器 初始化ABCD 128位MD5哈希值 分组迭代处理 输入块数 ≥ 1 时循环执行 后一块的输出是前一块的输入

压缩函数处理完所有分组后,ABCD四个寄存器的最终值拼接起来,就是你看到的那个32位十六进制字符串。

MD5校验的核心逻辑:两个文件,内容完全相同 → 分组顺序、内容、长度完全一致 → 压缩函数每次迭代的中间结果也完全一致 → 最终哈希值必然一致。反之只要有一个比特不同,最终哈希值就会天差地别。这就是我们能拿MD5做文件一致性校验的根本原因。

避坑指南

讲几个我在项目里踩过的坑:

  • 填充顺序:先补1后补0,不是反着来。我有一个同事犯过这个错误,调了三个小时才发现。
  • 长度用了比特还是字节?必须是比特(bit),不是字节(byte)。原始消息长度要乘以8。
  • 小端序输出:最终128位哈希值,每个32位字内部是小端序,但输出的时候要按大端序写字符串。这个转换规则我每次都得查一下文档。
  • 空文件的边界情况:消息长度为0,填充后只有1个Block,压缩函数照样执行。不要忽略这个情况。

压缩函数里的64步常数和移位量,我一般不手动记忆,直接查RFC1321标准文档。但自己实现一遍整个算法,对理解MD5的本质很有帮助。