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-128 | 16字节(128位) | 10轮 | 足够抵御所有已知攻击,暴力破解需10³⁸年 |
| AES-192 | 24字节(192位) | 12轮 | 更高安全余量,适用于长期机密 |
| AES-256 | 32字节(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⁸)上的数学运算(乘法逆元+仿射变换)构造,使得加密过程具有非线性特性,能够抵抗差分密码分析和线性密码分析。
具体操作步骤:
- 取出状态矩阵中的一个字节(例如
0x53) - 将这个字节分成两半:高4位和低4位
0x53→ 高4位 =0x5(行号),低4位 =0x3(列号)
- 用这两个值作为行索引和列索引,在S盒中查找
- 查第
0x5行第0x3列 → 得到0xED
- 用查到的值替换原来的字节
0x53→0xED
- 对状态矩阵中的全部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⁸)上的矩阵乘法进行混合,使得单个字节的变化能够扩散到整列。
具体操作步骤:
- 取出状态矩阵中的一列(4个字节)
- 将这4个字节看作一个列向量
- 用固定的4×4矩阵与之相乘(在GF(2⁸)有限域上)
- 得到新的4个字节,替换原来的列
- 对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));
}
逻辑分解:
x << 1:左移一位(相当于乘以2)(x >> 7) & 1:提取最高位(第7位)- 如果最高位是1,异或
0x1B(因为多项式溢出需要修正) - 这实现了 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就是一个固定的数学变换,任何人都可以解密。正是轮密钥加让加密过程依赖于用户提供的密钥,从而保证了安全性。
具体操作步骤:
- 取出当前状态的16个字节
- 取出当前轮次的16字节轮密钥
- 将状态和轮密钥的对应字节进行XOR
- 用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 = CC ⊕ 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
关键特性:
- 相同明文产生不同密文:只要IV不同,同一明文加密结果就不同
- 错误传播:一个密文块损坏会影响两个明文块(当前块和下一块)
- 需要填充:数据长度必须是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
关键特性:
- 不需要填充:可以加密任意长度的数据(流式)
- 加密和解密使用相同的AES方向(都用加密函数)
- 错误传播:一个字节损坏会影响后续所有块(直到损坏的块移出窗口)
代码实现(你代码中的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
关键特性:
- 不需要填充:流式加密,任意长度
- 加密和解密完全相同(都是生成密钥流 + XOR)
- 无错误传播:一个密文块损坏只影响对应的明文块
- 抗噪声:适合噪声信道(如无线通信)
代码实现(你代码中的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
...
关键特性:
- 不需要填充:流式加密,任意长度
- 加密和解密完全相同(都是生成密钥流 + XOR)
- 支持并行处理:每个块的加密完全独立
- 支持随机访问:可以解密任意块而不需要解密前面的块
- 性能最优:可高度并行化
计数器如何管理?(你的代码实现)
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 五种模式对比总结
| 特性 | ECB | CBC | CFB | OFB | CTR |
|---|---|---|---|---|---|
| 需要填充 | ✅ | ✅ | ❌ | ❌ | ❌ |
| 并行加密 | ✅ | ❌ | ❌ | ❌ | ✅ |
| 并行解密 | ✅ | ✅ | ❌ | ❌ | ✅ |
| 错误传播 | 无 | 影响2块 | 持续影响 | 无 | 无 |
| 加密≠解密 | 是 | 是 | 否 | 否 | 否 |
| 随机访问 | ✅ | ❌ | ❌ | ❌ | ✅ |
| 安全性 | ❌最差 | ✅好 | ✅好 | ✅好 | ✅好 |
模式选择建议:
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 文件加密 | CBC 或 CTR | CBC成熟稳定,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, …, 255 | 256个 |
为什么是 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 乘法规则
两个字节相乘,分两步:
- 多项式相乘(普通多项式乘法)
- 除以固定多项式取余数,确保结果不超过 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 的”倒数” |
| xTime | GF(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的安全性从何而来。