C语言实现SM4国密算法:从原理到工程实践与性能优化

📅 2026/7/14 5:20:58
C语言实现SM4国密算法:从原理到工程实践与性能优化
1. 项目概述为什么选择用C语言实现SM4如果你是一名嵌入式开发者、安全协议工程师或者对底层性能有极致追求的C程序员那么“用C语言手搓一个SM4国密算法”这个想法很可能已经在你脑海里盘旋过。SM4作为我国官方认定的商用分组密码标准其设计简洁、安全性高在金融、物联网、政务系统等领域有着广泛的应用前景。但当你真正准备动手时可能会发现网上能找到的C语言实现要么过于学术化、可读性差要么为了追求极致的速度而牺牲了代码的清晰度和可移植性对于想深入理解算法原理或需要将其集成到特定项目中的开发者来说并不友好。这个项目的核心目标就是打造一个安全性与性能兼备同时代码清晰、易于理解和集成的SM4算法C语言实现。我们不仅要让加密解密跑起来更要弄清楚每一轮迭代、每一次S盒替换背后的数学逻辑和工程考量。我会带你从零开始一步步构建出完整的SM4算法并深入探讨如何在不同的应用场景如ECB、CBC模式下使用它最后还会分享性能优化的实战技巧和那些在官方文档里找不到的“踩坑”经验。无论你是想学习国密算法原理还是需要在你的下一个C语言项目中集成可靠的加密模块这篇文章都将是一份详实的实战指南。2. SM4算法核心原理深度拆解在动手写代码之前我们必须吃透SM4的“内功心法”。SM4是一种分组密码算法顾名思义它把明文数据切成固定大小的“块”来处理。它的块大小和密钥长度都是128位即16个字节。整个算法的核心可以概括为两个部分轮函数F和密钥扩展算法。它们都基于一个叫做32轮迭代的广义Feistel结构。2.1 广义Feistel结构与轮函数FFeistel结构是许多经典密码如DES的基础它的一个巨大优点是加解密过程可以使用相同的结构仅密钥使用顺序相反这极大地简化了硬件和软件的实现。SM4采用了广义Feistel结构其轮函数F是安全性的核心。假设每一轮输入的128位数据被等分为4个32位的字( X_0, X_1, X_2, X_3 )。当前轮的轮密钥为 ( rk_i )也是一个32位字。那么轮函数F的输出即下一轮的 ( X_4 )由以下公式定义 [ X_4 F(X_0, X_1, X_2, X_3, rk_i) X_0 \oplus T(X_1 \oplus X_2 \oplus X_3 \oplus rk_i) ] 这里的 ( \oplus ) 表示32位字的按位异或运算。关键点在于T变换。T变换由两个子变换复合而成( T(.) L(\tau(.)) )。非线性变换τ这是一个由4个并行的8进8出的S盒S-Box构成。它将输入的32位字拆分成4个字节a0, a1, a2, a3每个字节独立地通过一个固定的查找表S盒进行替换输出4个新的字节b0, b1, b2, b3再组合成一个32位字。S盒是密码算法中提供“混淆”特性的关键其设计需要满足严格的密码学特性如非线性、差分均匀性等。SM4的S盒是固定的我们需要在代码中将其定义为一个256字节的常量数组。线性变换L这是一个线性扩散层目的是将S盒输出的“混淆”效果扩散到整个字中。对于32位输入BL变换定义为 [ L(B) B \oplus (B \lll 2) \oplus (B \lll 10) \oplus (B \lll 18) \oplus (B \lll 24) ] 其中 ( \lll ) 表示循环左移。这个操作确保了输入中一个比特的改变会迅速影响到输出中的多个比特提供了“扩散”特性。注意循环左移与普通逻辑左移不同移出的高位比特会补充到低位。在C语言中我们需要用((x n) | (x (32 - n)))这样的组合操作来实现32位内的循环左移。一轮操作完成后数据字进行左移( (X_1, X_2, X_3, X_4) ) 成为下一轮的输入。如此迭代32轮。2.2 密钥扩展算法从初始密钥生成轮密钥加密需要32个轮密钥 ( rk_0, rk_1, ..., rk_{31} )。它们由一个128位的初始密钥MK由4个32位字MK0, MK1, MK2, MK3组成通过密钥扩展算法生成。首先系统参数FK和固定参数CK会参与运算。FK是固定的系统参数CK是预先定义好的固定参数序列每个CK_i也是一个32位字。密钥扩展步骤首先计算中间值 ( (K_0, K_1, K_2, K_3) (MK_0 \oplus FK_0, MK_1 \oplus FK_1, MK_2 \oplus FK_2, MK_3 \oplus FK_3) )。然后对于 ( i 0, 1, ..., 31 )计算 [ rk_i K_{i4} K_i \oplus T(K_{i1} \oplus K_{i2} \oplus K_{i3} \oplus CK_i) ] 这里的 ( T ) 变换与加密中的T变换类似但线性变换L不同 [ L(B) B \oplus (B \lll 13) \oplus (B \lll 23) ] 非线性变换τ即S盒替换与加密过程完全相同。为什么密钥扩展也需要S盒和变换这增强了密钥扩展算法的非线性特性即使初始密钥只有微小的差异产生的轮密钥序列也会截然不同这能有效抵抗相关的密钥攻击。L变换的循环左移位数1323与加密中的L变换2101824不同这是为了在加密和密钥扩展之间提供算法上的区分进一步增加安全性。理解了这些我们就掌握了SM4算法的“灵魂”。接下来我们将把这些数学公式和逻辑转化为清晰、高效的C代码。3. C语言实现从结构定义到核心函数我们将采用自底向上的方式构建代码首先定义基础数据类型和常量然后实现核心变换函数最后组装成完整的加密/解密和密钥扩展函数。3.1 基础类型与常量定义为了确保可移植性特别是处理32位无符号整数和字节序问题时我们需要进行明确的定义。#include stdint.h // 使用标准整数类型 // 定义SM4中使用的32位无符号整数类型 typedef uint32_t sm4_word_t; // 定义16字节128位的密钥和数据块类型 typedef struct { uint8_t b[16]; } sm4_block_t; // 系统参数FK static const sm4_word_t FK[4] { 0xA3B1BAC6, 0x56AA3350, 0x677D9197, 0xB27022DC }; // 固定参数CK共32个此处仅示例前4个 static const sm4_word_t CK[32] { 0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269, // ... 省略后续28个 }; // S盒256字节的查找表 static const uint8_t SBOX[256] { 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, // ... 省略后续内容 };实操心得字节序问题。SM4算法标准定义中的数据字都是大端序Big-Endian即高位字节存储在低地址。而我们的主机可能是小端序如x86。一种常见的处理方法是在从字节流加载数据到sm4_word_t时显式地进行字节序转换使用ntohl或手动移位组合在算法内部运算全部使用主机字节序最后输出时再转换回大端序。另一种更高效、更清晰的做法是约定我们的C实现内部统一使用大端序来表示字。这意味着我们在GET_WORD和PUT_WORD宏中就要完成转换。本文为了代码清晰和与标准文档对应采用后一种“内部大端序”的约定。// 从字节数组大端序加载一个32位字 #define SM4_GET_U32_BE(b, i) \ (((sm4_word_t)((b)[(i)] 0xff) 24) | \ ((sm4_word_t)((b)[(i)1] 0xff) 16) | \ ((sm4_word_t)((b)[(i)2] 0xff) 8) | \ ((sm4_word_t)((b)[(i)3] 0xff))) // 将一个32位字存储到字节数组大端序 #define SM4_PUT_U32_BE(n, b, i) \ do { \ (b)[(i)] (uint8_t)((n) 24); \ (b)[(i)1] (uint8_t)((n) 16); \ (b)[(i)2] (uint8_t)((n) 8); \ (b)[(i)3] (uint8_t)((n)); \ } while (0) // 32位循环左移 #define SM4_ROTL(x, n) (((x) (n)) | ((x) (32 - (n))))3.2 核心变换函数的实现基于上面的宏我们可以实现τ变换、L变换、L‘变换以及完整的T和T’变换。// 非线性变换τ32位字通过S盒替换 static sm4_word_t sm4_tau(sm4_word_t x) { uint8_t a[4], b[4]; // 将字x分解为4个字节注意大端序a0是最高位字节 a[0] (x 24) 0xff; a[1] (x 16) 0xff; a[2] (x 8) 0xff; a[3] x 0xff; // 每个字节通过S盒替换 b[0] SBOX[a[0]]; b[1] SBOX[a[1]]; b[2] SBOX[a[2]]; b[3] SBOX[a[3]]; // 将4个字节组合回32位字 return ((sm4_word_t)b[0] 24) | ((sm4_word_t)b[1] 16) | ((sm4_word_t)b[2] 8) | ((sm4_word_t)b[3]); } // 加密线性变换L static sm4_word_t sm4_L(sm4_word_t x) { return x ^ SM4_ROTL(x, 2) ^ SM4_ROTL(x, 10) ^ SM4_ROTL(x, 18) ^ SM4_ROTL(x, 24); } // 密钥扩展线性变换L static sm4_word_t sm4_L_prime(sm4_word_t x) { return x ^ SM4_ROTL(x, 13) ^ SM4_ROTL(x, 23); } // 加密合成变换T static sm4_word_t sm4_T(sm4_word_t x) { return sm4_L(sm4_tau(x)); } // 密钥扩展合成变换T static sm4_word_t sm4_T_prime(sm4_word_t x) { return sm4_L_prime(sm4_tau(x)); }3.3 密钥扩展函数实现现在我们可以实现密钥扩展函数它接收一个16字节的密钥生成32个轮密钥并存储到数组中。/** * brief SM4密钥扩展 * param key 输入密钥16字节 * param rk 输出轮密钥数组必须至少有32个sm4_word_t的空间 */ void sm4_key_schedule(const uint8_t key[16], sm4_word_t rk[32]) { sm4_word_t K[36]; // 中间密钥我们需要用到K[0]到K[35] sm4_word_t MK[4]; // 将16字节密钥加载为4个大端序字 MK[0] SM4_GET_U32_BE(key, 0); MK[1] SM4_GET_U32_BE(key, 4); MK[2] SM4_GET_U32_BE(key, 8); MK[3] SM4_GET_U32_BE(key, 12); // 计算初始K K[0] MK[0] ^ FK[0]; K[1] MK[1] ^ FK[1]; K[2] MK[2] ^ FK[2]; K[3] MK[3] ^ FK[3]; // 迭代生成轮密钥 for (int i 0; i 32; i) { sm4_word_t tmp K[i1] ^ K[i2] ^ K[i3] ^ CK[i]; rk[i] K[i4] K[i] ^ sm4_T_prime(tmp); } }3.4 加解密轮函数与主函数加解密过程共享同一个轮函数结构只是轮密钥的使用顺序相反。/** * brief SM4一轮加密/解密变换 * param X 输入状态数组4个字 * param rk 本轮轮密钥 * return 本轮输出的字即下一轮的X4 */ static sm4_word_t sm4_round(sm4_word_t X[4], sm4_word_t rk) { return X[0] ^ sm4_T(X[1] ^ X[2] ^ X[3] ^ rk); } /** * brief SM4加密一个数据块 * param rk 密钥扩展得到的轮密钥数组 * param input 输入明文块16字节 * param output 输出密文块16字节 */ void sm4_encrypt_block(const sm4_word_t rk[32], const uint8_t input[16], uint8_t output[16]) { sm4_word_t X[36]; // 足够存储所有轮的状态 // 加载明文 X[0] SM4_GET_U32_BE(input, 0); X[1] SM4_GET_U32_BE(input, 4); X[2] SM4_GET_U32_BE(input, 8); X[3] SM4_GET_U32_BE(input, 12); // 32轮迭代 for (int i 0; i 32; i) { X[i4] sm4_round(X[i], rk[i]); } // 最终输出反序 SM4_PUT_U32_BE(X[35], output, 0); // 最后一轮产生的X[35]对应最终输出的X[0] SM4_PUT_U32_BE(X[34], output, 4); // X[34]对应最终输出的X[1] SM4_PUT_U32_BE(X[33], output, 8); // X[33]对应最终输出的X[2] SM4_PUT_U32_BE(X[32], output, 12); // X[32]对应最终输出的X[3] } /** * brief SM4解密一个数据块 * param rk 密钥扩展得到的轮密钥数组注意解密时需使用逆序轮密钥 * param input 输入密文块16字节 * param output 输出明文块16字节 */ void sm4_decrypt_block(const sm4_word_t rk[32], const uint8_t input[16], uint8_t output[16]) { sm4_word_t X[36]; // 加载密文 X[0] SM4_GET_U32_BE(input, 0); X[1] SM4_GET_U32_BE(input, 4); X[2] SM4_GET_U32_BE(input, 8); X[3] SM4_GET_U32_BE(input, 12); // 32轮迭代使用逆序轮密钥 for (int i 0; i 32; i) { X[i4] sm4_round(X[i], rk[31 - i]); // 关键rk[31-i] } // 最终输出反序 SM4_PUT_U32_BE(X[35], output, 0); SM4_PUT_U32_BE(X[34], output, 4); SM4_PUT_U32_BE(X[33], output, 8); SM4_PUT_U32_BE(X[32], output, 12); }至此一个完整的、可工作的SM4 ECB模式电子密码本模式加解密核心已经完成。你可以使用sm4_key_schedule生成轮密钥然后对任意16字节的数据块调用sm4_encrypt_block或sm4_decrypt_block。4. 工作模式扩展从ECB到CBC与CTR直接使用上述块加密函数ECB模式加密多个块是有安全问题的。相同的明文块会产生相同的密文块这不能隐藏数据模式。因此我们需要实现更安全的工作模式。4.1 CBC模式实现与要点CBC密码块链接模式是最常用的模式之一。它引入了一个初始化向量IV并将前一个密文块与当前明文块异或后再加密。/** * brief SM4-CBC加密 * param rk 加密轮密钥 * param iv 初始化向量16字节 * param input 输入明文长度必须是16字节的倍数 * param output 输出密文大小与输入相同 * param length 数据总长度字节 */ void sm4_cbc_encrypt(const sm4_word_t rk[32], const uint8_t iv[16], const uint8_t *input, uint8_t *output, size_t length) { uint8_t block[16]; uint8_t feedback[16]; // 用于存储上一个密文块 if (length % 16 ! 0) { // 错误处理CBC要求数据是块大小的整数倍。实际应用中可能需要填充。 return; } memcpy(feedback, iv, 16); // 初始反馈使用IV for (size_t i 0; i length; i 16) { // 明文块与反馈值异或 for (int j 0; j 16; j) { block[j] input[i j] ^ feedback[j]; } // 加密 sm4_encrypt_block(rk, block, output[i]); // 更新反馈值为当前密文块 memcpy(feedback, output[i], 16); } } /** * brief SM4-CBC解密 * param rk 解密轮密钥即加密轮密钥的逆序 * param iv 初始化向量必须与加密时相同 * param input 输入密文 * param output 输出明文 * param length 数据总长度 */ void sm4_cbc_decrypt(const sm4_word_t rk[32], const uint8_t iv[16], const uint8_t *input, uint8_t *output, size_t length) { uint8_t block[16]; uint8_t feedback[16]; uint8_t cur_cipher[16]; if (length % 16 ! 0) { return; } memcpy(feedback, iv, 16); for (size_t i 0; i length; i 16) { memcpy(cur_cipher, input[i], 16); // 保存当前密文块 // 解密当前块 sm4_decrypt_block(rk, cur_cipher, block); // 与反馈值上一个密文块异或得到明文 for (int j 0; j 16; j) { output[i j] block[j] ^ feedback[j]; } // 更新反馈值为当前密文块用于下一个块解密 memcpy(feedback, cur_cipher, 16); } }重要注意事项填充Padding。上述CBC实现假设输入数据长度已经是16字节的倍数。现实中数据长度往往是任意的。因此在加密前需要对明文进行填充在解密后需要去除填充。PKCS#7是一种最常用的填充方案。例如如果需要填充n个字节则每个填充字节的值都是n。解密后读取最后一个字节的值n并检查最后n个字节是否都等于n以验证并移除填充。忘记处理填充是CBC模式实现中最常见的错误之一。4.2 CTR模式简介与优势CTR计数器模式将分组密码转换为流密码。它使用一个计数器Counter和Nonce一次性数字生成密钥流然后与明文进行异或。其最大优点是并行化加密和解密操作完全相同且可以预先计算密钥流和无需填充。实现思路是生成一个连续的计数器序列如Nonce || Counter用SM4加密这个序列得到密钥流块再与明文/密文异或。CTR模式的安全性要求同一个Key, Nonce对绝不能重复使用来加密不同的消息否则会导致密钥流重用严重破坏安全性。void sm4_ctr_crypt(const sm4_word_t rk[32], const uint8_t nonce[12], // 假设96位Nonce const uint8_t *input, uint8_t *output, size_t length) { uint8_t counter_block[16]; uint8_t keystream_block[16]; uint32_t counter 0; // 32位计数器 size_t pos 0; memcpy(counter_block, nonce, 12); // Nonce放在前12字节 while (pos length) { // 构造计数器块Nonce || Counter (大端序) SM4_PUT_U32_BE(counter, counter_block, 12); // 加密计数器块生成密钥流 sm4_encrypt_block(rk, counter_block, keystream_block); // 密钥流与明文/密文异或 size_t bytes_to_xor (length - pos) 16 ? (length - pos) : 16; for (size_t i 0; i bytes_to_xor; i) { output[pos i] input[pos i] ^ keystream_block[i]; } pos bytes_to_xor; counter; // 计数器递增 } }5. 性能优化实战从查表法到指令集加速一个基础的SM4实现已经可以工作但在对性能敏感的场景如网络数据包加密、大文件处理下我们需要对其进行优化。5.1 查表法优化T-table这是最经典的对称加密优化技术。观察T变换T(x) L(τ(x))。对于所有可能的32位输入x其T(x)的结果是固定的。我们可以预先计算一个大小为256的表因为τ变换是4个独立的S盒每个S盒输入8位但更有效的是为整个T变换构造4个1KB256个32位字的查找表。具体来说我们将32位输入x的每个字节分别通过不同的T_i表查找然后异或起来。这需要重新推导T变换的公式使其满足T(x) T0[a0] ^ T1[a1] ^ T2[a2] ^ T3[a3]其中a0, a1, a2, a3是x的4个字节T0, T1, T2, T3是预先计算好的表。// 预先计算T-table (需要在初始化时完成) static sm4_word_t T0[256], T1[256], T2[256], T3[256]; void sm4_init_tables(void) { for (int i 0; i 256; i) { sm4_word_t x ((sm4_word_t)i 24); // 假设该字节在最高位 sm4_word_t y sm4_T(x); // 调用基础的T变换计算 T0[i] y; x ((sm4_word_t)i 16); y sm4_T(x); T1[i] y; x ((sm4_word_t)i 8); y sm4_T(x); T2[i] y; x (sm4_word_t)i; y sm4_T(x); T3[i] y; } } // 使用T-table的快速轮函数 static sm4_word_t sm4_round_fast(sm4_word_t X[4], sm4_word_t rk) { sm4_word_t tmp X[1] ^ X[2] ^ X[3] ^ rk; return X[0] ^ T0[(tmp 24) 0xff] ^ T1[(tmp 16) 0xff] ^ T2[(tmp 8) 0xff] ^ T3[tmp 0xff]; }使用查表法后一轮加密从多次S盒查找、移位、异或操作减少为4次查表和3次异或性能提升非常显著。但代价是增加了约4KB的静态数据并且可能影响CPU缓存效率。5.2 并行处理与流水线SM4的32轮迭代是串行的但我们可以利用现代CPU的SIMD指令集如SSE、AVX、NEON来同时加密多个独立的数据块。例如使用128位SIMD寄存器可以同时处理4个32位字。我们需要将数据重新组织为“结构数组”AoS到“数组结构”SoA的形式即把多个块的X0放在一个向量里多个块的X1放在另一个向量里以此类推。然后使用SIMD指令并行执行异或、查表可能需要使用向量查表指令如_mm_shuffle_epi8模拟等操作。这种优化在加解密大量数据时效果极好但实现复杂且严重依赖特定平台。5.3 专用指令集支持一些现代的CPU架构直接提供了对SM4算法的硬件加速指令。例如ARMv8.2-A及以上版本的ARM架构提供了SM4指令扩展。使用内联汇编或编译器 intrinsics 调用这些指令可以获得最高的性能和能效比。如果你的目标平台是ARM服务器或高端嵌入式设备务必检查是否支持此特性。// 示例ARM Neon intrinsics (需编译器支持) #include arm_neon.h // 使用vsm4ekeyq_u32进行密钥扩展vsm4eq_u32进行加密轮操作。优化策略选择建议通用嵌入式平台Cortex-M系列优先考虑代码尺寸和内存占用基础实现或轻度优化即可。查表法可能因占用RAM/ROM过多而不适合。高性能应用x86/ARM A系列首选查表法它是性能与实现复杂度之间的最佳平衡。如果性能要求极高且数据量巨大再考虑SIMD并行优化。支持硬件指令的平台无条件使用硬件指令这是终极解决方案。6. 安全实现关键点与常见陷阱实现一个密码算法正确性和性能固然重要但安全性才是生命线。以下是一些必须警惕的陷阱。6.1 侧信道攻击防御密码算法在物理设备上运行时其功耗、电磁辐射、执行时间等“侧信道”信息可能泄露密钥。时间攻击算法的执行时间如果依赖于密钥或数据攻击者可能通过精确测量时间来推断信息。我们的查表法实现其访问内存地址依赖于数据tmp的字节值理论上存在缓存时序攻击的风险。一种缓解措施是使用常时间编程技术例如用按位操作组合成S盒功能或者使用对所有可能输入都访问相同缓存线大小的查找表。功耗分析在智能卡等设备上简单的实现可能因条件分支或数据依赖的功耗差异而泄露信息。需要使用掩码等技术进行防护。对于大多数软件应用如果运行在用户态的通用操作系统上来自远程的网络计时攻击难度很大。但如果你在为智能卡、HSM硬件安全模块或高安全等级的嵌入式系统编写代码必须考虑这些因素通常需要密码工程专家的介入。6.2 内存安全与密钥管理清零敏感数据在函数栈或堆中使用的密钥、中间状态如轮密钥rk、反馈寄存器feedback在使用完毕后应立即用memset_s或类似的安全内存清零函数进行覆盖防止这些数据残留在内存中被其他进程或核心转储读取。void secure_erase(void *ptr, size_t len) { volatile uint8_t *p (volatile uint8_t *)ptr; while (len--) { *p 0; } }密钥生命周期不要在代码中硬编码密钥。密钥应该通过安全的渠道输入如安全芯片、密钥管理服务并在使用后尽快销毁。轮密钥也应视为敏感数据。避免使用ECB模式如前所述ECB模式不安全应始终使用CBC带合适的IV和填充、CTR或GCM等认证加密模式。6.3 测试与验证如何确保你的实现是正确的标准测试向量国密标准文档附录中提供了标准的加密、解密、密钥扩展测试向量。你必须用这些向量来验证你的基础算法实现。这是第一步也是最重要的一步。边界测试测试空数据、单个块、多个块、非对齐长度对于支持流模式如CTR等情况。随机性测试用大量随机密钥和明文进行加密检查密文的统计特性如位均衡性虽然这不能证明安全性但可以排除明显的错误。交叉验证用另一个可靠的实现如OpenSSL的SM4实现如果可用作为参考进行随机数据加解密的交叉验证。7. 集成与应用一个简单的文件加密工具示例最后我们将上面的模块组合起来实现一个简单的命令行文件加密工具使用SM4-CBC模式。这能展示如何在实际项目中组织代码。// sm4_file.c (部分关键代码) #include stdio.h #include stdlib.h #include string.h // ... 包含之前实现的sm4.h头文件 // PKCS#7 填充 size_t pkcs7_pad(uint8_t *buf, size_t data_len, size_t block_size) { size_t pad_len block_size - (data_len % block_size); if (pad_len 0) pad_len block_size; memset(buf data_len, (uint8_t)pad_len, pad_len); return data_len pad_len; } int pkcs7_unpad(const uint8_t *buf, size_t padded_len, size_t *data_len) { if (padded_len 0 || padded_len % 16 ! 0) return -1; uint8_t pad_byte buf[padded_len - 1]; if (pad_byte 0 || pad_byte 16) return -1; for (size_t i padded_len - pad_byte; i padded_len; i) { if (buf[i] ! pad_byte) return -1; } *data_len padded_len - pad_byte; return 0; } int main(int argc, char *argv[]) { if (argc ! 5) { fprintf(stderr, Usage: %s encrypt|decrypt input file output file key(16 hex chars)\n, argv[0]); return 1; } int mode strcmp(argv[1], encrypt) 0 ? 1 : 0; // 1加密0解密 FILE *fin fopen(argv[2], rb); FILE *fout fopen(argv[3], wb); uint8_t key[16]; // 从命令行参数解析16进制密钥此处省略解析代码 // ... uint8_t iv[16] {0}; // 实际应用中IV必须是随机且不可预测的 // 应使用安全的随机数生成器如 /dev/urandom 或 CryptGenRandom // for(int i0; i16; i) iv[i] rand(); // 错误不要用rand()。 sm4_word_t rk[32]; sm4_key_schedule(key, rk); uint8_t in_buf[1024]; // 读缓冲区 uint8_t out_buf[1024 16]; // 写缓冲区考虑填充 size_t bytes_read; if (mode) { // 加密 // 写入IV到文件头部解密时需要相同的IV fwrite(iv, 1, 16, fout); while ((bytes_read fread(in_buf, 1, sizeof(in_buf), fin)) 0) { size_t padded_len pkcs7_pad(in_buf, bytes_read, 16); sm4_cbc_encrypt(rk, iv, in_buf, out_buf, padded_len); // 更新IV为最后一个密文块用于下一个循环如果数据分多次加密 memcpy(iv, out_buf padded_len - 16, 16); fwrite(out_buf, 1, padded_len, fout); } } else { // 解密 // 从文件头部读取IV if (fread(iv, 1, 16, fin) ! 16) { /* 错误处理 */ } fseek(fout, 0, SEEK_SET); // 准备写入解密后数据 while ((bytes_read fread(in_buf, 1, sizeof(in_buf), fin)) 0) { sm4_cbc_decrypt(rk, iv, in_buf, out_buf, bytes_read); // 更新IV为当前密文块用于下一个循环 memcpy(iv, in_buf bytes_read - 16, 16); // 如果是最后一个块需要去除填充 if (feof(fin)) { size_t data_len; if (pkcs7_unpad(out_buf, bytes_read, data_len) ! 0) { fprintf(stderr, Padding error!\n); break; } fwrite(out_buf, 1, data_len, fout); } else { fwrite(out_buf, 1, bytes_read, fout); } } } fclose(fin); fclose(fout); // 安全擦除栈上的敏感数据 secure_erase(key, sizeof(key)); secure_erase(rk, sizeof(rk)); secure_erase(iv, sizeof(iv)); return 0; }这个示例涵盖了文件I/O、填充、CBC模式、IV管理和基本的错误处理。在实际产品中你需要加入更完善的错误检查、安全的随机数生成用于IV和密钥、以及可能的认证如使用HMAC或GCM模式来保证密文的完整性。通过这个从原理到实现从基础到优化再到安全实践和集成的完整旅程你应该已经掌握了用C语言实现一个工业级SM4加密算法的核心知识与技能。记住密码学实现无小事始终对安全性保持敬畏并通过严格的测试来验证你的代码。