news 2026/7/18 5:21:13

C语言手搓AES-128:从原理到实现的嵌入式加密实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言手搓AES-128:从原理到实现的嵌入式加密实战

1. 项目概述:为什么用C语言手搓AES?

如果你是一名嵌入式工程师、安全领域的开发者,或者单纯是一个对“加密”这件事感到好奇的C语言爱好者,那么亲手用C语言实现一遍AES加密算法,绝对是一个能让你功力大增的“硬核”项目。这不仅仅是调用一个库函数那么简单,而是深入到算法的心脏,去理解每一个字节是如何在数学变换中“起舞”,最终变成一堆看似无意义的密文。

AES,全称高级加密标准,早已成为我们数字生活的基石。从HTTPS的加密连接,到手机App的本地数据保护,再到加密U盘,背后几乎都有它的身影。市面上成熟的库很多,比如OpenSSL里的AES实现已经过千锤百炼。那我们为什么还要自己造轮子?原因很直接:理解、控制和移植。当你为一个资源受限的MCU开发固件,或者需要在一个特殊的、无法使用标准库的环境下实现加密时,自己写的、精简的C代码就是唯一的选择。你能清楚地知道每一行代码在做什么,能精确控制内存和CPU的使用,出了问题也能顺着逻辑一步步调试。

这个项目,就是带你从零开始,不依赖任何外部加密库,只用纯C语言,实现一个完整的AES-128加密和解密流程。我们会从最基础的S盒变换开始,一步步搭建起加密的“流水线”。我会分享我在实现过程中踩过的坑,比如字节序的处理、状态矩阵的变换顺序,以及如何写出既高效又易于理解的代码。最终,你将得到一个可以独立编译运行的C程序,输入一段明文和密钥,它就能输出标准的AES密文,并且能正确解密回来。

2. AES算法核心原理与C语言映射

在动手写代码之前,我们必须先搞清楚AES到底在玩什么“魔术”。AES是一种对称分组加密算法,简单说就是加密和解密用同一把钥匙。我们以最常用的AES-128为例,它的密钥长度是128位(16字节),每次处理一个128位(16字节)的数据块。

AES的核心操作都在一个4x4的字节矩阵上,这个矩阵叫做“状态”。加密过程,就是对这个状态矩阵进行多轮的、可逆的变换。每一轮操作都包含四个步骤,而C语言实现,本质上就是为这些步骤编写精确的算术和逻辑运算。

2.1 状态矩阵与字节存储

在C语言里,我们如何表示这个4x4的状态矩阵?最直观的方式是用一个二维数组uint8_t state[4][4]。但是,AES的输入数据是顺序的16字节数组。这里就涉及到第一个关键点:填充顺序

假设我们的输入明文字节数组是in[16] = {0x00, 0x01, 0x02, ..., 0x0f}。在AES标准中,这个数组是按列优先的顺序填充到状态矩阵中的。也就是说:

  • state[0][0] = in[0],state[1][0] = in[1],state[2][0] = in[2],state[3][0] = in[3](第一列)
  • state[0][1] = in[4],state[1][1] = in[5]... 以此类推。

注意:这个顺序非常关键!很多自己实现的AES加解密结果对不上,第一步查这里就对了。我习惯在代码里写一个专门的state_to_arrayarray_to_state函数来处理这个转换,并在函数开头加上明确的注释,避免后面自己都绕晕。

2.2 轮密钥加:最简单的异或运算

这是每一轮的第一个也是最后一个步骤,操作很简单:将状态矩阵的每一个字节与当前轮的轮密钥的对应字节进行异或。在C语言中,这就是一个双层循环里的state[r][c] ^= round_key[r][c]

轮密钥是从初始的主密钥通过一个叫做“密钥扩展”的算法派生出来的。每一轮都需要一个不同的128位轮密钥。因此,密钥扩展算法的正确实现,是整个AES正确性的基础。

2.3 字节代换:查表法的艺术

字节代换是一个非线性变换,它为加密提供了“混淆”特性。每个字节通过一个固定的S盒进行替换。这个S盒是一个256字节的查找表。

在C语言实现中,我们绝对不应该在运行时去计算S盒的替换值(虽然它有数学公式),那样太慢了。标准做法是预定义S盒和逆S盒为静态常量数组

static const uint8_t sbox[256] = { 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, // ... 省略剩余内容 }; static const uint8_t inv_sbox[256] = { 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, // ... 省略剩余内容 };

这样,字节代换操作就变成了极其高效的数组查表:state[r][c] = sbox[state[r][c]]。这是用空间换时间的经典案例,在嵌入式环境下,这256字节的ROM占用通常是完全可以接受的。

2.4 行移位:字节位置的“舞蹈”

行移位操作对状态矩阵的每一行进行循环左移。第0行不移位,第1行左移1个字节,第2行左移2个字节,第3行左移3个字节。

在C语言中,这需要一些技巧。你不能简单地用memmove,因为它是行内的循环移位。我通常用一个临时变量来手动进行交换。例如,对于第1行(索引为1):

uint8_t temp = state[1][0]; state[1][0] = state[1][1]; state[1][1] = state[1][2]; state[1][2] = state[1][3]; state[1][3] = temp;

虽然看起来有点“笨”,但这样写非常清晰,避免了使用复杂的模运算,在大多数编译器上也能生成高效的代码。

2.5 列混合:有限域上的矩阵乘法

这是AES中最复杂的一步,它提供了“扩散”特性。它把状态矩阵的每一列视为一个系数在GF(2^8)有限域上的多项式,并与一个固定的多项式进行乘法运算,然后模一个不可约多项式。

对于C语言实现,我们同样采用查表法来优化。列混合可以表示为状态矩阵与一个固定矩阵的乘法。通过预先计算好一个“混合列查表”,我们可以将复杂的有限域乘法和加法,转化为几次查表和异或。标准实现中,会定义gm2gm3gm1等表,分别对应乘以2、乘以3等操作。

实操心得:初次实现时,我建议先写出完整的有限域乘法函数gf_multiply,并基于它实现列混合,以验证逻辑正确性。在确认算法正确后,再将其替换为效率高得多的查表法。这能帮你更好地理解底层原理,调试时也更有底气。

3. 密钥扩展算法的C语言实现

密钥扩展是AES的发动机,它为每一轮加密生成所需的“燃料”。对于AES-128,我们需要从16字节的主密钥,扩展出11个轮密钥(第0轮为初始密钥,外加10轮加密每轮一个),总共176字节。

3.1 扩展流程详解

扩展算法以4字节(一个字)为单位进行。前4个字(W[0]-W[3])就是原始密钥。之后的每一个字 W[i] 都依赖于前面的字。

  • 如果i不是4的倍数,那么W[i] = W[i-4] ^ W[i-1]
  • 如果i是4的倍数,那就复杂一些:先将W[i-1]循环左移一个字节,然后对每个字节进行S盒替换,接着再与一个轮常量Rcon[i/4]进行异或,最后再与W[i-4]异或。

轮常量Rcon是一个固定的数组,用于消除对称性。在C语言中,我们这样定义:

static const uint8_t Rcon[11] = { 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 };

注意,Rcon[0]在计算中并不会被使用,我们通常从Rcon[1]开始用。

3.2 C语言实现代码与解析

下面是一个清晰的密钥扩展函数实现:

void key_expansion(const uint8_t *key, uint8_t *round_keys) { uint8_t temp[4]; int i; // 第一个轮密钥就是原始密钥 for (i = 0; i < 16; i++) { round_keys[i] = key[i]; } // 生成后续的轮密钥 for (i = 4; i < 44; i++) { // AES-128共需要44个字(11*128位/32位) // 1. 将前一个字存入temp for (int j = 0; j < 4; j++) { temp[j] = round_keys[(i-1)*4 + j]; } // 2. 如果i是4的倍数,进行特殊变换 if (i % 4 == 0) { // 循环左移一个字节 uint8_t t = temp[0]; temp[0] = temp[1]; temp[1] = temp[2]; temp[2] = temp[3]; temp[3] = t; // 字节代换(S盒) for (int j = 0; j < 4; j++) { temp[j] = sbox[temp[j]]; } // 与轮常量异或 temp[0] ^= Rcon[i/4]; } // 3. 生成新的字:W[i] = W[i-4] ^ temp for (int j = 0; j < 4; j++) { round_keys[i*4 + j] = round_keys[(i-4)*4 + j] ^ temp[j]; } } }

注意事项round_keys数组需要足够大,以容纳所有扩展后的密钥。对于AES-128,我们需要11 * 16 = 176字节。在函数外部,最好用uint8_t round_keys[176]这样的数组来声明,并确保其生命周期覆盖整个加解密过程。

4. 完整加密流程的C语言分步实现

有了状态矩阵操作和轮密钥,我们就可以组装完整的加密流程了。AES-128共有10轮加密,加上初始的轮密钥加,总共11轮操作。

4.1 加密函数框架

void aes_encrypt(const uint8_t *in, uint8_t *out, const uint8_t *round_keys) { uint8_t state[4][4]; int round; // 1. 将输入拷贝到状态矩阵(注意列优先顺序) array_to_state(in, state); // 2. 初始轮密钥加 add_round_key(state, &round_keys[0]); // 3. 进行前9轮标准轮函数 for (round = 1; round < 10; round++) { sub_bytes(state); shift_rows(state); mix_columns(state); // 注意:最后一轮没有列混合 add_round_key(state, &round_keys[round * 16]); } // 4. 最后一轮(无列混合) sub_bytes(state); shift_rows(state); add_round_key(state, &round_keys[10 * 16]); // 5. 将状态矩阵写回输出 state_to_array(state, out); }

4.2 核心操作函数的实现细节

让我们深入看看几个核心函数的具体实现。

add_round_key轮密钥加:

void add_round_key(uint8_t state[4][4], const uint8_t *round_key) { for (int r = 0; r < 4; r++) { for (int c = 0; c < 4; c++) { // 注意轮密钥也是按列优先顺序存储的 // 计算轮密钥中对应字节的索引 state[r][c] ^= round_key[c * 4 + r]; } } }

这里索引c * 4 + r是另一个容易出错的地方。因为我们的state[行][列],而轮密钥在内存中是连续存储的,第一列的前4个字节对应state[0][0], state[1][0], state[2][0], state[3][0]

mix_columns列混合(查表法优化版):这是性能关键点。我们使用预先计算好的gm2gm3表。

// 预计算好的查表,用于列混合中乘以2和乘以3的操作 static const uint8_t gm2[256] = { /* ... 通过有限域计算生成的256字节表 ... */ }; static const uint8_t gm3[256] = { /* ... 通过有限域计算生成的256字节表 ... */ }; void mix_columns(uint8_t state[4][4]) { uint8_t t[4]; for (int c = 0; c < 4; c++) { // 对每一列操作 // 将当前列拷贝到临时数组,方便计算 for (int r = 0; r < 4; r++) { t[r] = state[r][c]; } // 列混合的矩阵乘法(查表优化版) state[0][c] = gm2[t[0]] ^ gm3[t[1]] ^ t[2] ^ t[3]; state[1][c] = t[0] ^ gm2[t[1]] ^ gm3[t[2]] ^ t[3]; state[2][c] = t[0] ^ t[1] ^ gm2[t[2]] ^ gm3[t[3]]; state[3][c] = gm3[t[0]] ^ t[1] ^ t[2] ^ gm2[t[3]]; } }

这个函数看起来有点复杂,但其实就是把有限域上的矩阵乘法展开,并用查表gm2gm3替代了乘法运算。gm2[x]的结果就等于在GF(2^8)上计算(x * 2) mod 0x11b

4.3 解密流程的逆向实现

解密是加密的逆过程,步骤相反,且使用的变换也是加密变换的逆变换。顺序大致为:逆向轮密钥加 -> 逆行移位 -> 逆字节代换 -> 逆向轮密钥加(对于第一轮)-> 逆列混合(中间轮)。需要注意的是,解密流程也可以优化为与加密类似的结构,但需要使用为解密预计算好的逆S盒、逆行移位和逆列混合表。

一个直观但非最优的解密实现如下:

void aes_decrypt(const uint8_t *in, uint8_t *out, const uint8_t *round_keys) { uint8_t state[4][4]; int round; array_to_state(in, state); // 初始轮(使用最后一轮密钥) add_round_key(state, &round_keys[10 * 16]); inv_shift_rows(state); inv_sub_bytes(state); // 中间9轮 for (round = 9; round > 0; round--) { add_round_key(state, &round_keys[round * 16]); inv_mix_columns(state); // 注意:解密需要逆列混合 inv_shift_rows(state); inv_sub_bytes(state); } // 最终轮密钥加(使用初始密钥) add_round_key(state, &round_keys[0]); state_to_array(state, out); }

inv_mix_columns的实现同样可以采用查表法,但需要一套不同的预计算表(对应乘以0x0e, 0x0b, 0x0d, 0x09的矩阵)。为了代码简洁和可维护性,我建议将加密和解密的查表分别定义。

5. 项目集成、测试与性能优化

实现所有函数后,我们需要一个main函数把它们串起来,并进行严格的测试。

5.1 完整的可运行示例与测试向量

使用标准测试向量是验证实现正确性的黄金法则。例如,NIST官方提供的AES-128测试向量:

  • 密钥:2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c
  • 明文:32 43 f6 a8 88 5a 30 8d 31 31 98 a2 e0 37 07 34
  • 密文:39 25 84 1d 02 dc 09 fb dc 11 85 97 19 6a 0b 32

我们在main函数中调用:

int main() { uint8_t key[16] = {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c}; uint8_t plaintext[16] = {0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, 0x34}; uint8_t ciphertext[16]; uint8_t decrypted[16]; uint8_t round_keys[176]; // 1. 密钥扩展 key_expansion(key, round_keys); // 2. 加密 aes_encrypt(plaintext, ciphertext, round_keys); printf("密文:"); print_hex(ciphertext, 16); // 应与标准密文一致 // 3. 解密 aes_decrypt(ciphertext, decrypted, round_keys); printf("解密后明文:"); print_hex(decrypted, 16); // 应与原始明文一致 return 0; }

如果输出结果与标准测试向量完全一致,那么恭喜你,你的AES实现核心部分是正确的!

5.2 从ECB到更安全的模式

我们上面实现的是最基本的ECB模式,即每个16字节块独立加密。但ECB模式有一个致命弱点:对于相同的明文块,总是产生相同的密文块。这可能导致模式泄露信息。

在实际项目中,我们绝不能直接使用ECB模式加密有意义的数据。必须使用更安全的模式,如CBC(密码块链接)或CTR(计数器)模式。这些模式需要一个额外的“初始化向量”,并引入了块之间的依赖性。

以CBC模式为例,它的加密过程可以很容易地基于我们的ECB函数构建:

void aes_cbc_encrypt(const uint8_t *in, uint8_t *out, size_t len, const uint8_t *key, const uint8_t *iv) { uint8_t round_keys[176]; uint8_t block[16]; uint8_t feedback[16]; // 保存上一个密文块,用于与当前明文异或 key_expansion(key, round_keys); memcpy(feedback, iv, 16); // 用IV初始化反馈 for (size_t i = 0; i < len; i += 16) { // 1. 当前明文块与上一个密文块(或IV)异或 for (int j = 0; j < 16; j++) { block[j] = in[i + j] ^ feedback[j]; } // 2. 用ECB加密异或后的结果 aes_encrypt(block, &out[i], round_keys); // 3. 将本次密文块保存为下一次的反馈 memcpy(feedback, &out[i], 16); } }

解密过程则是反向操作。切记,IV必须是随机的且不可预测的,每次加密都应使用新的IV。

5.3 性能优化与内存考量

对于嵌入式或高性能场景,我们可以进一步优化:

  1. 将轮密钥展开:在密钥扩展后,直接将轮密钥转换为uint8_t round_key[11][4][4]的三维数组形式,这样在add_round_key中可以直接用state[r][c] ^= round_key[round][r][c],省去索引计算。
  2. 合并查表:极致的优化会将sub_bytesshift_rowsmix_columns合并成基于表的操作,但这会显著增加代码复杂性和内存占用(需要4KB或更大的表)。这通常只在x86等桌面平台由专业库(如AES-NI指令集)完成。
  3. 减少函数调用开销:对于性能极其敏感的场合,可以将加密轮循环展开,并将所有操作内联在一个函数里。但这会牺牲代码的可读性和可维护性。

在资源受限的8位或16位MCU上,我们的查表法实现(约占用1KB的S盒和逆S盒,加上列混合表)通常是一个很好的平衡点。务必根据你的目标平台(RAM/ROM大小,CPU频率)做出选择。

5.4 常见问题与调试技巧实录

自己实现加密算法,调试是最大的挑战。以下是我踩过坑后总结的排查清单:

现象可能原因排查方法
加密结果与标准测试向量对不上1. 状态矩阵填充顺序错误
2. 轮密钥索引计算错误
3. S盒数据错误
1. 单步调试,对比array_to_state后状态矩阵的值。
2. 打印每一轮开始前的状态矩阵和轮密钥,与已知正确中间值对比。
3. 校验S盒数组的第一个和最后几个值是否正确。
解密后无法还原明文1. 解密流程步骤顺序错误
2. 逆变换函数实现有误(如逆列混合)
3. 轮密钥使用顺序错误
1. 确保解密步骤严格是加密的逆序,且逆变换正确。
2. 单独测试逆变换函数:用已知数据加密后立即解密,看是否能还原。
3. 确认解密时轮密钥是从最后一轮开始往前用的。
在多块数据加密时,只有第一块正确工作模式问题,可能错误地复用了ECB模式,或者在CBC模式下IV处理错误。检查是否在每块加密前正确进行了异或操作(CBC模式),并确保IV的传递和使用正确。
在特定平台(如ARM)上结果不对字节序问题。你的代码可能隐式依赖了x86的小端序。检查所有从字节数组到更大数据类型(如uint32_t)的转换,确保是显式按字节构造,而不是直接进行类型强转或内存拷贝。

一个非常实用的调试方法是中间值对比。在网上可以找到一些提供AES中间步骤详细值的测试网站或文献。在加密第一轮后,将你的状态矩阵、轮密钥与之对比,能快速定位问题出在哪一步。

最后,安全提醒:这个自实现的AES库适用于学习和特定嵌入式环境。在安全性要求极高的生产环境(如服务器、金融系统),请务必使用经过严格审计和广泛测试的专业库,如OpenSSL、mbed TLS等。它们经过了侧信道攻击防护、时序攻击防护等大量我们未考虑的加固。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/18 5:20:58

游戏中的线性插值

文章目录线性插值Mathf.Lerp控制灯光强度Vector3.Lerp物体的空间运动Color.Lerp颜色渐变线性插值 线性插值是指找到两个给定值之间的某个百分比值。 Lerpa(b−a)∗t&#xff0c;t∈[0,1]Lerp a (b - a) * t&#xff0c;t∈[0,1]Lerpa(b−a)∗t&#xff0c;t∈[0,1] 例如&am…

作者头像 李华
网站建设 2026/7/18 5:20:42

HarmonyOS7 手势交互页实战:点击、长按、拖拽 不只是会用,还要用得顺手

文章目录每个手势都要有明确反馈拖拽和捏合要注意收口手势冲突别靠猜完整代码写在最后手势交互最容易写成“能触发就行”。但真到页面里&#xff0c;点击、双击、长按、滑动、拖拽、捏合放在一起&#xff0c;重点就不是 API 名字&#xff0c;而是每个区域到底响应什么、状态怎么…

作者头像 李华
网站建设 2026/7/18 5:20:29

人形机器人从仓库到家庭:核心技术演进与商业化路径解析

1. 项目概述&#xff1a;从仓库到客厅的人形机器人浪潮最近几年&#xff0c;如果你关注科技新闻&#xff0c;会发现一个有趣的现象&#xff1a;人形机器人&#xff08;Humanoid Robots&#xff09;的新闻不再仅仅出现在实验室论文或科幻电影的宣传片里&#xff0c;而是越来越多…

作者头像 李华
网站建设 2026/7/18 5:20:11

PHP开发全栈指南:从基础到架构实战

1. PHP资源大全&#xff1a;从入门到精通的完整指南作为一名使用PHP开发超过8年的全栈工程师&#xff0c;我深知优质学习资源对程序员成长的重要性。今天要分享的这个"国外程序员收集整理的PHP资源大全"堪称PHP领域的宝藏库&#xff0c;它系统性地整理了从基础语法到…

作者头像 李华
网站建设 2026/7/18 5:20:08

SQL分组聚合原理:GROUP BY与HAVING执行顺序与实战避坑

1. 为什么 GROUP BY 和 HAVING 是 SQL 里最常被误解、也最常被写错的两个关键词刚入行那会儿&#xff0c;我带过几个实习生&#xff0c;几乎每个人在写完第一个带聚合的查询后&#xff0c;都会卡在同一个地方&#xff1a;想筛出“平均订单金额超过 500 的产品线”&#xff0c;结…

作者头像 李华