
简介“AES128位加密过程详解”面向具备C/C基础、希望理解对称分组密码实现细节的开发者与学习者也可供嵌入式方向读者参考。文档从Rijndael算法入选AES的背景讲起梳理128位密钥、16字节分组下的轮变换、圈数、密钥扩展与解密逆变换并给出SubBytes查表、ShiftRow字节换位、MixColumn有限域运算及AddRoundKey的实现思路还结合AVR汇编片段说明查表与xtime等底层操作并附AES与3DES在密钥长度、速度、资源消耗方面的对比便于建立从原理到代码的完整认识。资源包共1个doc文件约110KB篇幅紧凑适合作为算法入门提纲或实现对照笔记。已有1678人学习说明其在AES原理梳理与C实现参考方面具有一定实用价值。1. 对不上的那串密文AES-128 为什么值得从头拆一遍前段时间接手一个存量通信网关协议文档里只留了一句话加密字段采用 AES 128 位加密密钥固定 16 字节。抓包拿到密文几个在线工具挨个试没一个对得上。顺着排查填充方式、State 的行列顺序、密钥扩展的轮常数三个地方各埋了一个坑。这份资料的价值就在这儿它没停在「AES 是对称分组密码」这种结论上而是把 S 盒怎么来的、列混合的有限域乘法怎么算、密钥怎么从 16 字节摊成 44 个字、单片机上怎么查表优化一层层摊开。适合要自己实现或移植 AES 的开发者也适合手上有个黑匣子加密模块、需要反推算法的人。读完至少能做成一件事给一组密钥和明文把每一轮的中间状态算出来再和代码逐字节对上。2. 算法骨架16 字节明文怎么变成 4×4 的 State 矩阵很多人第一次实现 AES 都会卡在同一个地方——代码看着和规范一模一样跑出来的密文就是不对。问题往往不在轮函数本身而在最开始的几步分组怎么切、状态怎么摆、第几轮该省哪个操作。这一章先把骨架搭起来把参数、数据布局、主循环三件事钉死后面的轮函数才有地方挂。2.1 Nb、Nk、Nr 三个参数把分组和轮数定死AES 的三个核心参数是 Nb、Nk、Nr它们分别表示「分组有多少个 32 位字」「密钥有多少个 32 位字」「要迭代多少轮」。对标题里的 AES-128 来说Nb 固定为 4一个分组就是 4 个字也就是 16 字节、128 位Nk 也是 4种子密钥 16 字节轮数 Nr 由 Nk 推出来公式是 Nr Nk 6所以是 10 轮。密钥长度Nk字Nr轮扩展密钥字数 4×(Nr1)扩展密钥字节数128 位41044176192 位61252208256 位81460240这张表最右边的 44 个字是后面所有代码的地基。密钥扩展阶段要把用户给的 16 字节种子密钥扩成 44 个 32 位字其中前 4 个字就是原始密钥剩下 40 个字是 10 轮要用的轮密钥每轮消耗 4 个字。很多实现里数组只开了 16 或 32 个位置跑一小段明文没问题跑长了就开始读到脏数据这是血泪经验。分组长度恒为 16 字节这个约束也决定了 AES 本身不是「任意长度字符串加密器」。你喂给它 100 字节它只认 16 字节一块剩下的要靠分组模式和填充去解决这部分放到避坑那章细说。2.2 明文铺进 State列主序是最容易翻车的一步AES 的所有轮函数都作用在一个叫 State 的 4×4 字节矩阵上。规范里写的是state[r][c] in[r 4*c]意思是输入的第 0 到第 3 个字节是 State 的第 0 列从上到下而不是第 0 行。换句话说State 是按列填充的。这个细节决定了你后面所有下标换算。我的习惯是用一维数组存 State扁平下标i r 4*c这样state[i]和输入in[i]天然一一对应铺进去的时候连循环都不用写。只有在 ShiftRows、MixColumns 这种要按行或按列操作的函数里才把 r、c 显式拆出来。# aes_core.py —— AES-128 骨架 NB 4 # 分组长度单位 32 位字固定 4 → 16 字节 NK 4 # AES-128 的密钥长度单位 32 位字 NR 10 # 轮数 Nk 6 def bytes_to_state(block): 16 字节按列主序铺进 4x4 状态矩阵扁平下标 i r 4*c assert len(block) 16, AES 分组必须是 16 字节 return list(block) # 顺序存放即可行列换算交给各轮函数 def add_round_key(state, round_key): round_key 是 4 个字的列表第 c 个字对应 State 的第 c 列 for c in range(4): for r in range(4): state[r 4 * c] ^ round_key[c][r]add_round_key是四件套里唯一直接改 State 的线性操作就是把轮密钥按列异或进去没有任何查表。注意循环里我写的是round_key[c][r]不是round_key[r][c]——轮密钥的第 c 个字按字节展开正好对上 State 的第 c 列这个下标顺序一旦反过来加密解密的中间结果就全乱了。2.3 加密主循环9 轮全量第 10 轮砍掉列混合骨架搭好之后加密流程其实只有三段先做一次初始轮密钥加然后做 9 次完整轮变换最后做一次不带 MixColumns 的收尾轮。def encrypt_block(block, round_keys): block: 16 字节明文round_keys: 44 个 32 位字 state bytes_to_state(block) add_round_key(state, round_keys[0:4]) # 初始轮密钥加 for rnd in range(1, NR): # 第 1~9 轮四件套全上 sub_bytes(state) shift_rows(state) mix_columns(state) add_round_key(state, round_keys[4*rnd:4*rnd4]) sub_bytes(state) # 第 10 轮没有 MixColumns shift_rows(state) add_round_key(state, round_keys[40:44]) return bytes(state)为什么最后一轮要砍掉 MixColumns因为列混合是纯线性变换放在最后一轮后面没有后续的非线性操作去配合它制造扩散攻击者可以直接把它和解密侧的逆列混合对消白算一次。标准里省掉它既不影响安全性又省下一次矩阵乘。反过来解密侧对应的做法是第一轮不做 InvMixColumns中间轮才做。解密流程和加密并不对称顺序是AddRoundKey → InvShiftRows → InvSubBytes → AddRoundKey → InvMixColumns注意中间有两个 AddRoundKey 夹着逆字节替换。我第一次写的时候按加密顺序镜像了一遍结果只有全零密钥能解对换个密钥就废调了整整一下午才发现是这里。写解密前先把这 4 行的顺序抄在纸上比在 IDE 里试错快得多。3. 轮函数四件套S 盒、行移位、列混合、轮密钥加的可抄实现骨架跑起来之后真正决定输出对不对的就是四个轮函数。这一章逐个拆每个函数给出可运行代码、参数含义以及失败时会看到什么现象。其中列混合用到了 GF(2^8) 上的乘法这是整份资料里最需要动脑的部分单独展开。3.1 S 盒GF(2^8) 求逆加仿射变换不是随便一张表S 盒是 AES 唯一提供非线性的部件。规范给的是一张 16×16 的常量表但表里的值不是随机填的它的生成规则是对每个字节 a先求它在 GF(2^8) 上的乘法逆元 b0 的逆规定为 0再对 b 做一次仿射变换最后异或 0x63。按这个规则写一份生成版比手抄 256 个常量更不容易出错也方便验证手上那张表是不是抄对了。def gmul(a, b): GF(2^8) 乘法模多项式 x^8 x^4 x^3 x 1即 0x11B p 0 for _ in range(8): if b 1: p ^ a hi a 0x80 # 记录最高位决定要不要做模约减 a (a 1) 0xFF if hi: a ^ 0x1B # 0x11B 的低 8 位 b 1 return p def ginv(a): GF(2^8) 上的乘法逆元a 为 0 时返回 0 if a 0: return 0 for x in range(1, 256): if gmul(a, x) 1: return x def rotl8(x, n): return ((x n) | (x (8 - n))) 0xFF def build_sbox(): 按 逆元 仿射变换 生成 S 盒S[0x00] 应等于 0x63S[0x01] 应等于 0x7C sbox [] for a in range(256): b ginv(a) s b ^ rotl8(b, 1) ^ rotl8(b, 2) ^ rotl8(b, 3) ^ rotl8(b, 4) ^ 0x63 sbox.append(s 0xFF) return sbox SBOX build_sbox() def sub_bytes(state): for i in range(16): state[i] SBOX[state[i]]这段build_sbox是教学版256 次循环里每次都要跑一遍暴力求逆跑一次大概几十毫秒初始化阶段用没问题但绝不能放进加密循环。生产代码一般是把 256 字节的表硬编码成常量数组或者干脆在编译期生成。查表的时候有个容易忽略的点S 盒的索引是把字节拆成高 4 位和低 4 位高位是行、低位是列。写成SBOX[x 4][x 0x0F]或者一维表SBOX[x]都对但如果你把行列顺序搞反0x27 该查出来是 0xCC你会查到 0x3A密文从第一个字节就错。3.2 行移位四行偏移量 0、1、2、3方向不能反ShiftRows 做的是字节换位第 0 行不动第 1 行左移 1 字节第 2 行左移 2 字节第 3 行左移 3 字节。四个偏移量刚好覆盖 0 到 3目的是让每个字节在若干轮之后能扩散到全部四列。def shift_rows(state): 第 r 行左移 r 字节。扁平下标下新 state[r 4c] 旧 state[r 4*((cr)%4)] out [0] * 16 for r in range(4): for c in range(4): out[r 4 * c] state[r 4 * ((c r) % 4)] state[:] out def inv_shift_rows(state): 解密用第 r 行右移 r 字节 out [0] * 16 for r in range(4): for c in range(4): out[r 4 * c] state[r 4 * ((c - r) % 4)] state[:] out这段代码短但方向写反是最常见的错误之一而且症状很隐蔽加密侧写错、解密侧也跟着错自测能闭环密文却和任何工具都对不上。验证办法很简单拿一个只有第 1 行有值的 State 跑一遍看 0x01 有没有从左端跑到右端。3.3 列混合GF(2^8) 上的矩阵乘法xtime 是核心MixColumns 把 State 的每一列当成 GF(2^8) 上的多项式左乘一个固定矩阵。写出来是b0 02·a0 ⊕ 03·a1 ⊕ 01·a2 ⊕ 01·a3 b1 01·a0 ⊕ 02·a1 ⊕ 03·a2 ⊕ 01·a3 b2 01·a0 ⊕ 01·a1 ⊕ 02·a2 ⊕ 03·a3 b3 03·a0 ⊕ 01·a1 ⊕ 01·a2 ⊕ 02·a3这里的乘法和加法都不是普通整数运算。加法就是异或乘法是 GF(2^8) 乘法。系数只有 01、02、03 三种而 03 可以拆成 02 ⊕ 01所以整段代码只需要一个原语乘 0x02也就是常说的 xtime。xtime 的规则很干净被乘数左移一位如果原值最高位是 1左移后再异或 0x1B。这个 0x1B 来自模多项式 0x11B 的低 8 位作用是防止结果溢出到 8 位之外。def xtime(a): GF(2^8) 中乘 0x02 a 1 if a 0x100: a (a ^ 0x11B) 0xFF return a def mix_columns(state): for c in range(4): a0, a1, a2, a3 state[4*c], state[4*c1], state[4*c2], state[4*c3] state[4*c] xtime(a0) ^ (xtime(a1) ^ a1) ^ a2 ^ a3 state[4*c1] a0 ^ xtime(a1) ^ (xtime(a2) ^ a2) ^ a3 state[4*c2] a0 ^ a1 ^ xtime(a2) ^ (xtime(a3) ^ a3) state[4*c3] (xtime(a0) ^ a0) ^ a1 ^ a2 ^ xtime(a3)xtime(a1) ^ a1就是乘 0x03 的等价写法不需要再单独写一个 gmul 调用。这个写法比查表慢一点但在没有预计算表的环境里是最省空间的。拿规范里的经典例子验证输入列db 13 53 45经过一轮 MixColumns 应该得到8e 4d a1 bc。手算第一个字节02·db ad03·13 35再异或 53 和 45ad ⊕ 35 ⊕ 53 ⊕ 45 8e。对上了就说明 xtime 和系数都没写错。解密侧用的是逆矩阵系数是 0e、0b、0d、09同样可以拆成 xtime 的多次调用。资料里提到过一个优化思路解密矩阵和加密矩阵之间存在线性关系可以先做加密的列混合、再补一个简单变换把倍乘次数从 4 次降到更低。我在实际项目里一般直接上查表因为逆列混合的系数拆起来实在费劲代码可读性也差。4. 密钥扩展16 字节种子密钥怎么摊成 44 个轮密钥字轮函数对了密钥不对照样出不了正确密文。密钥扩展是整份资料里最容易被跳过的部分因为它不像轮函数那样有直观的物理意义但只要有一个轮常数写错后面的 9 轮就全废。这一章把三个零件和一次完整推演都摆出来。4.1 RotWord、SubWord、Rcon 三个零件扩展密钥用一个 44 行的字数组 w 表示每行 4 字节。前 Nk 行直接拷贝种子密钥从第 Nk 行开始每一行由「前一行」和「前 Nk 行」异或得到。但每遇到下标是 Nk 倍数的行前一行要先过三个变换RotWord 是把 4 字节循环左移一个位置[A,B,C,D]变成[B,C,D,A]。SubWord 是对这 4 个字节逐个查 S 盒。Rcon 是轮常数第 i 个只有首字节非零值等于 GF(2^8) 里 2 的 i-1 次幂也就是从 0x01 开始不断 xtime得到 01、02、04、08、10、20、40、80、1B、36 共 10 个。def rot_word(word): 4 字节循环左移一位 return word[1:] word[:1] def sub_word(word): 4 个字节逐个过 S 盒 return [SBOX[b] for b in word] def build_rcon(n10): 生成轮常数每个是 4 字节只有首字节有效 rcon, cur [], 1 for _ in range(n): rcon.append([cur, 0, 0, 0]) cur xtime(cur) # 0x80 再乘 2 会回到 0x1B不是 0x100 return rcon def key_expansion(key): key: 16 字节返回 44 个 32 位字每个字用 4 字节列表表示 assert len(key) 16 w [list(key[4*i:4*i4]) for i in range(NK)] rcon build_rcon() for i in range(NK, 4 * (NR 1)): temp w[i-1][:] if i % NK 0: temp sub_word(rot_word(temp)) temp[0] ^ rcon[i // NK - 1][0] # 只异或首字节 w.append([w[i-NK][j] ^ temp[j] for j in range(4)]) return wbuild_rcon里那行cur xtime(cur)值得特别注意当 cur 是 0x80 时左移得到 0x100异或 0x11B 后是 0x1B而不是 0x00。我见过有人用cur (cur 1) 0xFF来生成前面 8 个常数都对第 9 个开始全错导致第 8 轮的轮密钥就是坏的。4.2 用测试向量把 w[4] 和 w[8] 手推一遍拿资料里的标准输入验证种子密钥000102030405060708090a0b0c0d0e0f拆成四个字 w[0] 到 w[3] 分别是00010203、04050607、08090a0b、0c0d0e0f。推 w[4]i4 是 Nk 的倍数取 temp w[3] 0c 0d 0e 0fRotWord 得0d 0e 0f 0cSubWord 查表得d7 ab 76 fe异或 Rcon[0] 01 00 00 00得d6 ab 76 fe再和 w[0] 异或结果是d6 aa 74 fd。推 w[8]temp w[7] d6 ab 76 feRotWord 得ab 76 fe d6SubWord 得62 38 bb f6异或 Rcon[1] 02 00 00 00得60 38 bb f6和 w[4] d6 aa 74 fd异或得b6 92 cf 0b。这两组值和公开的标准扩展密钥表一致。写完key_expansion后先打印前 9 个字逐个对一遍比直接跑整段加密再猜哪里错要省事得多。4.3 解密轮密钥倒着用不要重新扩展一遍解密的轮密钥不需要另外算一套。加密的 44 个字倒过来就是解密的顺序解密初始用w[40:44]第 1 轮用w[36:40]一直退到w[0:4]。中间如果做 InvMixColumns需要在轮密钥上也做一次 InvMixColumns否则和加密侧对不齐。这里踩过一次坑我在解密函数里重新调用了一次key_expansion想「生成解密专用密钥」结果生成的是同一组东西解密自然失败。后来才想明白AES 的密钥调度是单向的解密侧只是取用顺序不同不存在「反向扩展」这个步骤。5. 避坑与排查五个让密文对不上的典型现场算法逻辑写对只是第一步真正把人卡住的是工程细节。下面这五条都是我在联调现场真实遇到过的按「现象 → 原因 → 解决」整理遇到密文对不上时建议按顺序自查。5.1 长度不对齐16 字节之外的数据去哪了现象加密 20 字节的报文结果变成 32 字节或者接口直接抛异常说长度不是 16 的倍数。原因AES 是分组密码一次只能吃 16 字节多出来的部分必须由分组模式和填充方案处理。资料里通篇在讲单分组加密没提模式和填充照着实现出来的函数碰到 20 字节自然不知道怎么办。解决先确认对端用的是什么模式。常见做法是 CBC 或 CTRCBC 需要 PKCS#7 填充并配一个 16 字节 IVCTR 是流式的不需要填充但要用计数器。填充方式必须和对方一致PKCS#7 补0x05五个字节、ZeroPadding 补五个0x00两者解出来的明文长度是不同的。联调时先把模式和填充写进接口约定别指望靠试。5.2 State 行列错位自测闭环对外全错现象自己的加密和自解能互相还原但拿在线工具或对端设备一算密文完全不同。原因State 按列填充写成了按行填充或者 ShiftRows 的行偏移方向搞反。加密解密两侧用同一套错误约定内部当然自洽一对外就露馅。解决用00112233445566778899aabbccddeeff这组明文和000102030405060708090a0b0c0d0e0f密钥标准密文是69c4e0d86a7b0430d8cdb78070b4c55a。这个向量能对上说明从填充到轮函数全链路正确对不上就逐轮打印中间 State和规范附录里的中间值比对。5.3 密钥编码16 个字符不等于 16 字节现象用「1234567890abcdef」当密钥代码里 assert 长度 16 能过但和对方算出来不一样。原因多数语言里字符串转字节默认走 UTF-8ASCII 字符一个占一字节没问题但如果密钥里有中文或全角符号16 个字符可能变成 48 字节被截断后前 16 字节根本不是你以为的那 16 个。另一类问题是把 32 个 hex 字符当密钥用实际需要的是它对应的 16 字节二进制。解决接口层统一约定密钥以 hex 字符串传输落地前先bytes.fromhex()转成 16 字节再进算法。C 侧同理别把std::string直接传进去先确认它的size()是不是 16。5.4 把 AES 当成非对称算法概念错导致设计错现象设计接口时按「公钥加密、私钥解密」的思路分出两把密钥结果发现加密和解密必须用同一把。原因资料里有一句表述容易误导人说「AES 密码是一个非对称密码体制」紧接着又说加解密用相同密钥。这句自相矛盾。AES 从头到尾都是对称分组密码「不对称」说的是加密和解密的计算量不对称解密因为逆列混合系数更复杂比加密慢。解决一把密钥两处用密钥分发靠别的机制比如用非对称算法协商出这把 AES 密钥这才是常见组合。看到资料里出现这类矛盾表述以算法本身的定义为准。5.5 最后一轮多算或少算一次列混合现象前 9 轮的中间状态和标准值完全一致第 10 轮开始对不上。原因加密最后一轮跳过了 MixColumns如果实现时把 10 轮写成统一循环最后一轮就会多算一次列混合解密侧对应的错误是在第一轮多算了一次 InvMixColumns。解决把主循环拆成「9 次完整轮 1 次收尾轮」别图省事用一个 for 加 if。收尾轮固定是 SubBytes、ShiftRows、AddRoundKey 三步解密收尾轮是 InvShiftRows、InvSubBytes、AddRoundKey。6. 二次开发从能跑到能上线的三处改造代码跑通标准向量只是起点要真放进项目里还得动三刀。第一刀是查表。前面那版mix_columns每次调用要执行 16 次xtime一轮 4 列就是 64 次条件异或。常见做法是把 S 盒、xtime 表、甚至整列的 T 表预计算出来T 表把「查 S 盒 行移位 列混合」三步合成一次查表和三次异或在需要处理大量报文的场景能把吞吐拉高一个量级代价是 4 张 1KB 左右的表。嵌入式设备上把xtime也做成 256 字节的表用空间换时间更划算。# 预计算 xtime 表替换掉条件异或分支 XTIME [(a 1) ^ 0x1B if a 0x80 else (a 1) for a in range(256)] def mix_columns_fast(state): x XTIME for c in range(4): a0, a1, a2, a3 state[4*c], state[4*c1], state[4*c2], state[4*c3] state[4*c] x[a0] ^ x[a1] ^ a1 ^ a2 ^ a3 state[4*c1] a0 ^ x[a1] ^ x[a2] ^ a2 ^ a3 state[4*c2] a0 ^ a1 ^ x[a2] ^ x[a3] ^ a3 state[4*c3] x[a0] ^ a0 ^ a1 ^ a2 ^ x[a3]注意mix_columns_fast里x[a0] ^ a0就是 03·a0 的另一种写法既省了一次查表也避免了分支预测失败。这段代码和前面的慢速版必须能算出同一个结果改完先拿标准向量回归。第二刀是接口封装。资料里那份从 C# 照搬到 C 的实现类结构是「构造函数传密钥、Cipher 方法传两个数组」用起来很朴素。实际项目里我会把它包成encrypt(plain, key, iv, mode)和decrypt(cipher, key, iv, mode)两个入口内部维护一个上下文对象避免每次加密都重建 S 盒和轮密钥。轮密钥可以按密钥做缓存同一把密钥连续处理多个分块时把它缓存下来能省掉十几轮扩展。第三刀是验证。每次改动后回归一遍下面几组向量全部通过再提交用例密钥明文期望密文标准向量000102030405060708090a0b0c0d0e0f00112233445566778899aabbccddeeff69c4e0d86a7b0430d8cdb78070b4c55a全零全 0016 字节全 0016 字节66e94bd4ef8a2c3b884cfa59ca342b2e单比特翻转标准向量同上把明文首字节 0x00 改成 0x01与标准向量对比观察雪崩效应还有一条教训我曾把一个改过的列混合实现直接上了生产本地只测了一组向量结果那组刚好没覆盖到0x80边界值线上跑到第 8 轮才开始出现偏差排查了两天才定位到xtime里少了 0xFF。从那以后我每次改完 AES 相关代码都会把零密钥、全 FF、带 0x80 字节的三组用例强制走一遍再拿自测的加解密循环交叉验证一次。希望帮到你。本文还有配套的精品资源点击获取