42、C++项目实战:AES加密算法实现

说实话,AES这个算法,我在刚入行那会儿觉得它特别神秘。总觉得能写出加密算法的人都是大神。后来真正啃完标准文档,自己动手撸了一遍,才发现——嗯,它其实是一套非常精巧的数学变换,每一步都有明确的规则。

今天我们就来手撕AES-128加密。我会带着你从字节代换、行移位、列混合,到轮密钥加,一步步把整个流程走通。你想想看,当你亲手实现出一个能加密、能解密的完整模块,那种成就感,绝对比调库爽多了。

AES的核心结构

AES处理的基本单位是字节。128位的数据,正好排成一个4x4的矩阵,我们叫它状态矩阵。我习惯用uint8_t state[4][4]来存它。

整个加密过程,说白了就是对这个矩阵反复做四件事:

  • SubBytes:字节代换,用S盒把每个字节换成另一个
  • ShiftRows:行移位,每一行向左循环移不同的位数
  • MixColumns:列混合,对每一列做伽罗瓦域上的矩阵乘法
  • AddRoundKey:轮密钥加,把状态矩阵和轮密钥异或

对于AES-128,一共要做10轮。最后一轮不执行MixColumns。

核心要点:AES的每一轮操作都是可逆的。解密就是加密的逆过程,只是顺序反过来,并且把S盒、行移位、列混合都换成对应的逆操作。

SVG:AES-128加密流程总览

先上一张图,让你对整个流程有个直观印象。我个人做项目前,一定会先画这种流程图,不然写到后面容易乱。

AES-128 加密流程 明文 (128位) AddRoundKey (初始) 第 1 ~ 9 轮 SubBytes → ShiftRows → MixColumns → AddRoundKey SubBytes → ShiftRows → AddRoundKey 密文 (128位)

字节代换:S盒的妙用

SubBytes这一步,就是把状态矩阵里的每个字节,通过S盒映射成另一个字节。S盒是一个256字节的查找表,它是固定的。

我在项目中遇到过一个问题:一开始图省事,每次加密都现场计算S盒。结果性能惨不忍睹。后来改成预计算,直接查表,速度提升了将近10倍。

// 预计算的S盒(前16个值示例)
static const uint8_t SBOX[256] = {
    0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5,
    0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
    // ... 省略中间 ...
};

void sub_bytes(uint8_t state[4][4]) {
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            state[i][j] = SBOX[state[i][j]];
        }
    }
}

小技巧:解密时用的逆S盒(InvSBox)也是固定的。我建议你把S盒和逆S盒放在同一个头文件里,用static const声明,避免重复定义。

行移位:简单的循环左移

ShiftRows的逻辑非常直观。状态矩阵的4行,分别左移0、1、2、3个字节。

  • 第0行:不移
  • 第1行:左移1字节
  • 第2行:左移2字节
  • 第3行:左移3字节

说白了,就是把矩阵里的数据打散,让列和列之间产生关联。解密时反过来,右移对应的位数就行。

void shift_rows(uint8_t state[4][4]) {
    uint8_t temp;
    
    // 第1行左移1位
    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位
    temp = state[2][0];
    state[2][0] = state[2][2];
    state[2][2] = temp;
    temp = state[2][1];
    state[2][1] = state[2][3];
    state[2][3] = temp;
    
    // 第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;
}

列混合:最烧脑的一步

MixColumns是AES里数学味道最浓的一步。它把每一列的4个字节,看成是一个多项式,然后在伽罗瓦域GF(2^8)上做乘法。

公式长这样:

new_col[0] = (2 * col[0]) ^ (3 * col[1]) ^ col[2] ^ col[3];
new_col[1] = col[0] ^ (2 * col[1]) ^ (3 * col[2]) ^ col[3];
new_col[2] = col[0] ^ col[1] ^ (2 * col[2]) ^ (3 * col[3]);
new_col[3] = (3 * col[0]) ^ col[1] ^ col[2] ^ (2 * col[3]);

这里的乘法和加法,都不是普通的整数运算。加法就是异或。乘法要特殊处理:如果结果超过0xFF,还要再异或一个0x1B。

注意:我曾经在这里踩过一个坑——直接用*运算符做乘法。结果加密出来的数据完全不对。后来才想起来,伽罗瓦域上的乘法必须用专门的gf_mul函数。千万别偷懒。

uint8_t gf_mul(uint8_t a, uint8_t b) {
    uint8_t result = 0;
    uint8_t temp = b;
    
    for (int i = 0; i < 8; i++) {
        if (a & (1 << i)) {
            result ^= temp;
        }
        temp = (temp << 1) ^ (temp & 0x80 ? 0x1B : 0x00);
    }
    return result;
}

void mix_columns(uint8_t state[4][4]) {
    uint8_t col[4];
    for (int j = 0; j < 4; j++) {
        for (int i = 0; i < 4; i++) {
            col[i] = state[i][j];
        }
        state[0][j] = gf_mul(2, col[0]) ^ gf_mul(3, col[1]) ^ col[2] ^ col[3];
        state[1][j] = col[0] ^ gf_mul(2, col[1]) ^ gf_mul(3, col[2]) ^ col[3];
        state[2][j] = col[0] ^ col[1] ^ gf_mul(2, col[2]) ^ gf_mul(3, col[3]);
        state[3][j] = gf_mul(3, col[0]) ^ col[1] ^ col[2] ^ gf_mul(2, col[3]);
    }
}

轮密钥加与密钥扩展

AddRoundKey最简单,就是把状态矩阵和轮密钥逐字节异或。

void add_round_key(uint8_t state[4][4], uint8_t round_key[4][4]) {
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            state[i][j] ^= round_key[i][j];
        }
    }
}

密钥扩展稍微复杂一点。AES-128需要11个轮密钥(初始1个 + 10轮每轮1个)。每个轮密钥都是4x4的矩阵。扩展算法从初始密钥开始,每4个字(16字节)为一组,通过循环移位、S盒代换和轮常量异或,生成下一组。

步骤说明
1. 初始化把用户输入的16字节密钥,直接作为第0个轮密钥
2. 循环生成每4个字为一组,后一个字由前一个字和上一个组的对应字决定
3. 特殊处理每组的第一个字,需要先循环左移1字节,再查S盒,最后异或轮常量

建议:密钥扩展可以提前算好,把所有轮密钥存在一个数组里。加密时直接取用,不用每次都重新算。我习惯用uint8_t round_keys[11][4][4]来存。

完整加密函数

把上面所有步骤串起来,就是完整的AES-128加密。你看,其实没那么复杂。

void aes_encrypt(uint8_t plaintext[16], uint8_t key[16], uint8_t ciphertext[16]) {
    uint8_t state[4][4];
    uint8_t round_keys[11][4][4];
    
    // 初始化状态矩阵
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            state[i][j] = plaintext[i + 4 * j];
        }
    }
    
    // 密钥扩展
    key_expansion(key, round_keys);
    
    // 初始轮密钥加
    add_round_key(state, round_keys[0]);
    
    // 第1到9轮
    for (int round = 1; round <= 9; round++) {
        sub_bytes(state);
        shift_rows(state);
        mix_columns(state);
        add_round_key(state, round_keys[round]);
    }
    
    // 第10轮(没有MixColumns)
    sub_bytes(state);
    shift_rows(state);
    add_round_key(state, round_keys[10]);
    
    // 输出密文
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            ciphertext[i + 4 * j] = state[i][j];
        }
    }
}

解密:反过来走一遍

解密就是加密的逆过程。顺序完全反过来:

  • 先用第10个轮密钥做AddRoundKey
  • 然后做InvShiftRows(右移)
  • 再做InvSubBytes(用逆S盒)
  • 接着是InvMixColumns
  • 最后用第0个轮密钥收尾

注意,解密时轮密钥的使用顺序和加密时相反。这一点很容易搞混,我刚开始写的时候就在这里翻过车。

经验之谈:写解密函数的时候,我建议你对照着加密函数一行一行地写。加密先做什么,解密就后做什么。加密用S盒,解密就用逆S盒。这样不容易出错。

测试与验证

写完代码,一定要用官方测试向量验证。NIST提供了很多标准测试数据。我一般会用下面这个:

// 测试向量(NIST示例)
// 密钥: 2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c
// 明文: 32 43 f6 a8 88 5a 30 8d 31 31 98 a2 e0 37 07 34
// 密文: 39 25 84 1d 02 dc 09 fb dc 11 85 97 19 6a 0b 32

如果你的程序输出和这个一致,那恭喜你,你的AES实现是正确的。

嗯,到这里,AES-128的核心实现就讲完了。你想想看,从字节代换到密钥扩展,每一步其实都有清晰的数学逻辑支撑。写一遍,胜过读十遍理论。动手试试吧。


公众号:蓝海资料掘金营,微信deep3321