文章目錄
一、算法原理概述
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
值相等。
基本流程
填充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 輸出即爲算法結果。
壓縮函數
每輪循環中的一次迭代運算邏輯
- 對A迭代:a <— b+ ((a+ g(b, c, d) + X[k] + T[i]) <<<s)
- 緩衝區(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
用站長之家的工具驗證,結果正確
六、源代碼
/**
* @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
}
}