上个月用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 算法符合标准