
简介面向嵌入式安全开发者的AES 128 CBC加解密C语言实现针对资源受限的MSP430F149超低功耗单片机完成验证可用于传感器节点、物联网终端等场景的数据保护学习与移植参考。代码围绕AES轮函数、字节代换、行移位、列混淆、轮密钥加以及CBC模式的初始化向量处理和块间异或展开兼顾算法正确性与底层可移植性。压缩包共4个文件以2个.c源文件和2个.h头文件为主分别承载算法实现、接口声明与加解密调用逻辑整体约8KB体量精简便于阅读和裁剪。目前已有1448人学习下载。读者可从中梳理AES 128与CBC模式的完整实现脉络理解密钥扩展、IV管理、加解密流程及嵌入式环境下的内存与接口适配并借鉴其在MSP430F149上的验证思路为安全通信和数据保护项目提供可复用的代码基础。1. 16 位 2KB RAM 的片子上跑 AES-128-CBC这份 C 代码省掉的到底是什么MSP430F149 只有 2KB RAM、60KB Flash8MHz 主频还不带硬件乘法器在它上面做 AES 128 加密解密真正卡脖子的不是算力是 RAM 预算和查表位置。这份基于 C 语言的 AES-128 CBC 加密解密代码核心就三件事把 256 字节 S 盒用const钉进 Flash、把 11 组轮密钥压成 176 字节、用异或链把每一块密文和上一块绑死让相同的明文块在报文里不会露出相同的密文。代码已在 MSP430F149 上跑通加密和解密两条链路适合低功耗无线节点的帧加密、仪表数据上行的隐私保护也适合那些要在 8/16 位单片机上做对称加密、又不想引第三方库的场景。你只要会基本的 C知道什么是分组、什么是 IV就能照着下面的步骤把这份代码接进自己的工程。2. AES-128-CBC 的算法骨架状态矩阵、轮密钥与链式异或怎么落到 C 里在 16 位单片机上写 AES最容易出问题的往往不是算法本身而是“怎么把这套数学映射到一维数组上”。这一步要是搞错了代码看着通顺跑出来的密文却永远和 PC 端对不上而且错得特别隐蔽——可能只有第二块开始错也可能只有某几个字节错。所以先要把三件事固定下来状态矩阵的下标约定、轮密钥的生成顺序、加解密主循环的对称写法。这一章不会去推导有限域乘法的数学证明只讲清楚三件事以及它们怎么在 C 代码里形成边界。2.1 状态矩阵的列优先存放决定后面所有下标AES 把 16 字节明文排成一个 4×4 的字节矩阵规则是按列填in[0]落在第 0 行第 0 列in[1]落在第 1 行第 0 列in[4]才落到第 0 行第 1 列。这个约定一旦选错后面的 ShiftRows 下标会全部错位而错位之后代码依然是“能跑”的只是结果不对——这就是很多人第一版跑出来对不上的原因。MSP430 是 16 位地址空间用二维数组s[4][4]意味着每次访问都要算一次r * 4 c编译器还不一定能把它优化掉。直接用一维uint8_t s[16]行r、列c的元素就是s[r 4*c]访问是一次简单的加法省下来的周期在 10 轮循环里会累积得很可观。S 盒是整个算法里唯一的大表。256 字节放 Flash 还是放 RAM直接决定你的 RAM 够不够用/* aes128.c —— S 盒与逆 S 盒必须用 const 修饰才会落到 .rodata(Flash) */ static const uint8_t sbox[256] { 0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16 }; /* 字节替换直接查表一次循环完成 16 个字节 */ static void sub_bytes(uint8_t s[16]) { uint8_t i; for (i 0; i 16u; i) { s[i] sbox[s[i]]; } } /* 行移位第 1 行左移 1第 2 行左移 2第 3 行左移 3等价于右移 1 */ static void shift_rows(uint8_t s[16]) { uint8_t t; /* 行 1下标 1,5,9,13 循环左移一位 */ t s[1]; s[1] s[5]; s[5] s[9]; s[9] s[13]; s[13] t; /* 行 2下标 2,6,10,14 循环左移两位等价于两两交换 */ t s[2]; s[2] s[10]; s[10] t; t s[6]; s[6] s[14]; s[14] t; /* 行 3下标 3,7,11,15 循环左移三位等价于右移一位 */ t s[15]; s[15] s[11]; s[11] s[7]; s[7] s[3]; s[3] t; }上面代码的逻辑说明很直接sub_bytes是 16 次查表没有分支shift_rows用四个临时变量完成三行轮转避免额外的缓冲区。参数说明上要注意一点shift_rows只接受 16 字节的一维数组调用方必须保证数组长度恰好是 16否则越界读会直接踩到相邻变量——在只有 2KB RAM 的片子上越界一次就足以让某个全局变量莫名其妙地变值。真正需要停下来想一秒的是列混合。它要做有限域 GF(2^8) 上的乘法常见做法有两种一种是把mul(a,b)写成循环 8 次的通用乘法代码短但慢另一种是预先展开成xtime乘 2 后按需异或0x1b的多次调用。对于 MSP430 这种没有硬件乘法器的平台xtime方案每个字节只需要一条移位加一条条件异或优势很明显/* 乘 2左移一位若最高位原本是 1再异或 0x1bAES 规定的不可约多项式低 8 位 */ static uint8_t xtime(uint8_t x) { return (uint8_t)((uint8_t)(x 1) ^ (uint8_t)((x 0x80u) ? 0x1bu : 0x00u)); } /* 列混合对每一列做 [2 3 1 1; 1 2 3 1; 1 1 2 3; 3 1 1 2] 的矩阵乘 */ static void mix_columns(uint8_t s[16]) { uint8_t c, a0, a1, a2, a3; for (c 0; c 4u; c) { a0 s[4u * c 0u]; a1 s[4u * c 1u]; a2 s[4u * c 2u]; a3 s[4u * c 3u]; s[4u * c 0u] (uint8_t)(xtime(a0) ^ (uint8_t)(xtime(a1) ^ a1) ^ a2 ^ a3); s[4u * c 1u] (uint8_t)(a0 ^ xtime(a1) ^ (uint8_t)(xtime(a2) ^ a2) ^ a3); s[4u * c 2u] (uint8_t)(a0 ^ a1 ^ xtime(a2) ^ (uint8_t)(xtime(a3) ^ a3)); s[4u * c 3u] (uint8_t)((uint8_t)(xtime(a0) ^ a0) ^ a1 ^ a2 ^ xtime(a3)); } }每一行赋值对应矩阵的一行2x用xtime3x用xtime(x) ^ x。这里有一个在 16 位单片机上必须写清楚的强制类型转换uint8_t在做运算时会被整型提升到int如果不加强转回uint8_t高位可能保留一些脏数据。大部分编译器在-Os下能优化掉它但加上强转就等于给未来的自己买个保险代码可读性反而更好。2.2 密钥扩展176 字节轮密钥与 Rcon 的取舍AES-128 要做 10 轮每轮用一个 16 字节的轮密钥加上最开始的白化一共需要 11 组也就是 176 字节。这 176 字节是在 a本文还有配套的精品资源点击获取