本文阐述MD5算法的基本原理及其C++实现方法,并给出源代码
算法原理概述
MD5即Message-Digest Algorithm 5 (信息-摘要算法 5) ,是一种被广泛使用的密码散列(hash)函数,它使用小端模式,输入任意不定长的消息,产生的是128位(16个字节)的散列值(hash value),用于确保信息传输的完整性和一致性。
MD5算法不是足够安全的,可以找到两个不同的512位的块,它们通过的MD5的hash值相同
但对于有意义的消息,还没有两个不同的消息,它们的MD5的hash值相同
总体结构
MD5算法的基本流程如下图所示:
可以分成几个流程:
- 填充
- 分块
- 循环压缩
- 得出结果
使用unsigned char作为byte,unsigned int作为bit32
模块分解
填充
算法的输入我使用字符串,而一个字符占一个字节(8位),而填充后的消息位数是512位(64字节)的倍数,因此可以按字节来填充,填充需要注意的是最后8个字节要使用原始消息长度的低8个字节来填充,其余需要填充的部分,第一个字节使用0x80
,其余的字节都使用0x00
。
计算填充字节数的方法如下:
1 | //count表示要填充的字节数 |
中间填充0x80
,0x00
就不必赘述,需要注意填充原始消息长度的低8个字节这个操作,要按照小端存储来把长度转为8个连续的字节,可以采用移位运算>>
来帮助实现。
填充完成后,消息长度为64k字节,为了方便之后的步骤,还需要把消息分块。
分块
分块操作主要是为了把填充好的消息分割成每64字节一组,也可以表示成一组16个32位的字(bit32)。
我使用vector<bit32>
来存储每个分组里面的32位字,再用一个vector
来存放所有的分组,因此最终分块后的消息存放在vector<vector<bit32> >
中(vector<bit32>
长度固定为16)
需要注意,填充后的消息中按byte
存储,因此需要把连续的4个byte
转成bit32
,这里又需要按照小端存储的模式,比如4个byte
值分别为0x01
,0x02
,0x03
,0x04
,转成bit32
的值为0x04030201
。可以借助左移位运算符<<
来实现。
循环压缩
这一部分包含了MD5算法进行加密的主要操作,主要流程是以512位的分组为单位,每一分组经过4轮循环,每轮循环16次迭代,输出128位的结果,存放在缓冲区中,作为下一轮循环缓冲区的输入。
4轮循环的逻辑如图所示:
从缓冲区输入128位,从消息分组输入512位,输出结果128位,要注意结果是由循环的结果加上缓冲区的值得到的(加法为模232加法)。
每轮循环中迭代的逻辑如图所示:
其中:
- a,b,c,d是缓冲区的当前值
- g是4个轮函数之一,输入输出都是
bit32
,进行不同的逻辑运算 <<<s
是指把bit32
循环左移s
位,s
可查表得到X[k]
是当前处理消息分组的第k
个bit32
,在每一轮循环中都由不同的公式计算出来T[i]
通过查表得到,32bit字- 所有的加法都是模232加法
需要注意,在迭代过程中缓冲区的值会发生变化,因此要提前存储下循环开始时缓冲区的值,用于之后与循环结果相加。
4个轮函数逻辑如图所示:
每轮循环中X[k]
所取的k
的计算方法为:
取j
为当前迭代轮次
- 第一轮循环:
k = j
- 第二轮循环:
k = (1 + 5 * j) % 16
- 第三轮循环:
k = (5 + 3 * j) % 16
- 第四轮循环:
k = (7 * j) % 16
得出结果
经过前面的操作,我们已经得到了4个bit32
的结果,但最直白的输出最好是字符串,许多在线工具也是把计算结果转成了32位长度的16进制字符串,便于使用。
要得到16进制字符串类型的结果,首先需要把这4个bit32
转成16个byte
,再把每一个byte
用两位16进制字符来表示。
把bit32
转成byte
的过程又一次需要用到小端存储模式:
1 | for (int i = 0; i < 4; ++i) { |
之后输出每一个byte
,分别计算出它除以16的商和余数即可:
1 | char HEX[16] = { |
最终得到的是一个长度为32的16进制字符串。
数据结构
首先最重要的是表示方法,由于C++里char
占8位,int
占32位,因此分别使用unsigned char
和unsigned int
来表示byte
和bit32
是很合适的。
此外,要存放消息,使用vector
这个数据结构十分方便,对每一个分块使用vector<bit32>
来存放16个bit32
,所有的分块存放在一个vector
中方便检查填充和分块是否出错。
此外,用到了MD5算法固定使用的一些表:
1 | //T表,共64个元素,每个元素为32位字,也称为加法常数 |
源代码
在此贴出MD5算法主要部分的源码,完整源码见github
1 |
|
编译运行结果
首先来测试一个简单的字符串的MD5散列值:
1 | $ g++ MD5.cpp -o MD5 |
使用在线MD5加密工具查看结果:
再测试一个很长的字符串:
1 | $ g++ MD5.cpp -o MD5 |
使用在线工具查看结果:
可以看到,结果是正确的,因此实现的MD5算法是正确的。