1. 项目概述与背景最近在做一个涉及金融数据交换的项目对接方明确要求使用国密算法对传输报文进行加密。这让我不得不暂时放下熟悉的AES、RSA转头扎进国密算法的世界里。SM4作为对称加密算法是国密体系中的核心成员与SM2非对称、SM3杂凑算法并称“三驾马车”。它的定位类似于国际上的AES但在算法结构、密钥长度和具体运算上有着自己的设计哲学。对于国内从事金融、政务、物联网等对数据安全有合规性要求的开发者来说掌握SM4是一项必备技能。这篇文章我就把自己从零学习SM4到最终用Python实现加解密的完整过程、踩过的坑以及核心代码逻辑做一个详细的梳理和记录。无论你是刚开始接触国密还是正在寻找一个可靠、清晰的Python实现参考希望这篇记录都能帮到你。SM4是一种分组密码算法分组长度和密钥长度均为128位。这意味着它一次处理128比特16字节的明文数据加密密钥也是128比特。算法采用32轮迭代的“非平衡Feistel网络”结构这个结构特点是加解密过程高度一致仅轮密钥的使用顺序相反这在硬件实现和代码编写上带来了很大便利。与国际上广泛使用的AES算法相比SM4的设计更侧重于在硬件上的高效实现和抗侧信道攻击但在软件实现上通过合理的优化也能达到不错的性能。理解其背后的设计思路对于写出正确、高效的代码至关重要。2. SM4算法核心原理深度拆解要真正实现一个算法死记硬背代码是不可取的必须深入到其数学和逻辑内核。SM4的加密过程可以看作是对128位的数据块进行一系列复杂的置换和代换操作。这些操作围绕几个核心部件展开轮函数F、S盒非线性变换τ、线性变换L以及密钥扩展算法。只有搞懂每一个部件的输入、输出和目的你写出的代码才不会是一个不知所以然的“黑盒”。2.1 算法整体结构与流程SM4的加密解密以32轮迭代为核心。它将128位的输入明文或密文分成4个32位的字记为(X₀, X₁, X₂, X₃)。在每一轮迭代中会生成一个新的32位字并淘汰掉最旧的一个字。具体来说对于第i轮i从0到31新的字X_{i4}由前面四个字和本轮轮密钥rk_i通过轮函数F计算得出X_{i4} F(X_i, X_{i1}, X_{i2}, X_{i3}, rk_i) X_i ⊕ T(X_{i1} ⊕ X_{i2} ⊕ X_{i3} ⊕ rk_i)这里的⊕表示32位字的按位异或运算。T是一个由非线性变换τ和线性变换L复合而成的可逆变换即T(.) L(τ(.))。经过32轮迭代后我们得到了36个32位字(X₀, X₁, ..., X₃₅)。最后的输出密文或解密后的明文并不是简单地取最后四个字而是对最后四个字进行一个反序变换(Y₀, Y₁, Y₂, Y₃) (X₃₅, X₃₄, X₃₃, X₃₂)。这个反序操作是SM4算法定义的一部分加解密都必须遵守。解密过程与加密过程完全一致这是Feistel结构的一个优美特性。唯一的区别在于解密时使用的轮密钥序列(rk₀, rk₁, ..., rk₃₁)需要是加密时轮密钥序列的逆序即(rk₃₁, rk₃₀, ..., rk₀)。因此密钥扩展只需要做一次加解密时按不同顺序使用即可。注意很多初学者在实现时容易忽略最后的反序输出步骤导致加解密结果对不上。务必牢记32轮迭代后需要将(X₃₂, X₃₃, X₃₄, X₃₅)反序输出为(Y₀, Y₁, Y₂, Y₃)。2.2 核心部件S盒τ变换与线性变换L变换轮函数F的核心是T变换而T变换由τ和L复合而成。τ变换就是S盒替换它是算法中唯一的非线性来源直接决定了算法的混淆强度。SM4的S盒是一个固定的8位输入、8位输出的查找表。它将一个32位字中的每一个字节共4个字节独立地通过这个S盒进行替换。例如对于一个32位字A (a0, a1, a2, a3)每个a是一个字节τ(A)的结果是(Sbox(a0), Sbox(a1), Sbox(a2), Sbox(a3))。S盒的设计经过了严格密码学分析具有良好的非线性度和差分均匀性。L变换是一个线性变换它对一个32位字B进行操作定义为L(B) B ⊕ (B 2) ⊕ (B 10) ⊕ (B 18) ⊕ (B 24)这里的表示32位循环左移。线性变换的目的是提供扩散使得S盒输出的变化能够迅速影响到整个数据块。τ和L的复合即先进行S盒替换τ再进行线性变换L共同构成了强大的T变换。在代码实现时为了提高效率我们通常会预计算一个名为“T表”的查找表。因为T变换的输入是一个32位字输出也是一个32位字。我们可以预先计算出所有可能的32位输入0x00000000 到 0xFFFFFFFF经过T变换后的结果存储在一个大小为256的表中通过分段查表加速而非4GB的全表。但在Python这种解释型语言中直接按公式实现τ和L代码更清晰且对于学习目的而言性能足够。在追求极致性能的C语言实现中才会大量使用查表法。2.3 密钥扩展算法解析密钥扩展算法的任务是将用户输入的128位加密密钥MK扩展成32个32位的轮密钥rk_i。这个过程本身也是一个类似加密的迭代过程但使用了固定的系统参数FK和固定参数CK。首先将128位MK分为4个32位字(MK₀, MK₁, MK₂, MK₃)。然后与系统参数FK进行异或得到初始的(K₀, K₁, K₂, K₃)(K₀, K₁, K₂, K₃) (MK₀ ⊕ FK₀, MK₁ ⊕ FK₁, MK₂ ⊕ FK₂, MK₃ ⊕ FK₃)系统参数FK是固定的FK₀0xA3B1BAC6, FK₁0x56AA3350, FK₂0x677D9197, FK₃0xB27022DC。引入FK的目的是消除弱密钥增加密钥扩展的随机性。接着进行32轮迭代来生成轮密钥。对于 i 从 0 到 31rk_i K_{i4} K_i ⊕ T(K_{i1} ⊕ K_{i2} ⊕ K_{i3} ⊕ CK_i)注意这里使用的变换是T而不是加密中的T。T变换与T变换结构相同都是L(τ(.))但其中的线性变换L与加密中的L不同L(B) B ⊕ (B 13) ⊕ (B 23)固定参数CK是一组预先定义好的32个32位常数每个CK_i用于一轮密钥扩展。CK的生成有固定规则通常我们直接使用标准中给出的常量值。密钥扩展完成后我们就得到了rk_0到rk_31。加密时按rk_0到rk_31的顺序使用解密时则按rk_31到rk_0的顺序使用。实操心得在实现密钥扩展时务必仔细核对FK、CK的常数值以及L变换的移位常数13和23。这是最容易因笔误导致加解密失败的地方。建议将FK、CK以及S盒的数据以列表形式直接定义在代码开头并加上清晰的注释说明来源。3. Python完整代码实现与逐行解读理解了原理接下来就是动手实现。我将代码分为几个部分常量定义、基础工具函数、核心变换函数、密钥扩展、加解密主函数以及工作模式处理。我会对每一部分进行详细解读。3.1 常量与基础函数定义首先我们需要定义算法所需的所有常量S盒、FK、CK。这些数据来源于国家密码管理局发布的《SM4分组密码算法》标准文档。# SM4算法常量定义 # S盒 256个字节 用于非线性τ变换 S_BOX [ 0xD6, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7, 0x16, 0xB6, 0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05, 0x2B, 0x67, 0x9A, 0x76, 0x2A, 0xBE, 0x04, 0xC3, 0xAA, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99, # ... 此处省略中间部分以节省篇幅实际代码需包含完整的256个值 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D ] # 系统参数 FK FK [0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC] # 固定参数 CK 共32个 每个为32位字 CK [ 0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269, 0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9, 0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249, 0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9, 0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229, 0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299, 0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209, 0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279 ] # 基础工具函数 def left_rotate(n, b): 32位循环左移 return ((n b) | (n (32 - b))) 0xFFFFFFFF def byte2int(b): 字节串转换为整数大端序 return int.from_bytes(b, byteorderbig, signedFalse) def int2byte(n, length4): 整数转换为指定长度的字节串大端序 return n.to_bytes(length, byteorderbig, signedFalse)left_rotate函数实现了32位整数的循环左移并确保结果被限制在32位内通过 0xFFFFFFFF。byte2int和int2byte用于处理Python字节bytes与整数之间的转换并明确指定了大端序big-endian这是SM4算法标准所规定的字节序。3.2 核心变换函数实现接下来实现最关键的τ变换、L/L‘变换以及复合的T/T’变换。def tau_transformation(a): 非线性τ变换对32位字a的每个字节进行S盒替换 a_bytes int2byte(a) # 将32位整数拆成4个字节 b_list [] for byte in a_bytes: b_list.append(S_BOX[byte]) # 查S盒 # 将替换后的4个字节重新组合成32位整数 return byte2int(bytes(b_list)) def l_transformation(b): 加密线性变换L return b ^ left_rotate(b, 2) ^ left_rotate(b, 10) ^ left_rotate(b, 18) ^ left_rotate(b, 24) def l_prime_transformation(b): 密钥扩展线性变换L return b ^ left_rotate(b, 13) ^ left_rotate(b, 23) def t_transformation(z): 加密用T变换T(.) L(τ(.)) return l_transformation(tau_transformation(z)) def t_prime_transformation(z): 密钥扩展用T变换T(.) L(τ(.)) return l_prime_transformation(tau_transformation(z))这里清晰地分离了各个变换。tau_transformation负责S盒替换l_transformation和l_prime_transformation分别对应加密和密钥扩展的线性变换。t_transformation和t_prime_transformation则是它们的复合。这种模块化的实现让代码逻辑清晰易于调试和验证。3.3 密钥扩展算法实现密钥扩展函数接收一个16字节的密钥字节串输出32个轮密钥的列表。def key_expansion(mk): 密钥扩展算法 :param mk: 16字节的加密密钥 (bytes) :return: 包含32个轮密钥rk的列表 (list of int) # 1. 将MK分为4个32位字 mk_words [byte2int(mk[i:i4]) for i in range(0, 16, 4)] # 2. 与FK异或得到K0-K3 k [mk_words[i] ^ FK[i] for i in range(4)] # 3. 32轮迭代生成轮密钥rk_i 和 K_{i4} rk [0] * 32 for i in range(32): # 计算中间值 tmp k[i1] ^ k[i2] ^ k[i3] ^ CK[i] # 应用T变换 tmp t_prime_transformation(tmp) # 生成新的K_{i4}和轮密钥rk_i new_k k[i] ^ tmp k.append(new_k) rk[i] new_k return rk代码严格遵循了原理部分描述的步骤。首先分割密钥然后异或FK最后进行32轮迭代。在每一轮中计算k[i1] ^ k[i2] ^ k[i3] ^ CK[i]经过t_prime_transformation变换后再与k[i]异或结果既作为新的k[i4]存入列表k中也作为本轮轮密钥rk[i]。注意列表k最终会有36个元素K0到K35但我们只需要前32个作为轮密钥。3.4 加解密单块处理函数这是算法的核心迭代过程处理一个16字节的数据块。def _crypt_block(input_block, rk, modeencrypt): 加/解密单个16字节数据块 :param input_block: 16字节输入块 (bytes) :param rk: 轮密钥列表 (list of int) :param mode: encrypt 或 decrypt :return: 16字节输出块 (bytes) # 1. 将输入块分为4个32位字 x [byte2int(input_block[i:i4]) for i in range(0, 16, 4)] # 2. 32轮迭代 for i in range(32): if mode encrypt: round_key rk[i] # 加密顺序rk0, rk1, ..., rk31 else: round_key rk[31 - i] # 解密顺序rk31, rk30, ..., rk0 # 计算轮函数F tmp x[i1] ^ x[i2] ^ x[i3] ^ round_key tmp t_transformation(tmp) new_x x[i] ^ tmp x.append(new_x) # 3. 反序变换并合并输出 # 迭代后x有36个元素最后四个是X32, X33, X34, X35 # 输出应为 Y (X35, X34, X33, X32) y x[35], x[34], x[33], x[32] output_bytes b.join([int2byte(word) for word in y]) return output_bytes这个函数体现了SM4加解密的一致性。通过mode参数控制轮密钥的使用顺序。加密时正向使用rk解密时反向使用。迭代完成后严格按标准进行反序输出。这是整个算法中最容易出错的一步务必确认下标是否正确。3.5 工作模式封装与填充处理分组密码不能直接加密任意长度的数据需要配合工作模式如ECB、CBC和填充方案如PKCS#7。这里我们实现最常用的CBC模式。def pad(data, block_size16): PKCS#7填充 padding_len block_size - (len(data) % block_size) padding bytes([padding_len] * padding_len) return data padding def unpad(padded_data): PKCS#7去填充 padding_len padded_data[-1] # 简单的有效性检查 if padding_len 1 or padding_len 16: raise ValueError(Invalid padding length.) if padded_data[-padding_len:] ! bytes([padding_len] * padding_len): raise ValueError(Invalid padding bytes.) return padded_data[:-padding_len] def sm4_cbc_encrypt(plaintext, key, iv): SM4-CBC模式加密 :param plaintext: 明文字节串 :param key: 16字节密钥 :param iv: 16字节初始化向量 :return: 密文字节串 if len(key) ! 16: raise ValueError(Key must be 16 bytes long.) if len(iv) ! 16: raise ValueError(IV must be 16 bytes long.) # 扩展密钥 rk key_expansion(key) # 填充明文 padded_plaintext pad(plaintext) # CBC加密 ciphertext b prev_block iv # 第一个块的前一个块是IV for i in range(0, len(padded_plaintext), 16): block padded_plaintext[i:i16] # CBC模式明文块与前一个密文块或IV异或 block_to_encrypt bytes(a ^ b for a, b in zip(block, prev_block)) encrypted_block _crypt_block(block_to_encrypt, rk, modeencrypt) ciphertext encrypted_block prev_block encrypted_block return ciphertext def sm4_cbc_decrypt(ciphertext, key, iv): SM4-CBC模式解密 :param ciphertext: 密文字节串长度应为16的倍数 :param key: 16字节密钥 :param iv: 16字节初始化向量 :return: 明文字节串 if len(key) ! 16: raise ValueError(Key must be 16 bytes long.) if len(iv) ! 16: raise ValueError(IV must be 16 bytes long.) if len(ciphertext) % 16 ! 0: raise ValueError(Ciphertext length must be a multiple of 16 bytes.) rk key_expansion(key) plaintext b prev_block iv for i in range(0, len(ciphertext), 16): block ciphertext[i:i16] # 先解密当前块 decrypted_block _crypt_block(block, rk, modedecrypt) # 再与前一个密文块或IV异或得到明文块 plaintext_block bytes(a ^ b for a, b in zip(decrypted_block, prev_block)) plaintext plaintext_block prev_block block # 更新前一个块为当前密文块 # 去除填充 return unpad(plaintext)pad和unpad函数实现了PKCS#7填充这是确保数据长度是分组整数倍的标准方法。sm4_cbc_encrypt和sm4_cbc_decrypt则完整实现了CBC模式。CBC模式引入了初始化向量IV增强了安全性避免了ECB模式中相同明文块产生相同密文块的问题。在解密时注意prev_block的更新是使用当前密文块而不是解密后的中间块这是CBC模式的标准操作。3.6 完整示例与测试最后我们写一个简单的测试来验证整个实现的正确性。if __name__ __main__: # 测试用例参考官方文档或已知向量 key b1234567890abcdef # 16字节密钥 iv babcdefghijklmnop # 16字节IV CBC模式需要 plaintext bHello, SM4! This is a test message. print(f原始明文: {plaintext}) print(f密钥: {key.hex()}) print(fIV: {iv.hex()}) # 加密 ciphertext sm4_cbc_encrypt(plaintext, key, iv) print(f\n加密后密文(hex): {ciphertext.hex()}) # 解密 decrypted sm4_cbc_decrypt(ciphertext, key, iv) print(f解密后明文: {decrypted}) # 验证 if decrypted plaintext: print(\n✅ 加解密测试成功) else: print(\n❌ 加解密测试失败)运行这段代码如果一切正确你将看到“加解密测试成功”的输出。你也可以寻找官方的测试向量进行更严格的验证确保算法实现的每一个细节都符合标准。4. 常见问题、调试技巧与性能考量自己动手实现密码算法调试是绕不开的一环。下面分享几个我踩过的坑和总结的技巧。4.1 典型问题与排查清单加解密结果不对输出全是乱码或固定值检查字节序这是最常见的问题。SM4算法规定所有数据输入、输出、密钥、中间字都采用大端序Big-Endian。在byte2int和int2byte函数中必须明确指定byteorderbig。如果你错误地使用了小端序结果必然错误。检查S盒、FK、CK数据确保从可靠来源如国标文档完整、准确地复制了这些常量表。一个字节的错误都可能导致整个算法失效。建议将你的常量与官方文档逐字节核对。检查轮密钥顺序确认加密时使用rk[0]到rk[31]解密时使用rk[31]到rk[0]。检查反序输出确认在32轮迭代后输出是(X[35], X[34], X[33], X[32])而不是(X[32], X[33], X[34], X[35])。CBC模式解密后只有第一个块错误后续块正确检查IV处理在CBC解密时prev_block的更新必须使用当前密文块而不是解密后的中间块。这是CBC模式解密的经典错误。检查加解密流程确认加密时是先异或再加密解密时是先解密再异或。解密后提示“Invalid padding”密文被篡改或密钥错误如果密文在传输或存储中发生任何改变或者使用了错误的密钥/IV进行解密解密得到的数据去填充时就会失败因为填充字节不再合规。填充实现有误检查你的pad和unpad函数。unpad函数应该验证填充字节的值和长度是否合理这是防止“Padding Oracle”攻击的良好实践。性能问题加密大文件速度很慢Python解释型语言的局限纯Python实现的密码算法性能无法与C语言相比。这是预期之内的。优化建议使用查表法将T变换t_transformation的结果预计算并存储在表中可以避免每轮都进行耗时的S盒查找和循环移位运算。使用NumPy或C扩展对于性能敏感的生产环境应考虑使用cryptography等成熟库如果支持SM4或者用C语言编写核心模块并通过Python调用。并行化处理在ECB模式下各数据块独立可以并行加密。但在CBC模式下由于链式依赖无法并行。4.2 安全使用注意事项密钥管理是关键算法本身是安全的但密钥如果泄露或管理不当一切皆空。切勿将密钥硬编码在代码中或明文存储在配置文件里。应使用安全的密钥管理系统。选择正确的工作模式永远不要使用ECB模式加密有意义的数据因为它不能隐藏数据模式。至少使用CBC模式并确保IV是随机且不可预测的。对于现代应用更推荐使用认证加密模式如GCM它能同时提供保密性和完整性。此代码用于学习本文提供的代码旨在帮助理解SM4算法原理。在生产环境中强烈建议使用经过广泛审计和验证的密码学库如gmssl国产或cryptography如果其版本支持SM4。这些库经过了更多优化和安全审查。侧信道攻击这个简单的Python实现没有考虑任何侧信道攻击防护如计时攻击。在实际的硬件或需要高安全等级的软件实现中必须采用常数时间编程等技术来防御这类攻击。4.3 与其他系统的对接在实际项目中你很可能需要与其他系统如Java后端、硬件加密机进行SM4加解密交互。以下几点至关重要参数对齐确保双方在以下方面完全一致算法SM4。密钥长度128位16字节。分组模式如CBC。填充方式如PKCS#7/PKCS#5。初始化向量IVCBC模式必须使用且需协商好如何传递有时会拼接在密文前。数据格式密文和IV通常以十六进制字符串或Base64编码传输需统一。使用标准测试向量验证在联调前双方先用官方或公认的测试向量验证各自的实现是否正确。这能快速定位问题是出在算法实现还是参数配置上。关注库的版本差异不同密码库对SM4的支持程度和默认参数可能不同。例如有些库可能将PKCS#7填充称为PKCS#5填充在16字节分组下等价。务必仔细阅读文档。实现一个密码算法是一次深刻的学习过程它迫使你关注每一个比特和每一个步骤的精确性。通过这次对SM4从原理到代码的完整梳理我不仅掌握了算法本身更对分组密码的设计之美有了更深体会。希望这份详细的记录能成为你学习国密算法路上的一块有用的垫脚石。如果在实现过程中遇到其他问题多回归算法标准文档多使用小的测试向量进行单步调试问题总能被定位和解决。
从原理到实践:Python实现国密SM4算法详解
1. 项目概述与背景最近在做一个涉及金融数据交换的项目对接方明确要求使用国密算法对传输报文进行加密。这让我不得不暂时放下熟悉的AES、RSA转头扎进国密算法的世界里。SM4作为对称加密算法是国密体系中的核心成员与SM2非对称、SM3杂凑算法并称“三驾马车”。它的定位类似于国际上的AES但在算法结构、密钥长度和具体运算上有着自己的设计哲学。对于国内从事金融、政务、物联网等对数据安全有合规性要求的开发者来说掌握SM4是一项必备技能。这篇文章我就把自己从零学习SM4到最终用Python实现加解密的完整过程、踩过的坑以及核心代码逻辑做一个详细的梳理和记录。无论你是刚开始接触国密还是正在寻找一个可靠、清晰的Python实现参考希望这篇记录都能帮到你。SM4是一种分组密码算法分组长度和密钥长度均为128位。这意味着它一次处理128比特16字节的明文数据加密密钥也是128比特。算法采用32轮迭代的“非平衡Feistel网络”结构这个结构特点是加解密过程高度一致仅轮密钥的使用顺序相反这在硬件实现和代码编写上带来了很大便利。与国际上广泛使用的AES算法相比SM4的设计更侧重于在硬件上的高效实现和抗侧信道攻击但在软件实现上通过合理的优化也能达到不错的性能。理解其背后的设计思路对于写出正确、高效的代码至关重要。2. SM4算法核心原理深度拆解要真正实现一个算法死记硬背代码是不可取的必须深入到其数学和逻辑内核。SM4的加密过程可以看作是对128位的数据块进行一系列复杂的置换和代换操作。这些操作围绕几个核心部件展开轮函数F、S盒非线性变换τ、线性变换L以及密钥扩展算法。只有搞懂每一个部件的输入、输出和目的你写出的代码才不会是一个不知所以然的“黑盒”。2.1 算法整体结构与流程SM4的加密解密以32轮迭代为核心。它将128位的输入明文或密文分成4个32位的字记为(X₀, X₁, X₂, X₃)。在每一轮迭代中会生成一个新的32位字并淘汰掉最旧的一个字。具体来说对于第i轮i从0到31新的字X_{i4}由前面四个字和本轮轮密钥rk_i通过轮函数F计算得出X_{i4} F(X_i, X_{i1}, X_{i2}, X_{i3}, rk_i) X_i ⊕ T(X_{i1} ⊕ X_{i2} ⊕ X_{i3} ⊕ rk_i)这里的⊕表示32位字的按位异或运算。T是一个由非线性变换τ和线性变换L复合而成的可逆变换即T(.) L(τ(.))。经过32轮迭代后我们得到了36个32位字(X₀, X₁, ..., X₃₅)。最后的输出密文或解密后的明文并不是简单地取最后四个字而是对最后四个字进行一个反序变换(Y₀, Y₁, Y₂, Y₃) (X₃₅, X₃₄, X₃₃, X₃₂)。这个反序操作是SM4算法定义的一部分加解密都必须遵守。解密过程与加密过程完全一致这是Feistel结构的一个优美特性。唯一的区别在于解密时使用的轮密钥序列(rk₀, rk₁, ..., rk₃₁)需要是加密时轮密钥序列的逆序即(rk₃₁, rk₃₀, ..., rk₀)。因此密钥扩展只需要做一次加解密时按不同顺序使用即可。注意很多初学者在实现时容易忽略最后的反序输出步骤导致加解密结果对不上。务必牢记32轮迭代后需要将(X₃₂, X₃₃, X₃₄, X₃₅)反序输出为(Y₀, Y₁, Y₂, Y₃)。2.2 核心部件S盒τ变换与线性变换L变换轮函数F的核心是T变换而T变换由τ和L复合而成。τ变换就是S盒替换它是算法中唯一的非线性来源直接决定了算法的混淆强度。SM4的S盒是一个固定的8位输入、8位输出的查找表。它将一个32位字中的每一个字节共4个字节独立地通过这个S盒进行替换。例如对于一个32位字A (a0, a1, a2, a3)每个a是一个字节τ(A)的结果是(Sbox(a0), Sbox(a1), Sbox(a2), Sbox(a3))。S盒的设计经过了严格密码学分析具有良好的非线性度和差分均匀性。L变换是一个线性变换它对一个32位字B进行操作定义为L(B) B ⊕ (B 2) ⊕ (B 10) ⊕ (B 18) ⊕ (B 24)这里的表示32位循环左移。线性变换的目的是提供扩散使得S盒输出的变化能够迅速影响到整个数据块。τ和L的复合即先进行S盒替换τ再进行线性变换L共同构成了强大的T变换。在代码实现时为了提高效率我们通常会预计算一个名为“T表”的查找表。因为T变换的输入是一个32位字输出也是一个32位字。我们可以预先计算出所有可能的32位输入0x00000000 到 0xFFFFFFFF经过T变换后的结果存储在一个大小为256的表中通过分段查表加速而非4GB的全表。但在Python这种解释型语言中直接按公式实现τ和L代码更清晰且对于学习目的而言性能足够。在追求极致性能的C语言实现中才会大量使用查表法。2.3 密钥扩展算法解析密钥扩展算法的任务是将用户输入的128位加密密钥MK扩展成32个32位的轮密钥rk_i。这个过程本身也是一个类似加密的迭代过程但使用了固定的系统参数FK和固定参数CK。首先将128位MK分为4个32位字(MK₀, MK₁, MK₂, MK₃)。然后与系统参数FK进行异或得到初始的(K₀, K₁, K₂, K₃)(K₀, K₁, K₂, K₃) (MK₀ ⊕ FK₀, MK₁ ⊕ FK₁, MK₂ ⊕ FK₂, MK₃ ⊕ FK₃)系统参数FK是固定的FK₀0xA3B1BAC6, FK₁0x56AA3350, FK₂0x677D9197, FK₃0xB27022DC。引入FK的目的是消除弱密钥增加密钥扩展的随机性。接着进行32轮迭代来生成轮密钥。对于 i 从 0 到 31rk_i K_{i4} K_i ⊕ T(K_{i1} ⊕ K_{i2} ⊕ K_{i3} ⊕ CK_i)注意这里使用的变换是T而不是加密中的T。T变换与T变换结构相同都是L(τ(.))但其中的线性变换L与加密中的L不同L(B) B ⊕ (B 13) ⊕ (B 23)固定参数CK是一组预先定义好的32个32位常数每个CK_i用于一轮密钥扩展。CK的生成有固定规则通常我们直接使用标准中给出的常量值。密钥扩展完成后我们就得到了rk_0到rk_31。加密时按rk_0到rk_31的顺序使用解密时则按rk_31到rk_0的顺序使用。实操心得在实现密钥扩展时务必仔细核对FK、CK的常数值以及L变换的移位常数13和23。这是最容易因笔误导致加解密失败的地方。建议将FK、CK以及S盒的数据以列表形式直接定义在代码开头并加上清晰的注释说明来源。3. Python完整代码实现与逐行解读理解了原理接下来就是动手实现。我将代码分为几个部分常量定义、基础工具函数、核心变换函数、密钥扩展、加解密主函数以及工作模式处理。我会对每一部分进行详细解读。3.1 常量与基础函数定义首先我们需要定义算法所需的所有常量S盒、FK、CK。这些数据来源于国家密码管理局发布的《SM4分组密码算法》标准文档。# SM4算法常量定义 # S盒 256个字节 用于非线性τ变换 S_BOX [ 0xD6, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7, 0x16, 0xB6, 0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05, 0x2B, 0x67, 0x9A, 0x76, 0x2A, 0xBE, 0x04, 0xC3, 0xAA, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99, # ... 此处省略中间部分以节省篇幅实际代码需包含完整的256个值 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D ] # 系统参数 FK FK [0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC] # 固定参数 CK 共32个 每个为32位字 CK [ 0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269, 0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9, 0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249, 0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9, 0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229, 0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299, 0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209, 0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279 ] # 基础工具函数 def left_rotate(n, b): 32位循环左移 return ((n b) | (n (32 - b))) 0xFFFFFFFF def byte2int(b): 字节串转换为整数大端序 return int.from_bytes(b, byteorderbig, signedFalse) def int2byte(n, length4): 整数转换为指定长度的字节串大端序 return n.to_bytes(length, byteorderbig, signedFalse)left_rotate函数实现了32位整数的循环左移并确保结果被限制在32位内通过 0xFFFFFFFF。byte2int和int2byte用于处理Python字节bytes与整数之间的转换并明确指定了大端序big-endian这是SM4算法标准所规定的字节序。3.2 核心变换函数实现接下来实现最关键的τ变换、L/L‘变换以及复合的T/T’变换。def tau_transformation(a): 非线性τ变换对32位字a的每个字节进行S盒替换 a_bytes int2byte(a) # 将32位整数拆成4个字节 b_list [] for byte in a_bytes: b_list.append(S_BOX[byte]) # 查S盒 # 将替换后的4个字节重新组合成32位整数 return byte2int(bytes(b_list)) def l_transformation(b): 加密线性变换L return b ^ left_rotate(b, 2) ^ left_rotate(b, 10) ^ left_rotate(b, 18) ^ left_rotate(b, 24) def l_prime_transformation(b): 密钥扩展线性变换L return b ^ left_rotate(b, 13) ^ left_rotate(b, 23) def t_transformation(z): 加密用T变换T(.) L(τ(.)) return l_transformation(tau_transformation(z)) def t_prime_transformation(z): 密钥扩展用T变换T(.) L(τ(.)) return l_prime_transformation(tau_transformation(z))这里清晰地分离了各个变换。tau_transformation负责S盒替换l_transformation和l_prime_transformation分别对应加密和密钥扩展的线性变换。t_transformation和t_prime_transformation则是它们的复合。这种模块化的实现让代码逻辑清晰易于调试和验证。3.3 密钥扩展算法实现密钥扩展函数接收一个16字节的密钥字节串输出32个轮密钥的列表。def key_expansion(mk): 密钥扩展算法 :param mk: 16字节的加密密钥 (bytes) :return: 包含32个轮密钥rk的列表 (list of int) # 1. 将MK分为4个32位字 mk_words [byte2int(mk[i:i4]) for i in range(0, 16, 4)] # 2. 与FK异或得到K0-K3 k [mk_words[i] ^ FK[i] for i in range(4)] # 3. 32轮迭代生成轮密钥rk_i 和 K_{i4} rk [0] * 32 for i in range(32): # 计算中间值 tmp k[i1] ^ k[i2] ^ k[i3] ^ CK[i] # 应用T变换 tmp t_prime_transformation(tmp) # 生成新的K_{i4}和轮密钥rk_i new_k k[i] ^ tmp k.append(new_k) rk[i] new_k return rk代码严格遵循了原理部分描述的步骤。首先分割密钥然后异或FK最后进行32轮迭代。在每一轮中计算k[i1] ^ k[i2] ^ k[i3] ^ CK[i]经过t_prime_transformation变换后再与k[i]异或结果既作为新的k[i4]存入列表k中也作为本轮轮密钥rk[i]。注意列表k最终会有36个元素K0到K35但我们只需要前32个作为轮密钥。3.4 加解密单块处理函数这是算法的核心迭代过程处理一个16字节的数据块。def _crypt_block(input_block, rk, modeencrypt): 加/解密单个16字节数据块 :param input_block: 16字节输入块 (bytes) :param rk: 轮密钥列表 (list of int) :param mode: encrypt 或 decrypt :return: 16字节输出块 (bytes) # 1. 将输入块分为4个32位字 x [byte2int(input_block[i:i4]) for i in range(0, 16, 4)] # 2. 32轮迭代 for i in range(32): if mode encrypt: round_key rk[i] # 加密顺序rk0, rk1, ..., rk31 else: round_key rk[31 - i] # 解密顺序rk31, rk30, ..., rk0 # 计算轮函数F tmp x[i1] ^ x[i2] ^ x[i3] ^ round_key tmp t_transformation(tmp) new_x x[i] ^ tmp x.append(new_x) # 3. 反序变换并合并输出 # 迭代后x有36个元素最后四个是X32, X33, X34, X35 # 输出应为 Y (X35, X34, X33, X32) y x[35], x[34], x[33], x[32] output_bytes b.join([int2byte(word) for word in y]) return output_bytes这个函数体现了SM4加解密的一致性。通过mode参数控制轮密钥的使用顺序。加密时正向使用rk解密时反向使用。迭代完成后严格按标准进行反序输出。这是整个算法中最容易出错的一步务必确认下标是否正确。3.5 工作模式封装与填充处理分组密码不能直接加密任意长度的数据需要配合工作模式如ECB、CBC和填充方案如PKCS#7。这里我们实现最常用的CBC模式。def pad(data, block_size16): PKCS#7填充 padding_len block_size - (len(data) % block_size) padding bytes([padding_len] * padding_len) return data padding def unpad(padded_data): PKCS#7去填充 padding_len padded_data[-1] # 简单的有效性检查 if padding_len 1 or padding_len 16: raise ValueError(Invalid padding length.) if padded_data[-padding_len:] ! bytes([padding_len] * padding_len): raise ValueError(Invalid padding bytes.) return padded_data[:-padding_len] def sm4_cbc_encrypt(plaintext, key, iv): SM4-CBC模式加密 :param plaintext: 明文字节串 :param key: 16字节密钥 :param iv: 16字节初始化向量 :return: 密文字节串 if len(key) ! 16: raise ValueError(Key must be 16 bytes long.) if len(iv) ! 16: raise ValueError(IV must be 16 bytes long.) # 扩展密钥 rk key_expansion(key) # 填充明文 padded_plaintext pad(plaintext) # CBC加密 ciphertext b prev_block iv # 第一个块的前一个块是IV for i in range(0, len(padded_plaintext), 16): block padded_plaintext[i:i16] # CBC模式明文块与前一个密文块或IV异或 block_to_encrypt bytes(a ^ b for a, b in zip(block, prev_block)) encrypted_block _crypt_block(block_to_encrypt, rk, modeencrypt) ciphertext encrypted_block prev_block encrypted_block return ciphertext def sm4_cbc_decrypt(ciphertext, key, iv): SM4-CBC模式解密 :param ciphertext: 密文字节串长度应为16的倍数 :param key: 16字节密钥 :param iv: 16字节初始化向量 :return: 明文字节串 if len(key) ! 16: raise ValueError(Key must be 16 bytes long.) if len(iv) ! 16: raise ValueError(IV must be 16 bytes long.) if len(ciphertext) % 16 ! 0: raise ValueError(Ciphertext length must be a multiple of 16 bytes.) rk key_expansion(key) plaintext b prev_block iv for i in range(0, len(ciphertext), 16): block ciphertext[i:i16] # 先解密当前块 decrypted_block _crypt_block(block, rk, modedecrypt) # 再与前一个密文块或IV异或得到明文块 plaintext_block bytes(a ^ b for a, b in zip(decrypted_block, prev_block)) plaintext plaintext_block prev_block block # 更新前一个块为当前密文块 # 去除填充 return unpad(plaintext)pad和unpad函数实现了PKCS#7填充这是确保数据长度是分组整数倍的标准方法。sm4_cbc_encrypt和sm4_cbc_decrypt则完整实现了CBC模式。CBC模式引入了初始化向量IV增强了安全性避免了ECB模式中相同明文块产生相同密文块的问题。在解密时注意prev_block的更新是使用当前密文块而不是解密后的中间块这是CBC模式的标准操作。3.6 完整示例与测试最后我们写一个简单的测试来验证整个实现的正确性。if __name__ __main__: # 测试用例参考官方文档或已知向量 key b1234567890abcdef # 16字节密钥 iv babcdefghijklmnop # 16字节IV CBC模式需要 plaintext bHello, SM4! This is a test message. print(f原始明文: {plaintext}) print(f密钥: {key.hex()}) print(fIV: {iv.hex()}) # 加密 ciphertext sm4_cbc_encrypt(plaintext, key, iv) print(f\n加密后密文(hex): {ciphertext.hex()}) # 解密 decrypted sm4_cbc_decrypt(ciphertext, key, iv) print(f解密后明文: {decrypted}) # 验证 if decrypted plaintext: print(\n✅ 加解密测试成功) else: print(\n❌ 加解密测试失败)运行这段代码如果一切正确你将看到“加解密测试成功”的输出。你也可以寻找官方的测试向量进行更严格的验证确保算法实现的每一个细节都符合标准。4. 常见问题、调试技巧与性能考量自己动手实现密码算法调试是绕不开的一环。下面分享几个我踩过的坑和总结的技巧。4.1 典型问题与排查清单加解密结果不对输出全是乱码或固定值检查字节序这是最常见的问题。SM4算法规定所有数据输入、输出、密钥、中间字都采用大端序Big-Endian。在byte2int和int2byte函数中必须明确指定byteorderbig。如果你错误地使用了小端序结果必然错误。检查S盒、FK、CK数据确保从可靠来源如国标文档完整、准确地复制了这些常量表。一个字节的错误都可能导致整个算法失效。建议将你的常量与官方文档逐字节核对。检查轮密钥顺序确认加密时使用rk[0]到rk[31]解密时使用rk[31]到rk[0]。检查反序输出确认在32轮迭代后输出是(X[35], X[34], X[33], X[32])而不是(X[32], X[33], X[34], X[35])。CBC模式解密后只有第一个块错误后续块正确检查IV处理在CBC解密时prev_block的更新必须使用当前密文块而不是解密后的中间块。这是CBC模式解密的经典错误。检查加解密流程确认加密时是先异或再加密解密时是先解密再异或。解密后提示“Invalid padding”密文被篡改或密钥错误如果密文在传输或存储中发生任何改变或者使用了错误的密钥/IV进行解密解密得到的数据去填充时就会失败因为填充字节不再合规。填充实现有误检查你的pad和unpad函数。unpad函数应该验证填充字节的值和长度是否合理这是防止“Padding Oracle”攻击的良好实践。性能问题加密大文件速度很慢Python解释型语言的局限纯Python实现的密码算法性能无法与C语言相比。这是预期之内的。优化建议使用查表法将T变换t_transformation的结果预计算并存储在表中可以避免每轮都进行耗时的S盒查找和循环移位运算。使用NumPy或C扩展对于性能敏感的生产环境应考虑使用cryptography等成熟库如果支持SM4或者用C语言编写核心模块并通过Python调用。并行化处理在ECB模式下各数据块独立可以并行加密。但在CBC模式下由于链式依赖无法并行。4.2 安全使用注意事项密钥管理是关键算法本身是安全的但密钥如果泄露或管理不当一切皆空。切勿将密钥硬编码在代码中或明文存储在配置文件里。应使用安全的密钥管理系统。选择正确的工作模式永远不要使用ECB模式加密有意义的数据因为它不能隐藏数据模式。至少使用CBC模式并确保IV是随机且不可预测的。对于现代应用更推荐使用认证加密模式如GCM它能同时提供保密性和完整性。此代码用于学习本文提供的代码旨在帮助理解SM4算法原理。在生产环境中强烈建议使用经过广泛审计和验证的密码学库如gmssl国产或cryptography如果其版本支持SM4。这些库经过了更多优化和安全审查。侧信道攻击这个简单的Python实现没有考虑任何侧信道攻击防护如计时攻击。在实际的硬件或需要高安全等级的软件实现中必须采用常数时间编程等技术来防御这类攻击。4.3 与其他系统的对接在实际项目中你很可能需要与其他系统如Java后端、硬件加密机进行SM4加解密交互。以下几点至关重要参数对齐确保双方在以下方面完全一致算法SM4。密钥长度128位16字节。分组模式如CBC。填充方式如PKCS#7/PKCS#5。初始化向量IVCBC模式必须使用且需协商好如何传递有时会拼接在密文前。数据格式密文和IV通常以十六进制字符串或Base64编码传输需统一。使用标准测试向量验证在联调前双方先用官方或公认的测试向量验证各自的实现是否正确。这能快速定位问题是出在算法实现还是参数配置上。关注库的版本差异不同密码库对SM4的支持程度和默认参数可能不同。例如有些库可能将PKCS#7填充称为PKCS#5填充在16字节分组下等价。务必仔细阅读文档。实现一个密码算法是一次深刻的学习过程它迫使你关注每一个比特和每一个步骤的精确性。通过这次对SM4从原理到代码的完整梳理我不仅掌握了算法本身更对分组密码的设计之美有了更深体会。希望这份详细的记录能成为你学习国密算法路上的一块有用的垫脚石。如果在实现过程中遇到其他问题多回归算法标准文档多使用小的测试向量进行单步调试问题总能被定位和解决。