转载自:http://blog.163.com/cissyxxy0629@126/blog/static/28702276201022691836522/
最近读一本书《加密与解密》,其中讲解MD5算法的那一段让人不知所云,云里雾里。我甚至怀疑作者是否真的懂MD5算法,抑或是从网上胡乱攒了些文字冒充行家。为了使我对作者的鄙视更有力,我决定自己整理一份较清晰明了,通俗易懂的中文的MD5算法说明。
●MD5算法简介
MD5算法是单向散列算法的一种。单向散列算法也称为HASH算法,是一种将任意长度的信息压缩至某一固定长度(称之为消息摘要)的函数(该压缩过程不可逆)。Hash函数可用于数字签名、信息完整性检查等用途。常见的散列算法还有SHA、RIPE-MD、HAVAL、N-Hash等。
●MD5 算法流程详解
MD5 算法将输入的信息进行分组,每组512 位(64个 字节),顺序处理完所有分组后输出128 位结果。
将这128 位用十六进制表示便是常见的32 字符的MD5 码,而所谓的16 字符的MD5 码,其实是这32 字符
中间的16 个字符。
在每一组消息的处理中,都要进行4 轮、每轮16 步、总计64 步的处理。其中每步计算中含一次左循
环移位,每一步结束时将计算结果进行一次右循环移位。详见下方流程(未优化)。
算法流程:
一. 初始化
▲(a) 设置二维数组 g_nTable[4][16],他有64 个常量,对应每组处理的4×16=64 步。由于是常量,也可
以在计算时直接嵌入数据。数组中的每个g_nTable[i][j]元素通过公式: 2 * sin(16* 1) 32 i + j + 计算后取整得到。
这里i∈[0,3],j∈[0,15],16*i+j+1 的单位为弧度,而非角度。具体结果如下:
unsigned int g_nTable[4][16] = {
{ 0xD76AA478,0xE8C7B756,0x242070DB,0xC1BDCEEE,
0xF57C0FAF,0x4787C62A,0xA8304613,0xFD469501,
0x698098D8,0x8B44F7AF,0xFFFF5BB1,0x895CD7BE,
0x6B901122,0xFD987193,0xA679438E,0x49B40821 },
{ 0xF61E2562,0xC040B340,0x265E5A51,0xE9B6C7AA,
0xD62F105D,0x02441453,0xD8A1E681,0xE7D3FBC8,
0x21E1CDE6,0xC33707D6,0xF4D50D87,0x455A14ED,
0xA9E3E905,0xFCEFA3F8,0x676F02D9,0x8D2A4C8A },
{ 0xFFFA3942,0x8771F681,0x6D9D6122,0xFDE5380C,
0xA4BEEA44,0x4BDECFA9,0xF6BB4B60,0xBEBFBC70,
0x289B7EC6,0xEAA127FA,0xD4EF3085,0x04881D05,
0xD9D4D039,0xE6DB99E5,0x1FA27CF8,0xC4AC5665 },
{ 0xF4292244,0x432AFF97,0xAB9423A7,0xFC93A039,
0x655B59C3,0x8F0CCC92,0xFFEFF47D,0x85845DD1,
0x6FA87E4F,0xFE2CE6E0,0xA3014314,0x4E0811A1,
0xF7537E82,0xBD3AF235,0x2AD7D2BB,0xEB86D391 }};(这个表的存在是在IDA中确认MD5算法的关键。)
▲(b) 初始化每步左循环移位的位数g_nMove[4][16],对应每轮处理的4×16=64 步处理。由于是常量,
也可以在计算时直接嵌入数据。(此数据有规律,实际代码中可以对此进行优化)。
int g_nMove[4][16] = {
{ 7,12,17,22, 7,12,17,22, 7,12,17,22, 7,12,17,22 },
{ 5, 9,14,20, 5, 9,14,20, 5, 9,14,20, 5, 9,14,20 },
{ 4,11,16,23, 4,11,16,23, 4,11,16,23, 4,11,16,23 },
{ 6,10,15,21, 6,10,15,21, 6,10,15,21, 6,10,15,21 }};
▲(c) 初始化g_nResult[4]。g_nResult 用于存放当前分组的计算结果,但同时又参与下一组信息的处理过程。直至最后一组计算结束,g_nResult 即为所需的MD5 码。
g_nResult 数组成员应为32 位长,这样总计32×4=128 位。
unsigned int g_nResult[4] = { 0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476 };
二. 信息的读取、填充
▲(a) 将需加密的信件信息(如一份文件)分次读取到缓冲区中,一次最好读取64*n 个字节,这样就是n 组,方便处理。
▲(b) 判断信息是否已全部读完,没有则对刚才读取的信息分组进行计算,如果已经读完了就要在信息尾部进行适当的填充。
▲(c) 对信息进行填充,使其字节数除以64 时余数为56。比如在处理一个文件时,
(1) 最后一次读取为70 字节,70%64=6 小于56,则需在尾部填充56-6=50 个字节,得(70+50)
%64=56。注:若消息为64n 倍数字节,则最后一次读取0 字节,据本规则将填充56 字节。
(2) 最后一次读取为124 字节,124%64=60 大于56 了,则先将这一组填满(此处为4 字节)再
在下一组空间上填56 个字节,得(124+4+56)%64=56。
(3) 最后一次读取为120 字节,120%64=56 等于56,此时仍需填充,填充字节总数为64,即一
组,得(120+64)%64=56。
知道了填充长度,那用什么数据来填充呢?填充的第一个字节为128,其余字节全为0,128的 二进制数为1000 0000(vc 中用(unsigned char)128 表示),0 的二进制应为0000 0000。
经过上面的填充后最后一组只有56 字节,还有剩余的8 字节呀(64字节一组)?这8 个字节用于存放消息填充前
的总长度,而且单位不是字节,是位。vc下可以用unsigned __int64 类型变量保存消息总长度,
(操作文件时直接取得文件长度后乘8 保存到变量中,填充时用内存拷贝函数拷贝过去即可。)
▲(d) 进入 分组处理,计算MD5码。
三. 分组处理
这是MD5算法最核心的环节,在这里对每一组消息进行4 轮、每轮16 步、总计64 步的处理。在进行讲解之前,需要先介绍4 个逻辑函数F,G,H,I,分别对应4 轮运算,它们将参与运算。
第一轮逻辑函数:F(b,c,d)=(b&c)|((~b)&d) 参与第一轮的16 步运算 (b,c,d均为32位数)
第二轮逻辑函数:G(b,c,d)=(b&d)|(c&(~d)) 参与第二轮的16 步运算
第三轮逻辑函数:H(b,c,d)= b^c^d 参与第三轮的16 步运算
第四轮逻辑函数:I(b,c,d)= c^(b|(~d)) 参与第四轮的16 步运算
再引入一个移位函数MOVE(X,n),它将整型变量X 左循环移n 位,如变量X 为32 位,则MOVE(X,n)= (X
<< n) | (X >> (32 - n))。
(& 为按位与,| 为按位或,~ 为按位取非,^ 为按位异或。b、c、d 均为unsigned int。)
处理开始:
▲(a) 将数组g_nResult 内容复制到数组g_nTemp(类型大小与g_nResult 同)。
▲(b) 将新的一组内容从缓冲区读到unsigned int unBuff[16]中。(unsigned int unBuff[16]为512字节长)
▲(c) 第一轮计算:j 从0 循环到15,轮数ln=0,i=j%16=j。将下面1)- 4)执行16 遍。
(1) unsigned int Temp = g_nTemp[0] + F(g_nTemp[1], g_nTemp[2], g_nTemp[3]) + unBuff[i]
+ g_nTable[ln][i]。
(2) Temp = MOVE(Temp, g_nMove[ln][j])。
(3) g_nTemp[0] = g_nTemp[1] + Temp。
(4) 将 g_nTemp 数组右循环移位4 字节,即{Temp = g_nTemp[3]; g_nTemp[3]= g_nTemp[2];
g_nTemp[2]= g_nTemp[1]; g_nTemp[1]= g_nTemp[0]; g_nTemp[0]= Temp;}
▲(d) 第二轮计算:j 从0 循环到15, 轮数ln=1,i=(1+5*j)%16,使用循环函数G,其他同第一轮。
▲(e) 第三轮计算:j 从0 循环到15, 轮数ln=2,i=(5+3*j)%16,使用循环函数H,其他同第一轮。
▲(f) 第四轮计算:j 从0 循环到15, 轮数ln=3,i=(7*j)%16,使用循环函数I,其他同第一轮。
▲(g) 累加结果:g_nResult[0] += g_nTemp [0]; g_nResult[1] += g_nTemp [1];
g_nResult[2] += g_nTemp [2]; g_nResult[3] += g_nTemp [3];
▲(h) 如果缓冲区中还有分组未处理完,则转回到a)。
四. 输出结果
判断消息(或文件)是否已经处理完了,如果没有则转到二.消息读取与填充,如果已经处理完了,
则g_nResult 就是结果,从低地址开始用16 进制逐个输出字节便得MD5 码。