我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

AES加密改进:第二密钥与动态S盒的设计实现

AES加密改进:第二密钥与动态S盒的设计实现 敏感文本落到磁盘或者走网络之前多数人的第一反应是“交给AES加密跑一遍”。这个习惯没什么问题AES作为行业标准算法安全性和性能都经过长期检验。但你有没有想过一个问题所有使用标准AES的设备用的都是同一张S盒、同一套轮函数。一旦攻击者积累足够多的侧信道样本或者针对固定代数结构做高阶逼近默认算法的那点“底牌”其实是被研究得很透的。我最近在做一套文本加密改进方案时重点就落在两件事上一是给AES增加一把第二密钥二是对SubBytes操作做参数化改造。简单说就是让加密算法不再“千人一面”而是跟着密钥走——每一套密钥对应一套不同的S盒和轮内掩码。这篇内容会从设计思路、轮函数结构、代码实现到实测数据完整过一遍适合正在做加密类项目、或者对密码算法改造感兴趣的开发者参考。1. 为什么给AES加第二密钥1.1 标准AES在实战中的三个软肋先说个容易被忽略的事实。AES-128的密钥空间是2的128次方暴力破解在计算上基本不可行这是它的优点。但算法本身的行为是固定的S盒固定、轮函数固定、扩散矩阵固定。攻击者不需要对每个密钥重新分析算法结构只需要盯着密钥这一个未知量去打。第一个软肋是代数结构固定。AES的S盒本质上是有限域GF(2^8)上的乘法逆元加一个仿射变换这个代数表达式是公开的。密码分析者可以构造低次多元方程尝试用代数攻击的方法去解密钥。虽然对完整AES来说目前还解不动但结构固定意味着攻击者可以提前做大量离线预计算。第二个软肋是侧信道攻击。芯片在执行查表操作时功耗曲线、电磁辐射和时间延迟都会泄露查表索引的信息。标准AES的S盒对所有设备、所有密钥都一样攻击者采集到一批功耗曲线之后就能建立“索引值-功耗特征”的模板。模板一旦建好换一台设备、换一批密钥都不需要重新采集直接套用。第三个软肋是格式泄露。文本数据往往有固定结构比如JSON的括号、Base64的填充字符、UTF-8的字节头。如果实现时不注意分组模式直接用ECB那相同的明文块就会输出相同的密文块数据分布特征直接暴露在密文里。1.2 改进思路让加密行为随密钥“个性化”要解决上面这些问题光靠换分组模式是不够的因为模式只是掩盖结构没有改变算法内核。我的做法是引入一把独立的第二密钥K2让它参与两件事参与S盒的构造用K2派生出一组仿射变换参数生成一张动态S盒。同一套算法不同K2得到不同S盒攻击者没法提前预制模板。参与轮内掩码生成每一轮SubBytes输出之后再与K2派生的轮掩码异或。这样一来即使主密钥K1发生部分泄露K2仍然是一道独立防线。这个设计的关键在于K1和K2的角色完全分开。K1继续负责标准的密钥扩展和轮密钥生成K2负责算法行为的“个性化”。两者组合在一起实际密钥空间从128位扩大到256位同时算法实例本身也变成了密钥相关结构。1.3 一个容易理解的生活类比你可以把标准AES想象成一栋公寓楼里的统一防盗门。门锁质量确实好但每个住户的锁芯结构完全一样开锁公司研究透一把锁就等于研究透了整栋楼。改进方案做的事情有两件第一每个住户入住时都换一个独立锁芯动态S盒第二进门除了钥匙K1还要刷一张门卡K2。锁芯和门卡都由门卡里的信息决定别人就算把门卡数据抄走一份也没法直接推导出另一个住户的锁芯结构。2. 改进后的轮函数长什么样2.1 SubBytes的改进原理从固定S盒到参数化S盒AES的SubBytes是整个算法里唯一的非线性操作它由一个固定的8x8替换表完成。这个替换表的数学形式是输入字节x先计算它在GF(2^8)上的乘法逆元0的逆元规定为0对逆元做一次仿射变换y A·x⁻¹ ⊕ b其中A是8x8仿射矩阵b是常量字节0x63。标准AES里的矩阵A和常量b都是写死的。改进方案的核心就是把A和b变成由K2派生的动态参数。只要A可逆整个S盒仍然是双射加解密可以正常进行同时S盒的代数表达式随着K2变化不再是公开的固定结构。这里有一个必须强调的点不能随便选一个矩阵就上。A的可逆性只是必要条件安全指标也要过关。我后面在实测部分会展开讲动态S盒生成之后要用非线性度和差分均匀度去筛选质量不合格就重新生成。2.2 第二密钥注入轮函数的位置我选择的注入点是在SubBytes输出之后、ShiftRows之前。加密时每一轮的状态更新可以写成state SubBytes_K2(state) # 动态S盒查表 state state XOR mask[r] # 第二密钥轮掩码 state ShiftRows(state) state MixColumns(state) state state XOR roundKey[r]为什么选这个位置有几个考虑。首先SubBytes是非线性操作第二密钥掩码紧跟其后相当于在非线性输出上又叠加了一层密钥相关白化扩散效果更直接。其次解密时只需要在InvSubBytes之前异或同一个掩码即可逆过来实现简单不容易出错。第三掩码不参与S盒构造即使掩码被侧信道抓到攻击者也无法直接反推K2因为掩码是K2经过哈希迭代派生出来的存在单向性。我实际测试时还把掩码按轮迭代更新第r轮的掩码由第r-1轮的掩码哈希后取前16字节生成。这样每一轮的掩码都不一样进一步打散轮之间的统计规律。2.3 解密流程如何镜像对齐改进之后最怕的就是解密乱码。其实只要加密和解密严格按照逆操作顺序就没有问题。解密单块数据的流程是这样的state ciphertext XOR roundKey[10] state InvShiftRows(state) state state XOR mask[10] state InvSubBytes_K2(state) for r in 9 down to 1: state state XOR roundKey[r] state InvMixColumns(state) state InvShiftRows(state) state state XOR mask[r] state InvSubBytes_K2(state) state state XOR roundKey[0]注意InvShiftRows和InvSubBytes的顺序问题。标准AES里两者顺序可以交换因为它们作用在不同的维度上——一个按字节置换一个按字节替换。但在加入掩码之后必须保证加密时先查S盒后异或掩码解密时先异或掩码后查逆S盒这是一个最容易踩坑的地方。2.4 为什么不动MixColumns和ShiftRows有朋友问过我既然要改进为什么不连MixColumns一起改我的回答是尽量少改经过验证的部分。MixColumns使用的是MDS矩阵它保证了差分传播的最优分支数这是AES抵抗差分密码分析的基石。如果你动这个矩阵就必须重新验证明文差分的扩散能力安全性论证的复杂度会上升好几个量级。ShiftRows是纯字节置换不涉及代数结构改了只会更差不会更好。所以我的方案把所有改动集中在SubBytes和密钥注入上保持其他轮函数原封不动。这样做的优点是原有安全论证可以复用新增风险点可控。3. 动态S盒与双密钥的代码实现3.1 环境准备与整体架构我用的语言是Python因为做实验和验证最方便。只需要标准库hashlib不依赖额外的密码学库。加解密的主体轮函数需要自己实现因为标准库不提供向AES轮函数里插入自定义S盒的入口。整个实现分成三层底层工具函数GF(2^8)上的多项式乘法、幂运算、逆元计算S盒构建模块根据K2生成可逆仿射矩阵、构建S盒和逆S盒轮函数集成把动态S盒和轮掩码接进加解密主流程。下面这一段是GF(2^8)的基础运算代码是整个方案的地基def gf_mul(a, b, mod0x11B): res 0 while b: if b 1: res ^ a a 1 if a 0x100: a ^ mod b 1 return res def gf_pow(a, e): res 1 while e: if e 1: res gf_mul(res, a) a gf_mul(a, a) e 1 return res def gf_inv(a): return 0 if a 0 else gf_pow(a, 254)这里要注意gf_inv不是用整数模运算而是在GF(2^8)多项式域上做快速幂。因为有限域的乘法群大小为255任意非零元素的254次幂等于它的逆元。这个细节如果搞错生成的S盒就完全不对了。3.2 生成可逆仿射矩阵接下来是根据K2派生仿射矩阵。核心思路是把K2和随机数做SHA-256生成64字节的随机流每8字节拼成矩阵的一行然后判断这个8x8二进制矩阵是否可逆。不可逆就换nonce重新来。import hashlib def is_invertible(mat): m [r for r in mat] for col in range(8): pivot -1 for r in range(col, 8): if (m[r] col) 1: pivot r break if pivot -1: return False m[col], m[pivot] m[pivot], m[col] for r in range(8): if r ! col and ((m[r] col) 1): m[r] ^ m[col] return True def build_sbox_from_seed(k2, nonce_base1): nonce nonce_base while True: data hashlib.sha256(k2 bytes([nonce])).digest() data hashlib.sha256(data bytes([nonce ^ 0xFF])).digest() mat list(data[:8]) if is_invertible(mat): const data[8] break nonce 1 # 参数化仿射变换生成S盒 sbox [] for x in range(256): inv gf_inv(x) y 0 for bit_i in range(8): out_bit (const bit_i) 1 for bit_j in range(8): if (mat[bit_i] bit_j) 1: out_bit ^ (inv bit_j) 1 y | (out_bit bit_i) sbox.append(y) inv_sbox [0] * 256 for i, v in enumerate(sbox): inv_sbox[v] i return sbox, inv_sbox, mat, const生成S盒时的仿射变换外层的bit_i循环处理输出的8个比特位mat[bit_i]里存的是该输出位依赖哪些输入位。const则提供仿射变换需要的常量偏移。因为矩阵可逆这个变换是双射S盒不会有重复输出。有一点值得注意GF(2^8)上随机8x8矩阵可逆的概率大约是28.8%。也就是说平均生成三四次就能遇到一个可逆矩阵不会出现卡很久的情况。如果你的代码出现长时间循环基本可以断定是矩阵构造逻辑写错了后面排查部分会细说。3.3 轮掩码派生与加解密集成掩码我就是用K2做迭代哈希每一轮取哈希值的前16字节作为掩码def derive_round_masks(k2, rounds): masks [] cur k2 for _ in range(rounds): cur hashlib.sha256(cur).digest() masks.append(list(cur[:16])) return masks轮函数集成的关键逻辑如下。因为完整AES轮函数代码比较长这里给出最能体现改进点的单块加解密流程def encrypt_block(state, round_keys, sbox, masks): state xor_bytes(state, round_keys[0]) for r in range(1, 10): state [sbox[b] for b in state] # 动态S盒查表 state xor_bytes(state, masks[r]) # 第二密钥掩码 state shift_rows(state) state mix_columns(state) state xor_bytes(state, round_keys[r]) state [sbox[b] for b in state] state xor_bytes(state, masks[10]) state shift_rows(state) state xor_bytes(state, round_keys[10]) return state def decrypt_block(state, round_keys, inv_sbox, masks): state xor_bytes(state, round_keys[10]) state inv_shift_rows(state) state xor_bytes(state, masks[10]) state [inv_sbox[b] for b in state] # 先恢复掩码再查逆盒 for r in range(9, 0, -1): state xor_bytes(state, round_keys[r]) state inv_mix_columns(state) state inv_shift_rows(state) state xor_bytes(state, masks[r]) state [inv_sbox[b] for b in state] state xor_bytes(state, round_keys[0]) return stateshift_rows、mix_columns和它们的逆操作可以直接复用标准AES实现不需要改动。这是刻意为之的原因我在2.4节已经说过。3.4 一个重要自检手段退化为标准AES拿到代码之后第一件事不是急着加密数据而是做一个自检把K2设成全零同时把派生出来的仿射矩阵换成标准AES矩阵、常量换成0x63、掩码全部置零。这样改进算法就应该退化为标准AES。这时候你用同样的明文和K1跑一遍改进版和标准AES库结果必须完全一致。如果不一致说明轮函数集成的顺序有问题先排查再继续。这个手段我强烈建议保留以后每次改代码都能用上。4. 实测数据改动到底带来多少收益4.1 安全指标怎么测密码算法的安全性不能靠感觉要用量化指标说话。我主要看四个指标非线性度衡量S盒与所有线性函数的最小距离。标准AES的S盒非线性度是112越接近这个值说明抗线性密码分析能力越强。差分均匀度遍历所有输入差分和输出差分统计最大出现次数。标准AES是4值越小抗差分密码分析能力越强。严格雪崩准则输入任意一个比特翻转每个输出比特的翻转概率应尽可能接近50%。代数次数S盒作为布尔函数组合后的代数次数标准AES是7低于7会更容易受到代数攻击。4.2 抽样测试结果我随机取了10组不同的K2每个都生成动态S盒并做指标筛选数据大概分布如下指标标准AES固定S盒动态S盒抽样范围说明非线性度112108~112最低值仍在安全线以上差分均匀度最大值44~8超过8直接弃用重生成代数次数77动态筛选时稳定严格雪崩准则输出翻转率约0.50.49~0.51与标准AES持平从数据来看不是每个随机生成的S盒都能达到标准AES的水平但通过生成后筛选这个步骤可以保证最终使用的S盒质量不差。我又做了雪崩效应的对比测试随机生成64组明文每组只翻转明文1个比特统计密文128个比特的翻转情况。标准AES和改进方案的平均翻转比特数都在64左右方差约32符合理想随机函数的表现。密钥敏感度测试结果也类似——K1或K2任意一个比特变化密文都会有接近一半的比特发生翻转。4.3 性能开销与降级路线性能是很多人关心的问题。动态S盒和轮掩码带来的额外开销有两部分一次性初始化开销和每块加密的增量开销。一次性开销生成S盒、筛选指标、派生掩码在普通PC上用Python实现的耗时大约是几毫秒。对于文本加密场景这个成本完全可以接受。每块增量开销加密时每条路径上只多了一次16字节的异或操作查表次数和标准AES相同。实测下来整体加密耗时大约比标准AES增加10%~15%。如果你对性能更敏感可以做一个降级配置L1阶段只启用动态S盒不用轮掩码L2阶段全部启用。这样至少能保住动态S盒带来的主要收益把额外开销降到极低。5. 常见问题与排查实录5.1 解密乱码的三种原因我在调试过程中遇到的解密乱码问题基本逃不出三个原因。第一个是逆S盒表构建错误。最直接的表现是加密能跑通解密出来的第一块就乱码。请检查inv_sbox是不是通过sbox反查生成的不要试图用仿射矩阵的逆矩阵去数学推导那样容易出错。第二个是掩码轮次不匹配。加密第3轮用的掩码解密时必须也在对应位置用同一个掩码。如果代码里把数组索引搞错一位前面几轮解密正常到某一轮开始突然乱码优先排查这个。第三个是模式与填充不一致。如果你自己实现了分组加密外面套了CBC模式那么加密和解密必须使用相同的IV和相同的PKCS#7填充逻辑。最常见的错误是解密时忘了移除填充字节导致最后多出几个\x0x之类的字符。5.2 动态S盒生成卡住不动如果代码在生成S盒阶段长时间跑不出去先别怀疑概率低。8x8二进制矩阵可逆概率接近30%按理说试几次就能成功。碰到这种情况重点检查is_invertible函数里的高斯消元是否写对。一个典型错误是只做了“消下三角”没有做“回代”导致判断结果错误。再一个典型错误是矩阵行的排列顺序和仿射变换使用时不一致——生成的时候按一行8字节排使用的时候却按列读。把这两种情况排除掉基本就能跑通了。5.3 文本结构泄露的隐性坑改进算法本身再安全如果分组模式用错还是会泄露信息。前文提到过文本数据有大量重复结构。比如一个JSON文件里会有很多}字符如果这些字符落在同一分组位置上ECB模式会输出相同的密文块攻击者一眼就能看出哪里有重复内容。所以生产环境一定要用CBC模式加随机IV或者更激进的CTR模式。每加密一条新消息都要重新生成IV。这个原则和标准AES完全一样改进方案并没有豁免权。5.4 快速排查清单症状优先排查项解决办法完全乱码轮函数顺序先用退化为标准AES的自检手段单块乱码掩码索引检查加密解密掩码轮次是否对齐尾部多字符填充逻辑确认PKCS#7移除逻辑正确生成S盒卡死可逆矩阵判断检查高斯消元回代部分密文长度异常分组模式确认使用CBC/CTR并正确填充6. 工程化落地的最后几步6.1 双密钥管理的工程实践一个很容易被忽略的工程问题是K2丢了数据就永远解不开了。不同于标准AES只要保管好K1改进方案多了一把K2密钥管理的复杂度和风险就相应增加。我的建议是K1和K2分开存储。K1放在主密钥管理系统里K2放在独立的配置文件或硬件安全模块中。备份时两把密钥分别备份到不同的物理介质避免一把钥匙全拿走的局面。如果K2是通过口令派生的还可以考虑用KDF函数把口令和盐哈希成128位作为K2这样即使用户只记住口令实际使用的密钥仍然具有足够熵。6.2 适合用什么场景这套方案不是拿来替代标准AES成为新的通用标准的它的定位是在特定场景下增加攻击者的分析成本。最适合的场景有三个本地敏感文本存储比如配置文件里的数据库口令、API Token用动态S盒加密后即使文件被拖走攻击者拿标准AES的分析工具也解不开。需要长期保存的加密档案固定S盒的方案在若干年后密码分析技术提升可能存在批量破解风险动态S盒相当于加了一层保险。嵌入式设备通信设备每次固件更新可以轮换K2实现“固件级算法更新”不需要改通信协议。如果你只是给普通Web请求做传输加密直接用TLS就好没有必要上这套方案。改进算法的价值在于长期对抗和定制化不在于替代成熟的标准协议。6.3 后续还可以怎么扩展这个方案后续有两个很自然的扩展方向。一个是可以把S盒的生成参数扩展到每轮不同即10轮使用10张不同的动态S盒进一步打散轮函数结构间的统计相关性。代价是初始化开销变大但对某些高安全需求的场景是值得的。另一个方向是在S盒筛选时加入更多约束条件比如要求代数次数不低于7、差分均匀度不超过4、非线性度不低于110。把筛选条件收紧生成的S盒质量就更接近标准AES。这样即使动态S盒暴露给攻击者他们也没法利用质量缺陷发起有效攻击。我在实际使用中的体会是密码算法的改进最忌“为了改而改”。改一个字节背后是安全性、性能、兼容性和维护成本的一连串权衡。第二密钥加动态SubBytes这套组合拳改动集中在最值得动的地方又能明显提高分析成本算是性价比比较高的一个方向。如果你也在做类似的自定义加密方案建议先从本文的自检手段开始把轮函数和逆轮函数对齐再逐步加入更多密钥参与逻辑每一步都要用可量化的指标去验证。
返回列表