
简介面向嵌入式与金融安全场景的AES加密算法C语言完整实现涵盖ECB、CBC、CFB、CTR四种工作模式支持128/192/256位密钥长度适合需要底层对称加密代码的开发者、密码学学习者及金融POS安全认证相关项目参考。压缩包共6个文件包含3个C源文件、2个头文件和1个Makefile整体仅16KBC文件负责AES核心算法与配套测试程序头文件声明接口与辅助函数Makefile便于在Linux环境下一键编译。资源已在Ubuntu 16.04上编译并测试通过下载后可直接运行验证也可将AES模块提取出来集成到自己的工程中省去重复造轮子的时间。目前已有5316人学习下载作为简洁紧凑的AES多模式实现既有完整的加解密功能又有清晰的代码结构适合用作教学示例或项目基础代码。 去年给一个嵌入式网关做固件升级包加密我把手边能找到的AES代码翻了个遍要么是只支持ECB模式的教学Demo要么是OpenSSL那种编译链能把MCU工程拖到崩溃的“重型武器”。最后干脆自己拉了一套纯C实现支持AES-128/192/256三种密钥长度以及ECB、CBC、CFB、CTR四种常见工作模式。这篇文章把整个实现过程、原理重点、接口设计和实操中踩过的坑都写出来适合要在单片机或普通C工程里加加密、又不想引入大型加密库的开发者参考。1. 为什么自己在C语言里实现AES嵌入式场景下的真实选择1.1 直接移植开源库的问题很多人第一反应是用OpenSSL或者MbedTLS。OpenSSL功能全但在嵌入式平台上体积确实感人就算裁剪也要处理一堆编译选项。MbedTLS相对友好体积可控但它有自己的平台抽象层和内存管理接口想“塞”进一个裸机工程配置起来也需要耐心。网上流传的那些“AES代码”也未必能直接用。我见过不少版本算法流程本身没错但只能加密一个16字节分组根本没有模式层的概念还有的把S盒和逆S盒都写死成静态数组但解密函数实现有误更常见的是密钥只支持128位想换192位或256位几乎没有扩展性。这些都逼着你最终还是要自己把代码过一遍。1.2 自己实现的两个前提自己写AES并不是为了证明比开源库厉害而是有两类场景特别适合一是纯学习想彻底搞懂S盒怎么来的、密钥扩展怎么算、四种模式区别在哪二是产品确实只需要一个小而稳的加密模块不想为几个函数引入一整个第三方库。如果你的产品要求高安全级别、要过认证或者面对的是强对抗环境那我不建议自己写直接用经过验证的库更稳妥。但如果你的需求是“嵌入式设备上做数据加密、对性能没有极致要求、想完全掌控代码逻辑”那自己拉一套反而更踏实。1.3 这套代码的设计目标我给自己定了几个硬性目标不依赖任何平台库纯标准C不使用动态内存分配所有缓冲区由调用方提供或通过上下文结构体管理分组加密算法与工作模式彻底解耦底层只做16字节块加密/解密上层跑ECB、CBC、CFB、CTR密钥长度通过参数传入一套代码处理128/192/256三种长度。这样的结构后面不管是换平台还是接新业务成本都很低。2. AES算法核心运转逻辑从字节代换到密钥扩展的每一步2.1 状态矩阵与列优先填充AES的固定分组长度是128位也就是16字节。这16字节输入会被摆成一个4x4的状态矩阵注意是按列填充输入的第0到第3字节填第0列第4到第7字节填第1列以此类推。很多实现出现互操作问题根源就在于把列优先填成了行优先。uint8_t state[4][4]; for (int col 0; col 4; col) { for (int row 0; row 4; row) { state[row][col] input[col * 4 row]; } }只要保证“外部字节序”和“内部状态矩阵”的映射一致加解密自洽没问题但如果你要和其他语言、其他库互通就必须严格按标准来。2.2 四个基本运算AES的每一轮加密由四个运算组成。SubBytes字节代换每个字节通过S盒查表替换成另一个字节。S盒本身是GF(2^8)乘法逆元再做仿射变换得到的工程上直接把生成的256字节表放在ROM里查效率远高于实时计算。ShiftRows行移位状态矩阵第0行不动第1行循环左移1字节第2行左移2字节第3行左移3字节。解密时反向右移。MixColumns列混合对每一列的4个字节做GF(2^8)多项式乘法相当于一个固定的矩阵乘法。实现上通常用xtime宏来算“乘2”操作再组合出“乘3”等多字节结果避免浮点和除法。AddRoundKey轮密钥加把状态矩阵的16字节与当前轮的16字节轮密钥异或这是唯一用到密钥的运算也保证了每一轮都让数据和密钥充分混合。2.3 轮数与密钥扩展轮数取决于密钥长度AES-128是10轮AES-192是12轮AES-256是14轮。公式是Nr Nk 6其中Nk是密钥长度除以32128位对应Nk4256位对应Nk8。密钥扩展负责把原始密钥扩展成(Nr1)组轮密钥每组16字节。初始Nk个32位字就是原来的密钥之后的每个字等于前一个字和Nk位置之前的字的异或。每当索引碰到Nk的整数倍先做RotWord循环移位、再对每个字节做S盒替换、最后和轮常数Rcon异或。AES-256还要多一条规则如果当前索引除以8余4也要先做一次SubWord。这个细节不处理256位密钥的密文就会直接错。3. 四种分组模式工作机制对比ECB的坑与CTR的“流式加密”3.1 ECB看起来最简单使用场景却最窄ECB模式把每块明文独立用同一把密钥加密。结构简单、可以并行、单个密文块出错只影响对应的那一个明文块但这些优点掩盖不了一个致命问题相同明文块永远产生相同密文块。加密一张有明显色块的位图或者一段结构规则的固件数据密文里会残留明显的模式特征攻击者不需要破解AES就能猜出很多信息。我的建议是多块数据的通信加密不要用ECB。它比较适合的场景是单块数据比如封装一个随机会话密钥或者加密一个固定长度的随机数。3.2 CBC用IV打破模式代价是加密不能并行CBC模式先把当前明文块和上一个密文块异或再进入AES加密。第一块用来异或的“上一个密文块”就是IV初始向量。这样即使明文相同只要IV不同或前一块密文不同最终密文也不同。CBC的约束条件很明确加密必须串行因为当前块依赖前一块加密结果解密却可以并行因为解密每个块只需要D(K, C_i)和C_{i-1}两个数据。IV必须每次随机生成且不可预测并且同一个密钥下绝对不能用同一个IV加密两条消息。IV不需要保密但一般需要随密文一起传输。3.3 CFB把AES当流加密器用加解密走同一个函数CFB模式把AES当成一个伪随机流生成器先用密钥加密上一个密文块再把结果与当前明文异或得到密文。特别注意CFB的解密也调用AES加密函数不是AES解密函数。因为这个特性CFB可以处理任意长度的数据而不需要补齐到16字节整数倍非常适合按字节处理的流式场景比如终端设备之间的持续通信流。CFB根据反馈粒度分为CFB-1、CFB-8、CFB-128等常用的是CFB-8和CFB-128。它的缺点是数据依赖链比较长加解密都无法并行。3.4 CTR计数器模式加密解密完全对称CTR模式的做法是用一个初始计数器值由nonce和计数器字段组成从0开始递增每个计数器值做一次AES加密得到的伪随机流和明文异或就是密文。CTR最舒服的地方在于加密和解密是完全相同的函数只是输入换成密文、输出得到明文。它天然支持随机访问知道某个位置的计数器值就能单独解密那一段数据磁盘加密、网络协议包都很喜欢CTR。前提是计数器值不能重复使用否则两个数据流的异或结果会直接泄露明文信息。3.5 选型参考表模式加解密是否需要不同函数是否需要IV能否并行加密能否随机访问典型场景ECB需要不需要可以按块可以单块密钥封装CBC需要需要不可以不可以文件、固件批量加密CFB不需要都用加密函数需要不可以不可以流式通信数据CTR不需要都用加密函数需要可以可以磁盘加密、协议包加密4. C语言接口与状态设计把这个加密库“焊”进你的工程4.1 核心上下文结构体设计接口时要考虑一件事CFB和CTR是流模式一次调用可能只处理几个字节如果每个字节都触发一次完整AES块加密性能会很难看。所以我用一个上下文结构体保存中途状态。typedef struct { int mode; /* AES_MODE_ECB / CBC / CFB / CTR */ int key_bits; /* 128 / 192 / 256 */ uint8_t round_key[240]; /* AES-256最多需要 15组 x 16字节 240字节 */ uint8_t iv[16]; /* 保存当前IV或计数器状态 */ uint8_t stream_block[16]; /* CFB/CTR流模式缓存当前块的伪随机输出 */ size_t block_offset; /* 已消费到当前缓存块的哪个字节 */ } aes_ctx;round_key数组按最大尺寸240字节分配这样一套结构体就能覆盖三种密钥长度省去动态分配。stream_block和block_offset专门为流模式准备保证即使每次只喂一个字节底层也不用每次都重新跑完整分组加密。4.2 分组算法与模式层分离接口分三层职责清晰初始化层aes_init(ctx, key, key_bits)内部完成密钥扩展可选aes_set_iv(ctx, iv)单独设置IV或计数器初始值。块级函数aes_encrypt_block(ctx, in, out)和aes_decrypt_block(ctx, in, out)只处理16字节分组实现AES的轮运算。模式级函数aes_ecb_crypt、aes_cbc_encrypt、aes_cbc_decrypt、aes_cfb_crypt、aes_ctr_crypt负责把数据切成16字节块按模式规则链式处理。这样设计的好处是如果你以后想加GCM这类带认证的模式只需要复用块级函数不需要动底层AES。密钥扩展只在init时做一次频繁加密短数据时性能损失很小。4.3 IV与流模式的内部状态管理CBC、CFB、CTR的IV都不是一次性参数它会在处理过程中被持续更新。比如CBC加密完第一个块后C_i会成为下一个块的“IV”CFB加密完一个块后移位寄存器要装填新的密文块。把这些状态放在ctx里就能支持分片调用/* 第一次调用 */ aes_cbc_encrypt(ctx, buf1, out1, len1); /* 第二次调用ctx内部已经保存了上一次的最后一个密文块继续从断点处理 */ aes_cbc_encrypt(ctx, buf2, out2, len2);如果业务上要求每次调用都从相同的初始状态开始就在分片处理前重新调用aes_set_iv重置。5. 128/192/256三种密钥长度的统一实现技巧5.1 用Nk统一密钥扩展逻辑三种密钥长度本质上只差两个参数Nk密钥包含的32位字个数和Nr轮数。代码里不要写死三个分支而是用参数驱动int nk key_bits / 32; int nr nk 6; int total_words 4 * (nr 1); /* 轮密钥总字数44/52/60 */密钥扩展数组用uint8_t w[240]这样的扁平数组每4个字节构成一个32位字步长按Nk动态调整。这样一套逻辑处理三种密钥长度未来添加新长度也只需要改参数。5.2 Rcon轮常数边界密钥扩展里必须的轮常数Rcon数组很多实现只准备到Rcon[10]0x36。AES-256的密钥扩展实际还会用到更后面的值虽然用到第7个左右就够但为了稳妥我建议直接把Rcon按GF(2^8)的乘法规则准备到足够长数组多占几字节容量不是什么问题。static const uint8_t rcon[] { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a };遇到轮常数超出0x80时就要异或0x1B这其实是GF(2^8)里不可约多项式x^8x^4x^3x1在起作用。理解这一点后Rcon数组不管多长都能自己推算出来。5.3 解密时轮密钥逆序使用加密时AddRoundKey按第0组、第1组……第Nr组顺序用。解密时要逆过来从最后一组轮密钥开始往回调。最简单可靠的实现是密钥扩展只做一次得到完整的(Nr1)组轮密钥然后解密时按组从后往前取。const uint8_t *round_key_group ctx-round_key (nr - round) * 16;如果你看过那些追求极端性能的实现它们会把解密轮密钥预先做InvMixColumns变换这样解密内部就不需要额外处理但代码复杂度会明显上升。我建议第一版先做“正向扩展、反向取用”的版本正确性更容易保证。6. 实测数据与排错清单文档里不会写的坑6.1 不同平台下的速度参考我同一套纯C查表版本在桌面Linux上用gcc -O2编译AES-128-CBC加密能跑到几十MB/s以上具体数值受CPU、编译器优化和是否碰巧向量化影响很大换到72MHz的STM32F103单片机速度就掉到每秒几百KB这个量级。如果对速度敏感第一步是把S盒换成T-table4张1KB的查找表能把每轮的多步运算合并成少量查表和异或再往下就是针对特定CPU做AES-NI或汇编优化。但对绝大多数物联网设备场景纯C查表版已经完全够用。6.2 高频错误Top 5现象根本原因修复思路和其他语言库加密结果不一致状态矩阵行/列填充顺序错误对比标准测试向量检查列优先填充逻辑CTR/CFB解密出来是乱码解密用了AES_Decrypt而不是AES_Encrypt改为统一走加密函数生成伪随机流再异或CBC多段加密后中间某块错乱IV状态没有随数据持续更新把当前密文块写回ctx的iv字段密钥改成256位后栈溢出round_key数组按176字节分配统一按AES-256上限240字节分配最后一块不足16字节被丢弃ECB/CBC没有处理Padding加PKCS#7填充/去填充函数CTR/CFB不需要6.3 一次CBC解密前16字节乱码的实际排查有一次联调对端平台用AES-128-CBC解密其他块都正常唯独第一个16字节块是乱码。第一反应是密钥不对但密钥错的话不可能后面都解对。于是先按FIPS-197官方测试向量跑块级解密确认AES底层函数正确再检查数据流发现发送端把IV放在帧头接收端解析时按照自己的协议头定义偏移读错IV实际错位了4字节。排查思路给后来人一个参考遇到解密局部出错先证明块级函数是对的再检查IV、再检查Padding、最后检查数据截断。顺序对了问题很快就能定位。别上来就怀疑密钥密钥错通常是全部乱码而不是局部乱码。这套C语言实现我在好几个工程里复用过当时踩过的坑如今都变成了注释写进代码。如果让我重写一版第一件事就是把S盒换成T-table提升性能但回过头看先把分组算法、模式层和接口边界理清楚永远比闷头优化更重要。本文还有配套的精品资源点击获取