[C++] MD5加密演算法原理及實現
參考文獻:
1. RFC1321 - R. Rivest
2. 中山大學 蔡國揚 老師的 Web安全課件
演算法概述
- MD5 使用 little-endian,輸入任意不定長度資訊,以 512 位長進行分組,生成四個32位資料,最後聯合起來輸出固定 128 位長的資訊摘要。
- MD5 演算法的基本過程為:求餘、取餘、調整長度、與連結變數進行迴圈運算、得出結果。
在 RFC1321 中,演算法共分為五步,對於每一步的細節我都會舉出例子來更方便的理解。另外有一點需要注意的是,下文中若無特別說明,都是以位元為單位來闡述演算法。
基本流程圖
一、Append Padding Bits
在原始訊息的尾部進行填充,使得填充後的訊息位數 L mod 512 = 448。
填充規則為,先填充一個 1
,然後剩餘的填充 0
。並且填充是必須的,即使原始訊息的長度模 512 後正好為 448 位元,也要進行填充。總之,填充的長度至少為 1 位元,最多為 512 位元。
例如,原始訊息為 12345678
,總長度為 8 * 8 = 64 位元,那麼需要填充 384 位元,即填充 1000
…. 後面還有 380 個 0
。
填充後的訊息用 16 進製表示(此處省略 0x
)為
31 32 33 34 35 36 37 38 80 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
二、Append Length
計算原始訊息(未填充 padding 前)的長度,用長 64 位的 b
表示。若 b
大於 b
填充至第一步填充後的訊息尾部。
此時,填充後得到的訊息總長度為 512 的倍數,也是 16 的倍數。將填充後的訊息分割為 L 個 512 位的分組,
注意,實際填充時不是直接將長度的 64 位二進位制表示接上去就可以。而是先用兩個 32 位的字來表示原始訊息長度 b
,將低位的字先填充,然後再填充高位的字,並且每個字在填充時使用 little-endian。
little-endian
:將低位位元組排放在記憶體的低地址端,高位位元組排放在記憶體的高地址端。
例如,原始訊息為 12345678
,總長度為 8 * 8 = 64 位元,用 64 位二進位制表示為 00000000 00000000 00000000 00000000 00000000 00000000 00000000 01000000
。分成兩個 32 位的字:
- 高位:
00000000 00000000 00000000 00000000
- 低位:
00000000 00000000 00000000 01000000
低位位元組的 little-endian 表示為 01000000 00000000 00000000 00000000
。
因此,應該填充的 64 位為 01000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
。
三、Initialize MD Buffer
初始化一個 128 位的 MD 緩衝區,也表示為 4 個 32 位暫存器 (A, B, C, D),用來迭代計算儲存資訊摘要。
對於 4 個 32 位的暫存器 A、B、C、D 分別初始化為 16 進位制初始值,採用小端規則
word | little-endian | ||||
---|---|---|---|---|---|
A | 01 | 23 | 45 | 67 | 0x67452301 |
B | 89 | AB | CD | EF | 0xEFCDAB89 |
C | FE | DC | BA | 98 | 0x98BADCFE |
D | 76 | 54 | 32 | 10 | 0x10325476 |
四、Process Message in 16-Word Blocks
首先,定義四個輪函式,每個函式以 3 個 32 位字為輸入,輸出 1 個 32 位字。
Function | return |
---|---|
F(X,Y,Z) | |
G(X,Y,Z) | |
H(X,Y,Z) | |
I(X,Y,Z) |
以第二步分割後的 512 位元的分組為單位,每一個分組
另外,512 位元的分組再分割為 16 個 32 位元的字,記為
特別注意: 這裡的 X[k] 並不是順序讀取 32 位元直接形成,而是需要對讀取的 4 個位元組按照 little-endian 進行轉換。
例如,原始資訊為 12345678
,經過第一、二步填充後的十六進位制(此處省略 0x
)表示為
31 32 33 34 35 36 37 38 80 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0
對於 X[0],順序讀取 32 位元直接形成得到的是 0x31323334
,而實際計算時應該為 0x34333231
。
X[0…7] = {0x34333231, 0x38373635, 0x00000080, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000}
X[8…15] = {0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000040, 0x00000000}
Hmd5 的具體步驟大致為,
- 輸入上一輪的 128 位結果
CVq−1 和第 q 個分組Yq - 用輪函式 F 和 T 表的 [1…16] 項及 X[i] 對上一輪的結果
CVq−1 進行 16 次迭代計算 - 用輪函式 G 和 T 表的 [17…32] 項及 X[ρ2i] 對第 2 步的結果進行 16 次迭代計算
- 用輪函式 H 和 T 表的 [33…48] 項及 X[ρ3i] 對第 3 步的結果進行 16 次迭代計算
- 用輪函式 I 和 T 表的 [49…64] 項及 X[ρ4i] 對第 4 步的結果進行 16 次迭代計算
- 將上一輪結果
CVq−1 的 4 個 32 位的字與第 5 步產生的 4 個 32 位的字分別進行模232 加法,得到CVq
模
232 加法大致為,兩個 32 位字相加,若有第 33 位的進位,則捨棄。例如,0xFFFFFFFF + 0x00000001 = 0x00000000。
Hmd5 的 2~5 步,每輪的一步運算邏輯為,
說明:
- a, b, c, d 分別為 MD 緩衝區 (A, B, C, D) 的當前值
- g:輪函式 (F, G, H, I 中的一個)
- <<< s:將 32 位輸入迴圈左移 s 位
- X[k]: 當前處理訊息分組的第 k 個 32 位字
- T[i]: T 表的第 i 個元素,32 位字
- +:模
232 加法
每次計算後,要對 MD 緩衝區進行迴圈右移。記一步運算後 MD 緩衝區為 (AA, BB, CC, DD),迴圈右移即令 A = DD, B = AA, C = BB, D = CC
流程圖:
各輪迭代中的 X[k]
- 輪函式 F 迭代,X[i], i = 0, 1,…, 15
- 輪函式 G 迭代,X[ρ2i], ρ2i = (1 + 5i) mod 16, i = 0, 1,…, 15
- 輪函式 H 迭代,X[ρ3i], ρ3i = (5 + 3i) mod 16, i = 0, 1,…, 15
- 輪函式 I 迭代,X[ρ4i], ρ4i = 7i mod 16, i = 0, 1,…, 15
用表格更清晰的表示為
輪函式 | X[k] 中 k 依次為 |
---|---|
F | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] |
G | [1, 6, 11, 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12] |
H | [5, 8, 11, 14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, 15, 2] |
I | [0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9] |
T 表的生成
T[1…8] = {0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501}
T[9…16] = {0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821}
T[17…24] = {0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x2441453, 0xd8a1e681, 0xe7d3fbc8}
T[25…32] = {0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a}
T[33…40] = {0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70}
T[41…48] = {0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665}
T[49…56] = {0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1}
T[57…64] = {0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391}
五、Output
根據 MD 緩衝區最後的結果 (A, B, C, D) 輸出資訊摘要,從 A 到 D,從低位元組至高位元組的順序輸出。
例如,原始資訊為 12345678
經過上述步驟處理後得到的 (A, B, C, D) = {0xd25ad525, 0x0a40aa83, 0x6dc764f4, 0xad073c71}
- A:輸出 25 d5 5a d2
- B:輸出 83 aa 40 0a
- C:輸出 f4 64 c7 6d
- D:輸出 71 3c 07 ad
最終,輸出結果為 25d55ad283aa400af464c76d713c07ad
C++ 實現
一、自己的實現方法(不可加密未知長度的原始訊息)
我的實現方法是按照參考文獻中的五步,一步一步做的,沒有參照文獻後面附錄中的程式碼實現。這種實現方法在使用時必須滿足一個前提,即在一開始就知道整個原始訊息及其長度,因為這種實現方法在前兩步就對原始訊息進行填充,然後才一組一組進行處理,不能對長度未知的原始訊息進行加密,這算是個缺陷吧。因此,後面我會給出 L. Peter Deutsch 的實現方法。
MD5.hpp
/*
* file: md5.hpp
* author: Els-y
* time: 2017-10-16 21:08:21
*/
#ifndef _MD5_H
#define _MD5_H
#include <string>
#include <vector>
#include <cstring>
#include <cmath>
#include <iostream>
#include <bitset>
using std::string;
using std::vector;
using std::bitset;
using std::cout;
using std::endl;
using std::sin;
using std::abs;
// default little-endian
class MD5 {
public:
MD5();
~MD5();
string encrypt(string plain);
// 輸出擴充套件後的訊息
void print_buff();
private:
// 128 位 MD 緩衝區,md[0...3] = {A, B, C, D}
vector<unsigned int> md;
// 儲存擴充套件後的訊息
unsigned char* buffer;
// 擴充套件後的訊息長度,以位元組為單位
unsigned int buffer_len;
// 存放 4 個輪函式的陣列
unsigned int (MD5::*round_funcs[4])(unsigned int, unsigned int, unsigned int);
// 初始化 MD 緩衝區
void init_md();
// 填充 padding 和 length
void padding(string plain);
void clear();
void h_md5(int groupid);
// 4 個輪函式
unsigned int f_rf(unsigned int x, unsigned int y, unsigned int z);
unsigned int g_rf(unsigned int x, unsigned int y, unsigned int z);
unsigned int h_rf(unsigned int x, unsigned int y, unsigned int z);
unsigned int i_rf(unsigned int x, unsigned int y, unsigned int z);
// 返回 MD 緩衝區轉換後的 string 格式密文
string md2str();
// 返回 buffer 中 [pos, pos + 3] 四個位元組按照 little-endian 組成的 X
unsigned int uchar2uint(int pos);
// 返回 unsigned char 對應的十六進位制 string
string uchar2hex(unsigned char uch);
// 返回 val 迴圈左移 bits 位的值
unsigned int cycle_left_shift(unsigned int val, int bits);
// 返回第 round 輪迭代中,第 step 步的 X 對應下標
int get_x_index(int round, int step);
};
#endif
MD5.cpp
/*
* file: md5.cpp
* author: Els-y
* time: 2017-10-16 21:08:21
*/
#include "MD5.hpp"
/* -- public --*/
MD5::MD5() {
buffer = NULL;
round_funcs[0] = &MD5::f_rf;
round_funcs[1] = &MD5::g_rf;
round_funcs[2] = &MD5::h_rf;
round_funcs[3] = &MD5::i_rf;
}
MD5::~MD5() {
clear();
}
string MD5::encrypt(string plain) {
init_md();
clear();
padding(plain);
int group_len = buffer_len / 64;
for (int i = 0; i < group_len; ++i) h_md5(i);
return md2str();
}
void MD5::print_buff() {
cout << "buffer_len = " << buffer_len << endl;
for (int i = 0; i < buffer_len; ++i) {
bitset<8> ch = buffer[i];
cout << ch << " ";
}
cout << endl;
}
/* -- private --*/
// 初始化 MD 緩衝區
void MD5::init_md() {
md = vector<unsigned int>({0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476});
}
// 填充 padding 和 length
void MD5::padding(string plain) {
unsigned int plain_len = plain.size();
unsigned long long plain_bits_len = plain.size() * 8;
unsigned int fill_bits_len = plain_bits_len % 512 == 448 ? 512 : (960 - plain_bits_len % 512) % 512;
unsigned int fill_len = fill_bits_len / 8;
buffer_len = plain_len + fill_len + 8;
buffer = new unsigned char[buffer_len];
// 複製原始訊息
for (int i = 0; i < plain_len; ++i) buffer[i] = plain[i];
// 填充 padding
buffer[plain_len] = 0x80;
for (int i = 1; i < fill_len; ++i) buffer[plain_len + i] = 0;
// 填充原始訊息 length
for (int i = 0; i < 8; ++i) {
unsigned char ch = plain_bits_len;
buffer[plain_len + fill_len + i] = ch;
plain_bits_len >>= 8;
}
}
void MD5::clear() {
if (buffer != NULL) {
delete []buffer;
buffer = NULL;
}
}
void MD5::h_md5(int groupid) {
int buff_begin = 64 * groupid;
unsigned int next;
vector<unsigned int> last_md(md);
const unsigned int CYCLE_BITS[4][4] = {
{