第一次接触AES加密时,我被它优雅的设计深深吸引。作为目前最流行的对称加密算法,AES已经广泛应用于各类安全场景,从嵌入式设备到云计算平台都能看到它的身影。记得我在开发第一个智能门锁项目时,就是靠AES加密来保护用户密码和开锁指令的安全传输。
AES全称Advanced Encryption Standard,中文译为高级加密标准。它采用分组加密的方式,每次处理固定长度的数据块(128位)。与老旧的DES算法相比,AES不仅安全性更高,而且在现代处理器上的运行效率也更好。我实测过在STM32F4系列MCU上,AES-128加密速度可以达到50MB/s以上,完全能满足大多数嵌入式场景的需求。
AES算法的核心数据结构是一个4×4的字节矩阵,我们称之为状态矩阵(State Array)。这个矩阵就像是AES的"工作台",所有的加密操作都在这个工作台上进行。当我第一次实现AES算法时,最困惑的就是为什么要用矩阵来表示数据。后来发现,这种表示方式让后续的移位和混合操作变得非常直观。
举个例子,假设我们要加密的128位数据是"HelloWorld123456",转换成16进制表示后,可以按顺序填充到状态矩阵中:
code复制| 48(H) | 65(e) | 6C(l) | 6C(l) |
| 6F(o) | 57(W) | 6F(o) | 72(r) |
| 6C(l) | 64(d) | 31(1) | 32(2) |
| 33(3) | 34(4) | 35(5) | 36(6) |
在实际编程实现时,我发现状态矩阵的填充方式有两种选择:按列填充或按行填充。AES标准采用的是按列填充,也就是第一个字节放在(0,0)位置,第二个字节放在(1,0)位置,依此类推。这种填充方式在进行行移位操作时会特别方便。
在C语言中,我通常这样定义状态矩阵:
c复制typedef struct {
uint8_t state[4][4];
} AES_STATE;
初始化时,我会用一个简单的循环来完成填充:
c复制void init_state(AES_STATE *s, const uint8_t *input) {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
s->state[j][i] = input[i*4 + j];
}
}
}
AES的密钥扩展算法(Key Expansion)是我认为整个算法中最精妙的部分。它能够将一个初始密钥扩展成多个轮密钥(Round Key),每个轮密钥用于加密过程的不同阶段。在嵌入式项目中,我经常需要权衡是预先计算所有轮密钥,还是按需计算。对于资源受限的设备,后者通常更节省内存。
密钥扩展的核心是Rijndael的密钥调度算法。以128位密钥为例,初始密钥会被扩展成11个128位的轮密钥(包括初始轮密钥)。具体来说,算法会:
轮常量(Rcon)是密钥扩展中的关键参数。我发现很多初学者容易在这里出错,因为轮常量的计算有些反直觉。实际上,轮常量是一个指数递增的值:
code复制Rcon[i] = (RC[i], 0x00, 0x00, 0x00)
RC[1] = 0x01
RC[i] = 0x02 * RC[i-1] (在GF(2^8)域中)
在代码实现时,我通常会预计算前10个轮常量:
c复制static const uint8_t Rcon[10] = {
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36
};
字节替换是AES中最具密码学意义的操作。它通过一个称为S盒(Substitution Box)的查找表,将状态矩阵中的每个字节替换为另一个字节。这个S盒不是随意设计的,而是经过严密的数学推导,具有很好的非线性特性。
在实际项目中,我发现有两种实现方式:
对于嵌入式系统,我通常选择查找表方式。S盒的一个典型实现如下:
c复制static const uint8_t sbox[256] = {
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5,
// ... 其余S盒数据
};
void sub_bytes(AES_STATE *state) {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
state->state[i][j] = sbox[state->state[i][j]];
}
}
}
行移位操作相对简单,它通过循环左移状态矩阵的行来打乱数据。具体规则是:
这个操作在实现时需要注意边界条件。我常用的实现方式是:
c复制void shift_rows(AES_STATE *state) {
uint8_t temp;
// 第1行左移1字节
temp = state->state[1][0];
state->state[1][0] = state->state[1][1];
state->state[1][1] = state->state[1][2];
state->state[1][2] = state->state[1][3];
state->state[1][3] = temp;
// 第2行左移2字节(相当于交换0-2和1-3)
swap(&state->state[2][0], &state->state[2][2]);
swap(&state->state[2][1], &state->state[2][3]);
// 第3行左移3字节(相当于右移1字节)
temp = state->state[3][3];
state->state[3][3] = state->state[3][2];
state->state[3][2] = state->state[3][1];
state->state[3][1] = state->state[3][0];
state->state[3][0] = temp;
}
列混合是AES中最复杂的操作,它通过矩阵乘法将每列的4个字节进行混合。这个操作在有限域GF(2^8)中进行,使用固定的多项式乘法。
我花了很长时间才完全理解这个操作的数学原理。简单来说,它相当于用下面的固定矩阵乘以状态矩阵的每一列:
code复制| 02 03 01 01 |
| 01 02 03 01 |
| 01 01 02 03 |
| 03 01 01 02 |
在实现时,我创建了专门的有限域乘法函数:
c复制uint8_t gmul(uint8_t a, uint8_t b) {
uint8_t p = 0;
for (int i = 0; i < 8; i++) {
if (b & 1) p ^= a;
uint8_t hi_bit = (a & 0x80);
a <<= 1;
if (hi_bit) a ^= 0x1b; // x^8 + x^4 + x^3 + x + 1
b >>= 1;
}
return p;
}
轮密钥加是最简单的操作,就是将状态矩阵与当前轮的轮密钥进行按位异或。这个操作虽然简单,但却是AES安全性的关键,因为它将密钥材料混入状态矩阵中。
在实现时,我通常这样写:
c复制void add_round_key(AES_STATE *state, const uint8_t *round_key) {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
state->state[j][i] ^= round_key[i*4 + j];
}
}
}
AES-128的完整加密流程包括:
在嵌入式实现时,我通常会优化掉最后一轮的特殊处理,而是通过循环次数来控制:
c复制void aes_encrypt(AES_STATE *state, const uint8_t *key) {
uint8_t round_keys[176]; // 11轮×16字节
key_expansion(key, round_keys);
add_round_key(state, &round_keys[0]);
for (int round = 1; round < 10; round++) {
sub_bytes(state);
shift_rows(state);
mix_columns(state);
add_round_key(state, &round_keys[round*16]);
}
sub_bytes(state);
shift_rows(state);
add_round_key(state, &round_keys[10*16]);
}
在实际项目中,我发现几个有效的优化方法:
在ARM Cortex-M系列处理器上,使用这些优化后,AES加密速度可以提升3-5倍。不过要注意,优化可能会增加代码体积,需要在速度和空间之间权衡。
AES是分组密码,当数据长度不是16字节的整数倍时,需要进行填充。常见的填充方案有:
我在物联网项目中通常使用PKCS#7,因为它被广泛支持且安全性较好。实现时要注意验证填充的正确性,防止填充预言攻击。
AES有多种工作模式,适用于不同场景:
在智能家居项目中,我推荐使用CBC模式配合随机IV。记得每次加密都要使用不同的IV,否则会降低安全性。
在嵌入式设备中,AES实现可能面临时序攻击、功耗分析等侧信道攻击。防护措施包括:
我曾经在一个门锁项目中遇到功耗分析攻击,后来通过改进实现方式成功防护。这提醒我们,正确实现加密算法同样重要。