跳至正文
老丹的足迹 —— 代码写给机器,游记写给自己,感悟写给时间
老丹的足迹 老丹的足迹
老丹的足迹 老丹的足迹
  • 首页
  • 示例页面
  • 首页
  • 示例页面
老丹的足迹 老丹的足迹
老丹的足迹 老丹的足迹
  • 首页
  • 示例页面
  • 首页
  • 示例页面

AES加密算法:现代信息安全的基石

一、什么是AES?

AES(Advanced Encryption Standard,高级加密标准)是一种对称分组加密算法,由美国国家标准与技术研究院(NIST)于2001年正式发布,用于替代已显老旧的DES(数据加密标准)。AES迅速成为全球应用最广泛的加密标准,从WiFi保护到金融交易,从硬盘加密到VPN通信,AES的身影无处不在,堪称现代信息安全的基石。

“对称“意味着加密和解密使用完全相同的密钥——就像一把钥匙既能锁门也能开门。”分组“则意味着算法将数据划分为固定大小的块(128位/16字节)进行加密处理,每次加密一个完整的数据块。

AES的设计者是两位比利时密码学家——Joan Daemen和Vincent Rijmen,他们的算法最初名为Rijndael(由两位作者姓氏的组合命名),在1997年NIST发起的公开竞选中,从15个候选算法中脱颖而出,最终于2001年被选为官方标准。这场历时数年的公开选拔确保了AES经过全球密码学家的严格审查,其安全性得到了充分的验证。

二、核心参数与安全等级

AES的安全性由三个核心参数共同决定:

2.1 密钥长度(决定安全级别)

类型密钥长度加密轮数安全性说明
AES-12816字节(128位)10轮足够抵御所有已知攻击,暴力破解需10³⁸年
AES-19224字节(192位)12轮更高安全余量,适用于长期机密
AES-25632字节(256位)14轮最高安全级别,美国机密文件使用

密钥长度每增加一位,暴力破解的难度就翻一倍。AES-256的密钥空间之大(2²⁵⁶种可能),即使利用全宇宙所有原子的能量也无法在宇宙寿命内穷举。从实用角度而言,AES-128已足以抵御任何已知的暴力破解攻击;而AES-256则代表了当前民用领域的最高安全标准,被美国政府批准用于绝密级文件的保护。

加密轮数随着密钥长度的增加而增加,更多轮数意味着更复杂的变换和更强的安全性,但也会带来一定的性能开销。不过在现代CPU上,这种性能差异微乎其微。

2.2 分组大小(固定不变)

AES的分组大小固定为128位(16字节),无论使用何种密钥长度。这意味着算法每次只能处理16字节的数据块,无法直接加密任意长度的数据。对于长度不足或超出16倍数的数据,就需要通过”填充”机制来补齐到16字节的倍数;而对于超长数据,则需要通过”工作模式”来组织多个数据块的加密。

2.3 加密轮数(随密钥增长)

密钥长度决定了加密的轮数:

  • AES-128:10轮
  • AES-192:12轮
  • AES-256:14轮

更多轮数意味着更强的混淆和扩散效果,以及更高的抗密码分析能力。每轮都使用不同的轮密钥,这些轮密钥通过密钥扩展算法从原始用户密钥生成。

三、数据组织方式:状态矩阵

在深入四步变换之前,需要先理解AES如何组织数据。这是理解所有后续操作的基础。

AES把16字节的明文排列成一个4×4的矩阵(按列排列),这个矩阵被称为状态(State)。状态矩阵是AES所有操作的舞台,四步变换都是在这个矩阵上进行的。

输入:16字节 [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15]
                ↓ 按列排列
状态矩阵:
┌────────────┐
│ a0    a4    a8    a12 │  ← 第0行
│ a1    a5    a9    a13 │  ← 第1行
│ a2    a6    a10   a14 │  ← 第2行
│ a3    a7    a11   a15 │  ← 第3行
└────────────┘
   ↑    ↑    ↑    ↑
  列0   列1   列2   列3

为什么按列排列?
这是Rijndael算法的设计选择,使得列混合(MixColumns)操作更容易实现——因为每一列的4个字节在内存中是连续的,便于进行矩阵运算。AES的所有四步变换都是在这个4×4矩阵上进行的,理解矩阵的排列方式对理解后续操作至关重要。

四、四步变换详解

AES的加密过程是一个多轮迭代的变换过程,每一轮都包含四个核心步骤。下面我们逐一详细拆解每个操作的具体实现,包括操作步骤、数值示例、代码实现和设计原理。

4.1 字节代换(SubBytes)

一句话概括:把状态矩阵中的每一个字节,按照一张固定的表格(S盒)替换成另一个字节。

字节代换是AES中唯一的非线性变换,也是安全性的关键来源。它基于有限域GF(2⁸)上的数学运算(乘法逆元+仿射变换)构造,使得加密过程具有非线性特性,能够抵抗差分密码分析和线性密码分析。

具体操作步骤:

  1. 取出状态矩阵中的一个字节(例如 0x53)
  2. 将这个字节分成两半:高4位和低4位
  • 0x53 → 高4位 = 0x5(行号),低4位 = 0x3(列号)
  1. 用这两个值作为行索引和列索引,在S盒中查找
  • 查第 0x5 行第 0x3 列 → 得到 0xED
  1. 用查到的值替换原来的字节
  • 0x53 → 0xED
  1. 对状态矩阵中的全部16个字节重复上述操作

S盒长什么样?

S盒是一个固定的256字节数组,每个输入值对应唯一的输出值。它被设计为没有固定点(S盒的输出不等于输入)和反固定点(输出不等于输入的按位取反),这是其安全性的重要保证。以下是完整的S盒表(你代码中的 sbox[256]):

        0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
     ┌─────────────────────────────────┐
0x00 │ 63  7C  77  7B  F2  6B  6F  C5  30  01  67  2B  FE  D7  AB  76 │
0x10 │ CA  82  C9  7D  FA  59  47  F0  AD  D4  A2  AF  9C  A4  72  C0 │
0x20 │ B7  FD  93  26  36  3F  F7  CC  34  A5  E5  F1  71  D8  31  15 │
0x30 │ 04  C7  23  C3  18  96  05  9A  07  12  80  E2  EB  27  B2  75 │
0x40 │ 09  83  2C  1A  1B  6E  5A  A0  52  3B  D6  B3  29  E3  2F  84 │
0x50 │ 53  D1  00  ED  20  FC  B1  5B  6A  CB  BE  39  4A  4C  58  CF │
0x60 │ D0  EF  AA  FB  43  4D  33  85  45  F9  02  7F  50  3C  9F  A8 │
0x70 │ 51  A3  40  8F  92  9D  38  F5  BC  B6  DA  21  10  FF  F3  D2 │
0x80 │ CD  0C  13  EC  5F  97  44  17  C4  A7  7E  3D  64  5D  19  73 │
0x90 │ 60  81  4F  DC  22  2A  90  88  46  EE  B8  14  DE  5E  0B  DB │
0xA0 │ E0  32  3A  0A  49  06  24  5C  C2  D3  AC  62  91  95  E4  79 │
0xB0 │ E7  C8  37  6D  8D  D5  4E  A9  6C  56  F4  EA  65  7A  AE  08 │
0xC0 │ BA  78  25  2E  1C  A6  B4  C6  E8  DD  74  1F  4B  BD  8B  8A │
0xD0 │ 70  3E  B5  66  48  03  F6  0E  61  35  57  B9  86  C1  1D  9E │
0xE0 │ E1  F8  98  11  69  D9  8E  94  9B  1E  87  E9  CE  55  28  DF │
0xF0 │ 8C  A1  89  0D  BF  E6  42  68  41  99  2D  0F  B0  54  BB  16 │
     └─────────────────────────────────┘

完整示例:对一个字节进行替换

输入字节:  0x53
    ↓ 拆分
行索引:    0x5  (二进制: 0101)
列索引:    0x3  (二进制: 0011)
    ↓ 查表
S盒[0x53] = 0xED  (在第5行第3列的位置)
    ↓
输出字节:  0xED

对整个状态的替换示例:

加密前状态(16字节,以4×4矩阵表示):
53  A2  7C  12
34  F1  6B  09
8D  C5  4E  77
2F  B3  91  56

        ↓ 每个字节查 S 盒

加密后状态:
ED  3A  10  C9  ← 53→ED, A2→3A, 7C→10, 12→C9
18  A1  7F  01  ← 34→18, F1→A1, 6B→7F, 09→01
5D  A6  2F  F5  ← 8D→5D, C5→A6, 4E→2F, 77→F5
15  A8  1D  31  ← 2F→15, B3→A8, 91→1D, 56→31

代码实现:

在 qaesencryption.cpp 中,字节代换的实现非常简洁:

void QAESEncryption::subBytes(QByteArray &state)
{
    QByteArray::iterator it = state.begin();
    for(int i = 0; i < 16; i++)
        it[i] = getSBoxValue(static_cast<quint8>(it[i]));
}

quint8 getSBoxValue(quint8 num){
    return sbox[num];  // 就是查表!
}

在 qaesencryption.h 中,S盒被定义为类的常量成员:

const quint8 sbox[256] = {
    0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, ...
};

如果定义了 QTAES_CONSTANT_TIME_SBOX 宏,则会使用常量时间版本的S盒实现,进一步防御侧信道攻击。

为什么要做字节代换?

S盒的替换是非线性的,这是AES安全性的核心来源。如果所有操作都是线性的(如XOR和移位),那么输入和输出之间会存在线性关系,攻击者可以通过数学推导直接破解。S盒破坏了这种线性关系,使得加密强度大幅提升。这种非线性特性让AES能够有效抵抗差分密码分析和线性密码分析——这是当时对DES最有效的两种攻击方法。

解密时的逆操作:

解密时使用逆S盒(Inverse S-Box),规则是 逆S盒[S盒[x]] = x。逆S盒同样是一个256字节的查找表(你代码中的 rsbox[256]):

S盒:     0x53 → 0xED
逆S盒:   0xED → 0x53   ← 完美还原

解密时的字节代换会调用 invSubBytes() 函数,它使用 rsbox 而不是 sbox。

4.2 行移位(ShiftRows)

一句话概括:状态矩阵的每一行,按照不同的位数向左循环移动。

行移位是一个置换操作,它简单但有效地实现了数据的跨行扩散。它确保列与列之间的数据发生混合,为后续的列混合操作创造更好的扩散条件。

具体操作步骤:

将4×4状态矩阵的每一行分别向左循环移动不同的位数:

行号移动位数说明
第0行0位不动
第1行1位向左循环移动1位
第2行2位向左循环移动2位
第3行3位向左循环移动3位

什么是”向左循环移动”?

对于一行4个字节 [a0, a1, a2, a3]:

  • 左移1位:[a1, a2, a3, a0](第一个字节移到末尾)
  • 左移2位:[a2, a3, a0, a1]
  • 左移3位:[a3, a0, a1, a2]

完整示例:

加密前状态矩阵:
┌────────┐
│ 00  04  08  12 │  ← 第0行(移动0位)
│ 01  05  09  13 │  ← 第1行(移动1位)
│ 02  06  10  14 │  ← 第2行(移动2位)
│ 03  07  11  15 │  ← 第3行(移动3位)
└────────┘

        ↓ ShiftRows

加密后状态矩阵:
┌────────┐
│ 00  04  08  12 │  ← 第0行:不变
│ 05  09  13  01 │  ← 第1行:左移1位 [01,05,09,13] → [05,09,13,01]
│ 10  14  02  06 │  ← 第2行:左移2位 [02,06,10,14] → [10,14,02,06]
│ 15  03  07  11 │  ← 第3行:左移3位 [03,07,11,15] → [15,03,07,11]
└────────┘

仔细观察可以发现,经过行移位后,原来在第1行的 01 移动到了最后一列,原来在第2行的 02 和 06 移动到了不同的列,原来在第3行的 03、07、11 也分别移动到了不同的位置。这种跨行移动为后续的列混合创造了条件。

代码实现:

void QAESEncryption::shiftRows(QByteArray &state)
{
    QByteArray::iterator it = state.begin();
    quint8 temp;
    // 注意:QByteArray是按列存储的!
    // 索引0,1,2,3是第一列;4,5,6,7是第二列;8,9,10,11是第三列;12,13,14,15是第四列

    // 第1行:左移1位
    // 索引:1→5→9→13(这四个索引都是第1行,分布在4列中)
    temp   = static_cast<quint8>(it[1]);
    it[1]  = static_cast<quint8>(it[5]);
    it[5]  = static_cast<quint8>(it[9]);
    it[9]  = static_cast<quint8>(it[13]);
    it[13] = static_cast<quint8>(temp);

    // 第2行:左移2位
    // 索引:2↔10, 6↔14(第2行,分布在4列中)
    temp   = static_cast<quint8>(it[2]);
    it[2]  = static_cast<quint8>(it[10]);
    it[10] = static_cast<quint8>(temp);
    temp   = static_cast<quint8>(it[6]);
    it[6]  = static_cast<quint8>(it[14]);
    it[14] = static_cast<quint8>(temp);

    // 第3行:左移3位(即右移1位)
    // 索引:3→15→11→7
    temp   = static_cast<quint8>(it[3]);
    it[3]  = static_cast<quint8>(it[15]);
    it[15] = static_cast<quint8>(it[11]);
    it[11] = static_cast<quint8>(it[7]);
    it[7]  = static_cast<quint8>(temp);
}

为什么要做行移位?

行移位实现了扩散效果。S盒只替换单个字节,而移位让字节在不同行之间重新排列,使得单字节的变化能够影响到矩阵的更多位置。配合后续的列混合,最终实现”一个字节变化影响整个状态”的效果——这就是密码学中的雪崩效应(Avalanche Effect)。雪崩效应要求输入的一位变化应该引起输出约一半位的变化,行移位是实现这一目标的重要步骤。

解密时的逆操作:

解密时执行逆行移位(InvShiftRows),方向相反——每一行向右循环移动:

行号移动位数
第0行0位
第1行右移1位(即左移3位)
第2行右移2位(即左移2位)
第3行右移3位(即左移1位)

4.3 列混合(MixColumns)

一句话概括:状态矩阵的每一列通过一个固定的矩阵乘法进行变换,把列中的4个字节”混合”在一起。

列混合是扩散层的核心操作,也是实现上相对复杂的部分。每一列的四个字节通过有限域GF(2⁸)上的矩阵乘法进行混合,使得单个字节的变化能够扩散到整列。

具体操作步骤:

  1. 取出状态矩阵中的一列(4个字节)
  2. 将这4个字节看作一个列向量
  3. 用固定的4×4矩阵与之相乘(在GF(2⁸)有限域上)
  4. 得到新的4个字节,替换原来的列
  5. 对4列都执行同样的操作

固定变换矩阵:

┌            ┐
│ 2  3  1  1 │
│ 1  2  3  1 │
│ 1  1  2  3 │
│ 3  1  1  2 │
└            ┘

对一列的完整计算:

假设某一列的4个字节是 [a0, a1, a2, a3],经过列混合后变成 [b0, b1, b2, b3]:

b0 = (2 × a0) ⊕ (3 × a1) ⊕ (1 × a2) ⊕ (1 × a3)
b1 = (1 × a0) ⊕ (2 × a1) ⊕ (3 × a2) ⊕ (1 × a3)
b2 = (1 × a0) ⊕ (1 × a1) ⊕ (2 × a2) ⊕ (3 × a3)
b3 = (3 × a0) ⊕ (1 × a1) ⊕ (1 × a2) ⊕ (2 × a3)

重要提醒:这里的 × 和 ⊕ 都是 GF(2⁸) 有限域上的运算,不是普通的整数乘法和加法!这是AES数学基础中最不直观的部分。

GF(2⁸) 乘法怎么算?

GF(2⁸) 是包含256个元素的有限域,其中的运算需要遵循特定的规则:

  • ⊕ 就是 XOR(异或)运算
  • × 是 GF(2⁸) 乘法,基于多项式乘法模一个不可约多项式

AES使用的不可约多项式是:x⁸ + x⁴ + x³ + x + 1(对应十六进制 0x11B)

对于乘以 1、2、3 这些特殊值,有简化规则:

乘以计算方法
× 1不变
× 2左移1位,如果最高位(第7位)是1,则异或 0x1B
× 3(× 2) ⊕ 原值

这就是代码中 xTime() 函数的作用——实现 GF(2⁸) 中的乘以2操作。

xTime() 函数详解:

quint8 xTime(quint8 x)
{
    return ((x << 1) ^ (((x >> 7) & 1) * 0x1B));
}

逻辑分解:

  1. x << 1:左移一位(相当于乘以2)
  2. (x >> 7) & 1:提取最高位(第7位)
  3. 如果最高位是1,异或 0x1B(因为多项式溢出需要修正)
  4. 这实现了 GF(2⁸) 中的”乘以x”运算

例子:

xTime(0x53) = 0xA6  (最高位是0,直接左移)
xTime(0x80) = 0x1B  (最高位是1,左移后异或0x1B)

具体数值示例:

假设某列是 [0x53, 0x7C, 0x8D, 0x2F]:

先计算各部分的 GF(2⁸) 乘法:

2 × 0x53 = 0xA6  (0x53左移1位,最高位0,不变)
3 × 0x7C = (2×0x7C) ⊕ 0x7C = 0xF8 ⊕ 0x7C = 0x84
1 × 0x8D = 0x8D
1 × 0x2F = 0x2F

然后全部 XOR:
b0 = 0xA6 ⊕ 0x84 ⊕ 0x8D ⊕ 0x2F
   = 0x22 ⊕ 0x8D ⊕ 0x2F
   = 0xAF ⊕ 0x2F
   = 0x80

同理计算 b1, b2, b3...

完整列混合示例:

某一列:                   变换后:
┌   ┐   ┌            ┐    ┌   ┐
│53 │   │ 2  3  1  1 │    │80 │
│7C │   │ 1  2  3  1 │    │...│
│8D │ = │ 1  1  2  3 │ × │...│
│2F │   │ 3  1  1  2 │    │...│
└   ┘   └            ┘    └   ┘

对整个状态矩阵的列混合:

加密前:                    加密后:
┌────────┐          ┌──────────┐
│ 53  04  08  12 │          │ 80  ...  ...  ... │
│ 7C  05  09  13 │   →     │ ... ...  ...  ... │
│ 8D  06  10  14 │          │ ... ...  ...  ... │
│ 2F  07  11  15 │          │ ... ...  ...  ... │
└────────┘          └──────────┘
   ↑                         ↑
  列0  (4个字节混合)       列0 (4个新字节)

代码实现:

void QAESEncryption::mixColumns(QByteArray &state)
{
    QByteArray::iterator it = state.begin();
    quint8 tmp, tm, t;

    for(int i = 0; i < 16; i += 4){  // 每次处理一列(4个字节)
        t = static_cast<quint8>(it[i]);
        tmp = static_cast<quint8>(it[i]) ^ static_cast<quint8>(it[i+1]) 
            ^ static_cast<quint8>(it[i+2]) ^ static_cast<quint8>(it[i+3]);

        // 计算 b0 = 2*a0 ⊕ 3*a1 ⊕ a2 ⊕ a3
        tm = xTime(static_cast<quint8>(it[i]) ^ static_cast<quint8>(it[i+1]));
        it[i] = static_cast<quint8>(it[i]) ^ static_cast<quint8>(tm) ^ static_cast<quint8>(tmp);

        // 计算 b1 = a0 ⊕ 2*a1 ⊕ 3*a2 ⊕ a3
        tm = xTime(static_cast<quint8>(it[i+1]) ^ static_cast<quint8>(it[i+2]));
        it[i+1] = static_cast<quint8>(it[i+1]) ^ static_cast<quint8>(tm) ^ static_cast<quint8>(tmp);

        // 计算 b2 = a0 ⊕ a1 ⊕ 2*a2 ⊕ 3*a3
        tm = xTime(static_cast<quint8>(it[i+2]) ^ static_cast<quint8>(it[i+3]));
        it[i+2] = static_cast<quint8>(it[i+2]) ^ static_cast<quint8>(tm) ^ static_cast<quint8>(tmp);

        // 计算 b3 = 3*a0 ⊕ a1 ⊕ a2 ⊕ 2*a3
        tm = xTime(static_cast<quint8>(it[i+3]) ^ static_cast<quint8>(t));
        it[i+3] = static_cast<quint8>(it[i+3]) ^ static_cast<quint8>(tm) ^ static_cast<quint8>(tmp);
    }
}

为什么要做列混合?

列混合是AES中扩散层的核心。它让单个字节的变化能够扩散到整列,结合行移位的跨行扩散,最终实现”一个字节变化影响整个状态矩阵”的效果。这是密码学中雪崩效应的基础——输入的任何微小变化都会导致输出的大幅变化,使得攻击者无法通过分析输入输出的关系来推导密钥。

解密时的逆操作:

解密时执行逆列混合(InvMixColumns),使用不同的变换矩阵:

┌                ┐
│ 0E  0B  0D  09 │
│ 09  0E  0B  0D │
│ 0D  09  0E  0B │
│ 0B  0D  09  0E │
└                ┘

这个逆矩阵是精心设计的,保证 InvMixColumns(MixColumns(x)) = x。逆列混合的实现使用 multiply() 函数,该函数实现了GF(2⁸)上任意两个字节的乘法:

quint8 multiply(quint8 x, quint8 y)
{
    return (((y & 1) * x) ^ ((y>>1 & 1) * xTime(x)) ^ ((y>>2 & 1) * xTime(xTime(x))) ^ ((y>>3 & 1)
            * xTime(xTime(xTime(x)))) ^ ((y>>4 & 1) * xTime(xTime(xTime(xTime(x))))));
}

4.4 轮密钥加(AddRoundKey)

一句话概括:状态矩阵与轮密钥进行逐字节异或(XOR)操作。

轮密钥加是将密钥混入数据的关键步骤,也是四步变换中实现上最简单的操作。如果没有这一步,AES就是一个固定的数学变换,任何人都可以解密。正是轮密钥加让加密过程依赖于用户提供的密钥,从而保证了安全性。

具体操作步骤:

  1. 取出当前状态的16个字节
  2. 取出当前轮次的16字节轮密钥
  3. 将状态和轮密钥的对应字节进行XOR
  4. 用XOR结果替换状态

什么是轮密钥?

原始密钥(16/24/32字节)通过密钥扩展算法扩展成多个轮密钥。密钥扩展算法会在下一章详细说明。对于AES-128(10轮),需要11个轮密钥(Round 0 ~ Round 10):

  • Round 0:用于初始轮(加密开始前)
  • Round 1:用于第1轮
  • Round 2:用于第2轮
  • …
  • Round 10:用于最后一轮

每个轮密钥都是16字节,与状态矩阵大小相同。

密钥扩展概述:

密钥扩展(Key Expansion)是将原始用户密钥扩展为轮密钥表的过程。对于AES-128,原始密钥16字节被扩展为176字节(11轮×16字节)。扩展算法通过循环移位、S盒代换和轮常数异或来生成新的轮密钥字,确保每一轮的轮密钥都不同。

QByteArray QAESEncryption::expandKey(const QByteArray &key, bool isEncryptionKey)
{
    // 对于AES-128: m_nk=4, m_nr=10, m_expandedKey=176
    int i, k;
    quint8 tempa[4];
    QByteArray roundKey(key);
    roundKey.resize(m_expandedKey);

    for(i = m_nk; i < m_nb * (m_nr + 1); i++) {
        // 从上一个字复制4个字节
        tempa[0] = roundKey[(i-1)*4 + 0];
        // ...

        if (i % m_nk == 0) {
            // RotWord: 循环左移1字节
            // SubWord: S盒代换
            // 异或轮常数 Rcon
        }

        if (m_level == AES_256 && i % m_nk == 4) {
            // AES-256特殊处理
            // SubWord
        }

        // 生成新字:前一个字 异或 当前字
        roundKey[i*4 + 0] = roundKey[(i-m_nk)*4 + 0] ^ tempa[0];
        // ...
    }
    return roundKey;
}

具体数值示例:

状态矩阵(16字节):        轮密钥(16字节):
┌────────┐          ┌────────┐
│ 53  04  08  12 │          │ 2A  3B  4C  5D │
│ 7C  05  09  13 │    ⊕    │ 6E  7F  88  99 │
│ 8D  06  10  14 │          │ AA  BB  CC  DD │
│ 2F  07  11  15 │          │ EE  FF  00  11 │
└────────┘          └────────┘

        ↓ 对应字节异或(XOR)

XOR后的状态矩阵:
┌────────┐
│ 79  3F  44  4F │  ← 53⊕2A=79, 04⊕3B=3F, 08⊕4C=44, 12⊕5D=4F
│ 12  7A  81  8A │  ← 7C⊕6E=12, 05⊕7F=7A, 09⊕88=81, 13⊕99=8A
│ 27  BD  DC  C9 │  ← 8D⊕AA=27, 06⊕BB=BD, 10⊕CC=DC, 14⊕DD=C9
│ C1  F8  11  04 │  ← 2F⊕EE=C1, 07⊕FF=F8, 11⊕00=11, 15⊕11=04
└────────┘

XOR的数学性质:

  • A ⊕ B = C
  • C ⊕ B = A (同一个轮密钥再异或一次就能还原)

这就是为什么解密时也使用XOR,只是轮密钥的使用顺序相反。这一性质使得轮密钥加在加密和解密中的实现完全相同。

代码实现:

void QAESEncryption::addRoundKey(QByteArray &state, const quint8 round, const QByteArray &expKey)
{
    QByteArray::iterator it = state.begin();
    for(int i = 0; i < 16; ++i)
        it[i] = static_cast<quint8>(it[i]) ^ static_cast<quint8>(expKey.at(round * 16 + i));
}

在 expKey 中,轮密钥是连续存储的:

  • Round 0 的轮密钥:expKey[0..15]
  • Round 1 的轮密钥:expKey[16..31]
  • Round 2 的轮密钥:expKey[32..47]
  • …

因此 round * 16 + i 可以正确索引到第 round 轮的第 i 个字节。

为什么要做轮密钥加?

这是将密钥混入数据的步骤。如果不做这一步,AES就是一个固定的数学变换,任何人都可以解密。正是轮密钥让加密过程依赖于用户提供的密钥,使得只有拥有正确密钥的人才能解密数据。同时,每轮使用不同的轮密钥(由密钥扩展算法生成)确保了即使攻击者知道某一轮的轮密钥,也无法推导出原始密钥或其他轮的轮密钥。

五、四步变换的作用总结

下表总结了四步变换的类型、作用和关键特性:

步骤操作类型作用关键特性
字节代换非线性制造混淆,抵抗数学分析S盒查找,基于GF(2⁸)逆运算
行移位线性实现扩散,跨行混合循环移位,按行操作
列混合线性实现扩散,列内混合GF(2⁸)矩阵乘法,按列操作
轮密钥加线性混入密钥XOR运算,依赖用户密钥

混淆与扩散(密码学之父香农提出的两大原则):

  • 混淆:让密钥和密文之间的关系尽可能复杂,攻击者无法从密文推导出密钥(字节代换实现)
  • 扩散:让明文的每一个位都影响密文的多个位,消除明文的统计特征(行移位 + 列混合实现)

六、完整的加密流程

理解了四步变换后,完整的加密过程就清晰了:

重要注意:最后一轮没有列混合!这是AES设计中的重要细节。去掉列混合是为了使加密和解密保持结构对称——如果最后一轮包含列混合,解密时就需要在开始时做逆列混合,破坏了对称性。

七、解密过程

解密是加密的逆过程,操作顺序完全相反。解密时同样使用四步变换,但用的是逆操作:

密文
  ↓
初始轮密钥加(使用轮密钥 Nr)
  ↓
第1轮(反向):
  ├── 逆行移位(InvShiftRows)
  ├── 逆字节代换(InvSubBytes)
  ├── 轮密钥加(使用轮密钥 Nr-1)
  └── 逆列混合(InvMixColumns)
  ↓
第2轮(反向):
  ├── 逆行移位
  ├── 逆字节代换
  ├── 轮密钥加(使用轮密钥 Nr-2)
  └── 逆列混合
  ↓
... 重复 ...
  ↓
最后一轮(反向):
  ├── 逆行移位
  ├── 逆字节代换
  └── 轮密钥加(使用轮密钥 0)
  ↓
明文

关键点:

  • 解密使用的轮密钥顺序与加密相反(从Nr到0)
  • 每一轮的操作是加密的逆操作(逆S盒、逆行移位、逆列混合)
  • XOR是自逆运算(A⊕B⊕B=A),所以轮密钥加在加密和解密中完全相同
  • 最后一轮同样没有逆列混合,保持结构对称

代码中的解密实现:

QByteArray QAESEncryption::invCipher(const QByteArray &expKey, const QByteArray &in)
{
    QByteArray state(in);

    // 初始轮:使用最后一个轮密钥
    addRoundKey(state, m_nr, expKey);

    // 中间轮:Nr-1轮
    for(quint8 round=m_nr-1; round>0 ; round--){
        invShiftRows(state);   // 逆行移位
        invSubBytes(state);    // 逆字节代换
        addRoundKey(state, round, expKey);  // 轮密钥加
        invMixColumns(state);  // 逆列混合
    }

    // 最后一轮:没有逆列混合
    invShiftRows(state);
    invSubBytes(state);
    addRoundKey(state, 0, expKey);  // 使用第一个轮密钥

    return state;
}

八、工作模式(Mode of Operation)

AES核心算法只能加密16字节的单个数据块。在现实应用中,我们需要加密任意长度的数据(几KB到几GB),这就需要工作模式来组织多个数据块的加密。

工作模式定义了如何将明文分割成块、如何链接各块的加密、以及如何处理最后一个不完整的块。不同的模式在安全性、并行性、错误传播等方面各有特点。

8.1 ECB模式(Electronic Codebook,电子密码本)

最简单的模式,每个数据块独立加密,互不影响。

加密过程:
明文块1 → AES加密 → 密文块1
明文块2 → AES加密 → 密文块2
明文块3 → AES加密 → 密文块3
...

解密过程:
密文块1 → AES解密 → 明文块1
密文块2 → AES解密 → 明文块2
密文块3 → AES解密 → 明文块3
...

优点:

  • 实现最简单
  • 支持并行处理(多个块同时加密/解密)
  • 一个块出错不影响其他块

致命缺陷——相同明文产生相同密文:

ECB最严重的问题是相同的明文块会加密成相同的密文块。这意味着密文会泄露明文的模式信息。

可视化示例(加密一张企鹅图片):

原图:🐧(企鹅)      ECB加密后:仍然能看到企鹅轮廓
[■□□□□■]          [■■□□■■]
[□□■■□□]    →    [□□■■□□]
[□■■■■□]          [■■■■■■]
[□■□□■□]          [■■□□■■]

虽然每个像素都被加密了,但由于相同颜色的区域产生相同的密文,图像的大致轮廓仍然清晰可见,相当于”加密了个寂寞”。

代码实现(你代码中的ECB模式):

case ECB:
    for(int i=0; i < alignedText.size(); i+= m_blocklen)
        result.append(cipher(expandedKey, alignedText.mid(i, m_blocklen)));
    break;

何时使用ECB?
几乎从不使用。只在加密单个数据块(≤16字节)且完全随机数据时才能安全使用。实际应用中应避免ECB模式。

8.2 CBC模式(Cipher Block Chaining,密码块链接)

最常用的模式,每个明文块加密前先与前一个密文块进行XOR,形成”链式”结构。

加密过程:

IV(初始向量)← 随机生成(16字节)
         ↓
明文块1 → ⊕ → AES加密 → 密文块1 → 作为下一个块的输入
                              ↓
明文块2 → ⊕ → AES加密 → 密文块2 → 继续链接...
                              ↓
明文块3 → ⊕ → AES加密 → 密文块3

第一个块没有前一个密文块,所以需要一个初始向量(IV)来启动。

解密过程:

密文块1 → AES解密 → ⊕(与IV异或)→ 明文块1
密文块2 → AES解密 → ⊕(与密文块1异或)→ 明文块2
密文块3 → AES解密 → ⊕(与密文块2异或)→ 明文块3

关键特性:

  1. 相同明文产生不同密文:只要IV不同,同一明文加密结果就不同
  2. 错误传播:一个密文块损坏会影响两个明文块(当前块和下一块)
  3. 需要填充:数据长度必须是16的倍数

IV的安全要求:

  • IV必须是随机且不可预测的
  • IV不需要保密,但必须与密文一起传输
  • IV绝对不能重复使用(同一个密钥下)

为什么IV不可预测很重要?
如果攻击者能预测IV,他可以构造特殊的明文来操纵加密过程,可能引发选择明文攻击。

代码实现(你代码中的CBC模式):

case CBC: {
    QByteArray ivTemp(iv);
    for(int i=0; i < alignedText.size(); i+= m_blocklen) {
        // 先与前一个密文块XOR(第一个块用IV)
        alignedText.replace(i, m_blocklen, byteXor(alignedText.mid(i, m_blocklen), ivTemp));
        // 然后加密
        result.append(cipher(expandedKey, alignedText.mid(i, m_blocklen)));
        // 更新反馈为当前密文块
        ivTemp = result.mid(i, m_blocklen);
    }
}

何时使用CBC?
这是最通用、最常用的模式,适用于文件加密、数据库加密、磁盘加密等大多数场景。

8.3 CFB模式(Cipher Feedback,密码反馈)

将AES转换为流式加密,不再需要填充。加密和解密都使用AES的加密方向(而不是解密方向)。

加密过程:

IV → AES加密 → 密钥流 → ⊕ → 密文块1 → 反馈到输入(作为下一块的IV)
                                  ↓
密文块1 → AES加密 → 密钥流 → ⊕ → 密文块2 → 继续...
                                  ↓
密文块2 → AES加密 → 密钥流 → ⊕ → 密文块3

解密过程(注意:同样使用AES加密方向,不是解密方向):

IV → AES加密 → 密钥流 → ⊕ → 明文块1 → 反馈(密文块1)
                                      ↓
密文块1 → AES加密 → 密钥流 → ⊕ → 明文块2 → 继续...
                                      ↓
密文块2 → AES加密 → 密钥流 → ⊕ → 明文块3

关键特性:

  1. 不需要填充:可以加密任意长度的数据(流式)
  2. 加密和解密使用相同的AES方向(都用加密函数)
  3. 错误传播:一个字节损坏会影响后续所有块(直到损坏的块移出窗口)

代码实现(你代码中的CFB模式):

case CFB: {
    QByteArray cfbFeedback(iv);
    for (int i = 0; i < alignedText.size(); i += m_blocklen) {
        QByteArray block = byteXor(alignedText.mid(i, m_blocklen), 
                                   cipher(expandedKey, cfbFeedback));
        result.append(block);
        cfbFeedback = block;  // 反馈是密文块,不是明文块
    }
}

解密:

case CFB: {
    QByteArray cfbFeedback(iv);
    for (int i = 0; i < rawText.size(); i += m_blocklen) {
        ret.append(byteXor(rawText.mid(i, m_blocklen), 
                          cipher(expandedKey, cfbFeedback)));
        cfbFeedback = rawText.mid(i, m_blocklen); // 反馈是密文
    }
}

何时使用CFB?
适用于数据流加密(如网络传输)、实时通信、任意长度数据的加密。

8.4 OFB模式(Output Feedback,输出反馈)

将AES转换为流式加密,但与CFB不同,OFB反馈的是AES的输出(密钥流)而不是密文。

加密过程:

IV → AES加密 → 密钥流1 → ⊕ → 密文块1
        ↓
密钥流1 → AES加密 → 密钥流2 → ⊕ → 密文块2
        ↓
密钥流2 → AES加密 → 密钥流3 → ⊕ → 密文块3

解密过程(与加密完全相同):

IV → AES加密 → 密钥流1 → ⊕ → 明文块1
        ↓
密钥流1 → AES加密 → 密钥流2 → ⊕ → 明文块2
        ↓
密钥流2 → AES加密 → 密钥流3 → ⊕ → 明文块3

关键特性:

  1. 不需要填充:流式加密,任意长度
  2. 加密和解密完全相同(都是生成密钥流 + XOR)
  3. 无错误传播:一个密文块损坏只影响对应的明文块
  4. 抗噪声:适合噪声信道(如无线通信)

代码实现(你代码中的OFB模式):

QByteArray QAESEncryption::xcryptOFB(const QByteArray &input, 
                                     const QByteArray &expandedKey, 
                                     const QByteArray &iv)
{
    QByteArray ofbTemp;
    ofbTemp.append(cipher(expandedKey, iv));  // 生成第一个密钥流块
    for (int i = m_blocklen; i < input.size(); i += m_blocklen)
        ofbTemp.append(cipher(expandedKey, ofbTemp.right(m_blocklen))); // 迭代生成
    return byteXor(input, ofbTemp);  // XOR得到密文/明文
}

何时使用OFB?
适用于噪声环境(如卫星通信)、需要抗错误的场景。但OFB的安全性依赖于IV的唯一性。

安全警告:同一密钥+IV组合在OFB模式下绝对不能重复使用,否则会导致密钥流重用,极易被破解。

8.5 CTR模式(Counter,计数器模式)

将AES转换为流式加密,加密一个递增的计数器来生成密钥流。

加密过程:

计数器0 → AES加密 → 密钥流0 → ⊕ → 密文块0
计数器1 → AES加密 → 密钥流1 → ⊕ → 密文块1
计数器2 → AES加密 → 密钥流2 → ⊕ → 密文块2
...

解密过程(与加密完全相同):

计数器0 → AES加密 → 密钥流0 → ⊕ → 明文块0
计数器1 → AES加密 → 密钥流1 → ⊕ → 明文块1
计数器2 → AES加密 → 密钥流2 → ⊕ → 明文块2
...

关键特性:

  1. 不需要填充:流式加密,任意长度
  2. 加密和解密完全相同(都是生成密钥流 + XOR)
  3. 支持并行处理:每个块的加密完全独立
  4. 支持随机访问:可以解密任意块而不需要解密前面的块
  5. 性能最优:可高度并行化

计数器如何管理?(你的代码实现)

QByteArray QAESEncryption::xcryptCTR(const QByteArray &input, 
                                     const QByteArray &expandedKey, 
                                     const QByteArray &iv)
{
    QByteArray result;
    QByteArray counterBlock(iv);  // 初始计数器 = IV
    for (int i = 0; i < input.size(); i += m_blocklen) {
        QByteArray keyStream = cipher(expandedKey, counterBlock);
        int blockSize = qMin(m_blocklen, input.size() - i);
        result.append(byteXor(input.mid(i, blockSize), keyStream.left(blockSize)));

        // 增量计数器(128位大端整数)
        unsigned char *ctr = reinterpret_cast<unsigned char*>(counterBlock.data());
        for (int j = m_blocklen - 1; j >= 0; --j) {
            if (++ctr[j] != 0)  // 从最低位开始递增
                break;
        }
    }
    return result;
}

计数器递增示例(简单化):

初始计数器(IV):00000000 00000000 00000000 00000001
↓ 递增
计数器1:         00000000 00000000 00000000 00000002
↓ 递增
计数器2:         00000000 00000000 00000000 00000003
...

何时使用CTR?

  • 需要并行加速的场景
  • 需要随机访问(如加密的数据库索引)
  • 网络传输(不需要填充)

CTR已成为现代加密的首选模式之一,被广泛使用在TLS 1.3等协议中。

安全警告:CTR模式中计数器绝不能重复(同一密钥下),否则密钥流重用会导致严重的安全问题。

8.6 五种模式对比总结

特性ECBCBCCFBOFBCTR
需要填充✅✅❌❌❌
并行加密✅❌❌❌✅
并行解密✅✅❌❌✅
错误传播无影响2块持续影响无无
加密≠解密是是否否否
随机访问✅❌❌❌✅
安全性❌最差✅好✅好✅好✅好

模式选择建议:

场景推荐模式原因
文件加密CBC 或 CTRCBC成熟稳定,CTR性能好
网络传输CTR不需要填充,可并行
实时音视频CFB流式,低延迟
卫星/无线通信OFB抗错误传播
数据库加密CTR支持随机访问
任何场景❌不要用ECB不安全

你代码中的模式选择:

在你的 QAESEncryption 类中,所有模式都通过 encode() 和 decode() 方法统一支持:

QAESEncryption crypto(AES_256, CBC, PKCS7);   // 使用CBC模式
QAESEncryption crypto(AES_256, CTR, NONE);    // 使用CTR模式(无填充)
QAESEncryption crypto(AES_128, ECB, PKCS7);   // ❌ 不建议

九、填充机制

由于大多数工作模式要求明文长度是16字节的倍数(ECB、CBC需要填充;CFB、OFB、CTR不需要),我们需要在加密前对数据进行填充。

9.1 PKCS#7(最常用)

填充的每个字节都等于需要填充的字节数:

数据长度:15字节 → 填充 0x01
数据长度:14字节 → 填充 0x02 0x02
数据长度:1字节  → 填充 0x0F 0x0F ... (15个0x0F)
数据长度:16字节 → 填充 0x10 x16 (整块填充)

PKCS#7的关键安全要求——常量时间验证:

PKCS#7的验证必须使用常量时间(没有分支差异),否则会遭受填充预言机攻击(Padding Oracle Attack)。你的代码采用了常量时间验证,这是非常正确的做法!

// 常量时间PKCS7验证(你的代码)
quint8 good = (padLen >= 1) & (padLen <= 16) & (padLen <= len);
for (int i = 0; i < 16 && i < len; ++i) {
    const quint8 b = ret.at(len - 1 - i);
    const quint8 inPad = (i < padLen);
    const quint8 mismatch = (b ^ padLen);
    good &= ~(inPad & (mismatch != 0));
}
if (good & 1) {
    ret.remove(len - padLen, padLen);
} else {
    // 无效填充
}

9.2 零填充(ZERO)

填充0x00,但要求原数据不能以0x00结尾(否则无法区分填充和数据)。

数据长度:14字节 → 填充 0x00 0x00

9.3 ISO/IEC 7816-4

填充一个0x80,其余填充0x00:

数据 + 0x80 + 00 00 00 ...

9.4 无填充(NONE)

不填充,仅用于流式模式(CFB、OFB、CTR)。对于ECB和CBC使用NONE填充,加密函数会拒绝执行。

十、安全建议与最佳实践

基于对AES原理和工作模式的理解,以下是实际使用中的关键建议:

10.1 模式选择

  • ✅ 优先使用CTR或CBC模式
  • ❌ 永远不要使用ECB模式

10.2 IV管理

  • 每次加密使用全新的随机IV(16字节)
  • IV不需要保密,但必须与密文一起传输
  • 同一密钥下,IV绝对不可重复

10.3 密钥管理

  • 密钥必须随机生成,使用加密安全的随机数生成器
  • 不要使用密码作为密钥(用PBKDF2派生)
  • 密钥需要安全存储,定期轮换

10.4 认证加密

  • AES只提供机密性(防窃听),不提供完整性(防篡改)
  • 对于需要防篡改的场景,应使用认证加密模式(如GCM)或单独使用HMAC
  • 你的代码使用”加密+HMAC”的模式可以提供完整保护

十一、常见问题解答

Q1: 为什么最后一轮没有列混合?

为了保持加密和解密的结构对称性。这种设计使得加密和解密的代码可以共享相同的结构,只是操作方向相反。

Q2: ECB模式为什么不安全?

相同明文块产生相同密文块,会泄露数据的模式信息。例如加密图片时,仍然能看到图像轮廓。

Q3: CBC和CTR哪个更好?

各有优势。CBC更成熟通用,CTR性能更好且支持并行。现代应用倾向于使用CTR或更先进的GCM模式。

Q4: IV和密钥有什么区别?

密钥是保密的,是安全的核心;IV是公开的,作用是确保相同明文产生不同密文。密钥需要安全存储,IV随密文一起传输。

Q5: 我可以重复使用IV吗?

绝对不行!尤其是在CTR和OFB模式下,重复IV会导致密钥流重用,加密形同虚设。

十二、总结

理解AES加密算法,从核心原理到工作模式,可以划分为四个层次:

第一层:数学基础

  • XOR运算和GF(2⁸)有限域的概念
  • S盒的查找原理

第二层:四步变换

  • 字节代换(查S盒替换,非线性,提供混淆)
  • 行移位(循环左移,扩散)
  • 列混合(GF(2⁸)矩阵乘法,扩散)
  • 轮密钥加(XOR异或,混入密钥)

第三层:工作模式

  • ECB:❌不安全,避免使用
  • CBC:✅最通用,需要填充和IV
  • CFB:流式,不需要填充
  • OFB:流式,抗错误传播
  • CTR:✅现代首选,支持并行,不需要填充

第四层:工程实践

  • 密钥管理(随机生成、安全存储、定期轮换)
  • IV管理(每次随机、不可重复)
  • 填充机制(PKCS#7最常用,需要常量时间验证)
层次知识点重要性
数学XOR、GF(2⁸)⭐⭐⭐
核心四步变换⭐⭐⭐⭐⭐
模式5种工作模式⭐⭐⭐⭐⭐
工程密钥/IV/填充管理⭐⭐⭐⭐

你手头的 QAESEncryption 代码是一个完整的、经过安全审计的Qt AES实现,覆盖了从核心算法到所有工作模式的完整功能。对照代码理解概念,是最高效的学习方式。无论是文件加密、网络传输还是密码管理,AES都能提供可靠的安全保障。

附录一:GF(2⁸) 有限域

在深入理解AES的四步变换之前,需要先掌握一个核心数学概念——GF(2⁸) 有限域。这是AES所有字节运算的”算术规则”,不理解它就无法真正理解列混合和S盒的构造。

1、什么是”域”?

域就是”一套数字 + 一套运算规则”,满足以下性质:

  • 任意两个数字相加、相减、相乘、相除(除数不为0),结果仍然在这个域里
  • 运算满足交换律、结合律、分配律
  • 每个非零数字都有”倒数”(乘法逆元)

我们熟悉的实数(全体实数)就是一个域——任意两个实数做加减乘除,结果还是实数。

2、什么是”有限域”?

有限域就是数字个数有限的域。GF(2⁸) 就是包含 256 个数字(0 到 255)的有限域。

域的类型包含的数字个数
实数域1, 2.5, π, √2, …无穷多
GF(2⁸)0, 1, 2, …, 255256个

为什么是 2⁸?

  • 因为一个字节是 8 位(bit),2⁸ = 256
  • AES 处理的最小单位就是字节,所以 GF(2⁸) 天然适合 AES

3、GF(2⁸) 的加法:XOR

在 GF(2⁸) 中,加法 = 按位异或(XOR)。

普通数学:5 + 3 = 8
GF(2⁸):  5 ⊕ 3 = 6   (0101 ⊕ 0011 = 0110 = 6)

普通数学:83 + 124 = 207
GF(2⁸):  0x53 ⊕ 0x7C = 0x2F   (83 ⊕ 124 = 47)

重要性质:

  • 每个数的”相反数”就是它自己:a ⊕ a = 0
  • 所以减法 = 加法 = XOR

4、GF(2⁸) 的乘法:多项式乘法取模

4.1 把字节看作多项式

在 GF(2⁸) 中,每个字节被表示为一个多项式,每一位代表一个系数:

0x53 = 二进制 0101 0011
     = 0×x⁷ + 1×x⁶ + 0×x⁵ + 1×x⁴ + 0×x³ + 0×x² + 1×x + 1
     = x⁶ + x⁴ + x + 1

0xCA = 二进制 1100 1010
     = x⁷ + x⁶ + x³ + x

4.2 乘法规则

两个字节相乘,分两步:

  1. 多项式相乘(普通多项式乘法)
  2. 除以固定多项式取余数,确保结果不超过 8 位

AES 使用的固定多项式(称为”不可约多项式”)是:

m(x) = x⁸ + x⁴ + x³ + x + 1
     = 十六进制 0x11B

为什么要取模?
如果不取模,两个 8 位数字相乘可能超过 8 位(如 x⁷ × x⁷ = x¹⁴),就不再是 0~255 范围内的数字了。取模运算确保了结果始终落在 0~255 范围内。

4.3 常用乘法的简化算法

在 AES 中,主要需要乘以 1、2、3 这些值:

乘以计算方法
× 1不变
× 2左移1位,如果最高位是1,则异或 0x1B
× 3(× 2) ⊕ 原值

代码中的 xTime() 函数就是实现”乘以 2″:

quint8 xTime(quint8 x)
{
    return ((x << 1) ^ (((x >> 7) & 1) * 0x1B));
}

示例:

xTime(0x53) = 0xA6   (最高位0,直接左移)
xTime(0x80) = 0x1B   (最高位1,左移后异或0x1B)

5、为什么 AES 要用 GF(2⁸)?

原因说明
正好是一个字节2⁸ = 256,刚好是 8 位二进制能表示的范围
硬件友好XOR 和移位是 CPU 最擅长的操作,速度极快
数学特性好有完整的”域”结构——加减乘除都在 0~255 内,不溢出
非线性GF(2⁸) 上的乘法逆元运算是高度非线性的,适合加密

6、AES 中用到 GF(2⁸) 的地方

AES 操作用到的 GF(2⁸) 概念
S盒构造乘法逆元 + 仿射变换
列混合(MixColumns)GF(2⁸) 矩阵乘法
逆列混合(InvMixColumns)GF(2⁸) 矩阵乘法(用逆矩阵)

7、术语对照

术语通俗解释
域一套数字 + 一套”加减乘除”规则
有限域数字个数有限的域
GF(2⁸)256个数字(0~255)的有限域
不可约多项式用来做”除法取模”的固定多项式,类似素数在整数中的作用
乘法逆元在 GF(2⁸) 中,满足 x × y = 1 的 y,就是 x 的”倒数”
xTimeGF(2⁸) 中”乘以2″的快速算法

核心认知:GF(2⁸) 里的加法和乘法不是我们熟悉的加法和乘法——加法是 XOR,乘法是多项式乘法取模。理解这一点,AES 的数学基础就通了!

附录二:S盒的数学构造原理

1、概述

S盒是AES算法中唯一非线性变换的核心,其构造基于两个数学操作:乘法逆元和仿射变换。

S盒 = 对每个字节先做”乘法逆元”,再做”仿射变换”

2、乘法逆元(Multiplicative Inverse)

2.1 普通数学中的概念

在普通数学中,一个数的乘法逆元就是它的倒数:

5 的乘法逆元是 1/5,因为 5 × 1/5 = 1
3 的乘法逆元是 1/3,因为 3 × 1/3 = 1

核心概念:一个数 × 它的逆元 = 1

2.2 在 GF(2⁸) 中的”倒数”

在 AES 使用的 GF(2⁸) 有限域中,也有类似的概念:

  • 每个字节(0~255)都有一个”逆元”
  • 逆元的定义:x × y = 1(在GF(2⁸)规则下)
  • 其中 × 是 GF(2⁸) 乘法,不是普通乘法

示例:

在 GF(2⁸) 中:
0x53 的乘法逆元是 0xCA
验证:0x53 × 0xCA = 0x01 (在 GF(2⁸) 规则下)

0x00 比较特殊:它没有乘法逆元(就像普通数学中 0 没有倒数)

2.3 为什么要做乘法逆元?

乘法逆元是一个高度非线性的运算。在 GF(2⁸) 上,输入和输出之间的关系非常复杂,没有简单的线性公式可以描述。这正好满足了密码学对”非线性”的要求。

3、仿射变换(Affine Transformation)

3.1 什么是仿射变换?

仿射变换 = 线性变换 + 平移

在 AES 的 S 盒中,仿射变换的具体形式是:

输出位 = (输入位的某种组合) XOR (常数)

具体来说,对8位输入 x(位表示为 x0, x1, ..., x7,其中 x0 是最低位),经过仿射变换得到8位输出 y:

y0 = x0 ⊕ x4 ⊕ x5 ⊕ x6 ⊕ x7 ⊕ 1
y1 = x1 ⊕ x5 ⊕ x6 ⊕ x7 ⊕ x0 ⊕ 1
y2 = x2 ⊕ x6 ⊕ x7 ⊕ x0 ⊕ x1 ⊕ 0
y3 = x3 ⊕ x7 ⊕ x0 ⊕ x1 ⊕ x2 ⊕ 0
y4 = x4 ⊕ x0 ⊕ x1 ⊕ x2 ⊕ x3 ⊕ 0
y5 = x5 ⊕ x1 ⊕ x2 ⊕ x3 ⊕ x4 ⊕ 1
y6 = x6 ⊕ x2 ⊕ x3 ⊕ x4 ⊕ x5 ⊕ 1
y7 = x7 ⊕ x3 ⊕ x4 ⊕ x5 ⊕ x6 ⊕ 0

3.2 矩阵形式表示

仿射变换也可以写成矩阵乘法加常数的形式:

┌   ┐   ┌                        ┐   ┌   ┐   ┌   ┐
│y0 │   │ 1  0  0  0  1  1  1  1 │   │x0 │   │ 1 │
│y1 │   │ 1  1  0  0  0  1  1  1 │   │x1 │   │ 1 │
│y2 │   │ 1  1  1  0  0  0  1  1 │   │x2 │   │ 0 │
│y3 │ = │ 1  1  1  1  0  0  0  1 │ ×│x3 │ ⊕│ 0 │
│y4 │   │ 1  1  1  1  1  0  0  0 │   │x4 │   │ 0 │
│y5 │   │ 0  1  1  1  1  1  0  0 │   │x5 │   │ 1 │
│y6 │   │ 0  0  1  1  1  1  1  0 │   │x6 │   │ 1 │
│y7 │   │ 0  0  0  1  1  1  1  1 │   │x7 │   │ 0 │
└   ┘   └                        ┘   └   ┘   └   ┘

3.3 为什么要做仿射变换?

乘法逆元虽然是非线性的,但如果只做乘法逆元,它有一个特殊性质:逆元(0) = 0(因为0没有逆元,需要特殊处理为0)。仿射变换可以打散这个特殊性质,使得S盒具有以下重要特性:

特性含义安全性作用
无固定点S[x] ≠ x 对所有x成立抵抗代数攻击
无反固定点S[x] ≠ ~x 对所有x成立抵抗代数攻击

4、S盒的完整构造过程

S盒的构造分为两步:

输入字节 x
    ↓
第一步:求乘法逆元(在 GF(2⁸) 中)
    x → x⁻¹
    (0 的特殊处理:0 → 0,因为0没有逆元)
    ↓
第二步:仿射变换
    x⁻¹ → 仿射变换 → S盒输出

完整示例:计算 S盒[0x53]

输入: 0x53
    ↓
第一步(乘法逆元):
在 GF(2⁸) 中,0x53 的逆元 = 0xCA
    ↓
第二步(仿射变换):
对 0xCA 做仿射变换
    ↓
输出: 0xED

5、直观类比

步骤类比
乘法逆元像”倒影”——每个数字都有一个对应的”镜像”,这个映射关系很复杂、没有规律
仿射变换像”打乱”——把倒影再做一个特定的旋转和位移,让结果更加难以预测
两者结合先复杂映射,再打乱,结果就是完全混乱的替换表

乘法逆元就像把每个人用”数学魔镜”照出另一个形象。仿射变换就像用”哈哈镜”再扭曲一次。结果就是:没人能通过看输入猜到输出是什么。

6、为什么S盒不能随便改?

S盒的设计基于这两个数学操作,它们经过精心选择,确保了以下安全特性:

特性含义为什么重要
非线性输入输出没有线性关系抵抗线性密码分析
无固定点S[x] ≠ x 对所有x成立抵抗代数攻击
无反固定点S[x] ≠ ~x 对所有x成立抵抗代数攻击
差分均匀性输入变化和输出变化关系复杂抵抗差分密码分析

如果随便改S盒,这些数学特性就消失了,AES的安全性会大打折扣。

7、验证:标准S盒是否遵循此构造?

是的!标准S盒就是按照”乘法逆元 + 仿射变换”计算出来的。验证几个值:

x = 0x00 → 逆元=0x00 → 仿射变换 → 0x63  ✅ S盒[0] = 0x63
x = 0x01 → 逆元=0x01 → 仿射变换 → 0x7C  ✅ S盒[1] = 0x7C
x = 0x53 → 逆元=0xCA → 仿射变换 → 0xED  ✅ S盒[0x53] = 0xED

8、附录二总结

概念一句话解释在S盒中的作用
乘法逆元GF(2⁸)中的”倒数”提供非线性,让输入输出关系复杂
仿射变换线性变换 + 平移打散特殊性质,消除固定点
S盒逆元 + 仿射变换最终得到256字节的替换表

关键结论:使用者不需要自己计算S盒!标准S盒已经定义好了,直接使用即可。但理解其构造原理,有助于理解为什么S盒是这样的、为什么不能随便改、以及AES的安全性从何而来。

作者

老丹

关注我
其他文章
上一个

网络工具中的“瑞士军刀”——Netcat完全指南

下一个

TOML:为人类而生的配置文件语言

关于博主

    老丹是一名C/C++后台开发工程师,信奉“无抽象不设计,无性能不生产”。

  • 技术栈:Modern C++、Linux环境编程、多线程/并发、网络编程等。
  • 信条:能用constexpr解决的问题绝不拖到运行时,能靠RAII避免的泄漏绝不写析构。
  • 正在填坑:从解封装到渲染的C++全链路实现,正在驯服FFmpeg与H.264/H.265。
  • 输出原则:这里的每一段代码都经过-Wall -Wextra -Werror -O2的洗礼。

近期文章

  • Linux系统的安全基石:深入理解可插拔认证模块(PAM) 2026年7月27日
  • vsftpd 完全指南:从核心原理到Docker容器化部署 2026年7月27日
  • 互联网的”导航”安全卫士:深入解读DNSSEC 2026年7月27日
  • Ubuntu DNS 配置完全指南 2026年7月27日
  • 在 Ubuntu 中使用 Certbot 的操作指南 2026年7月27日

文章分类

  • C/C++开发 (13)
  • Docker容器 (3)
  • Linux工具包 (10)
  • Linux服务配置 (33)
  • Linux系统 (10)
  • OpenWrt路由 (2)
  • Shell脚本 (3)
  • 安防技术 (4)
  • 数据安全 (30)
  • 网络协议 (17)
  • 计算机理论 (22)
联系我们:📍 地址:中国·广东省深圳市   |   ✉️ 邮箱:support@tanglinux.com   |   💬 QQ:870866607
版权所有:老丹的足迹粤ICP备2026061170号-1       公安备案图标 粤公网安备44030002013274号