md5

MD5 算法简介

来自: https://mp.weixin.qq.com/s/mG-GtYsD7vetKQKXqfTDHQ

MD5(Message Digest Algorithm 5,消息摘要算法第5版)算法的输入是一个任意长度的字符串(长度大于等于0),输出是一个128比特(bit)的字符串(或者说16个bytes)——当然,从计算时间的角度来说,说是任意长度,也不可能是无限长。

本文直奔主题,直接讲述 MD5 的算法。关于 MD5 的历史、Hash 相关内容,留待以后有机会再补上。

MD5 算法,简单地说,分为两大步骤。第一步就是数据补齐,第二步就是 MD5 的 Hash 算法。下面我们分别介绍这两大步骤。

 

一、数据补齐

所谓数据补齐,是将原来的数据长度补上一定的数据,使之总长度变成64字节(也就是512比特)的整数倍,如图1所示。 

 

图1 MD5 的数据补齐(1)

 

 

也就是说,原始数据的长度是 x,补齐数据的长度是 p,那么总长度 L:

L = x + p = n * 512 bits

图1中所补齐的数据,又分为两部分,如图2所示。

 

 图2 MD5 的数据补齐(2)

 

 

图2中的长度单位是比特。图2中,所补齐的数据分为两部分 d1、d2。

其中,d2 占有64比特,它的值等于原始数据d0 的长度,也就是图2 中的 x。

d2 的长度确定以后,d1 的长度(y)也就确定了,它要求:

x + y ≡ 448 mod 512

或者说

x + y = m * 512 + 448

以上两个公式的含义是一样的,它要求 x + y 等于512比特的整数倍再加上448比特(512 - 64 = 448)——这是为了使得 x + y + 64 正好是 512 的整数倍。

如果原始数据的长度 x 恰好满足如下条件:

x ≡ 448 mod 512

那么原始数据仍然需要补齐 d1,其中 d1 的长度 y = 512。也就是说,即使原始数据的长度等于“512比特的整数倍再加上448比特”,原始数据仍然需要再补齐512比特的数据 d1(当然,它紧接着还需要补齐数据 d2)。

数据d1的值,其赋值规则为:第1个比特是“1”,其余比特全是“0”。

 

至此,MD5 算法完成了第一步:数据补齐。如果不看细节的话,我们现在就拿到了一串补齐后的数据,这串数据的长度是512比特的整数倍。现在我们就可以进入 MD5 算法的第二步。

 

二、MD5 的 Hash 算法

Hash 算法,简单地说,就是输入一段任意长度的比特串,都输出一个固定长度的比特串,如图3所示。 

 

图3 Hash 示意

 

 

当然,图3的示意,固然表达了 Hash 的主要特征,但是仍然是过于简单了。本文由于篇幅和主题的原因,就不再深入探讨 Hash 的关键特征。

对于 MD5 而言,它的输出长度是固定的128比特。MD5 算法,可以这样理解,如图4所示。

 

图4 MD5 的理解

 

 

图4中关于 MD5 算法的示意,分为如下几个步骤。

(1)首先选取1个128比特的字符串作为种子数据

(2)然后将补齐后的数据按照512比特分为若干块

(3)第一块与种子数据一通搅拌,作为临时输出

(4)后续的每一块都与前一个搅拌的临时输出再一通搅拌

(5)直到最后一块与前一个搅拌的临时输出再一通搅拌后,这个搅拌的结果,再加上原来的种子数据,就是 MD5 的输出——1个128比特的字符串。

图4中的“一通搅拌”只是一个比方,它具体指的是什么呢?另外,图4中的种子数据,具体指的又是什么呢?下面我们分别讲述。

 

2.1 种子数据

所谓种子数据,也就是一种说法而已,其目的只是为了帮助理解。MD5 的输出是一个128比特的字符串,这个输出从何而来——如果暂时不管它从何而来,而是先给它赋一个初值呢?对于 MD5 的种子数据,就可以这么理解。

MD5 对于这个128比特的种子数据,定义为4个 word 类型(unsigned int)的数值,因为每个数值占有32比特,所以一共占有128比特,如图5所示。 

 

图5 MD5 的种子数据

 

 

图5中的4种子数据,分别是:(16进制表示的 unsigned int)

A = 0x67452301;

B = 0xefcdab89;

C = 0x98badcfe;

D = 0x10325476;

咋一看,这4个数据有点乱,其实这只不过是以“大端(Big-endian)”形式表达而已,如果以“小端(Little-endian)”形式表达,则非常易读易记,比如 A 就可以看作“0x01234567”。其余3个数(B、C、D)也非常易读易记,具体可以参见图5。

不过,这只是帮助阅读和记忆,我们还是要忘记所谓的大端和小端,也忘记所谓的“01234567”,尽管把 A 当作“0x67452301”即可(B、C、D),它真的不等于“0x01234567”。

然而,这还不是最关键的。最关键的是,A 为什么等于“0x67452301”?如果 A 等于其他的值,MD5 算法还会正确吗?关于这些问题,

对不起,我不知道!

当然,不知道也不用沮丧。因为随着 MD5 算法的继续展开,您会发现,所有的内容,咱可能都是只知道“是什么”,而不知道“为什么”!唉,有些问题,

习惯就好!

 

2.2 一通搅拌

定义好了128比特的种子数据以后,MD5 接下来的工作,就是将补齐后的数据分为一个个512比特的数据块,然后与种子数据一起,来个“一通搅拌”(搅拌的过程中,种子数据也发生变化),最后输出搅拌好的128比特数据,这个结果就是 MD5 算法所输出的 Hash 值。

搅拌的第一步,就是将种子数据,赋值给4个临时变量(unsigned int),如图6所示。 

 

图6 临时变量赋值

 

 

临时变量赋值,非常简单,就是将4个种子数据 A、B、C、D,分别赋值给临时变量 a、b、c、d(a = A; b = B; c = C; d = D)。至于图6中,为什么将 A、B、C、D 的顺序画成 B、C、D、A,这与算法的图形化表达有关(为了图形中尽量减少线条的交叉),没有什么特别含义。

搅拌的第二步,就是将每一个分割好的数据块(512比特/64字节),循环放入搅拌器进行搅拌,直到所有数据块都搅拌完毕。我们以第1块数据为例,看看其搅拌示意,如图7所示。 

 

图7 搅拌示意

 

 

图7中,MD5 搅拌算法示意,分为两部分。

(1)将变量 a、b、c、d 与分块数据一通搅拌以后,生成了 b2

(2)将变量 a、b、c、d 重新赋值:

a = d;

d = c;

c = b;

b = b2;

图7是一个示意,实际上 MD5 的“这通搅拌”,要稍微复杂一点。它的这通搅拌,对于每一个数据块而言,都是搅拌了4轮。不过,每一轮的算法都是一样的,区别仅仅是每一轮的算子和相关参数不同,如图8所示。 

 

图8 MD5 的四轮搅拌

 

 

MD5 的一通搅拌,其对应的数据块是512个比特,也就是64个字节。MD5 将这64个字节的数据,分成4组,也就是说每组16个字节。针对每一组,MD5 的搅拌算子和相关参数都不相同。

图8中的 Fj 是其中一个算子,一共有4个,分别对应 F0、F1、F2、F3。

图8中的 K[i]、s[i]是相关参数,i 的取值范围是[0, 63],对应的就是64个字节。

图8中的 Sum、SS、M[g]的含义是什么?K[i]、s[i]又分别等于多少,F0、F1、F2、F3 具体指的是什么?下面我们分别讲述。

 

2.2.1 算子 Fj

算子 Fj 就是对变量 b、c、d 做相关计算。一共有四个算子,分别如下:

F0 = (b & c) | ((~b) & d);

F1 = (d & b) | ((~d) & c);

F2 = b ^ c ^ d;

F4 =c ^ (b | (~d));

 

对应到代码,Fj 的赋值如下:

 

......

int F;

int i = 0;

        

for(i = 0; i < 64; ++i)

{

    if (i < 16)

    {

        F = (b & c) | ((~b) & d);

}

     else if(i < 32)

     {

         F = (d & b) | ((~d) & c);

     }

     else if( i < 48)

     {

         F = b ^ c ^ d;

     }

      else

      {

          F =c ^ (b | (~d));

      }

}

......

 

2.2.2 参数 K[i]

i 的取值范围是[0, 63],从 K[0] 到 K[63] 的取值分别为:

K[0] = 0xd76aa478;

K[1] = 0xe8c7b756;

K[2] = 0x242070db;

.......

K[i] 的取值规则是:

K[i] = unsigned int(abs(sin(i+1))*(2pow32));

还是那句话,鄙人只知道 K[i]是什么,不知道为什么 K[i] 要等于这个值。如果 K[i] 不是这样赋值,那么 MD5 算法会怎样?对不起,我也不知道!

 

2.2.3 参数 M[g]

参数 M 就是前文介绍的“补齐后的数据所切割的数据块”,每一块数据是512比特,也就是64个字节,也就是16个 unsigned int。这里比较绕人的是 g 的取值。g 的取值,并不是像传统的 i 的取值,从0一直递增到63,而是稍微有点复杂,对应的代码如下:

 

......

int i = 0;

        

for(i = 0; i < 64; ++i)

{

    if (i < 16)

    {

        g = i;

    }

     else if(i < 32)

     {

         g = (5 * i + 1) % 16;

     }

     else if( i < 48)

     {

         g = (3 * i + 5) % 16;

     }

      else

      {

          g = (7 * i) % 16;

      }

}

......

 

我们将这段代码翻译一下,如表1所示。

 

表1 g 与 i 的对应关系

i

g

两者的关系

[0, 15]

0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15

g = i

[16, 31]

1,6,11,0,5,10,15,4,9,14,3,8,13,2,7,12

g = (5 * i + 1) % 16

[32, 47]

5,8,11,14,1,4,7,10,13,0,3,6,9,12,15,2

g = (3 * i + 5) % 16

[48, 63]

0,7,14,5,12,3,10,1,8,15,6,13,4,11,2,9

g = (7 * i) % 16

 

通过表1可以看到,所谓 M[g],其本质上还是从 M[0] 到 M[15],也就是16个 unsigned int,也就是64个 bytes,也就是 512 个比特,只不过每一轮的顺序不同罢了。

 

2.2.4 求和

有了前面的铺垫,MD5 的求和,就比较简单了,如图9所示。 

 

图9 MD5 的求和算子

 

 

 

通过图9可以看到,Sum = Fj + M[g] + K[i]

 

2.2.5 Shift

依据图9中的算法计算出 Sum 以后,下一步就需要进行 Shift 计算,如图10所示。 

 

图10 MD5 中的 Shift 算子

 

 

图10中的 Shift 算子,其代码如下:

 

 

// 移动一定位数

private int shift(int a, int s)

{

// 右移的时候,高位一定要补零,而不是补充符号位

    return ((a << s) | (a >>> (32-s)));    

}

 

图10中的 Shift 的含义,就是将图9中所计算的 Sum,右移 s[i] 位,即:

SS = shift(Sum, s[i]);

其中,s[i]的取值如下:

 

private final int s[] =

{

      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            

 };

 

2.2.6 再度求和

紧接着图10,下一步需要继续求和,如图11所示。 

 

图11 MD5 的再度求和

 

 

图11所示的再度求和比较简单,就是:

b2 = b + SS;

 

2.2.7 变量重新赋值

经过上述一系列的操作以后,剩下的就是将4个临时变量重新赋值,如图12所示。 

 

图12 MD5 的临时变量重新赋值

 

 

2.2.8 重新修改种子数据

经过四轮搅拌之后,临时变量 a、b、c、d 应该是变得面目全非了。不过这4个临时变量变得面目全非,并不是 MD5 的本意。MD5 的本意,是要修改原来的4个种子数据,如图13所示。 

 

图13 修改原来的种子数据

 

通过图13可以看到,所谓修改原来的种子数据,也比较简单:

A = A + a;

B = B + b;

C = C + c;

D = D + d;

 

2.2.9 循环搅拌

2.2.1到2.2.8所介绍的搅拌算法,是针对其中一块数据(512比特)的处理内容。对于任意长度的字符串来说,它只需要补齐数据以后,然后切成一个个512比特的数据块,再循环调用搅拌算法即可。

 

MD5 整个算法的代码并不多,笔者将代码贴出来,可能更容易理解。

 

public class CMD5

{

// 四个链接变量

private final int A = 0x67452301;

    private final int B = 0xefcdab89;

    private final int C = 0x98badcfe;

    private final int D = 0x10325476;

    

    

    // ABCD的临时变量    

    private int Atemp, Btemp, Ctemp, Dtemp;

    

    private final int K[] =

    {

            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            

    };

    

    private final int s[] =

{

      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            

   };    

    

    private void mainLoop(int M[])

    {

        int F, g;

        int a = Atemp;

        int b = Btemp;

        int c = Ctemp;

        int d = Dtemp;

        

        int i = 0;

        

        for(i = 0; i < 64; ++i)

        {

            if (i < 16)

            {

                F = (b & c) | ((~b) & d);

                g = i;

            }

            else if(i < 32)

            {

                F = (d & b) | ((~d) & c);

                g = (5 * i + 1) % 16;

            }

            else if( i < 48)

            {

                F = b ^ c ^ d;

                g = (3 * i + 5) % 16;

            }

            else

            {

                F =c ^ (b | (~d));

                g = (7 * i) % 16;

            }

            

            int tmp = d;

            d = c;

            c = b;

            b = b + shift(a + F + K[i] + M[g], s[i]);

            a = tmp;

        }

        

        Atemp = a + Atemp;

        Btemp = b + Btemp;

        Ctemp = c + Ctemp;

        Dtemp = d + Dtemp;     

    }       

    

    // 初始化四个临时变量

    private void init()

    {

    Atemp = A;

        Btemp = B;

        Ctemp = C;

        Dtemp = D;

    }

    

    // 移动一定位数

    private int shift(int a, int s)

    {

        // 右移的时候,高位一定要补零,而不是补充符号位

        return ((a << s) | (a >>> (32-s)));

    }

        

    /*

    *填充函数

    *处理后应满足bits≡448(mod512),字节就是bytes≡56(mode64)

    *填充方式为先加一个1,其它位补零

    *最后加上64位的原来长度

    */

    private int[] padding(String str)

    {

    //以512位,64个字节为一组

    int num = ((str.length() + 8) / 64) + 1;

    

    //64/4=16,所以有16个整数

        int strByte[] = new int[num * 16];

              

        int i;

        

        // 全部初始化0

        for(i = 0; i < num * 16; ++i)

        {

            strByte[i] = 0;

        }        

        

        for(i = 0; i < str.length(); ++i)

        {

            // 一个整数存储四个字节,小端序

            strByte[i >> 2] |= str.charAt(i) << ((i % 4) * 8);

        }         

      

        //尾部添加1

        strByte[i >> 2] |= 0x80 << ((i % 4) * 8);

        

        

        // 添加原长度,长度指位的长度,所以要乘8,然后是小端序,

// 所以放在倒数第二个,这里长度只用了32位        

        strByte[num * 16 - 2] = str.length() * 8;

            

        return strByte;

    }                                 

    

    /*

    *整数变成16进制字符串

    */

    private String changeHex(int a)

    {

        String str="";

        

        for(int i = 0; i < 4; i++)

        {

            str += String.format("%2s", Integer.toHexString((((a >> i) * 8) % (1 << 8)) & 0xff)).replace(' ', '0');

        }

        

        return str;

    }    

    

    public String getMD5(String source)

    {

        init();

        

        int strByte[] = this.padding(source);

        

        int i = 0;

        int j = 0;

        

        for(i = 0; i < strByte.length/16; ++i)

        {        

        int num[] = new int[16];                

        

        for (j = 0; j < 16; ++j)

        {            

        num[j] = strByte[i * 16 + j];        

        }        

        

        mainLoop(num);

        }

        

        return changeHex(Atemp) +

changeHex(Btemp) +

changeHex(Ctemp) +

changeHex(Dtemp);     

    }

}

posted @ 2020-07-24 10:30  abel2020  阅读(543)  评论(0编辑  收藏  举报