1. 程式人生 > >[C++] MD5加密演算法原理及實現

[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 大於 264,即 64 位不夠表示原始訊息的長度時,只取低 64 位。將 b 填充至第一步填充後的訊息尾部。

此時,填充後得到的訊息總長度為 512 的倍數,也是 16 的倍數。將填充後的訊息分割為 L 個 512 位的分組,Y0,Y1,...,YL1

注意,實際填充時不是直接將長度的 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) (XY)(¬XZ)
G(X,Y,Z) (XZ)(Y¬Z)
H(X,Y,Z) XYZ
I(X,Y,Z) Y(X¬Z)

以第二步分割後的 512 位元的分組為單位,每一個分組 Yq (q = 0, 1, …, L - 1) 經過 4 輪迴圈的壓縮演算法,記為 Hmd5,對第三步初始化的 MD 緩衝區進行迭代更新,初始 MD 緩衝區記為 CV0=IV;第 q 個分組處理後的 MD 緩衝區記為 CVq=Hmd5(Yq1,CVq1),最終輸出結果為 CVL

另外,512 位元的分組再分割為 16 個 32 位元的字,記為 X0,X1,...,X15

特別注意: 這裡的 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 的具體步驟大致為,

  1. 輸入上一輪的 128 位結果 CVq1 和第 q 個分組 Yq
  2. 用輪函式 F 和 T 表的 [1…16] 項及 X[i] 對上一輪的結果 CVq1 進行 16 次迭代計算
  3. 用輪函式 G 和 T 表的 [17…32] 項及 X[ρ2i] 對第 2 步的結果進行 16 次迭代計算
  4. 用輪函式 H 和 T 表的 [33…48] 項及 X[ρ3i] 對第 3 步的結果進行 16 次迭代計算
  5. 用輪函式 I 和 T 表的 [49…64] 項及 X[ρ4i] 對第 4 步的結果進行 16 次迭代計算
  6. 將上一輪結果 CVq1 的 4 個 32 位的字與第 5 步產生的 4 個 32 位的字分別進行模 232 加法,得到 CVq

232 加法大致為,兩個 32 位字相加,若有第 33 位的進位,則捨棄。例如,0xFFFFFFFF + 0x00000001 = 0x00000000。

Hmd5 流程圖

H_md5 流程圖

Hmd5 的 2~5 步,每輪的一步運算邏輯為,

ab+((a+g(b,c,d)+X[k]+T[i])<<<s)
說明:
  • 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]

  1. 輪函式 F 迭代,X[i], i = 0, 1,…, 15
  2. 輪函式 G 迭代,X[ρ2i], ρ2i = (1 + 5i) mod 16, i = 0, 1,…, 15
  3. 輪函式 H 迭代,X[ρ3i], ρ3i = (5 + 3i) mod 16, i = 0, 1,…, 15
  4. 輪函式 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[i]=int(232|sin(i)|)

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] = {
        {