上个月用Golang和Cpp实现了 DES 算法,这次用Cpp来实现一个 MD5 算法,与 DES 相比,MD5 还是算比较简单的。
🚀 代码: Github
算法原理概述
MD5 算法全称MD5 信息摘要算法(MD5 Message-Digest Algorithm), 是一种比较常见和简单的密码散列函数,用于生成一个 128 位的散列值确保信息传输完整一致。由于 MD5 的不可逆性,一般会用来加密保存在数据库中的密码字串,又由于 MD5 存在扩散性,即使原来的数据只有 1 位错误,生成出来的散列值都会完全不同,因此也可以用于计算数据指纹。
MD5 算法的主要原理就是对原始数据经过多次的迭代,将数据中的每一位都重复混淆和扩散到散列值当中。
总体结构
其基本的算法流程如下图:

MD5 算法的基本过程为:填充、分块、缓冲区初始化、循环压缩、得出结果
将原始数据填充并划分为 512 位的块后,每一个数据块,对初始向量(IV)使用四个不同的生成函数进行 4 轮循环迭代,在每次循环中结合 T 表元素和 512 位的原始数据进行 16 次迭代,将数据充分扩散和混淆。
最后将初始向量经过迭代之后的值以 16 进制输出,就是 MD5 算法生成的散列值。
模块分解
我们可以将 MD5 算法分为填充分块和循环压缩两个基本模块
填充分块
首先是填充和分块
在加密压缩之前,我们先要保证数据是 512 位的整数倍,也就是说需要划分若干个为 512 位的数据块,如果一个数据块不够 512 位,就必须填充够 512 位。
由于消息的尾部还需要附加原始信息位数的低 64 位,因此需要保证最后一块的位数为512-64=488位。
由于这里用字符串作为原始消息,因此每一位字符是占 8 位(1Byte)的,因此每一个块就为64Byte,填充是使用1000......000来补全剩余的字符的,因此第一位填充0x80,后面填充0x00
首先计算需要填充的0x00的个数
| 1 | int paddingCount = len % 64; | 
如果目前最后一块的字节小于56,那只需要填充满这一块的前56位
如果目前最后一块的字节大于或等于56,就需要额外填充一块,使得直到填充满下一块的前56位
| 1 | // 读取数据 | 
最后把原始信息位数的低 64 位填充到最后一块,使得其长度达到512位
| 1 | vector<bit32> parts = bitset.GetBitSet(); | 
最后每一块得出 16 个32位的数据分块
循环压缩
MD5 算法中对于数据最主要的操作就在于循环压缩部分
首先需要对初始向量进行初始化
| 1 | A= 0x67452301 | 
需要注意的是,这里的初始向量在内存当中需要为小端编码保存,即将低位字节放在内存的低地址段。否则接下来进行移位的时候数据会有不同,C 和 C++都是使用小端编码的,而 Java 是采用大端编码。
对于每 512 位原始数据,进行 4*16 次迭代
在 4 轮迭代中,使用不同的函数对于上面初始化的B,C,D进行操作

然后在 4 次迭代中,进行 16 次迭代

其中
- g 为上面的函数 
- X 为本轮 16 个 32 位的分块的第 - k个 32 位分块- K 在每一轮中分别由不同的方法确定,这里将其生成为一个 64 个元素的表,根据当前迭代次数获取
 
- T 同样为一个表,是由指定函数生成的$T[i] = int(2^{32}\times|sin(i)|)$,为了计算速度,这里同样生成一个 64 个元素的表,根据迭代次数获取 
- s 为循环左移,同样是预设的一个 64 个元素的表 
经过 64 次迭代之后,将 ABCD 向右循环移动一个位置

对消息中的每 512 位做上面的迭代处理。
最后将 128 位的 ABCD 以 16 进制输出,就可以得到 32 位的 16 进制散列值字符串
注意:需要以小端模式输出
数据结构
首先定义上面的各种表
| 1 | 
 | 
C 语言源代码
MD5.h
| 1 | 
 | 
MD5.cpp
| 1 | 
 | 
test.cpp
| 1 | 
 | 
编译运行结果
首先对一个短的字符串测试

然后在网上找一个MD5 生成的工具进行比对测试

生成的结果是一致的
然后对一个比较长的字符串测试

其他工具生成结果

结果都是为96082dc5b1f48bac0d106942e65aaa3c
结果说明该 MD5 算法符合标准
 
        