MD5算法的Java實現

一、算法原理概述

MD5 即Message-Digest Algorithm 5 (信息-摘要算法5)

  • MD4 (1990)、MD5(1992, RFC 1321) 由Ron Rivest發明,是廣泛 使用的Hash 算法,用於確保信息傳輸的完整性和一致性。
  • MD5 使用little-endian(小端模式),輸入任意不定長度信息,以 512-bit 進行分組,生成四個32-bit 數據,最後聯合輸出固定 128-bit 的信息摘要。
  • MD5 算法的基本過程爲:填充、分塊、緩衝區初始化、循環壓 縮、得出結果。
  • MD5 不是足夠安全的。
    • Hans Dobbertin在1996年找到了兩個不同的512-bit 塊,它們 在MD5 計算下產生相同的hash 值。
    • 至今還沒有真正找到兩個不同的消息,它們的MD5 的hash 值相等。

基本流程

1543732734508

1543733052946

填充padding

  • 在長度爲Kbits 的原始消息數據尾部填充長度爲Pbits 的標識 100…0,1< P < 512 (即至少要填充1個bit),使得填充後的消息位 數爲:K + P=448 (mod 512).
    • 注意到當K=448 (mod 512) 時,需要P= 512.
  • 再向上述填充好的消息尾部附加K 值的低64位(即K mod 264), 最後得到一個長度位數爲K + P+ 64= 0 (mod 512) 的消息。

分塊

  • 把填充後的消息結果分割爲L個512-bit 分組:Y0, Y1, …, YL-1。
  • 分組結果也可表示成N個32-bit 字M0, M1, …, MN-1,N= Lx16。

初始化

  • 初始化一個128-bit 的MD 緩衝區,記爲CVq,表示成4個32-bit 寄存器(A, B, C,D);CV0= IV。迭代在MD 緩衝區進行,最後一 步的128-bit 輸出即爲算法結果。

壓縮函數

1543733261546

每輪循環中的一次迭代運算邏輯

  1. 對A迭代:a <— b+ ((a+ g(b, c, d) + X[k] + T[i]) <<<s)
  2. 緩衝區(A, B, C, D) 作循環輪換: (B, C, D, A) <—(A, B, C, D)

說明:

  • a, b, c, d: MD 緩衝區(A, B, C, D) 的當前值。
  • g: 輪函數(F, G, H, I中的一個)。
  • <<<s: 將32位輸入循環左移(CLS) s位。
  • X[k] : 當前處理消息分組的第k個(k= 0…15) 32位字,即 M(qx16+k)。
  • T[i] : T表的第i個元素,32位字;T表總共有64個元素,也 稱爲加法常數。
    • : 模232 加法。

二、總體結構

public class MD5 {
	//一系列常量數值
    
    //開始使用MD5加密
    private String start(String message){
        //分塊
        
        //填充
        
        //即小於448bit的情況,先填充100...0再填充K值的低64位
        //此時只會新增一個分組
        if(rest <= 56){
            //填充100...0
            //填充K值低64位
            //處理分組
             H(divide(inputBytes, i*64));
        //即大於448bit的情況,先填充100...0再填充K值的低64位
        //此時會新增兩個分組
        }else{
            //填充10000000
            //填充00000000
            //填充低64位
            //處理第一個尾部分組
            H(divide(inputBytes, i*64));
            //處理第二個尾部分組
             H(divide(inputBytes, i*64));
        }
        //將Hash值轉換成十六進制的字符串
        //小端方式!
    }
    
    //從inputBytes的index開始取512位,作爲新的512bit的分組
    private static long[] divide(byte[] inputBytes,int start){
    	//存儲一整個分組,就是512bit,數組裏每個是32bit,就是4字節,爲了消除符號位的影響,所以使用long
        
    }
    
    // groups[] 中每一個分組512位(64字節)
    // MD5壓縮函數
    private void H(long[] groups) {
    	//緩衝區(寄存器)數組
        //四輪循環
        for(int n = 0; n < 4; n++) {
        	//16輪迭代
        	for(int i = 0; i < 16; i++) {
            }
        }
        //加入之前計算的結果
        //防止溢出
    }
    public static void main(String []args){
        MD5 md=new MD5();
        String message = "helloMD5";
        System.out.println("MD5-Algorithm:\n\nOrigin Message: " + message);
        System.out.println("Result Message: " + md.start(message));
        System.out.println("Result Message(UpperCase): " + md.resultMessage.toUpperCase());
        //F0F99260B5A02508C71F6D81C15E9A44
        //3ED9E5F6855DBCDBCD95AC6C4FE0C0A5
    }
}

三、模塊分解

填充

填充時有兩種情況

  • 如果去掉整數個512bit的小組,剩下的位數不超過448bit,那樣就可以先填充100…00(如果正好是448bit就不用填充了),再填充用K值的低64位64bit,那樣的話就只是新增了一個小組
  • 如果去掉整數個512bit的小組,剩下的位數超過了448bit,那樣不夠填充64bit,所以需要填充100…00到512bit,構成一個小組;然後再在第二個小組填充448bit個0,最後填充K值的低64位bit,這樣就會新增兩個小組
//填充
        int rest = byteLen % 64;
        //即將填充的一個分組
        byte [] paddingBytes=new byte[64];
        //原來的尾部數據
        for(int i=0;i<rest;i++)
        	paddingBytes[i]=inputBytes[byteLen-rest+i];
        //即小於448bit的情況,先填充100...0再填充K值的低64位
        //此時只會新增一個分組
        if(rest <= 56){
            //填充100...0
            if(rest<56){
            	//填充10000000
            	paddingBytes[rest]=(byte)(1<<7);
            	//填充00000000
                for(int i=1;i<56-rest;i++)
                	paddingBytes[rest+i]=0;
            }
            //填充K值低64位
            for(int i=0;i<8;i++){
            	paddingBytes[56+i]=(byte)(K&0xFFL);
                K=K>>8;
            }
            //處理分組
            H(divide(paddingBytes,0));
        //即大於448bit的情況,先填充100...0再填充K值的低64位
        //此時會新增兩個分組
        }else{
            //填充10000000
            paddingBytes[rest]=(byte)(1<<7);
            //填充00000000
            for(int i=rest+1;i<64;i++)
            	paddingBytes[i]=0;
            //處理第一個尾部分組
            H(divide(paddingBytes,0));
            
            //填充00000000
            for(int i=0;i<56;i++)
            	paddingBytes[i]=0;

            //填充低64位
            for(int i=0;i<8;i++){
            	//這裏很關鍵,使用小端方式,即Byte數組先存儲len的低位數據,然後右移len
            	paddingBytes[56+i]=(byte)(K&0xFFL);
                K=K>>8;
            }
            //處理第二個尾部分組
            H(divide(paddingBytes,0));
        }

分塊

這裏調用了divide分組函數,將完整小組分成個數爲16,單個元素爲32bit的數組

再直接調用H壓縮函數,進行四輪循環和16次迭代

//分塊
//完整小組(512bit)(64byte)的個數
        int groupCount = byteLen/64;
        for(int i = 0;i < groupCount;i++){
        	//每次取512bit
            //處理一個分組
            H(divide(inputBytes, i*64));
        }

分組函數

這個函數傳入的參數是一個字節數組,以及一個代表從哪裏開始截取的int

效果就是將這個字節數組從start開始的64個字節組成一個

元素個數爲16,單個元素爲32bit的數組

採用的方法是每次取四個字節,採用小端的方式拼接成一個long型的整數

因爲int在某些情況下是4個字節,所以就是正好32bit,但是帶符號,就影響後面數據的運算

//從inputBytes的index開始取512位,作爲新的512bit的分組
    private static long[] divide(byte[] inputBytes,int start){
    	//存儲一整個分組,就是512bit,數組裏每個是32bit,就是4字節,爲了消除符號位的影響,所以使用long
        long [] group=new long[16];
        for(int i=0;i<16;i++){
        	//每個32bit由4個字節拼接而來
        	//小端的從byte數組到bit恢復方法
            group[i]=byte2unsign(inputBytes[4*i+start])|
                (byte2unsign(inputBytes[4*i+1+start]))<<8|
                (byte2unsign(inputBytes[4*i+2+start]))<<16|
                (byte2unsign(inputBytes[4*i+3+start]))<<24;
        }
        return group;
    }

MD5壓縮函數

用了兩層循環

第一層表示四輪循環

第二層表示16輪迭代

中間按照緩衝區運算的要求處理數據

這裏直接處理的是result數組,也就是真實的緩衝區,所以在開始暫存了它們當時的值爲a,b,c,d

運算完畢後要加上這些值

運算過程中以及運算結束會有一些防溢出的操作

(有時候沒有這個就會出錯)

// groups[] 中每一個分組512位(64字節)
    // MD5壓縮函數
    private void H(long[] groups) {
    	//緩衝區(寄存器)數組
        long a = result[0], b = result[1], c = result[2], d = result[3];
        //四輪循環
        for(int n = 0; n < 4; n++) {
        	//16輪迭代
        	for(int i = 0; i < 16; i++) {
            	result[0] += (g(n, result[1], result[2], result[3])&0xFFFFFFFFL) + groups[k[n][i]] + T[n][i];
                result[0] = result[1] + ((result[0]&0xFFFFFFFFL)<< S[n][i % 4] | ((result[0]&0xFFFFFFFFL) >>> (32 - S[n][i % 4])));
                //循環輪換
                long temp = result[3];
                result[3] = result[2];
                result[2] = result[1];
                result[1] = result[0];
                result[0] = temp;
            }
        }
        //加入之前計算的結果
        result[0] += a;
        result[1] += b;
        result[2] += c;
        result[3] += d;
        //防止溢出
        for(int n = 0; n < 4 ; n++) {
        	result[n] &=0xFFFFFFFFL;
        }
    }

最後結果轉換爲字符串

之前得到的結果就是result數組,四個元素,每個元素是一個32bit的數據

現在要把他們轉換爲字符串

但是需要小端的處理方式

long的低位作爲字符串的高位

每次以一個字節處理,32bit四個字節分別通過與運算和移位運算分離出來,再讓long的低位在前,高位在後,得到十六進制字符串就是MD5編碼的結果

//將Hash值轉換成十六進制的字符串
        //小端方式!
        for(int i=0;i<4;i++){
        	resultMessage += Long.toHexString(result[i] & 0xFF) +
            		Long.toHexString((result[i] & 0xFF00) >> 8) +
            		Long.toHexString((result[i] & 0xFF0000) >> 16) +
            		Long.toHexString((result[i] & 0xFF000000) >> 24);
  
        }

四、數據結構

long []groups 存儲小組

String resultMessage存儲結果字符串

//四個寄存器的初始向量IV,採用小端存儲
static final long A=0x67452301L
static final long B=0xefcdab89L
static final long C=0x98badcfeL
static final long D=0x10325476L

private long [] result={A,B,C,D}; 模擬存儲結果的四個寄存器

static final long T[][]迭代過程中採用的近似值int(2^32 x |sin(i)|)

static final int k[][] 表示X[k]中的的k取值,決定如何使用消息分組中的字

static final int S[][] 各次迭代中採用的做循環移位的s值

private static long g(int i, long b, long c, long d) 4輪循環中使用的生成函數(輪函數)g

五、運行結果

輸入原始消息:helloMD5

得到編碼結果爲:3ED9E5F6855DBCDBCD95AC6C4FE0C0A5

在這裏插入圖片描述

站長之家的工具驗證,結果正確1543732370194

六、源代碼

/**
 * @author Janking
 */
public class MD5 {
    //存儲小組
    long[] groups = null;
    //存儲結果
    String resultMessage = "";

    //四個寄存器的初始向量IV,採用小端存儲
    static final long A = 0x67452301L;
    static final long B = 0xefcdab89L;
    static final long C = 0x98badcfeL;
    static final long D = 0x10325476L;

    //java不支持無符號的基本數據(unsigned),所以選用long數據類型
    private long[] result = {A, B, C, D};

    static final long 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}};
    //表示X[k]中的的k取值,決定如何使用消息分組中的字
    static final int k[][] = {
            {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
            {1, 6, 11, 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12},
            {5, 8, 11, 14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, 15, 2},
            {0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9}};

    //各次迭代中採用的做循環移位的s值
    static final int S[][] = {
            {7, 12, 17, 22},
            {5, 9, 14, 20},
            {4, 11, 16, 23},
            {6, 10, 15, 21}};

    //4輪循環中使用的生成函數(輪函數)g
    private static long g(int i, long b, long c, long d) {
        switch (i) {
            case 0:
                return (b & c) | ((~b) & d);
            case 1:
                return (b & d) | (c & (~d));
            case 2:
                return b ^ c ^ d;
            case 3:
                return c ^ (b | (~d));
            default:
                return 0;
        }
    }

    //開始使用MD5加密
    private String start(String message) {
        //轉化爲字節數組
        byte[] inputBytes = message.getBytes();
        //6A 61 6E 6b 69 6e 67
        //獲取字節數組的長度
        int byteLen = inputBytes.length;
        //得到K值(以bit作單位的message長度)
        long K = (long) (byteLen << 3);
        //完整小組(512bit)(64byte)的個數
        int groupCount = byteLen / 64;

        //分塊
        for (int i = 0; i < groupCount; i++) {
            //每次取512bit
            //處理一個分組
            H(divide(inputBytes, i * 64));
        }

        //填充
        int rest = byteLen % 64;
        //即將填充的一個分組
        byte[] paddingBytes = new byte[64];
        //原來的尾部數據
        for (int i = 0; i < rest; i++)
            paddingBytes[i] = inputBytes[byteLen - rest + i];
        //即小於448bit的情況,先填充100...0再填充K值的低64位
        //此時只會新增一個分組
        if (rest <= 56) {
            //填充100...0
            if (rest < 56) {
                //填充10000000
                paddingBytes[rest] = (byte) (1 << 7);
                //填充00000000
                for (int i = 1; i < 56 - rest; i++)
                    paddingBytes[rest + i] = 0;
            }
            //填充K值低64位
            for (int i = 0; i < 8; i++) {
                paddingBytes[56 + i] = (byte) (K & 0xFFL);
                K = K >> 8;
            }
            //處理分組
            H(divide(paddingBytes, 0));
            //即大於448bit的情況,先填充100...0再填充K值的低64位
            //此時會新增兩個分組
        } else {
            //填充10000000
            paddingBytes[rest] = (byte) (1 << 7);
            //填充00000000
            for (int i = rest + 1; i < 64; i++)
                paddingBytes[i] = 0;
            //處理第一個尾部分組
            H(divide(paddingBytes, 0));

            //填充00000000
            for (int i = 0; i < 56; i++)
                paddingBytes[i] = 0;

            //填充低64位
            for (int i = 0; i < 8; i++) {
                //這裏很關鍵,使用小端方式,即Byte數組先存儲len的低位數據,然後右移len
                paddingBytes[56 + i] = (byte) (K & 0xFFL);
                K = K >> 8;
            }
            //處理第二個尾部分組
            H(divide(paddingBytes, 0));
        }
        //將Hash值轉換成十六進制的字符串
        //小端方式!
        for (int i = 0; i < 4; i++) {
            //解決缺少前置0的問題
            resultMessage += String.format("%02x", result[i] & 0xFF) +
                    String.format("%02x", (result[i] & 0xFF00) >> 8) +
                    String.format("%02x", (result[i] & 0xFF0000) >> 16) +
                    String.format("%02x", (result[i] & 0xFF000000) >> 24);

        }
        return resultMessage;
    }

    //從inputBytes的index開始取512位,作爲新的512bit的分組
    private static long[] divide(byte[] inputBytes, int start) {
        //存儲一整個分組,就是512bit,數組裏每個是32bit,就是4字節,爲了消除符號位的影響,所以使用long
        long[] group = new long[16];
        for (int i = 0; i < 16; i++) {
            //每個32bit由4個字節拼接而來
            //小端的從byte數組到bit恢復方法
            group[i] = byte2unsign(inputBytes[4 * i + start]) |
                    (byte2unsign(inputBytes[4 * i + 1 + start])) << 8 |
                    (byte2unsign(inputBytes[4 * i + 2 + start])) << 16 |
                    (byte2unsign(inputBytes[4 * i + 3 + start])) << 24;
        }
        return group;
    }

    //其實byte相當於一個字節的有符號整數,這裏不需要符號位,所以把符號位去掉
    public static long byte2unsign(byte b) {
        return b < 0 ? b & 0x7F + 128 : b;
    }

    // groups[] 中每一個分組512位(64字節)
    // MD5壓縮函數
    private void H(long[] groups) {
        //緩衝區(寄存器)數組
        long a = result[0], b = result[1], c = result[2], d = result[3];
        //四輪循環
        for (int n = 0; n < 4; n++) {
            //16輪迭代
            for (int i = 0; i < 16; i++) {
                result[0] += (g(n, result[1], result[2], result[3]) & 0xFFFFFFFFL) + groups[k[n][i]] + T[n][i];
                result[0] = result[1] + ((result[0] & 0xFFFFFFFFL) << S[n][i % 4] | ((result[0] & 0xFFFFFFFFL) >>> (32 - S[n][i % 4])));
                //循環輪換
                long temp = result[3];
                result[3] = result[2];
                result[2] = result[1];
                result[1] = result[0];
                result[0] = temp;
            }
        }
        //加入之前計算的結果
        result[0] += a;
        result[1] += b;
        result[2] += c;
        result[3] += d;
        //防止溢出
        for (int n = 0; n < 4; n++) {
            result[n] &= 0xFFFFFFFFL;
        }
    }

    public static void main(String[] args) {
        MD5 md = new MD5();
        String message = "helloMD5";
        System.out.println("MD5-Algorithm:\n\nOrigin Message: " + message);
        System.out.println("Result Message: " + md.start(message));
        System.out.println("Result Message(UpperCase): " + md.resultMessage.toUpperCase());
        //Origin Message: helloMD5
        //Result Message: 3ed9e5f6855dbcdbcd95ac6c4fe0c0a5
        //Result Message(UpperCase): 3ED9E5F6855DBCDBCD95AC6C4FE0C0A5
    }
}

七、參考資料

MD5算法原理

DES算法實現

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章