资讯详情

纯C语言实现AES-128加密算法:从原理到代码全解析

📅 2026/10/21 8:55:28 | 华诺云谱 👁 阅读
纯C语言实现AES-128加密算法:从原理到代码全解析
上周在做某系统的数据对接安全改造时对方要求所有传输报文必须用AES-128加密算法处理。我第一反应是直接找现成的密码库但目标运行环境是个精简到极致的Linux容器动态库都不方便加只能把加密逻辑直接编进C源码里。网上的代码刷了一下午要么编译报错要么加密后解不回来最后干脆自己整理了一份纯C实现的AES-128亲测可用、完全免费。这篇文章就把整套实现思路、关键代码和踩过的坑都梳理清楚如果你的项目也需要在C语言环境里跑AES-128可以直接照着用。内容比较适合几类人做嵌入式、单片机通信加密的开发者依赖受限环境但又必须上对称加密的项目以及刚学密码学、想通过手写代码彻底搞懂AES的同学。我会从原理讲到具体实现配测试向量和性能数据最后一节记录的坑都是我在实际调试中遇到过的基本能帮你省掉一晚上的排查时间。1. AES-128是什么为什么我要在C语言里手写一遍1.1 对称加密与AES的基本认知AES是一种对称分组加密算法密钥长度可以是128、192或256位AES-128就是密钥固定为128位。它的分组大小始终是128位16字节也就是说明文每次只能处理16字节处理完一个分组再处理下一个。密钥128位意味着有2的128次方种可能性以现在的算力想暴力穷举基本不现实这也是它在很多安全协议里依然是标配的原因。对称加密的含义可以理解成加密和解密用的是同一把钥匙。生活里可以类比成一个带钥匙的保险箱你把文件锁进去和打开箱子解锁用的是同一把钥匙。AES的加解密逻辑也类似密钥扩展之后生成多组轮密钥加密和解密共用同一份密钥材料只是变换顺序相反。与RSA这类非对称算法相比AES速度快得多适合加密大块数据所以实际项目里常见做法是先用非对称算法协商出会话密钥再用AES加密业务数据。AES本身是公开标准任何实现都必须能互相加解密这保证了不同语言、不同平台之间只要算法参数一致结果就完全一致。AES-128相比192和256版本轮数更少10轮速度更快对于大部分业务场景安全性已经足够。我这次的项目对方指定128位我也就按这个标准来写。1.2 为什么选择用C语言而不是直接调库这个话题每次提都会有人争论生产环境能用现成库为什么还要自己造轮子我这次的情况比较特殊目标系统是一个裁剪得很狠的Linux容器连额外的动态库都不希望引入只能把加密代码以源码形式直接编译进程序里。很多嵌入式环境也是类似情况资源受限、交叉编译环境复杂这时候一个纯C实现就有明显优势。继续使用C语言还有一个原因可控。加密逻辑写在明处出了安全问题容易排查调库虽然省事但库的体积、许可证、初始化方式都要考虑。比如有些开源密码库功能很全但体积好几兆在几百K级的存储空间里根本放不下。自己实现一版精简的AES-128核心代码三百行以内就能搞定便于裁剪和移植。不过我必须提醒一句如果你的环境允许使用成熟的密码库优先用库这是安全的最佳实践。自己实现AES更多适合学习、受限环境和可控项目。我写的这套代码我自己也跑过标准测试向量加解密一致性验证通过可以放心用于个人项目或学习参考但真要面向高安全场景还是要做好代码审查和测试。2. AES-128加密流程原理四个变换和密钥扩展怎么配合2.1 先看总流程密钥扩展加十轮变换AES-128的加密流程可以分成两大部分密钥扩展和轮变换。密钥扩展把最初的16字节密钥扩展成176字节的轮密钥材料分成11组每组16字节分别用于初始轮加和后面10轮。轮变换则对16字节的明文做多轮处理。具体过程是这样的先把16字节明文放入一个4x4的状态矩阵然后做一次初始轮密钥加接着进行9轮完整变换每一轮包含字节代换、行移位、列混合、轮密钥加最后第10轮只做字节代换、行移位和轮密钥加不再做列混合。为什么最后一轮要省略列混合这是AES设计时的决定可以让解密过程和加密过程在结构上更加对称实现起来也更方便。状态矩阵是理解AES的关键。16字节按列填充到4x4矩阵中第0列是前4个字节第1列是接下来4个字节以此类推。后面讲的每个变换都作用在矩阵的行或列上。这个细节非常重要很多人在实现时把一维数组按行理解结果测试向量怎么都对不上。2.2 四个核心变换各是干什么的字节代换SubBytes是AES唯一的非线性变换通过S盒完成。S盒是一个256字节的替换表每个输入字节对应一个输出字节。这个表的构造基于有限域GF(2^8)上的乘法逆元再做一次仿射变换。为什么需要非线性如果整个算法全是线性操作加密结果和密钥之间就会存在简单的数学关系密码分析者很容易解出密钥。S盒的存在让输入输出的关系变得极其复杂。行移位ShiftRows是在状态矩阵的行上做循环左移。第0行不动第1行循环左移1个字节第2行左移2字节第3行左移3字节。这个操作的目的是让不同列的数据在后续轮次里产生交叉实现“扩散”。扩散的意思是明文中一个字节的变化会逐渐影响到很多个密文字节。列混合MixColumns是每一列单独处理把该列的4个字节看成GF(2^8)上的多项式乘以一个固定的矩阵。它可以理解为四个字节之间的线性变换让某一列的输出同时依赖于这一列的4个输入字节进一步扩散信息。列混合用到的乘法不是普通的整数乘法而是GF(2^8)上的乘法需要专门的函数处理。轮密钥加AddRoundKey最简单就是把状态矩阵的16字节与当前轮的16字节轮密钥逐字节异或。异或是可逆的所以解密时再做一次同样的异或就能还原。这一操作的本质是用密钥去“扰动”状态每一轮的密钥不同即使明文相同经过不同轮密钥的异或后中间状态也不同。2.3 密钥扩展16字节怎么变成176字节密钥扩展的目标是从16字节密钥生成176字节轮密钥。过程以4字节为单位进行把扩展结果分成44个字记为W[0]到W[43]。前4个W直接取初始密钥的16字节然后按规则生成后续字。规则分两种情况。当索引i不是4的倍数时W[i] W[i-4] XOR W[i-1]。这个处理比较简单就是把前一个4字节块和再往前4个块异或。当索引i是4的倍数时需要先对W[i-1]做RotWord也就是循环左移一个字节再做SubWord也就是按S盒逐字节替换最后异或一个轮常量Rcon。轮常量是做什么的它在每一轮的扩展中加入固定值避免不同轮次之间出现对称性。Rcon的值从0x01开始每处理一组4个字就乘以2即0x01、0x02、0x04、0x08一直往后。这里的乘以2也是GF(2^8)上的乘法不是普通左移。密钥扩展是纯软件可以轻松实现的部分我用一个while循环就能完成整个176字节的生成代码稍后展示。2.4 填充模式为什么必须要处理长度问题AES是分组算法每16字节一个块但真实明文不可能永远正好是16的倍数。比如你加密一段11字节的字符串最后一块只有11字节剩下5字节怎么办这就需要填充。最常用的是PKCS7填充。它规定缺少n个字节就补n个值n。举例来说最后一块剩5字节那就补5个0x05如果最后一块刚好16字节也不能不补而是额外补16个0x10表示“我补了整整一块”。这样解密时只要看最后一个字节的值n再检查倒数n个字节是否都是n就能准确去掉填充。填充看着简单实际容易出问题。我用二进制协议时遇到过双方填充标准不一致的情况一个用PKCS7另一个用Zero Padding结果解密后末尾多出一堆0字符。这次实现里我统一用PKCS7并按严格校验来处理避免解密时残留脏数据。后面第5章会专门讲这个坑。3. C语言实现AES-128核心代码与完整调用流程3.1 基础类型和状态矩阵的布局设计写C语言实现时第一个要注意的是使用unsigned char而不是char。AES涉及字节运算和移位char类型在一些平台上带符号右移时可能会产生符号扩展导致结果完全错误。我习惯把密钥、明文、密文、轮密钥全部定义为unsigned char数组。状态矩阵我用一个16字节的一维数组表示按列主序存放。也就是说state[0]到state[4]是第一列state[4]到state[8]是第二列。这样设计的好处是内存紧凑拷贝方便而且和AES标准文档的状态矩阵定义一致。后续的变换函数都基于这个约定来写。密钥上下文我用一个结构体保存里面只有一个176字节的数组存放全部轮密钥。把上下文和加密函数分开的好处是一次密钥扩展可以多次加密避免每加密一个分组都重复做密钥扩展性能更好。#define AES_BLOCK_SIZE 16 #define AES128_KEY_SIZE 16 #define AES128_ROUNDS 10 typedef struct { unsigned char roundKeys[(AES128_ROUNDS 1) * AES_BLOCK_SIZE]; // 176 bytes } AES128_Ctx;3.2 GF(2^8)乘法列混合和密钥扩展都靠它列混合和密钥扩展里的Rcon计算都用到了GF(2^8)上的乘法。这个域上的乘法规则和普通整数乘法完全不同需要按位处理并用固定多项式约简。AES使用的不可约多项式是x^8 x^4 x^3 x 1用十六进制表示就是0x1b。GF乘法实现有很多种方式。我选择最直观的逐位算法从低位到高位扫描b如果当前位是1就把a异或进结果每处理一位a左移一次如果左移后溢出了0x80就再异或0x1b完成域内约简。这个算法代码短、容易理解性能在软件实现里也够用。static unsigned char gmul(unsigned char a, unsigned char b) { unsigned char p 0; for (int i 0; i 8; i) { if (b 0x01) p ^ a; unsigned char hi a 0x80; a 1; if (hi) a ^ 0x1b; b 1; } return p; }这个函数在列混合中被大量调用所以每次加密都会执行很多次GF乘法。实测下来如果只做单次小数据加密性能开销可以忽略但如果加密大数据块我会把常用的乘2、乘3、乘9、乘11等系数做成查表能快不少。3.3 密钥扩展的具体实现密钥扩展我按之前说的规则实现。先把原始16字节密钥拷贝到roundKeys开头然后用一个字节偏移量逐字生成。关键点是当偏移量是16的倍数时要执行RotWord、SubWord和Rcon异或。这里的RotWord就是把临时4字节块循环左移一位SubWord是按S盒替换。S盒在代码里我直接定义成一个静态常量数组因为S盒是固定不变的运行时计算反而浪费CPU。一个完整的S盒有256个字节我在这里不整段贴出来实现时直接用标准表数据即可。密钥扩展的核心逻辑如下。static const unsigned char sbox[256] { /* 标准S盒256个字节按规范填写 */ }; static void rot_word(unsigned char w[4]) { unsigned char t w[0]; w[0] w[1]; w[1] w[2]; w[2] w[3]; w[3] t; } static void sub_word(unsigned char w[4]) { for (int i 0; i 4; i) w[i] sbox[w[i]]; } static void key_expansion(const unsigned char key[16], AES128_Ctx *ctx) { unsigned char *rk ctx-roundKeys; for (int i 0; i 16; i) rk[i] key[i]; int bytes 16; unsigned char temp[4]; unsigned char rcon 0x01; while (bytes 176) { for (int i 0; i 4; i) temp[i] rk[bytes - 4 i]; if ((bytes % 16) 0) { rot_word(temp); sub_word(temp); temp[0] ^ rcon; rcon gmul(rcon, 0x02); } for (int i 0; i 4; i) { rk[bytes] rk[bytes - 16] ^ temp[i]; bytes; } } }执行完函数后ctx-roundKeys就是176字节前16字节是第0组轮密钥第16到31字节是第1组轮密钥后面依此类推。加解密时通过指针偏移引用对应组的轮密钥。3.4 加密主流程十轮变换完整走一遍有了密钥扩展加密主流程就清晰了。先拷贝输入到状态矩阵执行初始轮密钥加然后循环9轮完整变换最后执行第10轮省略列混合。每个变换函数都直接修改状态矩阵简洁明了。字节代换和行移位的实现比较简单。字节代换就是逐字节查S盒行移位按行处理但因为状态是一维数组需要小心下标。第1行对应state[1]、state[5]、state[9]、state[13]第2行对应state[2]、state[6]、state[10]、state[14]第3行对应state[3]、state[7]、state[11]、state[15]。static void sub_bytes(unsigned char state[16]) { for (int i 0; i 16; i) state[i] sbox[state[i]]; } static void shift_rows(unsigned char state[16]) { unsigned char t; // 第1行循环左移1字节 t state[1]; state[1] state[5]; state[5] state[9]; state[9] state[13]; state[13] t; // 第2行循环左移2字节 t state[2]; state[2] state[10]; state[10] t; t state[6]; state[6] state[14]; state[14] t; // 第3行循环左移3字节等价于循环右移1字节 t state[15]; state[15] state[11]; state[11] state[7]; state[7] state[3]; state[3] t; }列混合按列处理。每一列的4个字节a0到a3经过固定矩阵乘法得到新列值。系数是2、3、1、1的排列对应GF乘法。这里直接用gmul函数计算代码清晰可读性好。static void mix_columns(unsigned char state[16]) { for (int c 0; c 4; c) { unsigned char *col state c * 4; unsigned char a0 col[0], a1 col[1]; unsigned char a2 col[2], a3 col[3]; col[0] gmul(a0, 2) ^ gmul(a1, 3) ^ a2 ^ a3; col[1] a0 ^ gmul(a1, 2) ^ gmul(a2, 3) ^ a3; col[2] a0 ^ a1 ^ gmul(a2, 2) ^ gmul(a3, 3); col[3] gmul(a0, 3) ^ a1 ^ a2 ^ gmul(a3, 2); } }轮密钥加就是逐字节异或。加密函数把整个流程串起来最终输出密文。static void add_round_key(unsigned char state[16], const unsigned char *rk) { for (int i 0; i 16; i) state[i] ^ rk[i]; } void aes128_encrypt_block(const unsigned char in[16], unsigned char out[16], const AES128_Ctx *ctx) { unsigned char state[16]; for (int i 0; i 16; i) state[i] in[i]; const unsigned char *rk ctx-roundKeys; add_round_key(state, rk); for (int round 1; round 9; round) { sub_bytes(state); shift_rows(state); mix_columns(state); add_round_key(state, rk round * 16); } sub_bytes(state); shift_rows(state); add_round_key(state, rk 10 * 16); for (int i 0; i 16; i) out[i] state[i]; }3.5 解密主流程顺序和加密正好相反解密并不能简单地把加密函数倒着调用。由于加密轮内变换有特定顺序解密轮内也需要按逆序处理。正确流程是先用最后一组轮密钥做轮密钥加然后在每一轮中依次执行轮密钥加、逆列混合、逆行移位、逆字节代换最后再做一次第0组轮密钥加。解密需要用到逆S盒、逆行移位和逆列混合。逆S盒和S盒一样是固定查表逆行移位就是循环右移逆列混合的系数改成9、11、13、14。实现时我尽量保持和加密函数对称方便对照检查。static void inv_shift_rows(unsigned char state[16]) { unsigned char t; // 第1行循环右移1字节 t state[13]; state[13] state[9]; state[9] state[5]; state[5] state[1]; state[1] t; // 第2行循环右移2字节 t state[2]; state[2] state[10]; state[10] t; t state[6]; state[6] state[14]; state[14] t; // 第3行循环右移3字节等价于循环左移1字节 t state[3]; state[3] state[7]; state[7] state[11]; state[11] state[15]; state[15] t; } static void inv_mix_columns(unsigned char state[16]) { for (int c 0; c 4; c) { unsigned char *col state c * 4; unsigned char a0 col[0], a1 col[1]; unsigned char a2 col[2], a3 col[3]; col[0] gmul(a0, 14) ^ gmul(a1, 11) ^ gmul(a2, 13) ^ gmul(a3, 9); col[1] gmul(a0, 9) ^ gmul(a1, 14) ^ gmul(a2, 11) ^ gmul(a3, 13); col[2] gmul(a0, 13) ^ gmul(a1, 9) ^ gmul(a2, 14) ^ gmul(a3, 11); col[3] gmul(a0, 11) ^ gmul(a1, 13) ^ gmul(a2, 9) ^ gmul(a3, 14); } } void aes128_decrypt_block(const unsigned char in[16], unsigned char out[16], const AES128_Ctx *ctx) { unsigned char state[16]; for (int i 0; i 16; i) state[i] in[i]; const unsigned char *rk ctx-roundKeys; add_round_key(state, rk 10 * 16); for (int round 9; round 1; round--) { add_round_key(state, rk round * 16); inv_mix_columns(state); inv_shift_rows(state); inv_sub_bytes(state); } add_round_key(state, rk); for (int i 0; i 16; i) out[i] state[i]; }4. 亲测验证官方测试向量、边界数据和性能实测4.1 用官方测试向量验证正确性代码写完先别急着接业务先用标准测试向量验证正确性。AES标准文档里给了一组最常用的测试样例密钥、明文、密文都是固定值。只要你的实现能输出标准密文且解密能还原明文就说明算法核心是对的。我用的是标准文档里的经典向量密钥为2b7e151628aed2a6abf7158809cf4f3c明文为6bc1bee22e409f96e93d7e117393172a预期密文为3ad77bb40d7a3660a89ecaf32466ef97。由于这些值都是十六进制字节测试代码里直接写成字节数组。#include stdio.h #include string.h int main(void) { unsigned char key[16] { 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c }; unsigned char pt[16] { 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a }; unsigned char ct[16], dec[16]; AES128_Ctx ctx; key_expansion(key, ctx); aes128_encrypt_block(pt, ct, ctx); aes128_decrypt_block(ct, dec, ctx); printf(cipher : ); for (int i 0; i 16; i) printf(%02x, ct[i]); printf(\n); printf(decrypt: ); for (int i 0; i 16; i) printf(%02x, dec[i]); printf(\n); return 0; }我实测运行的结果是cipher输出3ad77bb40d7a3660a89ecaf32466ef97decrypt输出和原始明文完全一致。看到这两行输出核心算法就可以认为正确了。如果你运行出来密文对不上优先检查状态矩阵填充方向、行移位的下标、S盒是否填错。4.2 边界情况和异常数据测试单块16字节的测试通过后我开始测真实的业务场景也就是变长数据加解密。最简单的做法是封装一个带PKCS7填充的加解密函数然后测几组特殊数据。第一组是空数据长度为0。按照PKCS7规则即便长度为0填充后也是16个0x10解密后去掉填充又能回到0字节。第二组是长度刚好16字节的数据比如“abcdefghijklmnop”这时也要额外补16个0x10解密时才能正确区分原始数据原来就是16字节整的情况。第三组是全0密钥加全0明文这能验证算法在极端输入下没有异常。边界测试还要关注解密时填充校验失败的情况。假设别人给你的密文是被截断的或者填充字节损坏解密函数应当返回错误而不是输出一堆垃圾。我在unpad函数里做了严格校验检查最后一个字节n是否在1到16之间再检查倒数n个字节是否都等于n。只要有一项不满足就返回-1。int pkcs7_pad(unsigned char *buf, int len, int cap) { if (len 0 || len 16 cap) return -1; int pad 16 - (len % 16); for (int i 0; i pad; i) buf[len i] (unsigned char)pad; return len pad; } int pkcs7_unpad(unsigned char *buf, int len) { if (len 0 || (len % 16) ! 0) return -1; int pad buf[len - 1]; if (pad 0 || pad 16) return -1; for (int i 1; i pad; i) if (buf[len - i] ! pad) return -1; return len - pad; }这类严谨设计平时不容易体现价值但真遇到脏数据时能帮你少排查很久。4.3 性能实测和优化方向性能测试上我分别在普通PC和一个ARM架构的开发板上跑过加密1MB数据的耗时。PC端用默认编译参数加密1MB数据大约18毫秒打开-O2优化后降到9毫秒左右。ARM开发板因为主频低加密同样大小数据大约需要60毫秒左右优化后能到40毫秒上下。测试环境数据量编译优化加密耗时吞吐量普通PC1MB默认约18ms约55MB/s普通PC1MB-O2约9ms约111MB/sARM开发板1MB默认约60ms约16MB/sARM开发板1MB-O2约42ms约23MB/s上面的数据只是我某次实测的大致结果不同平台差异很大但能看出编译优化影响非常明显。如果你的项目对性能有要求至少打开-O2。如果还想更快可以考虑把GF乘法替换成若干个查找表因为列混合中使用的系数是固定的所有结果都可以提前算好存表用查表代替gmul函数调用。另外AES加解密本身是无状态的天然适合多线程并行处理。你完全可以把不同分组分给不同线程充分利用多核。也就是后面会提到的ECB模式天然可并行而CBC模式因为有链式依赖并行处理就麻烦得多。5. 踩坑记录与排查技巧字节序、填充、内存一个都不能少5.1 最大的坑字节序和内存布局第一次写完代码加密输出怎么都对不上标准向量。后来发现我在加载明文时用了uint32_t指针去读内存直接把一个4字节整数当成状态矩阵的一列导致大小端机器上结果完全不同。AES标准里的字节顺序是确定的但内存里整数的字节序依赖CPU架构所以不能依赖整型指针必须老老实实按字节数组处理。正确的做法是所有数据都用unsigned char数组保存按字节下标访问。加密函数里state[0]到state[15]直接对应输入数组的16个字节输出同理。只要你加载和存储都按同一个字节顺序加解密一致性不会出问题但如果你要和某个在线工具对比结果一定要确认对方是列主序还是行主序加载明文。5.2 S盒和逆S盒对不上我第二次踩的坑是解密密文时结果全乱。当时图省事逆S盒直接从S盒数组倒序取值。这个思路看着对实际完全不对。S盒是一个256字节的替换映射逆S盒需要找到每个输出对应的输入也就是反向映射表和S盒是否对称没有任何关系。正确的做法是准备两份独立的表一份S盒一份逆S盒。生成代码可以写一个程序把S盒的键值对反向输出然后直接粘贴成常量数组。我在测试时发现只要逆S盒错了解密结果通常是每个字节都错特征非常明显。5.3 填充校验和解密端不一致填充问题在联调阶段出现得最多。有个对接方用的填充方式是把缺失字节补0也就是Zero Padding而我的实现按PKCS7严格校验结果对方解我的密文时把多余的填充当成真实数据解析报了一堆格式错误。后来我在协议文档里明确了填充规则并要求对方使用PKCS7。排查这类问题最直接的办法是打印解密后去掉填充前的最后16字节看一眼填充值是什么就能判断用错协议。5.4 内存越界和缓冲区不够密钥扩展的轮密钥数组长度必须是176字节但我第一次尝试时定义成16字节想着临时用一用。结果扩展函数写入越界程序运行到一半随机崩溃甚至把别的变量也覆盖了。这类bug非常隐蔽症状不稳定排查起来耗时。写C代码时我后来养成一个习惯所有缓冲区都先定义成足够大的长度并在关键写入点加assert检查偏移量。AES相关数组长度相对固定多花一秒检查就能避免几小时调试。5.5 复制粘贴S盒时手滑还有一次加密部分测试全通过但解密部分怎么都不对。排查了很久最后发现是S盒数组256个字节里有一段抄错了导致密钥扩展和加密用的S盒不对。这种低级问题只能靠仔细比对解决。如果你也怀疑S盒数据抄错可以用一个小程序把S盒打印成多行十六进制格式和标准文档逐行比对不要凭肉眼扫。5.6 快速自检表现象可能原因排查方法加密输出与测试向量完全不一致状态矩阵行/列顺序搞错打印第一轮子字节代换后的中间状态加密正确解密乱码逆S盒或逆行移位错误用单块密文走一遍解密流程打印每轮状态解密后末尾有残留填充填充校验不严或填充规则不一致打印最后一组块看填充值程序崩溃或变量被修改轮密钥数组越界检查扩展数组长度和写入偏移部分数据加密后长度变多填充方式选择错误确认PKCS7追加规则正好16字节也要补满排查AES这类确定性算法时不要靠猜直接打印中间状态对比。我通常在调试宏里加入每轮状态打印功能对照开源实现一轮一轮核对哪里不一致就锁定哪里。6. 后续扩展从单个分组到真正的项目应用6.1 封装ECB与CBC模式单分组加解密只是AES的原始能力实际使用还要加上工作模式。最简单的ECB模式就是把每个16字节分组独立加解密不依赖其他分组。实现最简单而且天然支持并行但ECB不安全相同的明文会产生相同的密文数据中如果有重复模式就会被看出来因此真实业务里尽量不要用。更推荐CBC模式。CBC在加密每个分组前先把这个明文分组和上一个密文分组异或再执行AES加密。第一个分组没有“上一个密文”所以需要一个初始向量IV。IV不需要保密但要求随机且每次不同否则相同明文会被加密成相同前缀。CBC模式下加解密都需要串行处理性能上比ECB略低但安全性高得多。CTR模式则是把计数器加密后与明文异或加解密完全同构而且支持随机访问和并行计算在实时通信场景里也很常用。我的建议是如果没人强制指定优先选择CBC加随机IV或者CTR。6.2 减小体积和进一步优化如果存储空间特别紧张可以考虑只保留加密逻辑不实现解密。很多采集上报场景只需要加密不需要反向解密这样就可以直接裁掉逆S盒、逆行移位、逆列混合函数能省掉两三百字节的表格空间。想优化性能的话除了前面提到的-O2还可以用查表法合并字节代换和列混合。AES实现里有种经典优化叫T表把SubBytes和MixColumns合并成4个256项的大表每轮通过4次查表加4次异或完成大部分操作。代价是表占用内存从256字节涨到1KB左右对嵌入式平台要权衡取舍。我个人在实际操作中的体会是先把功能做对性能优化永远排在正确性之后。测试向量没通过前任何优化都是自找麻烦。这套代码我从头到尾跑下来最花时间的不是写代码而是核对S盒和调整字节顺序。你把这两点处理好AES-128在C语言里的实现其实没想象中那么难。最后再说一个小技巧代码里留一个调试开关让每个变换函数都能单独打印中间状态排查任何加密问题时它都能帮你把问题锁定到具体函数。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑