实战:AES加密(对称加密+字节操作)
说到加密,很多初学者第一反应就是「好难」。其实AES没你想的那么玄乎。我当年第一次接触AES时,也被那一堆S盒、密钥扩展给唬住了。后来真正上手写了一遍,才发现——说白了,就是一堆字节在来回折腾。
这一章,咱们就手撕一个AES-128加密。不依赖OpenSSL,纯C++实现。你想想看,能亲手写出一个工业级加密算法,这感觉多爽?
1. AES到底在干什么?
AES加密,本质上是对一个4×4的字节矩阵做多轮变换。每一轮干四件事:
- SubBytes:字节替换,查S盒
- ShiftRows:行移位,打乱顺序
- MixColumns:列混合,扩散数据
- AddRoundKey:加轮密钥,引入密钥
最后一轮少一个MixColumns。总共10轮(AES-128)。
核心思想:AES不是靠「藏」算法,而是靠「密钥」来保证安全。算法公开,密钥保密。这就是对称加密的精髓。
2. 先搭骨架:State矩阵
AES操作的核心是一个4×4的字节矩阵,叫State。明文16个字节按列填充进去:
// State 矩阵:4行4列,按列存储
// 明文: 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff
// 填充为:
// [00] [04] [08] [0c]
// [01] [05] [09] [0d]
// [02] [06] [0a] [0e]
// [03] [07] [0b] [0f]
using State = std::array<std::array<uint8_t, 4>, 4>;
我个人习惯用 std::array 而不是 vector,因为大小固定,性能更好,语义也更清晰。
3. S盒与SubBytes
S盒是一个256字节的查找表。每个字节的高4位是行号,低4位是列号。查表替换即可。
// S盒(前16个,完整版请查标准文档)
const uint8_t SBOX[256] = {
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5,
0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
// ... 完整256个
};
void subBytes(State& state) {
for (int i = 0; i < 4; ++i)
for (int j = 0; j < 4; ++j)
state[i][j] = SBOX[state[i][j]];
}
小技巧:S盒可以预计算逆S盒用于解密。我一般把S盒和逆S盒放在同一个头文件里,用 constexpr 数组,编译期就确定好了。
4. ShiftRows:行移位
这个操作很简单:第0行不移,第1行左移1字节,第2行左移2字节,第3行左移3字节。
void shiftRows(State& state) {
// 第1行左移1
uint8_t temp = state[1][0];
state[1][0] = state[1][1];
state[1][1] = state[1][2];
state[1][2] = state[1][3];
state[1][3] = temp;
// 第2行左移2
std::swap(state[2][0], state[2][2]);
std::swap(state[2][1], state[2][3]);
// 第3行左移3(等价于右移1)
temp = state[3][3];
state[3][3] = state[3][2];
state[3][2] = state[3][1];
state[3][1] = state[3][0];
state[3][0] = temp;
}
嗯,这里要注意:行移位是在行上操作,不是列。我第一次写的时候搞反了,结果加密出来的数据完全不对。排查了半天才发现是行列搞混了。
5. MixColumns:列混合
这是AES里最「数学」的一步。每一列看作一个多项式,在GF(2^8)域上乘以固定多项式。说白了就是一组固定的乘法和异或操作。
// GF(2^8)上的乘法,x乘法专用
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 = a & 0x80;
a <<= 1;
if (hi) a ^= 0x1b; // 不可约多项式 x^8 + x^4 + x^3 + x + 1
b >>= 1;
}
return p;
}
void mixColumns(State& state) {
for (int c = 0; c < 4; ++c) {
uint8_t a0 = state[0][c], a1 = state[1][c];
uint8_t a2 = state[2][c], a3 = state[3][c];
state[0][c] = gmul(2, a0) ^ gmul(3, a1) ^ a2 ^ a3;
state[1][c] = a0 ^ gmul(2, a1) ^ gmul(3, a2) ^ a3;
state[2][c] = a0 ^ a1 ^ gmul(2, a2) ^ gmul(3, a3);
state[3][c] = gmul(3, a0) ^ a1 ^ a2 ^ gmul(2, a3);
}
}
曾经踩过的坑:我曾经在嵌入式平台上用这个 gmul 函数,发现性能太差。后来改用查表法(提前算好所有256×256的组合),速度提升了将近10倍。如果你做高性能场景,建议用查表。
6. 密钥扩展
AES-128需要11个轮密钥(第0轮到第10轮),每个轮密钥16字节。从原始密钥通过递归方式生成。
using Word = std::array<uint8_t, 4>;
// 密钥扩展:生成44个Word(176字节)
std::array<Word, 44> keyExpansion(const std::array<uint8_t, 16>& key) {
std::array<Word, 44> w;
// 前4个Word直接拷贝
for (int i = 0; i < 4; ++i)
for (int j = 0; j < 4; ++j)
w[i][j] = key[i * 4 + j];
// 扩展
for (int i = 4; i < 44; ++i) {
Word temp = w[i - 1];
if (i % 4 == 0) {
// RotWord
std::rotate(temp.begin(), temp.begin() + 1, temp.end());
// SubWord
for (auto& b : temp) b = SBOX[b];
// XOR with Rcon
temp[0] ^= RCON[i / 4];
}
for (int j = 0; j < 4; ++j)
w[i][j] = w[i - 4][j] ^ temp[j];
}
return w;
}
RCON数组是固定的轮常数:{0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36}。
7. 组装:完整的加密函数
把上面所有零件拼起来:
std::array<uint8_t, 16> aesEncrypt(
const std::array<uint8_t, 16>& plaintext,
const std::array<uint8_t, 16>& key
) {
State state;
// 明文填充到State(按列)
for (int i = 0; i < 4; ++i)
for (int j = 0; j < 4; ++j)
state[j][i] = plaintext[i * 4 + j];
auto roundKeys = keyExpansion(key);
// 初始轮密钥加
addRoundKey(state, roundKeys, 0);
// 第1~9轮
for (int round = 1; round <= 9; ++round) {
subBytes(state);
shiftRows(state);
mixColumns(state);
addRoundKey(state, roundKeys, round);
}
// 第10轮(无MixColumns)
subBytes(state);
shiftRows(state);
addRoundKey(state, roundKeys, 10);
// 转回字节数组
std::array<uint8_t, 16> ciphertext;
for (int i = 0; i < 4; ++i)
for (int j = 0; j < 4; ++j)
ciphertext[i * 4 + j] = state[j][i];
return ciphertext;
}
8. 测试与验证
写完了怎么知道对不对?用官方测试向量验证:
| 输入 | 值(十六进制) |
|---|---|
| 明文 | 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff |
| 密钥 | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |
| 预期密文 | 69 c4 e0 d8 6a 7b 04 30 d8 cd b7 80 70 b4 c5 5a |
如果你的输出和这个一致,恭喜你,AES实现正确!
调试建议:我一般会在每轮结束后打印State矩阵,和标准中间值对比。这样能快速定位是SubBytes、ShiftRows还是MixColumns出了问题。
9. 性能优化方向
上面这个实现是教学版,性能一般。实际项目中可以这样优化:
- 查表法合并轮操作:把SubBytes、ShiftRows、MixColumns合并成4个T盒查表,一轮只需要16次查表和16次异或
- 使用SIMD指令:AES-NI指令集可以硬件加速,一条指令完成一轮
- 预计算轮密钥:如果多次加密使用同一密钥,提前算好所有轮密钥
我记得有一次给一个网络协议栈做加密优化,把软件AES换成AES-NI后,吞吐量从200Mbps直接飙到了2Gbps。硬件加速真不是盖的。
10. 写在最后
手写AES,说白了就是一次对字节操作和位运算的深度练习。你写一遍,就能彻底理解对称加密的底层逻辑。以后用OpenSSL或者其他库时,心里也有底——你知道它背后在干什么。
解密函数就是加密的逆过程,把S盒换成逆S盒,ShiftRows改成右移,MixColumns换成逆列混合,密钥扩展不变。留给你自己实现,试试看?
公众号:蓝海资料掘金营,微信deep3321