AES加密原理详解:从S盒、列混合到密钥扩展的完整流程
很多人天天在用 AES——加密压缩包、聊天软件的消息、浏览器里的 HTTPS 传输真被问一句“AES 的原理到底是什么、加密过程分几步走”能讲清楚的人其实不多。我见过不少后端开发者接口调了好几年一看到 S 盒、列混合、密钥扩展这几个词就头皮发麻。这篇文章就专门把这些东西拆开揉碎从 AES 的原理和完整加密过程讲起目标是让一个只写过业务代码的人也能顺着思路推一遍加密流程。内容适合完全没接触过密码学的初学者也适合“只会调用加密库、不明白内部发生了什么”的开发者。看的时候建议拿张纸和笔遇到关键计算跟着画一画效果比你单纯划屏幕好得多。1. 为什么绕不开 AES先搞清楚它在加密世界里扮演什么角色1.1 对称加密到底是怎么回事AES 的全称是 Advanced Encryption Standard中文一般叫高级加密标准。它属于对称加密算法——也就是说加密和解密用的是同一把密钥。你可以把对称加密想象成一把钥匙锁一个箱子发件人用钥匙把明文锁进箱子收件人用同一把钥匙开箱取出明文。整个过程只有一把钥匙谁拿到它谁就能看到内容。对称加密和非对称加密的分工不一样。非对称加密虽然解决了“钥匙怎么安全地送到对方手里”的问题但性能开销非常大通常只用来协商密钥、做数字签名这类小数据量场景。真正到了传输视频、文件、接口报文这种海量数据的场景靠非对称加密硬扛是不现实的必须用对称加密。而 AES 就是目前应用最广泛的对称分组密码几乎所有你接触到的加密传输、磁盘加密、Wi-Fi 加密、软件加密锁里都有它的影子。AES 这个标准的诞生源于一次算法公开征集。当时老一代分组加密标准已经暴露出密钥过短的问题急需一个安全、高效、能在各种设备上落地的新标准。最终胜出的算法由两位比利时密码学家设计和提交2001 年正式发布这就是今天我们说的 AES。1.2 AES 的基本参数分组、密钥和轮数AES 有三个硬性参数先背下来再说分组长度固定 128 位也就是 16 字节。无论你用的是哪种密钥长度每一次加密都只处理 16 字节明文。密钥长度支持 128 位、192 位、256 位三种对应 AES-128、AES-192、AES-256。轮数跟密钥长度挂钩AES-128 跑 10 轮AES-192 跑 12 轮AES-256 跑 14 轮。算法版本密钥长度位分组长度位加密轮数AES-12812812810AES-19219212812AES-25625612814很多人会问轮数是不是越多越好其实不是。轮数越多扩散效果越好但性能越差而且不是线性提升安全性。AES-128 到今天都没有已知的可行破解手段AES-256 更多是为未来量子计算场景和特定合规要求准备的冗余。10、12、14 这三个数字是设计者在安全性和实现效率之间取的一个平衡点标准发布后经历了全世界密码学家的多年分析目前依然稳固。需要注意的是AES 的分组长度固定是 128 位这一点和 Rijndael 原始算法有点区别。原始算法支持更多种分组长度但最终成为标准的版本只固定保留了 128 位分组。所以你在任何资料里看到 AES 分组长度都默认是 16 字节。2. AES 的宏观结构一次处理一个 128 位的“状态”块2.1 16 字节如何排成一个矩阵AES 加密的第一步是把 16 字节明文按列填入一个 4×4 字节矩阵这个矩阵叫“状态”。注意是按列填不是按行填。假设明文是P0 P1 P2 ... P15那么状态是这样排列的列0列1列2列3P0P4P8P12P1P5P9P13P2P6P10P14P3P7P11P15为什么要搞一个矩阵出来因为后面的行移位、列混合操作都是在这个二维结构上进行的。把字节线性排列成矩阵是分组密码最常见的组织方式它能让我们用“行”和“列”两个维度来设计扩散逻辑。如果你的数据长度正好是 16 字节的整数倍那每一块都这样处理。如果最后一块不够 16 字节就需要填充。至于怎么填充、用哪种填充方案AES 标准本身不规定这是由上层分组模式决定的后面实操部分会具体提。2.2 SPN 结构与老式 Feistel 结构的差异AES 采用的是 SPN 结构——代入-置换网络。这个名字听着玄乎其实就是两个动作不断组合替换S 盒和置换行移位、列混合。以前很多老式分组加密采用的是 Feistel 结构。Feistel 结构的一大特点是每一轮只把半个数据块送入变换函数另外一半原封不动地直接传给下一轮。这样做的优点是加密和解密流程天然对称实现简单缺点是扩散速度慢需要更多轮数才能把影响铺满整个数据块。AES 的 SPN 结构则是每一轮对全部 16 字节都动手没有“保留一半不动”的说法。这样做的好处是扩散更快、更彻底——明文一个字节发生变化经过几轮之后整个密文块都会剧烈变化这就是常说的“雪崩效应”。打个比方Feistel 像是一边揉面一边留一半不动揉半天才均匀SPN 则是把整块面翻来覆去全方位揉几圈下来就混匀了。AES 用更少的轮数就达到了老算法更多轮的扩散效果这是它在性能上胜出的一个重要原因。3. 轮函数四部曲加密过程的四个核心动作AES 每一轮加密都要执行四个固定步骤顺序是字节代换 → 行移位 → 列混合 → 轮密钥加。我建议你直接把它记成“换、移、混、加”四个字所有 AES 的轮函数都是这四个动作的组合。3.1 SubBytes用 S 盒做非线性替换第一步是字节代换也叫 SubBytes。这一步是 AES 里唯一引入非线性的地方也是最核心的安全保障。具体做法是状态矩阵里的每一个字节通过查 S 盒表替换成另一个字节。S 盒是一个 16×16 的查找表总共 256 个输入输出对。查表规则很简单字节的高 4 位作为行号字节的低 4 位作为列号交叉位置的值就是替换结果。举个例子状态里有一个字节是0x53。高 4 位是5低 4 位是3查 S 盒第 5 行第 3 列得到0x1A。于是这一格就从0x53变成了0x1A。S 盒的几个经典映射值可以帮你验证自己的查表逻辑0x00 → 0x630x01 → 0x7C0x10 → 0xCA。如果你用什么开源实现把完整 S 盒打印出来看到的第一个值一定是0x63。S 盒不是随便拍脑袋造的。它是通过有限域 GF(2^8) 上的乘法逆元加一个仿射变换构造出来的目的就是让输入和输出之间不存在简单的线性关系。为什么强调非线性因为如果整个加密过程所有步骤都是线性的那加密系统就成了一个巨型线性方程组攻击者拿到几组明文密文对就能把密钥解出来。非线性变换的存在使得这种代数攻击基本没有可行路径。读者不需要背 S 盒所有加密库里都已经内置好了这张表。但你需要理解一个事实S 盒是一个双射也就是一个萝卜一个坑——输入和输出是一一对应的。正因为这样解密的时候才能用逆 S 盒把字节换回去。3.2 ShiftRows让每一列的消息开始“串门”第二步叫行移位ShiftRows。这一步没有任何查表运算纯粹是搬动数据在状态矩阵里的位置。规则如下第 0 行不动第 1 行循环左移 1 字节第 2 行循环左移 2 字节第 3 行循环左移 3 字节什么叫循环左移就是超出左边界的字节从右边补回来。假设状态矩阵如下列0列1列2列3876E46A6F24CD73B8F5A910E2977A35C经行移位后变成列0列1列2列3876E46A64CD73BF2910E8F5A5C2977A3仔细观察你会发现原来在第一列的F2跑到了第四列3B跑到了第二列等等。这一步的意义在于让不同列之间的字节开始互相“串门”为下一步列混合打好基础。单独做行移位本身不产生混淆效果它的价值在于配合列混合。行移位相当于重新洗牌洗完之后列混合再对这个新牌面做整体运算两步叠加才能实现跨列的彻底扩散。3.3 MixColumns有限域乘法里真正的“搅浑水”第三步是列混合MixColumns也是 AES 里数学味道最浓的一步。很多人啃 AES 原理啃到这一步就放弃了我会尽量讲得直白一些。列混合的操作对象是状态的每一列。每一列的 4 个字节要和 AES 规定的固定 4×4 矩阵相乘得到一个新的 4 字节列。这个乘法矩阵是[2 3 1 1] [1 2 3 1] [1 1 2 3] [3 1 1 2]注意这里的乘法不是普通整数乘法而是定义在有限域 GF(2^8) 上的运算。如果你刚接触这个概念可以暂时把它理解为一种特殊的二进制运算规则字节在运算中要被当作多项式来处理加法和减法都统一成异或乘法则是“多项式相乘取模”。实际写代码时没有人真的去手动算多项式乘法。业界一般用两种方式实现要么预生成 4KB 的查询表查表加速要么用xtime算法手算。xtime的规则很好记一个字节乘以 2相当于左移一位如果左移后溢出原最高位是 1再异或一个固定值0x1B乘以 3就等于乘以 2 的结果异或原字节。我用一个具体例子走一遍。假如状态某一列是87 6E 46 A6我们要算新列的第一行新值 2*0x87 ⊕ 3*0x6E ⊕ 1*0x46 ⊕ 1*0xA6逐步拆开2*0x870x87左移一位得0x10E取低 8 位是0x0E因为最高位溢出再异或0x1B得0x15。3*0x6E先算2*0x6E 0xDC左移后没有溢出再异或0x6E得到0xB2。1*0x46就是0x46。1*0xA6就是0xA6。最后把这些结果全部异或0x15 ⊕ 0xB2 ⊕ 0x46 ⊕ 0xA6 0x47这一列四行全部算完后结果分别是行索引计算结果第 0 行0x47第 1 行0x1F第 2 行0x74第 3 行0x5E列混合的结果是新列里的每一个字节都依赖于原列的全部 4 个字节。也就是说只要原列里有一个字节变了新列的 4 个字节全会跟着变。这正是 AES 扩散性的主要来源。为什么矩阵系数偏偏选 1、2、3 这几个数核心原因是这个矩阵是一个 MDS 矩阵具备很好的可逆性和扩散性。可逆意味着解密时能用逆矩阵还原数据MDS 性质则保证了“输入不同输出差异最大化”让差分攻击变得极其困难。3.4 AddRoundKey唯一直接“动用钥匙”的一步第四步叫轮密钥加AddRoundKey。它的操作可以用一句话概括把状态矩阵和轮密钥矩阵做逐字节异或。异或运算是可逆的而且性能极高。一个字节和密钥字节异或一次解密时再和同一个密钥字节异或一次就能完全还原。这也是整个加密过程中唯一一步直接使用密钥的地方——字节代换、行移位、列混合这三步都是公开固定的操作不需要任何钥匙信息。为什么要在轮函数里安排这一步你可以这样理解如果没有轮密钥加那加密过程就是一套完全公开、固定不变的变换任何人都可以预先把变换结果算出来。轮密钥加的职责就是用密钥给这套公开变换“注入随机性”让不同密钥下的加密结果完全不同。在实际执行中AES 在进入第一轮之前会先做一次初始的轮密钥加这个动作在密码学里叫“白化”。它保证了攻击者面对的第一层数据就已经被密钥影响而不是把原始明文直接暴露给单元变换分析。到这里一轮加密的四个步骤就齐了换、移、混、加。一轮做完状态矩阵已经被替换、重排、混合并且混入了密钥。下一轮再把当前状态送进 S 盒继续“换移混加”如此循环十轮。4. 密钥扩展一把主密钥怎么变成 11 把轮密钥4.1 为什么轮密钥不能直接复用主密钥现在你一定有个疑问每一轮都要做轮密钥加一共 11 次初始白化加十轮是不是把同一把主密钥反复拿来异或就行了当然不行。如果每轮都用同一个密钥做异或那么不同轮之间的密钥差异就是零攻击者可以利用这种轮间相关性构造攻击业界称之为“相关密钥攻击”。所以 AES 需要一套密钥扩展算法从一把主密钥派生出 11 组互不相同的轮密钥。以 AES-128 为例主密钥是 16 字节可分成 4 个 32 位字。扩展后的轮密钥总量是(10 1) × 16 176字节也就是 44 个字。初始白化用前 4 个字第 1 轮用接下来 4 个字第 2 轮再用接下来 4 个字……直到第 10 轮用完最后 4 个字。密钥扩展算法是可逆的也就是说加密解密都需要它。加密的时候我们按顺序生成轮密钥依次使用解密的时候把轮密钥倒过来用就行。实际工程里一般预设好全部轮密钥需要时直接取不重复计算。4.2 扩展算法里的 RotWord、SubWord 与 RconAES-128 的密钥扩展核心逻辑可以这样描述。先把 16 字节主密钥切成 4 个字w[0]、w[1]、w[2]、w[3]。之后每生成一个新的 4 字节字都遵循一条规则w[i] w[i-4] ⊕ g(w[i-1])特殊之处在于g函数它只在每个密钥组的第一个字位置也就是i是 4 的倍数时触发。g函数对输入字执行三个操作RotWord把 4 个字节循环左移 1 字节。比如A B C D变成B C D A。SubWord把 4 个字节分别送进 S 盒做字节代换。异或轮常量 Rcon把结果的首字节和当前轮序号对应的常量异或。伪代码大致长这样# AES-128 密钥扩展伪代码 # Nk 4, Nb 4, Nr 10 # w 是长度为 44 的 4 字节字数组 # 初始w[0..3] 直接用主密钥拆出来的 4 个字 for i in range(4, 44): temp w[i - 1] if i % 4 0: temp SubWord(RotWord(temp)) temp[0] ^ Rcon[i // 4] w[i] w[i - 4] ^ temp这里的Rcon是一个常量表按标准规定前十个值是01 02 04 08 10 20 40 80 1B 36这个序列不是随手写的。它由有限域里的xtime迭代产生作用是让每一轮密钥都带上不同的“位置标记”。如果没有它密钥扩展就会呈现明显的周期规律轮密钥之间会变得高度相似安全强度大打折扣。关于密钥扩展网上很多简化版伪代码都漏了一个细节当使用 AES-256 时Nk8除了每组首字的 g 函数之外组内第 4 个位置的字节也要额外做一次 SubWord。这个很容易踩坑。如果你照着简化版代码实现 AES-256拿标准测试向量一比对结果多半对不上。到时候先检查这里而不是怀疑自己 S 盒写错了。5. 完整加密过程串一遍从明文块到密文块的十轮旅程5.1 一轮流程和完整十轮的差异现在可以把前面所有零件组装起来了。AES 加密一块 16 字节明文完整流程是这样的初始轮密钥加先把明文状态和w[0..3]这 4 个字异或。重复 9 次完整轮每轮依次执行 SubBytes → ShiftRows → MixColumns → AddRoundKey分别使用w[4..7]、w[8..11]、……、w[36..39]。第 10 轮最后一轮执行 SubBytes → ShiftRows → AddRoundKey使用最后的w[40..43]。你会发现最后一轮少了一个步骤MixColumns。这不是疏漏而是 AES 标准刻意设计的。去掉最后一轮列混合不会削弱安全性却能让解密流程在结构上和加密保持一种对称关系这样硬件实现时能用同一套模块正向跑加密、反向跑解密实现成本大大降低。所有轮数跑完之后把状态矩阵按列重新读出来就是 16 字节密文。5.2 手工推演一个字节和一个列混合的完整计算完整的十轮手工计算不现实但我们可以推演其中两个关键计算理解度会瞬间提升一个档次。S 盒查表推演假设初始轮密钥加之后状态矩阵某个格子恰好是0x53进入 SubBytes 后查 S 盒第 5 行第 3 列得到0x1A。这个格子就变成了0x1A。整个过程就是这么单纯。列混合推演假设某列进入列混合前是87 6E 46 A6我们要计算新列第一行。上面已经演示过完整步骤核心就是用xtime规则拆解乘法。新值 2*0x87 ⊕ 3*0x6E ⊕ 0x46 ⊕ 0xA6 0x15 ⊕ 0xB2 ⊕ 0x46 ⊕ 0xA6 0x47我强烈建议你拿纸笔自己把另外三行也算一遍然后和前面表格里的0x1F、0x74、0x5E对照。亲手算过一次之后你对“有限域乘法”这几个字的恐惧感会彻底消失。提示列混合乘法中的异或满足交换律你可以先算任意两个字节的异或结果再逐个合并。顺序不影响最终答案挑自己顺手的方式算。5.3 解密过程就是“倒着放一遍”AES 解密没有神秘的地方就是把加密步骤完全倒过来。加密顺序是SubBytes → ShiftRows → MixColumns → AddRoundKey。解密则反过来先做 AddRoundKey使用逆序轮密钥再做 InvShiftRows把循环左移变成循环右移再做 InvSubBytes用逆 S 盒最后做 InvMixColumns用逆矩阵。这里有一个很容易绕晕的点轮密钥的使用顺序也是倒过来的。加密时最后用的那一组轮密钥解密时最先用加密时最先用的初始轮密钥解密时最后用。另外AddRoundKey 这一步自反——同一个轮密钥异或两次就能还原所以解密时不存在“逆轮密钥加”做一次一样的异或即可。如果你感兴趣可以发现 AES 的解密流程其实可以经过数学变换重新排成一个和加密结构非常相似的样子轮密钥和列混合的顺序做一些调整。这是实现优化的空间但理解层面你只需要记住“反着来”这一个原则就够了。6. 新手误区与实用避坑清单6.1 常见误区速查表我接触过不少 AES 使用者和初学者下面这些误区出现的频率非常高整理成表方便你自查常见误区真相建议做法认为 AES 密文越“怪”越安全安全性取决于密钥强度和实现方式用标准库、标准模式别自造算法同一个 IV 或 nonce 反复使用在 CTR/GCM 模式下是灾难级漏洞IV、nonce 必须保证唯一以为 AES-128 很容易被破解目前没有任何可行的实际攻击合规要求没有特殊说明时 AES-128 足够以为“加密了”就等于“防篡改”AES 只保证机密性不保证完整性使用 GCM、CCM 等带认证的模式拿 AES 当哈希用AES 可逆不能做完整性摘要完整性校验请用哈希算法自己组合替换、移位来“增强” AES标准之外的改动反而可能引入漏洞永远使用经过验证的标准实现这里特别想多说一句很多人觉得 AES 加了密就万事大吉其实“加密”和“防篡改”是两码事。有人把传输报文用 CBC 模式加密后发现中间人篡改了密文接收方解出来的是一堆乱码或含义被悄悄改掉的文字。CBC 模式本身不提供完整性保护你需要额外的认证手段。这也是现在工业界越来越推荐 GCM 模式的原因——它把加密和完整性校验合到一起做了。6.2 关于分组模式ECB 为什么被嫌弃AES 独立的一块 16 字节加密只是“分组密码”的骨架真正使用的时候还要解决一个关键问题数据长度往往超过 16 字节到底怎么切块、怎么把多块结果关联起来这就引出了分组模式。你可以理解为分组密码是零件分组模式是流水线两者配合才能处理任意长度的真实数据。最常见的反面教材是 ECB 模式。ECB 把所有明文块独立加密同样的明文块永远得到同样的密文块。这个特性听起来无害实际会泄露大量结构信息。一个最经典的演示——把一张位图明文用 ECB 模式加密得到的密文图里你依然能看出原图的轮廓。因为图像里相同颜色区域被加密成了相同密文整体的形状信息完全暴露了。所以在新手阶段直接记住两个结论不要用 ECB推荐使用 GCM 模式。GCM 不仅不泄露结构还自带完整性认证是目前安全性和易用性平衡最好的 AEAD 方案之一。7. 动手环节用 Python 跑通 AES-GCM 加解密7.1 最小可运行代码原理讲了这么多不上手跑一遍总觉得缺了点什么。我用 Python 的密码学库演示一个最小可运行的 AES-GCM 加解密示例整个过程不需要关心填充问题。import os from Crypto.Cipher import AES # AES-256 密钥32 字节随机数据 key os.urandom(32) # GCM 推荐使用 12 字节 nonce nonce os.urandom(12) plaintext bAES principle demo: the quick brown fox jumps over the lazy dog # 加密 cipher AES.new(key, AES.MODE_GCM, noncenonce) ciphertext, tag cipher.encrypt_and_digest(plaintext) # 解密 dec AES.new(key, AES.MODE_GCM, noncenonce) message dec.decrypt_and_verify(ciphertext, tag) print(message.decode())几个要点key是加密的核心机密绝不能泄露。代码里用os.urandom(32)生成实际生产环境应该走密钥管理系统。nonce不需要保密可以随密文一起传输但绝不能重复使用。哪怕同一个密钥下用了两次相同的 nonce也会导致严重的密码学灾难。tag是认证标签用于验证密文在传输过程中是否被篡改。解密时如果密文或标签不对decrypt_and_verify会直接抛异常。7.2 三个帮助你理解 AES 的小实验读十遍原理不如做三个小实验。建议你在上面代码基础上改一改亲眼看看现象。实验一改一个密文字节看会发生什么。把ciphertext的最后一个字节改成别的值再运行解密。你会发现解密直接抛出异常提示认证失败。这就是 GCM 的认证能力——密文一旦被动过手脚系统立刻报警。实验二换一个 nonce加密同样的明文。固定同一把key换一个nonce对完全相同的一段明文加密。你会发现两次结果完全不同。这不是因为随机而是因为 nonce 的变化改变了加密的初始化状态所以相同明文得到了不同密文。这更反衬出 nonce 唯一性的重要——如果 nonce 重用这种“每块不一样”的保障就会失效。实验三故意用一个错误的密钥解密。把key的最后一个字节改掉然后用这个错误密钥去解密。你会发现解密抛出异常或得到完全无法辨认的乱码。AES 对密钥极度敏感一个比特的差异都会让密文面目全非。做完这三个实验你对 AES 的“密钥敏感性”和 GCM 的“完整性保护”会有切肤的体感这比任何图表都更能帮你建立直觉。8. 最后说点我的实操体会我最初啃 AES 原理时也卡在 S 盒和列混合这两块很久。后来发现一个笨办法特别管用把四个轮函数缩成“换移混加”四个字口头反复念念到形成条件反射。之后再去看那些论文和标准文档突然就顺了。有一件事我印象很深第一次手动算列混合因为xtime里“溢出异或 0x1B”这个条件总忘算出来的结果对照标准测试向量完全对不上。排查了半天才发现0x87左移后最高位溢出了需要额外异或。这个坑如果你也在手推千万注意观察每个字节的最高位。我的建议是不要停留在“调库成功”的成就感上。找个周末下午把状态矩阵排出来手动推完一轮再用库里的结果对照一下。这个过程会狠狠刷新你对密码学的认知。之后无论你是去看更底层的高性能实现还是研究侧信道攻击与防护都会觉得前面有清晰的地图。AES 是整个密码学世界里最适合作为深度起点的算法没有之一。