资讯动态

AES-128加密流程手算全解:从S盒替换到密钥扩展的工程实践

发布时间:2026/10/14 18:14:59 来源:尧图企业网站定制
写过加解密模块的同行应该都有同感刚接触 AES 时网上教程不少可一到关键步骤就含糊了。要么扔一张 S 盒表让你自己查要么丢一句“列混淆就是矩阵乘法”然后没了下文。真正自己动手推一遍才发现每个环节都有细节而细节不对密文就对不上。这篇文章我要用一组标准测试向量把 AES-128 的加密流程从头到尾手算一遍从状态矩阵、轮密钥加、S 盒替换、行移位、列混淆到密钥扩展每一步都会给出可核对的中间结果。最后再用 Python 把完整流程跑通并补充填充模式、工作模式和跨语言实现时容易踩的坑。适合三种人看正在学密码学的学生、要写或维护加解密模块的开发者以及“会调库但心里没底”的技术爱好者。1. 先弄清楚AES到底在解决什么问题1.1 对称加密的直觉模型加密这件事本质上可以理解成“给一段数据上锁”。锁的钥匙只有一个加密用这把钥匙解密也用这把钥匙这就是对称加密的基本模型。AES 的全称是 Advanced Encryption Standard中文一般叫高级加密标准它属于对称分组密码。你可以把 AES 想成一个处理“标准尺寸数据块”的加工车间车间每次接收 16 个字节的原料经过十轮左右的复杂工序输出 16 个字节的产物。原料就是明文产物就是密文。整个加工过程中车间内部的每一道工序都完全由密钥控制——没有密钥你就算知道工序流程也逆不出原文。为什么需要 AES因为早期对称算法比如 DES密钥长度太短暴力破解成本已经变得可以接受。AES 的出现就是为了替代 DES它支持更长的密钥、更规整的数学结构而且在软硬件实现上都有不错的性能。到今天AES 已经成为信息加密领域事实上的标准从网页 HTTPS 到硬盘全盘加密到处都有它的影子。1.2 AES-128 / AES-192 / AES-256 的差别AES 有三个常用变体差别主要体现在密钥长度和加密轮数上变体密钥长度分组大小加密轮数AES-12816 字节128 bit16 字节10 轮AES-19224 字节192 bit16 字节12 轮AES-25632 字节256 bit16 字节14 轮密钥越长理论安全性越高但速度也会稍微慢一点。实际项目里 AES-128 已经足够用于绝大多数场景AES-256 则常见于对安全要求更高的数据存储或金融类系统。需要强调的是密钥长度不影响分组大小。不管你是 AES-128 还是 AES-256每次加密的输入都是 16 字节。区别只在于扩展出来的轮密钥数量不同内部的轮数不同。所以学会了 AES-128 的流程另外两种只是轮数和密钥长度的参数调整核心步骤完全一样。1.3 分组密码意味着什么“分组”这两个字很关键。AES 一次只能处理 16 字节如果你的数据只有 5 字节就得先补齐到 16 字节如果数据有 100 字节就得拆成多个 16 字节的块分别处理。前者引出填充模式后者引出工作模式。这两个话题看着像“外围配置”其实是实际工程中最容易出问题的地方。很多人把 AES 调通之后换一个接口、换一种语言就出现“加密成功但解密失败”的情况往往就是填充模式或工作模式没有对齐。这些我会在第六章和第七章展开现在你只需要记住AES 算法本身处理的是单个 16 字节块剩下的问题都是它的外围配套策略。2. 从字节到状态矩阵手算前必须建立的心理模型2.1 为什么所有数据都要先变成字节数组AES 的所有运算都在字节层面进行。不管你原始数据是字符串、图片还是二进制文件进入 AES 之前都会被拆成一个个字节。一个字节 8 bit16 个字节正好 128 bit这就是 AES 的分组大小。所以“0x0f”这种十六进制写法会成为后面所有演示的通用语言。一个字节用两个十六进制字符表示比如十进制的 15 写成 0x0f二进制的 00001111。一组 16 字节的数据就是 32 个十六进制字符写出来是00112233445566778899aabbccddeeff这样的长串。小技巧编程的时候无论是 Python 的bytes.fromhex(...)还是其他语言的 hex 解码函数做的事情就是把这串十六进制字符还原成真正参与运算的字节数组。后续所有算法步骤都不用关心原始数据的类型因为到算法层它已经是纯字节了。2.2 状态矩阵的排列方式AES 内部不是把 16 个字节排成一条直线处理的而是先排成一个 4x4 的矩阵然后在这个矩阵上做各种变换。这个矩阵叫状态矩阵。排列规则要特别注意16 个字节按列填入矩阵。第一个字节放在第 0 行第 0 列第二个字节放在第 1 行第 0 列第三个放在第 2 行第 0 列第四个放在第 3 行第 0 列第五个再回到第 0 行第 1 列。很多人按习惯从左到右、从上到下填入结果后面所有中间值都对不上这是第一个常见误区。以标准测试向量明文00112233445566778899aabbccddeeff为例它的状态矩阵长这样第0列第1列第2列第3列第0行004488cc第1行115599dd第2行2266aaee第3行3377bbff看懂这张表是手算的前提。后面每一步变换包括行移位、列混淆都是在这样的矩阵上进行的。2.3 选一组可复现的实例数据为了让整个过程可以一步一步核对我选了业界公认的一组 AES-128 标准测试向量明文00112233445566778899aabbccddeeff密钥000102030405060708090a0b0c0d0e0f加密结果ECB 模式69c4e0d86a7b0430d8cdb78070b4c55a这组向量是公开的技术资料用来验证实现是否正确。如果你跟着文章手算完最后一轮得到的密文和上面这个69c4e0d8...完全一致就说明你的每一步都算对了。密钥的状态矩阵和明文类似按列填入第0列第1列第2列第3列第0行0004080c第1行0105090d第2行02060a0e第3行03070b0f有了这两张矩阵接下来可以开始第一轮加密的推演了。3. AES-128 第一轮加密四个操作的手工推演3.1 第0步轮密钥加初试异或AES 的每一轮加密由四个操作组成但正式进入第一轮之前还有一个“第0步”轮密钥加。这一步做的事情非常直白就是把状态矩阵和密钥矩阵逐字节做异或XOR。为什么第一步偏偏是异或因为异或是 AES 中唯一让密钥直接影响状态的操作。后续的 S 盒替换、行移位、列混淆都不直接接触密钥它们负责把数据“搅浑”而密钥则通过异或不断把自己的信息注入到状态中。可以这样理解工序负责改变数据形态密钥负责决定到底怎么变。我们来做逐字节异或。状态矩阵和密钥矩阵对应位置00^00 00 44^04 40 88^08 80 cc^0c c0 11^01 10 55^05 50 99^09 90 dd^0d d0 22^02 20 66^06 60 aa^0a a0 ee^0e e0 33^03 30 77^07 70 bb^0b b0 ff^0f f0这一步的结果矩阵是第0列第1列第2列第3列第0行004080c0第1行105090d0第2行2060a0e0第3行3070b0f0按列读出来是00102030 40506070 8090a0b0 c0d0e0f0。这个值就是 FIPS-197 附录里第一轮开始时的标准状态。如果你自己实现 AES在这一步后打印状态看到的应该是它。3.2 S 盒替换查表背后的有限域求逆轮密钥加之后进入第一轮的第一个正式操作字节代换通常叫 SubBytes。规则很简单状态矩阵里的每一个字节查一张固定的 256 字节替换表用一个新字节替换掉旧字节。这张表就是 S 盒。S 盒是 AES 设计者精心构造的它的本质是在有限域 GF(2^8) 上求逆元素再做一次仿射变换。这个数学背景听起来吓人但实现的时候基本没人手算都是直接准备一张 256 字节的查找表。你只需要确保 S 盒表来源可靠查表结果正确即可。这里不需要把 16 个字节全部讲一遍重点看几个代表性替换S[0x00] 0x63 S[0x10] 0xca S[0x20] 0xb7 S[0x40] 0x09 S[0xc0] 0xba把当前矩阵每个字节都查表替换之后得到第0列第1列第2列第3列第0行6309cdba第1行ca536070第2行b7d0e0e1第3行0451e78c按列读是63cab704 0953d051 cd60e0e7 ba70e18c。这一步是整个算法唯一非线性的地方所以至关重要。如果全是线性运算整个加密过程本质上就是一个大型线性方程组黑客拿到几组明密文对就能反推出密钥。S 盒的非线性让这套代数求解方法失效。我自己的经验是手算时最容易出错的地方就在这里S 盒查错一个字节后面全盘皆输。建议给自己实现的 AES 写一个单元测试先在第一步和 SubBytes 之后打印状态和这里的标准值比对错了马上能定位。3.3 行移位为什么是左移而不是右移行移位ShiftRows的规则只有一句话第 0 行不动第 1 行循环左移 1 个字节第 2 行循环左移 2 个字节第 3 行循环左移 3 个字节。注意是“循环左移”移出去的字节会从右边补回来。我们对上一步的矩阵操作第 0 行保持63 09 cd ba。第 1 行原来是ca 53 60 70循环左移 1 位变成53 60 70 ca。第 2 行原来是b7 d0 e0 e1循环左移 2 位变成e0 e1 b7 d0。第 3 行原来是04 51 e7 8c循环左移 3 位等价于右移 1 位变成8c 04 51 e7。所以位移后的矩阵是第0列第1列第2列第3列第0行6309cdba第1行536070ca第2行e0e1b7d0第3行8c0451e7按列读是6353e08c 0960e104 cd70b751 bacad0e7。为什么是“左移”而不是其他移位原因是状态矩阵按列填充同一列的字节来自原始数据的不同位置。行移位之后第 0 列的数据分别来自原来的第 0 列、第 1 列、第 2 列、第 3 列数据开始跨列交叉。这一步为后面的列混淆打好了基础否则 MixColumns 只能影响自己那一列的字节扩散效果会大打折扣。3.4 列混淆整个算法最“数学”的一步列混淆MixColumns是 AES 里劝退最多人的一步因为它涉及到有限域乘法。但实际上它的规则非常机械把当前矩阵的每一列当成一个向量乘上一个固定的 4x4 矩阵。乘法公式对每一列的 4 个字节 c0、c1、c2、c3 是这样的c0 2*c0 ⊕ 3*c1 ⊕ c2 ⊕ c3 c1 c0 ⊕ 2*c1 ⊕ 3*c2 ⊕ c3 c2 c0 ⊕ c1 ⊕ 2*c2 ⊕ 3*c3 c3 3*c0 ⊕ c1 ⊕ c2 ⊕ 2*c3这里⊕是异或2*c和3*c并不是普通整数乘法而是在 GF(2^8) 有限域上的乘法。对没接触过密码学的读者可以这样理解这个乘法有一套固定的移位和异或规则我直接给你操作步骤照着算就行。2*c的计算规则如果 c 小于 0x80最高位是 0直接左移 1 位。如果 c 大于等于 0x80最高位是 1左移 1 位后再与 0x1b 进行异或。3*c的计算规则先算出2*c再和 c 本身异或即3*c 2*c ⊕ c。听起来抽象我们用当前矩阵第 0 列来演示。第 0 列是c063, c153, c2e0, c38c。先算几个需要的乘法2*63 0x63 0x80直接左移0xC6 3*53 2*53 0xA60x53 0x80直接左移 3*53 A6 ⊕ 53 F5 2*e0 0xe0 0x80左移得 0xC0再异或 0x1BC0 ⊕ 1B DB 2*8c 0x8c 0x80左移得 0x18再异或 0x1B18 ⊕ 1B 03然后代入第一个公式c0 2*63 ⊕ 3*53 ⊕ e0 ⊕ 8c C6 ⊕ F5 ⊕ E0 ⊕ 8C一步一步来C6 ⊕ F5 33 33 ⊕ E0 D3 D3 ⊕ 8C 5F所以第 0 列的第一个结果是5F。按同样方法算出完整 4 个字节第 0 列变成5F 72 64 15。把四列全部算完列混淆后的矩阵是第0列第1列第2列第3列第0行5f57f71d第1行72f5beb9第2行64bc3bf9第3行1592291a按列读是5f726415 57f5bc92 f7be3b29 1db9f91a。如果你实现时这串值对不上说明列混淆的乘法规则没写对重点检查 0x80 以上字节的异或 0x1b那一步。列混淆的作用是扩散让任意一个字节的变化经过这一轮后波及整列的 4 个字节。S 盒负责把单个字节彻底改变行移位负责跨列交叉列混淆负责把这股变化扩散到一整列三者配合AES 才能在十几轮之内实现足够的混淆与扩散。3.5 最后一轮为什么省掉列混淆细心的读者会发现一个“漏洞”AES 的最后一轮只有三个操作没有列混淆。这不是设计疏忽而是刻意为之。列混淆本身属于线性扩散操作它的作用是让字节的变化传播到整列。到最后一轮时整个数据的混淆和扩散已经在前面的轮次里完成了再补一次列混淆并不会提升安全性只会增加计算量。更关键的是去掉列混淆之后解密流程的结构会更对称AES 解密是加密的逆操作如果最后一轮包含列混淆解密时第一步就要处理一个逆列混淆实现起来不够优雅。省略它加解密流程在结构上更加整齐硬件电路实现更简洁。因此从性能到实现便利性最后一轮省略列混淆都是合理的。4. 密钥扩展轮密钥不是主密钥的重复4.1 为什么要密钥扩展如果每一轮都用同一个密钥做异或会出现一个严重问题整个加密流程里密钥的参与方式始终相同攻击者只要拿到一轮的密钥信息就等于拿到了全部密钥信息。AES 的做法是对主密钥做一次密钥扩展生成一组不同的轮密钥每一轮各用各的。密钥扩展的基本思想是从最初的 4 个字每个字 4 字节共 16 字节出发不断生成新的字。对于 AES-128需要生成 44 个字的扩展密钥每轮用 4 个字10 轮加上初始的轮密钥加正好 44 个字。AES-128 的扩展密钥生成规则可以写成前 4 个字直接取自主密钥w0, w1, w2, w3从w4开始每个新字等于前一个字和相隔 4 个位置的字异或当索引是 4 的倍数时前一个字要先做一轮特别的变换再参与异或这个“特别变换”包含三个动作循环左移、S 盒替换、异或轮常数。我们直接手算一次就明白了。4.2 RotWord、SubWord、Rcon 三步走当要生成的位置下标是 4 的倍数时需要对前一个字做以下三个操作RotWord把 4 字节的字循环左移 1 字节。比如0c0d0e0f变成0d0e0f0c。SubWord把每个字节按 S 盒表替换一遍和 SubBytes 用的是同一张表。Rcon把结果和轮常数异或。AES 的轮常数表前几个是01000000, 02000000, 04000000, 08000000, 10000000,...本质是 2 的幂在有限域里的表示作用是为了让每一轮的扩展密钥都不对称防止出现同密钥重复。对于 AES-128生成w4时是第 1 轮所以用Rcon[1] 01000000。后面生成w8用Rcon[2] 02000000依此类推。4.3 手算第一个扩展密钥我们的主密钥是00010203 04050607 08090a0b 0c0d0e0f所以w0 00010203 w1 04050607 w2 08090a0b w3 0c0d0e0f现在生成w4。因为索引 4 是 4 的倍数先处理w3第一步 RotWord0c0d0e0f - 0d0e0f0c第二步 SubWord逐字节查 S 盒S[0d] d7 S[0e] ab S[0f] 76 S[0c] fe得到d7ab76fe。第三步异或轮常数d7ab76fe ⊕ 01000000 d6ab76fe最后和w0异或d6ab76fe ⊕ 00010203 d6aa74fd所以w4 d6aa74fd。接下来几个字不需要特殊变换直接异或w5 w4 ⊕ w1 d6aa74fd ⊕ 04050607 d2af72fa w6 w5 ⊕ w2 d2af72fa ⊕ 08090a0b daa678f1 w7 w6 ⊕ w3 daa678f1 ⊕ 0c0d0e0f d6ab76fe这就是第一轮要用的 4 个字d6aa74fd d2af72fa daa678f1 d6ab76fe。有了它我们可以把第一轮最后的 AddRoundKey 补完。上一节的列混淆结果是5f726415 57f5bc92 f7be3b29 1db9f91a和第一轮密钥逐列异或5f^d6 89 57^d2 85 f7^da 2d 1d^d6 cb 72^aa d8 f5^af 5a be^a6 18 b9^ab 12 64^74 10 bc^72 ce 3b^78 43 f9^76 8f 15^fd e8 92^fa 68 29^f1 d8 1a^fe e4第一轮结束后的状态按列读是89d810e8 855ace68 2d1843d8 cb128fe4。后面每一轮都重复这套流程只是每轮使用的密钥字不同。10 轮全部跑完最终得到的数据就是我们想要的标准密文69c4e0d86a7b0430d8cdb78070b4c55a。4.4 用代码自动生成全部轮密钥手算到第一轮就够理解原理了真实代码里没人会手算 44 个字。下面这段 Python 演示了扩展密钥的核心逻辑def bytes_to_words(buf): return [int.from_bytes(buf[i:i4], big) for i in range(0, len(buf), 4)] def rot_word(w): return ((w 8) | (w 24)) 0xFFFFFFFF SBOX [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, # ... 完整256字节S盒这里省略实际使用时需要完整表 ] def sub_word(w): return ( (SBOX[(w 24) 0xff] 24) | (SBOX[(w 16) 0xff] 16) | (SBOX[(w 8) 0xff] 8) | SBOX[w 0xff] ) def key_expansion(key): rcon 0x01 w bytes_to_words(key) for i in range(4, 44): temp w[i - 1] if i % 4 0: temp sub_word(rot_word(temp)) ^ (rcon 24) rcon (rcon 1) ^ (0x11b if rcon 0x80 else 0) w.append(w[i - 4] ^ temp) return w这段代码可以作为参考实现也可以对照手算结果检查自己对密钥扩展的理解。5. 跑通实例用 Python 验证手算结果5.1 环境准备手算推演完了现在用真实代码把结果跑出来验证一遍。我用的是 PyCryptodomePython 生态里比较常用的密码学库。安装只需要一行命令pip install pycryptodome注意一下这个库的导入包名是Crypto不是PyCrypto。老项目里如果用的是已经停止维护的pycrypto记得先卸载再装两个包同时存在会冲突。5.2 用标准测试向量验证用 ECB 模式跑刚才手算的测试向量from Crypto.Cipher import AES key bytes.fromhex(000102030405060708090a0b0c0d0e0f) plaintext bytes.fromhex(00112233445566778899aabbccddeeff) cipher AES.new(key, AES.MODE_ECB) ciphertext cipher.encrypt(plaintext) print(ciphertext.hex())输出应该是69c4e0d86a7b0430d8cdb78070b4c55a看到这个结果说明标准实现完全正确也验证了我们前面的手算路线没有跑偏。ECB 模式之所以适合做测试向量验证是因为它没有任何初始化向量IV和链式依赖同样的明文永远得到同样的密文最适合用来做基础正确性检查。5.3 中间状态与手算的对应关系库封装好了但我们怎么确认自己的手算中间值是对的呢方法很简单在真正的实现里把每一轮结束后的状态打印出来。真实项目里这样做没有意义但学习和调试阶段非常有用。你可以在自写的简化实现中分别封装add_round_key、sub_bytes、shift_rows、mix_columns四个函数然后在每个函数结束后打印 4x4 状态矩阵。比如第一轮开始前的状态矩阵就应该是state [ [0x00, 0x40, 0x80, 0xc0], [0x10, 0x50, 0x90, 0xd0], [0x20, 0x60, 0xa0, 0xe0], [0x30, 0x70, 0xb0, 0xf0], ]SubBytes 后是state [ [0x63, 0x09, 0xcd, 0xba], [0xca, 0x53, 0x60, 0x70], [0xb7, 0xd0, 0xe0, 0xe1], [0x04, 0x51, 0xe7, 0x8c], ]和文章里 3.2 节的矩阵对比逐字节核对。差一个字节就说明那个环节的逻辑有问题。用这种“每步打印、逐步核对”的方式调试比加密结果对不上才去翻代码要快得多。6. 从单块到真实数据填充模式与分组模式6.1 PKCS7 填充的细节与边界情况AES 一次只处理 16 字节真实数据长度几乎不可能刚好是 16 的倍数所以必须在进入算法之前把数据补齐。最常见的补齐方案是 PKCS7。PKCS7 的规则非常直白缺几个字节就补几个几。比如数据还差 5 个字节满 16就补 5 个0x05。解密的时候程序看最后一个字节的值 n就知道最后 n 个字节都是填充字节直接去掉即可。这里的边界情况特别容易踩坑如果数据长度恰好是 16 的倍数是不是就不用填充了答案是必须再补整整 16 个0x10。如果不补解密端看到最后一个字节是合法的业务数据就无法判断它到底是填充字节还是原始数据的最后一个字节。这就好比快递箱子里刚好放满物品你仍然需要塞一张“没有填充”的说明单。实际编程中不要自己写填充逻辑直接用标准库的pad和unpadfrom Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad plaintext bHello, AES! block_size 16 padded pad(plaintext, block_size) print(padded.hex()) # 最后5个字节应该是 05 05 05 05 05 unpadded unpad(padded, block_size) print(unpadded) # bHello, AES!自己写填充还容易埋下漏洞尤其是不验证填充合法性的解密可能被 padding oracle 攻击利用。6.2 ECB、CBC、CTR、GCM 怎么选解决了填充问题还要处理多块数据的组织方式。这些组织方式叫分组模式直接决定了密文的安全属性。模式特点适用场景风险ECB每个块独立加密仅测试验证相同明文块产生相同密文块泄露数据模式CBC每块先与上一块密文异或再加密传统通用场景需要 IV不支持并行易受填充相关攻击CTR计数器加密后与明文异或流式、并行需求计数器与密钥组合不允许重复GCM认证加密自带完整性校验现代首选对 IV 唯一性要求高但工程规范已成熟ECB 模式的问题最直观一张图片用 ECB 加密后轮廓依然隐约可见因为同样颜色的色块加密后还是同样的密文。所以只要数据超过一块就不该用 ECB。我的建议是能上 GCM 就上 GCM。GCM 在加密的同时生成认证标签能检测密文是否被篡改一套流程解决保密性和完整性问题。CBC 是传统方案兼容性最好但它本身不提供防篡改能力需要额外叠加消息认证码比较麻烦。6.3 IV 的生成与传输CBC 和 GCM 都需要一个初始化向量 IV。IV 不像密钥需要保密但它有两条铁律随机性好且不能重复使用。IV 的长度是 16 字节可以用密码学安全的随机数生成器产生。在 Python 里就是from Crypto.Cipher import AES import os key os.urandom(16) iv os.urandom(16) plaintext bHello, AES with CBC! cipher AES.new(key, AES.MODE_CBC, iv)常见错误是“固定 IV”。如果每次加密都用同一个 IVCBC 模式退化成 ECB 类似的效果相同明文同样会暴露模式。IV 不会泄露密钥所以它通常直接拼在密文前面一起存储或传输接收方先取前 16 字节当 IV再解密剩余部分。建议的存储格式可以是Base64(iv ciphertext)因为原始字节不利于文本传输。收到后用对应逻辑拆分即可。7. 工程避坑清单跨语言互操作与常见错误7.1 模式、填充、编码三件套对照表实际项目里AES 加解密最常见的问题不是算法本身而是通信双方对参数的理解不一致。我整理了一张对照表联调之前照着核对一遍能省掉大量排查时间参数必须对齐的点容易出错的地方密钥长度128/192/256 bit 谁说了算字符串密钥长度不等于字节长度工作模式ECB/CBC/CTR/GCM 一致一个写 CBC一个写 ECB解密乱码填充模式PKCS7 几乎全兼容Java 的 PKCS5Padding 在 AES 里实际是 PKCS7初始化向量16 字节需随机传输后忘记拆分或类型不对数据编码UTF-8 / Base64 / Hex直接传字符串导致字节数不对特别要提一下 Java 的 PKCS5Padding 问题。PKCS5 最早是为 8 字节分组设计的但 Java 在 AES 里写AES/CBC/PKCS5Padding时实际行为是 PKCS7因为 AES 分组是 16 字节。很多人在和其他语言对接时看到“PKCS5”字样就以为不兼容其实 Java 这边一般不用改。7.2 那些“跑通了但解密失败”的经典问题跨语言加解密失败百分之八十是下面几个原因之一。第一个密钥处理错误。有些语言把密钥当成字符串直接传给加密库比如 Python 里key 0123456789abcdef这个字符串长度是 16 个字符但 UTF-8 编码后是 16 字节所以碰巧能用。一旦密钥字符串换成带中文或特殊字符的长字符串长度就完全对不上了。正确做法是确定字节长度或者用 Base64/Hex 解码密钥。第二个CBC 模式 IV 传递出错。常见情况是加密方把 IV 拼在密文后面解密方取前面 16 字节时少了偏移。修好之后解密出来的第一块正常后面的块乱码这就是 IV 取错的典型症状。第三个加密前忘记对明文编码。比如把中文字符串直接做str和bytes拼接或者没有encode(utf-8)到解密端再用decode(utf-8)就会报 Unicode 相关错误。记住一个原则加密永远加密字节串解密永远输出字节串。第四个GCM 模式的 nonce 长度不匹配。不同语言对 GCM 的默认 nonce相当于 IV长度支持不一样比如 96 bit12 字节是最广泛支持的有些库却允许任意长度。跨语言联调时显式固定 nonce 长度为 12 字节。7.3 认证与安全建议最后说点安全层面的经验。AES 加密本身只能保证“没密钥的人读不懂”但它不能阻止别人篡改密文。对于 CBC 模式篡改密文会导致解密后的明文被篡改且程序不一定能察觉。所以现代工程里要么直接用 GCM 这类认证加密模式要么在 CBC 之外再附加一个 HMAC-SHA256 签名。认证标签验证还有一个容易出错的地方比较标签时不要用普通等值运算要用常量时间比较。这是因为普通比较在遇到第一个不同字节时就返回攻击者可以借助时间差逐步猜出合法标签。标准库里的compare_digest就是为此设计的。密钥管理同样重要。把密钥硬编码在代码里是下策至少在配置文件中用环境变量注入。更稳妥的做法是把密钥交给专门的密钥管理服务应用运行时从服务获取。AES 本身再安全密钥泄露了一切都白搭。我自己在实际项目里见过太多“算法都对参数全错”的联调事故。归根结底AES 不是调一个函数就万事大吉的它是一整套需要双方严格对齐的协议。花一个小时把参数表核对清楚比出问题时花费一整天排查要划算得多。这也是为什么我坚持认为理解算法的每一步内部逻辑比单纯会调库重要得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑