实战:AES加密(对称加密+字节操作)

说到加密,很多初学者第一反应就是「好难」。其实AES没你想的那么玄乎。我当年第一次接触AES时,也被那一堆S盒、密钥扩展给唬住了。后来真正上手写了一遍,才发现——说白了,就是一堆字节在来回折腾。

这一章,咱们就手撕一个AES-128加密。不依赖OpenSSL,纯C++实现。你想想看,能亲手写出一个工业级加密算法,这感觉多爽?

1. AES到底在干什么?

AES加密,本质上是对一个4×4的字节矩阵做多轮变换。每一轮干四件事:

  • SubBytes:字节替换,查S盒
  • ShiftRows:行移位,打乱顺序
  • MixColumns:列混合,扩散数据
  • AddRoundKey:加轮密钥,引入密钥

最后一轮少一个MixColumns。总共10轮(AES-128)。

核心思想:AES不是靠「藏」算法,而是靠「密钥」来保证安全。算法公开,密钥保密。这就是对称加密的精髓。

AES-128 加密流程 明文(16字节) AddRoundKey(初始轮) 第1轮 ~ 第9轮(重复9次) SubBytes ShiftRows MixColumns AddRoundKey 第10轮:SubBytes → ShiftRows → AddRoundKey(无MixColumns)

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