3. AES算法原理(上):整体结构、字节代换、行移位

好,咱们正式开始啃 AES 的内核。 先给你打个底:AES 是一种 分组密码,每次处理 16 个字节(128 位)。 不管密钥是 128、192 还是 256 位,数据那块始终是 4×4 的字节矩阵。 我当年刚接触时总把密钥长度和分组大小搞混,后来在纸上画了十几遍才理顺——**分组固定,密钥可变**。

3.1 整体结构:一个轮函数的四次变换

AES 的加密过程可以理解成「重复执行一个标准轮函数」,最后再加一次特殊的 Final Round。 标准轮函数由四个子操作组成:

  1. SubBytes(字节代换)
  2. ShiftRows(行移位)
  3. MixColumns(列混合)
  4. AddRoundKey(轮密钥加)

Final Round 会去掉 MixColumns,只做 SubBytes → ShiftRows → AddRoundKey。 为什么?因为做混合那一步会给解密带来不必要的麻烦,这点咱们后面讲解密时再细说。

轮数依赖密钥长度:
AES-128 → 10 轮(包含 Final Round)
AES-192 → 12 轮
AES-256 → 14 轮

我个人习惯把 AES 的轮函数想象成一条「加工流水线」:每个字节先被 S‑box 替换,再按行打乱,然后混合列,最后跟本轮子密钥异或。 前两步(SubBytes 和 ShiftRows)负责「混淆 + 扩散」,是抵抗线性分析和差分分析的主力。下面咱们拆开看。

AES 加密轮函数结构(前两阶段) 输入 State (4×4) SubBytes (S‑box) ShiftRows (行移位) MixColumns + AddRoundKey 📌 本章聚焦前两个阶段 SubBytes 提供非线性替换,抵抗线性/差分攻击。 ShiftRows 把行间字节打散,实现扩散效果。 S‑box 查找表(256 字节) 移位偏移量:[0,1,2,3]

3.2 字节代换(SubBytes)—— 用 S‑box 给每个字节换脸

SubBytes 是 AES 里唯一的非线性操作。 算法把 State 矩阵里每个字节当作索引,去一个固定的 16×16 查找表(S‑box)里取出新值替换掉原来的字节。 说白了就是:每个字节都整容,且整容方案和它自身的位置无关,只跟它当前值有关。

避坑要点: S‑box 的设计基于有限域 GF(2⁸) 上的乘法逆元再加一个仿射变换。 我见过有人自己拍脑袋写查表,结果把逆元算错了,最终跑出来的密文对不上。 建议直接用官方标准表,别自己算,除非你想搞硬件优化。

用 C 写 SubBytes 的伪代码大概是这样的:

void SubBytes(uint8_t state[4][4], const uint8_t sbox[256]) {
    for (int row = 0; row < 4; row++) {
        for (int col = 0; col < 4; col++) {
            state[row][col] = sbox[state[row][col]];
        }
    }
}

你在实现时可能会想:为什么不用编号叫 row/col 而是用 row 对应行? 嗯,这里要注意:AES 的 State 是用列优先排布存储的,但 SubBytes 对每个字节独立操作,所以行列顺序影响不大。 真正让你头疼的是后面的 ShiftRows 和 MixColumns,它们依赖矩阵坐标。

3.3 行移位(ShiftRows)—— 把行内字节左转转

ShiftRows 做的事非常简单: 把 State 矩阵的每一行,分别向左循环移位不同的长度。

行号 移位字节数
第 0 行0
第 1 行1
第 2 行2
第 3 行3

第 0 行不动,第 1 行循环左移 1 个字节,第 2 行移 2 个,第 3 行移 3 个。 这样做的目的是把同一列里的字节散布到不同的列里。 你想,如果只有 SubBytes,攻击者很容易把统计规律限制在列内;ShiftRows 一搅,列与列之间的关联就断了。

曾经踩过的坑: 有次我在嵌入式平台上优化 AES,把 State 按行主序存储减少 cache miss。 结果 ShiftRows 的偏移量全部算错——因为行主序下“行”的概念变了。 一定要明确你的 State 是列优先还是行优先,再写移位逻辑。

ShiftRows 的代码实现也简单:

void ShiftRows(uint8_t state[4][4]) {
    // row 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;
    
    // row 2: 左移 2 = 右移 2,但实现时我习惯直接交换两次
    // 用循环或直接展开均可
    // ... 略,类似处理
}

实际项目中我更喜欢用查表法做 ShiftRows:预先算好字节在新矩阵中的索引,然后复制。 尤其是搞 AES-NI 指令集之前的老 CPU,查表比循环移位快很多。

3.4 第一阶段总结——为什么这两步要放在一起看?

SubBytes 提供了混淆:让字节与密钥的关系变得高度复杂。 ShiftRows 提供了扩散:让一个字节的变动能影响到后续多列。 两者合起来,再配合 MixColumns,就是 AES 抵抗线性密码分析和差分密码分析的底气所在。 我个人在做选型对比(AES vs SM4)时,经常会把 S‑box 的代数性质和移位模式拿出来做基准评估——这直接决定了算法的安全冗余和实现效率。

核心记忆点:
· SubBytes:每个字节独立非线性变换,用 S‑box 表。
· ShiftRows:按行循环移位,打散列关联。
· 两者都是可逆的,解密时做逆操作即可。

好了,前半段就聊到这儿。下面一章咱们接着看 MixColumns 和 AddRoundKey—— 这两步才是真正把密钥揉进数据里的环节,也是 AES 性能的瓶颈所在。