0%

浅析 MD5 算法

之前的工作中,频繁用到 MD5 值...

然后就一直在思考 MD5 值是如何算出来的,可惜没有太多时间,现在有时间了,深入研究一下。

前言

在正式介绍 MD5 算法之前,我们需要先知道它是什么。按照百度百科的解释,MD5 算法是一种信息摘要算法(MD5 Message-Digest Algorithm),本质上就是一种密码散列函数(所以它是一种哈希算法,不是加密算法),可以产生出一个 128 位(16 字节)的散列值(Hash Value),而这个散列值则可以用来确保信息传输的完整一致性,比如,对下载完成的文件进行 MD5 校验。从发展历史上来看,MD5 实际上是从 MD4 改进而来的,主要增加了算法的复杂度。

原理

MD5 算法的原理可以简单概括为:MD5 算法以 512 位分组处理输入信息,且每一分组又被划分为 16 个 32 位子分组,经过一系列计算后,得到四个 32 位数,将这四个 32 位数拼接在一起得到一个 128 位散列值。
假设输入信息的长度(比特数)为 $b$ 位,$b$ 可以为任意非负整数,则计算其 MD5 值需要经过以下步骤:

补位

由于 MD5 算法是以 512 位为一组进行信息处理,那么输入信息的长度就必须要是 512 的整数倍,长度不够就需要补位。设补位后的信息长度为 $LEN$,则 $LEN \% 512 = 448(bits)$,也即 $LEN = K * 512 + 56(byte)$。当输入信息已经满足 $LEN \% 512 = 448$ 时,仍然需要补位,且此时完整补 512 位。具体补位操作:第一位补 1,后面全部补 0。

尾部写入信息长度

前面提到的输入信息的长度 $b$ 会被表示为一个无符号 64 位整数,并写入到补位后的结果后面,当 $b > 2^{64}$ 时,$b$ 的高位会被截去。这样,补位后的数据块就为:$448 + 64 = 512$,正好满足计算需要。

初始化缓存变量

MD5 使用四个固定的“魔法数字”作为初始值(小端序,十六进制数):

1
2
3
4
A = 0x67452301
B = 0xEFCDAB89
C = 0x98BADCFE
D = 0x10325476

单块四轮运算

单块大小为 512 位,再拆成 16 个 32 位小块,使用四个混淆函数进行 4 轮循环运算,每轮 16 次,共 64 次。
四个混淆函数:
$$
\begin{align}
F(x, y, z) &= (x \& y) | (\neg x \& z) \\
G(x, y, z) &= (x \& z) | (y \& \neg z) \\
H(x, y, z) &= (x \oplus y \oplus z) \\
I(x, y, z) &= y \oplus (x | \neg z) \\
\end{align}
$$
每一次计算都会结合子块数据、固定正弦常数表、固定循环左移位数表来计算 A、B、C、D 的值,计算完成之后,再交换位置。

累加计算结果

每处理完一个 512 位分组,将临时计算的 A、B、C、D 累加到初始的缓存变量中,而非覆盖。

拼接最终结果

把最终 A、B、C、D 四个 32 位数按小端序拼接,合成 128 位结果。

小结

经过前面的介绍,我们了解了 MD5 算法的执行过程,以及它的一些特点,比如:

  1. MD5 算法理论支持任意比特长度输入,实际工程中输入均为字节对齐(即 8 的整数倍)。
  2. MD5 算法的最小计算单元是 512 位长的信息块,最终经过补位、尾部长度填充后的数据比特位长度一定是 512 的整数倍。
  3. MD5 算法每一步的计算过程是固定的,只是混淆函数和参与计算的常数不一致。
  4. MD5 算法中的字节序都是小端序。

实现

现在,我们考虑如何实现这个算法。有了上面的了解,我们发现这个算法并没有复杂的操作过程,只是在重复的进行计算,所以我们可以用 C 语言来实现它。

混淆函数和常量表

先从简单的入手,把固定的东西完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 循环左移函数
#define LEFT_ROTATE(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
// 四轮非线性混淆函数
#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
#define H(x, y, z) ((x) ^ (y) ^ (z))
#define I(x, y, z) ((y) ^ ((x) | (~z)))

// 常量值
// 初始 Magic Number,小端序
static const uint32_t MD5_INIT[4] = {
0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476
};
// 正弦常数表
static const uint32_t T[64] = {
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,
};
// 每轮循环左移位数
static const uint8_t S[64] = {
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
};

MD5 结构体

接着,我们考虑定义 MD5 结构体来保存计算过程中得到的一些中间值:

1
2
3
4
5
6
// MD5 上下文结构体
typedef struct {
uint32_t state[4]; // A, B, C, D
uint64_t count; // 总比特数
uint8_t buffer[64]; // 64 字节缓存,对应 512 位
} MD5_CTX;

功能函数

然后就是围绕 MD5 结构体的一系列功能函数。

md5_init

首先,我们需要将 MD5 上下文结构体初始化:

1
2
3
4
5
void md5_init(MD5_CTX *ctx) {
memcpy(ctx->state, MD5_INIT, sizeof(ctx->state));
ctx->count = 0;
memset(ctx->buffer, 0, sizeof(ctx->buffer));
}

md5_update

接着,我们将输入信息写入到 MD5 上下文结构体中,每当结构体中缓存满了(达到 512 位),就可以将这一块进行 MD5 计算;当数据不满 512 位时,先写入缓存,补位后在进行计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void md5_update(MD5_CTX *ctx, const uint8_t *input, size_t input_len) {
size_t buffer_idx = (ctx->count / 8) % 64; // 计算应该放入 MD5 上下文缓存的下标
ctx->count += input_len * 8; // 更新总位数计数
size_t fill = 64 - buffer_idx; // MD5 上下文缓存剩余容量

if(input_len >= fill) { // 缓存容量不够时
memcpy(&ctx->buffer[buffer_idx], input, fill); // 先填满缓存
md5_transform(ctx, ctx->buffer); // 缓存满后,立即计算
input += fill; // 指针跳过已处理的数据
input_len -= fill; // 减去已处理的长度
while(input_len >= 64) { // 直接计算,不经过缓存
md5_transform(ctx, input);
input += 64;
input_len -= 64;
}
buffer_idx = 0; // 清空缓存
}
memcpy(&ctx->buffer[buffer_idx], input, input_len); // 剩余数据存入缓存
}

这里,可能有人会疑惑为什么补位操作放在计算操作之后了。按照之前的分析,应该是先补位,然后再计算吧?实际上,这里之所以先计算,是因为输入信息长度大于等于 512 位,满足 MD5 计算要求,所以可以直接计算。而且即便是把补位操作放在计算之后,也不会对最终结果有任何影响。

md5_transform

现在,我们考虑如何计算 512 位数据。按照之前的分析,512 位会分为 16 个 32 位子块,每个子块会单独参与到计算中。在每轮运算中,需要先算出混淆函数的结果,然后与 A、正弦常数和单个 32 位子块累加在一起,左移指定位数后,轮换 A、B、C、D 的值。计算完成后,将 A、B、C、D 的值累加到 MD5 上下文结构体中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void md5_transform(MD5_CTX *ctx, const uint8_t data[64]) {
uint32_t a = ctx->state[0], b = ctx->state[1], c = ctx->state[2], d = ctx->state[3];
uint32_t x[16];

// 将 64 字节转为 16 个 32 位整数,也即将 512 位分为 16 个 32 位子块
for(int i = 0; i < 16; ++i) {
x[i] = (uint32_t)data[i * 4] | ((uint32_t)data[i * 4 + 1] << 8) |
((uint32_t)data[i * 4 + 2] << 16) | ((uint32_t)data[i * 4 + 3] << 24);
}

// 四轮循环运算,总共 64 步
for(int i = 0; i < 64; ++i) {
uint32_t f, g;
if(i < 16) {
f = F(b, c, d); g = i;
} else if(i < 32) {
f = G(b, c, d), g = (5 * i + 1) % 16;
} else if(i < 48) {
f = H(b, c, d), g = (3 * i + 5) % 16;
} else {
f = I(b, c, d), g = (7 * i) % 16;
}
// 轮换
uint32_t temp = d;
d = c;
c = b;
b = b + LEFT_ROTATE((a + f + T[i] + x[g]), S[i]);
a = temp;
}

// 累加更新状态
ctx->state[0] += a;
ctx->state[1] += b;
ctx->state[2] += c;
ctx->state[3] += d;
}

md5_final

最后,我们还需要考虑补位及最后一块数据块的计算。首先,补位的问题按照前面的补位规则进行实现即可;最后一块数据块的计算也比较直观,调用md5_update函数就可以完成补位数据写入和计算的操作,最后将得到的结果,写入到output中即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void md5_final(MD5_CTX *ctx, uint8_t output[16]) {
// 预先保存原始消息总比特数,补位长度不算在内
uint64_t bits = ctx->count;

// 补位,md5 最后一个块大小必须为 56 字节,448 位,这样后续写入长度后才是 64 字节,512 位,才能进行最终的 md5 计算
uint8_t padding[64] = {0x80}; // 首位补位 1,其余补 0
size_t buffer_idx = (ctx->count / 8) % 64;
size_t pad_len = (buffer_idx < 56) ? (56 - buffer_idx) : (120 - buffer_idx);
// 更新补位数据到缓存
md5_update(ctx, padding, pad_len);

// 将 64 位原始消息比特总长度转换为小端 8 字节数据
uint8_t len_buf[8];
for(int i = 0; i < 8; ++i) {
len_buf[i] = (bits >> (i * 8)) & 0xFF;
}
// 将总比特长度更新到 md5 消息末尾,并重新计算 md5
md5_update(ctx, len_buf, 8);

// 生成 md5
for(int i = 0; i < 4; ++i) {
output[i * 4] = ctx->state[i] & 0xFF;
output[i * 4 + 1] = (ctx->state[i] >> 8) & 0xFF;
output[i * 4 + 2] = (ctx->state[i] >> 16) & 0xFF;
output[i * 4 + 3] = (ctx->state[i] >> 24) & 0xFF;
}
}

注意:最后 8 字节写入的比特位数是原始消息的比特位数,补位数据的比特位数不算在内!

这里,我们还可以发现,尽管 MD5 算法理论上补位的最小单位是比特,但由于计算机最小的读写单位是字节,所以补位的数据一定是 8 的倍数。

md5_to_hex

为了方便测试和观察,再实现一个将 MD5 值转换为十六进制字符串的函数:

1
2
3
4
5
6
7
8
void md5_to_hex(const uint8_t digest[16], char hex_str[33]) {
const char *hex = "0123456789abcdef";
for(int i = 0; i < 16; ++i) {
hex_str[i * 2] = hex[digest[i] >> 4];
hex_str[i * 2 + 1] = hex[digest[i] & 0x0F];
}
hex_str[32] = '\0';
}

测试

实现完成后,再用一个简单的单元测试函数测试一下正确性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void unit_test() {
typedef struct {
const char *input;
const char *expect;
} MD5_Case;

MD5_Case test_cases[] = {
{"", "d41d8cd98f00b204e9800998ecf8427e"},
{"Hello, World!", "65a8e27d8879283831b664bd8b7f0ad4"},
{"a", "0cc175b9c0f1b6a831c399e269772661"},
{"123456", "e10adc3949ba59abbe56e057f20f883e"},
{"Hello MD5", "e5dadf6524624f79c3127e247f04b548"},
{"!@#$%^&*()", "05b28d17a7b6e7024b6e5d8cc43a8bf7"},
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "5eca9bd3eb07c006cd43ae48dfde7fd3"},
{"你好,世界", "dbefd3ada018615b35588a01e216ae6e"}
};

MD5_CTX ctx;
uint8_t digest[16];
char hex_result[33];

for(int i = 0; i < sizeof(test_cases) / sizeof(MD5_Case); ++i) {
printf("Input: %s\n", test_cases[i].input);

md5_init(&ctx);
md5_update(&ctx, (const uint8_t *)test_cases[i].input, strlen(test_cases[i].input));
md5_final(&ctx, digest);
md5_to_hex(digest, hex_result);

printf("MD5 Value: %s\n", hex_result);
printf("Expected: %s\n", test_cases[i].expect);
printf("Result: %s\n", (strcmp(hex_result, test_cases[i].expect) == 0 ? "Pass" : "Error"));
}
}

总结

到这里,本次 MD5 算法之旅就算结束了。在本篇文章中,做了两件事:

  1. 分析和探究了 MD5 算法的执行过程。
  2. 使用 C 语言实现了 MD5 算法,并进行了简单测试,测试结果符合预期。

整体代码量不算大,算上注释只有二百来行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
#include <stdio.h>
#include <stdint.h>
#include <string.h>

// 循环左移函数
#define LEFT_ROTATE(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
// 四轮非线性混淆函数
#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
#define H(x, y, z) ((x) ^ (y) ^ (z))
#define I(x, y, z) ((y) ^ ((x) | (~z)))

// MD5 上下文结构体
typedef struct {
uint32_t state[4]; // A, B, C, D
uint64_t count; // 总比特数
uint8_t buffer[64]; // 64 字节缓存,对应 512 位
} MD5_CTX;

// 常量值
// 初始 Magic Number,小端序
static const uint32_t MD5_INIT[4] = {
0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476
};
// 正弦常数表
static const uint32_t T[64] = {
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,
};
// 每轮循环左移位数
static const uint8_t S[64] = {
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
};

/**
* @brief MD5 算法核心计算函数,四轮 64 步运算
* @param ctx MD5 上下文
* @param data 512 位数据块(64 字节)
*/
static void md5_transform(MD5_CTX *ctx, const uint8_t data[64]) {
uint32_t a = ctx->state[0], b = ctx->state[1], c = ctx->state[2], d = ctx->state[3];
uint32_t x[16];

// 将 64 字节转为 16 个 32 位整数,也即将 512 位分为 16 个 32 位子块
for(int i = 0; i < 16; ++i) {
x[i] = (uint32_t)data[i * 4] | ((uint32_t)data[i * 4 + 1] << 8) |
((uint32_t)data[i * 4 + 2] << 16) | ((uint32_t)data[i * 4 + 3] << 24);
}

// 四轮循环运算,总共 64 步
for(int i = 0; i < 64; ++i) {
uint32_t f, g;
if(i < 16) {
f = F(b, c, d); g = i;
} else if(i < 32) {
f = G(b, c, d), g = (5 * i + 1) % 16;
} else if(i < 48) {
f = H(b, c, d), g = (3 * i + 5) % 16;
} else {
f = I(b, c, d), g = (7 * i) % 16;
}
// 轮换
uint32_t temp = d;
d = c;
c = b;
b = b + LEFT_ROTATE((a + f + T[i] + x[g]), S[i]);
a = temp;
}

// 累加更新状态
ctx->state[0] += a;
ctx->state[1] += b;
ctx->state[2] += c;
ctx->state[3] += d;
}

/**
* @brief 初始化 MD5 上下文
* @param ctx MD5 上下文指针
*/
void md5_init(MD5_CTX *ctx) {
memcpy(ctx->state, MD5_INIT, sizeof(ctx->state));
ctx->count = 0;
memset(ctx->buffer, 0, sizeof(ctx->buffer));
}

/**
* @brief 更新 MD5 数据,分批处理输入数据
* @param ctx MD5 上下文
* @param input 输入数据
* @param input_len 输入数据长度
*/
void md5_update(MD5_CTX *ctx, const uint8_t *input, size_t input_len) {
size_t buffer_idx = (ctx->count / 8) % 64; // 计算应该放入 MD5 上下文缓存的下标
ctx->count += input_len * 8; // 更新总位数计数
size_t fill = 64 - buffer_idx; // MD5 上下文缓存剩余容量

if(input_len >= fill) { // 缓存容量不够时
memcpy(&ctx->buffer[buffer_idx], input, fill); // 先填满缓存
md5_transform(ctx, ctx->buffer); // 缓存满后,立即计算
input += fill; // 指针跳过已处理的数据
input_len -= fill; // 减去已处理的长度
while(input_len >= 64) { // 直接计算,不经过缓存
md5_transform(ctx, input);
input += 64;
input_len -= 64;
}
buffer_idx = 0; // 清空缓存
}
memcpy(&ctx->buffer[buffer_idx], input, input_len); // 剩余数据存入缓存
}

/**
* @brief 生成 MD5 指纹
* @param ctx MD5 上下文
* @param output 输出 16 字节二进制串
*/
void md5_final(MD5_CTX *ctx, uint8_t output[16]) {
// 预先保存原始消息总比特数,补位长度不算在内
uint64_t bits = ctx->count;

// 补位,md5 最后一个块大小必须为 56 字节,448 位,这样后续写入长度后才是 64 字节,512 位,才能进行最终的 md5 计算
uint8_t padding[64] = {0x80}; // 首位补位 1,其余补 0
size_t buffer_idx = (ctx->count / 8) % 64;
size_t pad_len = (buffer_idx < 56) ? (56 - buffer_idx) : (120 - buffer_idx);
// 更新补位数据到缓存
md5_update(ctx, padding, pad_len);

// 将 64 位原始消息比特总长度转换为小端 8 字节数据
uint8_t len_buf[8];
for(int i = 0; i < 8; ++i) {
len_buf[i] = (bits >> (i * 8)) & 0xFF;
}
// 将总比特长度更新到 md5 消息末尾,并重新计算 md5
md5_update(ctx, len_buf, 8);

// 生成 md5
for(int i = 0; i < 4; ++i) {
output[i * 4] = ctx->state[i] & 0xFF;
output[i * 4 + 1] = (ctx->state[i] >> 8) & 0xFF;
output[i * 4 + 2] = (ctx->state[i] >> 16) & 0xFF;
output[i * 4 + 3] = (ctx->state[i] >> 24) & 0xFF;
}
}

/**
* @brief 将 16 字节二进制串转换为 32 字节十六进制字符串
* @param digest 原始 16 字节二进制串
* @param hex_str 转换后 32 字节十六进制字符串
*/
void md5_to_hex(const uint8_t digest[16], char hex_str[33]) {
const char *hex = "0123456789abcdef";
for(int i = 0; i < 16; ++i) {
hex_str[i * 2] = hex[digest[i] >> 4];
hex_str[i * 2 + 1] = hex[digest[i] & 0x0F];
}
hex_str[32] = '\0';
}

/**
* @brief 单元测试
*/
static void unit_test() {
typedef struct {
const char *input;
const char *expect;
} MD5_Case;

MD5_Case test_cases[] = {
{"", "d41d8cd98f00b204e9800998ecf8427e"},
{"Hello, World!", "65a8e27d8879283831b664bd8b7f0ad4"},
{"a", "0cc175b9c0f1b6a831c399e269772661"},
{"123456", "e10adc3949ba59abbe56e057f20f883e"},
{"Hello MD5", "e5dadf6524624f79c3127e247f04b548"},
{"!@#$%^&*()", "05b28d17a7b6e7024b6e5d8cc43a8bf7"},
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "5eca9bd3eb07c006cd43ae48dfde7fd3"},
{"你好,世界", "dbefd3ada018615b35588a01e216ae6e"}
};

MD5_CTX ctx;
uint8_t digest[16];
char hex_result[33];

for(int i = 0; i < sizeof(test_cases) / sizeof(MD5_Case); ++i) {
printf("Input: %s\n", test_cases[i].input);

md5_init(&ctx);
md5_update(&ctx, (const uint8_t *)test_cases[i].input, strlen(test_cases[i].input));
md5_final(&ctx, digest);
md5_to_hex(digest, hex_result);

printf("MD5 Value: %s\n", hex_result);
printf("Expected: %s\n", test_cases[i].expect);
printf("Result: %s\n", (strcmp(hex_result, test_cases[i].expect) == 0 ? "Pass" : "Error"));
}
}

int main() {

printf("=========== Unit Test ===========\n");
unit_test();

return 0;
}

Buy me a coffee ? :)