MD5算法与实现

背景

MD5全称Message-Digest Algorithm,就是信息摘要算法。是一种被广泛使用的密码散列函数,可以产生出一个128位的散列值,它是由美国密码学家Ron Rivest开发的,于1992年公开。最开始算法是MD,很快出现MD2,不过算法很脆弱,又改为研究MD3,不过最终研发失败。接下来又出现MD4,但仍不理想,最后研发出MD5,成为一个全世界广泛使用的算法。

如今,MD5被证实存在一系列弱点,可以被加以破解,同时也无法防止碰撞,所以已经不适合安全认证。但是,MD5算法因其普遍、稳定、快速的特点,仍广泛应用于普通数据的错误检查领域。

作为一个经典算法,这里就详细梳理一下整个流程。MD5标准文档见这里

详细步骤

填充

第一步是在初始消息中填充信息,使其达到一个要求的长度,即比512位的倍数少64位。如原始消息是1000位,512*3=1536,1536-64=1472,所以要填充472位。填充的内容是1个1和多个0

填充总是要进行的,即使消息长度已经比512的倍数少64位,仍要填充512位

添加长度

计算源消息的长度,表示成64位填充到末尾。如果消息的长度大于2^64,则使用低64位填充,即len mod 2^64

添加完之后,整体长度为512的倍数

分块

将输入以512位一组分块

初始化链接变量

初始化4个32位数字,16进制表示如下

A = 0x67452301
B = 0xEFCDAB89
C = 0x98BADCFE
D = 0x10325476

处理块

前面都是初始化操作,这里开始才是算法开始

复制

将4个初始化变量复制到4个变量abcd中,这四个变量组成一个128位寄存器,用于表示中间结果和最终结果

分解

将每块512位分解为16个子块,每个子块32位

轮次迭代

主循环有4轮,每轮有16次操作。每次操作对abcd的其中三个做一次非线性运算P,然后将所得结果加上第四个变量、一个子块和一个常数。再将所得结果向左循环移位,最后加上abcd的其中给一个,最后用该结果取代abcd其中之一。如下图所示

表示成数学表达式如下

a = b + ((a + P(b,c,d) + M[i] + t[k])<<<s)

P的每一轮操作如下:

轮次 P
1 (b AND c) OR ((NOT b) AND b)
2 (b AND d) OR (c AND (NOT d)
3 b XOR c XOR d
4 C XOR (b OR (NOTd)

轮次操作中常量t[i] = 4294967296*abs(sin(i))的整数部分。下面我们给出每一步详细操作:

首先定义P的4种运算:
F( X ,Y ,Z ) = ( X & Y ) | ( (~X) & Z )
G( X ,Y ,Z ) = ( X & Z ) | ( Y & (~Z) )
H( X ,Y ,Z ) =X ^ Y ^ Z
I( X ,Y ,Z ) =Y ^ ( X | (~Z) )

再定义4个函数:
FF(a ,b ,c ,d ,Mj ,s ,ti ) : a = b + ( (a + F(b,c,d) + Mj + ti) << s)
GG(a ,b ,c ,d ,Mj ,s ,ti ) : a = b + ( (a + G(b,c,d) + Mj + ti) << s)
HH(a ,b ,c ,d ,Mj ,s ,ti ) : a = b + ( (a + H(b,c,d) + Mj + ti) << s)
II(a ,b ,c ,d ,Mj ,s ,ti ) : a = b + ( (a + I(b,c,d) + Mj + ti) << s)

这4轮64步分别如下:
第一轮:
FF(a ,b ,c ,d ,M0 ,7 ,0xd76aa478 )
FF(d ,a ,b ,c ,M1 ,12 ,0xe8c7b756 )
FF(c ,d ,a ,b ,M2 ,17 ,0x242070db )
FF(b ,c ,d ,a ,M3 ,22 ,0xc1bdceee )
FF(a ,b ,c ,d ,M4 ,7 ,0xf57c0faf )
FF(d ,a ,b ,c ,M5 ,12 ,0x4787c62a )
FF(c ,d ,a ,b ,M6 ,17 ,0xa8304613 )
FF(b ,c ,d ,a ,M7 ,22 ,0xfd469501)
FF(a ,b ,c ,d ,M8 ,7 ,0x698098d8 )
FF(d ,a ,b ,c ,M9 ,12 ,0x8b44f7af )
FF(c ,d ,a ,b ,M10 ,17 ,0xffff5bb1 )
FF(b ,c ,d ,a ,M11 ,22 ,0x895cd7be )
FF(a ,b ,c ,d ,M12 ,7 ,0x6b901122 )
FF(d ,a ,b ,c ,M13 ,12 ,0xfd987193 )
FF(c ,d ,a ,b ,M14 ,17 ,0xa679438e )
FF(b ,c ,d ,a ,M15 ,22 ,0x49b40821 )
第二轮
GG(a ,b ,c ,d ,M1 ,5 ,0xf61e2562 )
GG(d ,a ,b ,c ,M6 ,9 ,0xc040b340 )
GG(c ,d ,a ,b ,M11 ,14 ,0x265e5a51 )
GG(b ,c ,d ,a ,M0 ,20 ,0xe9b6c7aa )
GG(a ,b ,c ,d ,M5 ,5 ,0xd62f105d )
GG(d ,a ,b ,c ,M10 ,9 ,0x02441453 )
GG(c ,d ,a ,b ,M15 ,14 ,0xd8a1e681 )
GG(b ,c ,d ,a ,M4 ,20 ,0xe7d3fbc8 )
GG(a ,b ,c ,d ,M9 ,5 ,0x21e1cde6 )
GG(d ,a ,b ,c ,M14 ,9 ,0xc33707d6 )
GG(c ,d ,a ,b ,M3 ,14 ,0xf4d50d87 )
GG(b ,c ,d ,a ,M8 ,20 ,0x455a14ed )
GG(a ,b ,c ,d ,M13 ,5 ,0xa9e3e905 )
GG(d ,a ,b ,c ,M2 ,9 ,0xfcefa3f8 )
GG(c ,d ,a ,b ,M7 ,14 ,0x676f02d9 )
GG(b ,c ,d ,a ,M12 ,20 ,0x8d2a4c8a )
第三轮
HH(a ,b ,c ,d ,M5 ,4 ,0xfffa3942 )
HH(d ,a ,b ,c ,M8 ,11 ,0x8771f681 )
HH(c ,d ,a ,b ,M11 ,16 ,0x6d9d6122 )
HH(b ,c ,d ,a ,M14 ,23 ,0xfde5380c )
HH(a ,b ,c ,d ,M1 ,4 ,0xa4beea44 )
HH(d ,a ,b ,c ,M4 ,11 ,0x4bdecfa9 )
HH(c ,d ,a ,b ,M7 ,16 ,0xf6bb4b60 )
HH(b ,c ,d ,a ,M10 ,23 ,0xbebfbc70 )
HH(a ,b ,c ,d ,M13 ,4 ,0x289b7ec6 )
HH(d ,a ,b ,c ,M0 ,11 ,0xeaa127fa )
HH(c ,d ,a ,b ,M3 ,16 ,0xd4ef3085 )
HH(b ,c ,d ,a ,M6 ,23 ,0x04881d05 )
HH(a ,b ,c ,d ,M9 ,4 ,0xd9d4d039 )
HH(d ,a ,b ,c ,M12 ,11 ,0xe6db99e5 )
HH(c ,d ,a ,b ,M15 ,16 ,0x1fa27cf8 )
HH(b ,c ,d ,a ,M2 ,23 ,0xc4ac5665 )
第四轮
II(a ,b ,c ,d ,M0 ,6 ,0xf4292244 )
II(d ,a ,b ,c ,M7 ,10 ,0x432aff97 )
II(c ,d ,a ,b ,M14 ,15 ,0xab9423a7 )
II(b ,c ,d ,a ,M5 ,21 ,0xfc93a039 )
II(a ,b ,c ,d ,M12 ,6 ,0x655b59c3 )
II(d ,a ,b ,c ,M3 ,10 ,0x8f0ccc92 )
II(c ,d ,a ,b ,M10 ,15 ,0xffeff47d )
II(b ,c ,d ,a ,M1 ,21 ,0x85845dd1 )
II(a ,b ,c ,d ,M8 ,6 ,0x6fa87e4f )
II(d ,a ,b ,c ,M15 ,10 ,0xfe2ce6e0 )
II(c ,d ,a ,b ,M6 ,15 ,0xa3014314 )
II(b ,c ,d ,a ,M13 ,21 ,0x4e0811a1 )
II(a ,b ,c ,d ,M4 ,6 ,0xf7537e82 )
II(d ,a ,b ,c ,M11 ,10 ,0xbd3af235 )
II(c ,d ,a ,b ,M2 ,15 ,0x2ad7d2bb )
II(b ,c ,d ,a ,M9 ,21 ,0xeb86d391 )

上面是网上大多是文章给出的方法,就是生硬的给出64轮具体操作,但其实每一步都是可以推算的,下面来说一下简单的方法:

首先对于常数,可以通过公式计算,t[i] = 4294967296*abs(sin(i))的整数部分,其中4294967296就是1<<32,即2^32
对于这个长度建议是直接写成常量,不然每次都计算也比较耗时:
private static final int T[] = { 
            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 
    };
    
对于每次取哪一小组明文,是根据轮次定的,定义j是小轮次,0 <= j < 64:
第一大轮就是按顺序取1~16小组 i = j
第二大轮的计算规则为 i = (1+5*j)%16 即  (j*5+1)&0x0f
第三大轮的计算规则为 i = (5+3*j)%16 即  (j*3+5)&0x0F
第四大轮的计算规则为 i = (7*j)%16   即  (j*7)&0x0F

对于移位,也是根据轮次定的,定义j是小轮次,0 <= j < 64:
第一大轮 从[7, 12, 17, 22]取,j%4
第一大轮 从[5, 9, 14, 20]取,j%4
第一大轮 从[4, 11, 16, 23]取,j%4
第一大轮 从[6, 10, 15, 21]取,j%4
若写到一个数组中:
public static final int S[] = new int[]{
            7, 12, 17, 22, 5, 9, 14, 20, 4,
            11, 16, 23, 6, 10, 15, 21
    };
可以通过如下公式取值:S[(pType << 2) | (j & 3)])  //pType指第几大轮,j指第几小轮

对于每次运算abcd数输入顺序:
可以明显观察到,每一小轮之后,下一轮的输入相当于把abcd做循环右移一位,而P运算的输入固定位bcd,所以做如下交换
a = d;
d = c;
c = b;
b = temp;

经过这64步之后,A=a,B=b,C=c,D=d,然后将ABCD作为处理下一明文块的输入

应用

MD5已经广泛使用在为文件传输提供一定的可靠性方面。例如,服务器预先提供一个MD5校验和,用户下载完文件以后,用MD5算法计算下载文件的MD5校验和,然后通过检查这两个校验和是否一致,就能判断下载的文件是否出错。

实现

下面来给出java版本的具体实现,关键步骤有注释,详细代码见这里

public String md5(String msg){
    byte[] msgBytes = msg.getBytes();
    int msgBytesLen = msgBytes.length;//原始信息长度,单位bit
    int numBlock = ((msgBytesLen + 8)>>>6) + 1; //总块数=(原始长度+8bit)/64bit + 1
    int totalLen = numBlock << 6; //补全后的长度 = 块数 * 64bit
    byte[] padBytes = new byte[totalLen - msgBytesLen]; //需要补的bit数
    padBytes[0] = (byte) 0x80; //补一个1和若干个0
    long msgLen = (long)msgBytesLen << 3; //计算出多少bit,长度*8
    for(int i = 0;i<8;i++){
        padBytes[padBytes.length - 8 + i] = (byte) msgLen;//从低位开始写入长度
        msgLen >>>= 8;
    }
    int a = A,b =B,c = C,d = D; //赋值常量
    int[] buffer  = new int[16]; //每块512位,16个int类型
    for (int i = 0;i<numBlock;i++){
        int index = i << 6;
        for (int j = 0;j<64;j++,index++)
            //index是msg的游标,j是buffer的游标,一个int类型存4个byte类型
            //从低位开始存
            buffer[j >>> 2] = ((int) ((index < msgBytesLen) ? msgBytes[index]
                    : padBytes[index - msgBytesLen]) << 24)
                    | (buffer[j >>> 2] >>> 8);
        int tempa = a; //记录abcd的临时变量
        int tempb = b;
        int tempc = c;
        int tempd = d;
        for (int j = 0;j<64;j++) { //64小轮
            int pType = j >>> 4; //判断是第几大轮
            int f = 0; //P运算的值
            int bufferIndex = j; //buffer的游标
            switch (pType){ //定义不同大轮的具体P运算
                case 0:
                    f = (b &c) | (~b & d);
                    break;
                case 1:
                    f = (b & d) | (c & ~d);
                    bufferIndex = (bufferIndex*5+1)&0x0f; //明文子组和轮数的关系
                    break;
                case 2:
                    f = b ^ c ^ d;
                    bufferIndex = (bufferIndex * 3 + 5) & 0x0F;
                    break;
                case 3:
                    f = c ^ (b | ~d);
                    bufferIndex = (bufferIndex * 7) & 0x0F;
                    break;
            }
            //运算
            int temp = b + Integer.rotateLeft(a + f + buffer[bufferIndex] + T[j], S[(pType << 2) | (j & 3)]);
            //交换abcd
            a = d;
            d = c;
            c = b;
            b = temp;
        }
        //分组处理完之后abcd各自累加
        a += tempa;
        b += tempb;
        c += tempc;
        d += tempd;
    }
    //abcd写到16个byte中
    byte[] out = new byte[16];
    int count = 0;
    for (int i = 0;i<4;i++){
        int n = (i == 0) ? a : ((i == 1) ? b : ((i == 2) ? c : d));
        for (int j = 0;j<4;j++){
            out[count++] = (byte)n;
            n>>>=8;
        }
    }
    //转为十六进制表示
    StringBuffer sb = new StringBuffer();
    for (byte bout : out){
        sb.append(HEX[(bout>>>4)&0xf]);
        sb.append(HEX[bout&0xf]);
    }
    return sb.toString();
}

简单测试后结果正确无误

题图来自unsplash:https://unsplash.com/photos/HsEz1XZ1TO8