简介:面向嵌入式安全开发者的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 Flash,8MHz 主频还不带硬件乘法器,在它上面做 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,再异或 0x1b(AES 规定的不可约多项式低 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用xtime,3x用xtime(x) ^ x。这里有一个在 16 位单片机上必须写清楚的强制类型转换:uint8_t在做<<运算时会被整型提升到int,如果不加强转回uint8_t,高位可能保留一些脏数据。大部分编译器在-Os下能优化掉它,但加上强转就等于给未来的自己买个保险,代码可读性反而更好。
2.2 密钥扩展:176 字节轮密钥与 Rcon 的取舍
AES-128 要做 10 轮,每轮用一个 16 字节的轮密钥,加上最开始的白化,一共需要 11 组,也就是 176 字节。这 176 字节是在 `a
本文还有配套的精品资源,点击获取