1. 项目概述最近在做一个需要数据安全传输的C#上位机项目客户明确要求使用国密算法。AES虽然通用但为了满足合规性最终选定了SM4。网上搜了一圈发现关于C#实现SM4的资料要么过于零散要么直接丢给你一个GitHub链接让你自己琢磨。我自己在集成和调试过程中把BouncyCastle库的用法、密钥处理、IV管理这些坑都踩了一遍也参考了像dds2333/SM4这样的开源实现。今天就把这些实战经验整理出来从算法原理、BouncyCastle库的集成调用到ECB/CBC模式的具体实现、密钥与初始向量的正确处理以及如何将加密数据持久化形成一个完整的、可落地的解决方案。无论你是需要在WinForm、WPF还是ASP.NET Core项目里集成SM4这篇文章都能给你提供清晰的路径和避坑指南。2. SM4算法核心原理与模式选择在动手写代码之前我们必须搞清楚SM4到底是什么以及为什么在CBC和ECB之间我们通常会毫不犹豫地选择CBC。SM4是一种分组密码算法和AES属于同一类别但它是我国官方认定的商用密码算法标准。它的分组长度和密钥长度都是128位16字节。这意味着无论你的原始数据有多长算法都会把它切成一个个128位的小块进行处理。2.1 算法核心流程简述SM4的加密过程围绕一个非线性的迭代结构展开。它会对一个128位的明文分组进行32轮相同的变换操作称为轮函数。每一轮都会用到轮密钥这个轮密钥是从你输入的128位主密钥通过密钥扩展算法派生出来的。轮函数里包含了非线性S盒替换、线性变换L等操作这些操作共同保证了算法的混淆和扩散特性使得密文与明文、密钥之间的关系变得极其复杂。解密过程就是加密过程的逆序使用同样的轮密钥但以相反的顺序应用。理解这一点很重要SM4的解密算法和加密算法在结构上是对称的这简化了实现但并不意味着它的安全性打折扣。2.2 ECB与CBC模式深度解析这是实际应用中最关键的选择之一选错了模式你的加密可能形同虚设。ECB模式是最基础的模式。它就像是一个没有经验的厨师把每一块肉数据块都用同样的方式、同样的调料密钥单独腌制。问题在于如果两块明文一模一样加密后的密文块也会一模一样。想象一下一张图片其天空部分是大片均匀的蓝色在ECB模式下加密后这部分可能会呈现出明显的、重复的纹理图案导致信息泄露。所以ECB模式一般不推荐用于加密有意义的数据序列它更适合加密随机数据或单个数据块。CBC模式则引入了“链条”的概念。在加密第一块明文之前它会先和一个叫做“初始向量”的随机数据块进行异或操作然后再加密。得到第一块密文后在加密第二块明文时会先将第二块明文与第一块密文进行异或再加密如此循环。这就好比炒菜时每炒完一盘锅底都会留下一些味道和油脂上一盘的密文这些“锅气”会影响到下一盘菜下一块明文的最终味道。这样一来即使两块明文完全相同加密后的密文也完全不同彻底消除了ECB的模式缺陷。IV的随机性和保密性至关重要但它不需要像密钥一样永久保密通常可以随密文一起存储或传输。注意在dds2333/SM4项目的早期版本中CBC模式的IV是写死在代码里的。这带来了一个严重问题如果程序重启IV丢失就无法解密之前加密的数据。后来作者通过将IV与密文一同保存的方案解决了这个问题这是CBC模式正确实现的必经之路。2.3 填充机制的必要性由于SM4是分组密码要求数据长度必须是128位的整数倍。但实际数据长度千变万化这就需要填充。最常用的是PKCS7填充。它的规则很简单如果需要填充N个字节那么每个填充字节的值就是N。例如一个块最后缺3个字节就填充0x03, 0x03, 0x03。解密后读取最后一个字节的值就知道要移除多少填充字节。BouncyCastle库已经内置了对PKCS7填充的支持我们直接调用即可这避免了手动实现填充可能引入的错误。3. 开发环境搭建与BouncyCastle库集成工欲善其事必先利其器。在C#中使用SM4最稳妥、最高效的方式就是借助成熟的密码学库——BouncyCastle。自己从头实现算法不仅容易出错还可能引入安全漏洞。3.1 创建项目与安装库首先打开Visual Studio创建一个新的项目根据你的需求选择控制台应用、WPF应用或类库。这里我以.NET 6的控制台应用为例因为它最通用。创建好项目后我们需要通过NuGet包管理器来安装BouncyCastle。你有两种方式Visual Studio图形界面在“解决方案资源管理器”中右键点击你的项目 - “管理NuGet程序包” - 在“浏览”标签页搜索“BouncyCastle”选择BouncyCastle.Cryptography并安装。命令行/包管理器控制台如果你喜欢命令行可以打开“工具”-“NuGet包管理器”-“包管理器控制台”输入以下命令Install-Package BouncyCastle.Cryptography安装完成后你会在项目依赖项中看到它。接下来在需要使用的代码文件顶部添加命名空间引用using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities.Encoders;Secure类提供了密码学相关的安全随机数生成器Encoders命名空间下的Hex类用于十六进制字符串的编解码这在调试和存储时非常有用。3.2 密钥的生成与处理规范SM4要求密钥是严格的16字节128位。但用户输入的密码可能长短不一我们需要一个健壮的处理流程。绝对禁止的做法直接使用Encoding.ASCII.GetBytes(“123456”)得到的6字节数组作为密钥或者简单地将长字符串截断成16字节。前者密钥空间太小极易被破解后者丢弃了部分熵。推荐的做法使用密钥派生函数从用户密码生成密钥。虽然BouncyCastle的SM4实现允许直接传入16字节密钥但为了安全我们应该引入SM3哈希算法。SM3也是国密算法用它来处理用户密码非常合适。流程如下将用户输入的密码字符串转换为字节数组。使用SM3算法对该字节数组进行哈希得到一个32字节的摘要。取这个32字节摘要的前16字节或后16字节但必须固定作为SM4的实际加密密钥。这样做的好处是无论用户输入多长多短的密码最终都会固定生成一个128位的高强度密钥。同时SM3的单向性保证了无法从最终的16字节密钥反推出原始密码。// 示例使用SM3从字符串派生SM4密钥 public static byte[] DeriveSm4KeyFromPassword(string password) { byte[] passwordBytes Encoding.UTF8.GetBytes(password); SM3Digest sm3 new SM3Digest(); sm3.BlockUpdate(passwordBytes, 0, passwordBytes.Length); byte[] digest new byte[sm3.GetDigestSize()]; // SM3摘要长度为32字节 sm3.DoFinal(digest, 0); // 取前16字节作为SM4密钥 byte[] sm4Key new byte[16]; Array.Copy(digest, 0, sm4Key, 0, 16); return sm4Key; }实操心得在实际项目中我强烈建议将派生出的16字节密钥或用于派生的原始密码通过安全的密钥管理系统进行存储而不是硬编码在代码或配置文件中。对于更高安全要求的场景可以考虑结合SecureRandom生成一个“盐值”与用户密码混合后再进行SM3哈希能有效抵御彩虹表攻击。4. 核心加密解密功能实现掌握了原理和准备工作后我们进入最核心的代码实现环节。我将分别展示ECB和CBC模式的完整实现并重点讲解CBC模式中IV的处理这个关键细节。4.1 ECB模式实现ECB模式没有IV实现相对简单但务必记住其使用限制。public class Sm4CryptoService { // ECB 加密 public static byte[] EncryptEcb(byte[] plainBytes, byte[] key) { // 1. 参数校验 if (key null || key.Length ! 16) throw new ArgumentException(SM4密钥必须为16字节128位。, nameof(key)); if (plainBytes null || plainBytes.Length 0) throw new ArgumentException(明文不能为空。, nameof(plainBytes)); // 2. 创建密钥参数 KeyParameter keyParam ParameterUtilities.CreateKeyParameter(SM4, key); // 3. 获取密码器并初始化为加密模式 // “SM4/ECB/PKCS7Padding” 指定了算法、模式和填充方式 IBufferedCipher cipher CipherUtilities.GetCipher(SM4/ECB/PKCS7Padding); cipher.Init(true, keyParam); // true 表示加密 // 4. 执行加密 return cipher.DoFinal(plainBytes); } // ECB 解密 public static byte[] DecryptEcb(byte[] cipherBytes, byte[] key) { // 参数校验同上... if (key null || key.Length ! 16) throw new ArgumentException(SM4密钥必须为16字节128位。, nameof(key)); if (cipherBytes null || cipherBytes.Length 0) throw new ArgumentException(密文不能为空。, nameof(cipherBytes)); KeyParameter keyParam ParameterUtilities.CreateKeyParameter(SM4, key); IBufferedCipher cipher CipherUtilities.GetCipher(SM4/ECB/PKCS7Padding); cipher.Init(false, keyParam); // false 表示解密 // 解密时DoFinal同样会处理PKCS7填充的移除 return cipher.DoFinal(cipherBytes); } }调用示例string originalText “这是一段需要加密的敏感数据”; byte[] key DeriveSm4KeyFromPassword(“MySecretPassword123”); byte[] plainData Encoding.UTF8.GetBytes(originalText); // 加密 byte[] encryptedData Sm4CryptoService.EncryptEcb(plainData, key); string encryptedHex Hex.ToHexString(encryptedData); // 转换为十六进制字符串便于查看或传输 Console.WriteLine($“ECB加密结果Hex: {encryptedHex}”); // 解密 byte[] decryptedData Sm4CryptoService.DecryptEcb(encryptedData, key); string decryptedText Encoding.UTF8.GetString(decryptedData); Console.WriteLine($“ECB解密结果: {decryptedText}”);4.2 CBC模式实现与IV管理策略CBC模式是实践中的主力其实现关键在于IV的生命周期管理。我们的目标是每次加密使用随机IV并将该IV与密文绑定确保后续能正确解密。public class Sm4CryptoService { // CBC 加密 - 返回一个包含IV和密文的复合字节数组 public static byte[] EncryptCbc(byte[] plainBytes, byte[] key) { if (key null || key.Length ! 16) throw new ArgumentException(“SM4密钥必须为16字节128位。”, nameof(key)); if (plainBytes null || plainBytes.Length 0) throw new ArgumentException(“明文不能为空。”, nameof(plainBytes)); // 1. 生成随机IV16字节 byte[] iv new byte[16]; using (var rng new SecureRandom()) { rng.NextBytes(iv); } // 2. 创建带IV的密钥参数 KeyParameter keyParam ParameterUtilities.CreateKeyParameter(“SM4”, key); ParametersWithIV keyParamWithIv new ParametersWithIV(keyParam, iv); // 3. 初始化和执行加密 IBufferedCipher cipher CipherUtilities.GetCipher(“SM4/CBC/PKCS7Padding”); cipher.Init(true, keyParamWithIv); byte[] cipherBytes cipher.DoFinal(plainBytes); // 4. 将IV和密文拼接在一起返回 // 常见格式[IV (16字节)][CipherText] byte[] result new byte[iv.Length cipherBytes.Length]; Buffer.BlockCopy(iv, 0, result, 0, iv.Length); Buffer.BlockCopy(cipherBytes, 0, result, iv.Length, cipherBytes.Length); return result; } // CBC 解密 - 从复合字节数组中分离IV和密文 public static byte[] DecryptCbc(byte[] combinedBytes, byte[] key) { // 参数校验... if (combinedBytes null || combinedBytes.Length 16) throw new ArgumentException(“复合数据长度不足应包含IV16字节和密文。”, nameof(combinedBytes)); // 1. 从数据头部提取IV前16字节 byte[] iv new byte[16]; Buffer.BlockCopy(combinedBytes, 0, iv, 0, 16); // 2. 提取实际的密文部分 byte[] cipherBytes new byte[combinedBytes.Length - 16]; Buffer.BlockCopy(combinedBytes, 16, cipherBytes, 0, cipherBytes.Length); // 3. 创建带IV的密钥参数并初始化解密器 KeyParameter keyParam ParameterUtilities.CreateKeyParameter(“SM4”, key); ParametersWithIV keyParamWithIv new ParametersWithIV(keyParam, iv); IBufferedCipher cipher CipherUtilities.GetCipher(“SM4/CBC/PKCS7Padding”); cipher.Init(false, keyParamWithIv); // 4. 执行解密 return cipher.DoFinal(cipherBytes); } }调用与数据持久化示例 加密后我们可以将EncryptCbc返回的复合字节数组直接写入文件。解密时从文件中读取整个字节数组传给DecryptCbc方法。// 加密并保存到文件 byte[] dataToEncrypt File.ReadAllBytes(“sensitive_document.pdf”); byte[] key DeriveSm4KeyFromPassword(“FilePassword!”); byte[] encryptedPackage Sm4CryptoService.EncryptCbc(dataToEncrypt, key); File.WriteAllBytes(“encrypted_document.sm4”, encryptedPackage); Console.WriteLine(“文件加密并保存成功。”); // 从文件读取并解密 byte[] encryptedPackageFromFile File.ReadAllBytes(“encrypted_document.sm4”); byte[] decryptedData Sm4CryptoService.DecryptCbc(encryptedPackageFromFile, key); File.WriteAllBytes(“decrypted_document.pdf”, decryptedData); Console.WriteLine(“文件解密成功。”);注意事项这种将IV和密文拼接的方式是最简单的一种。在更复杂的协议中IV可能会被放在文件头、数据包首部等特定位置甚至通过其他信道传输。核心原则是用于解密的IV必须与加密时使用的IV完全一致。另外虽然IV不需要像密钥一样永久保密但它必须是不可预测的随机值且同一个密钥下不应重复使用否则会削弱CBC模式的安全性。5. 在真实场景中的应用与封装将核心加解密方法封装好后我们需要把它们应用到具体的场景中比如加密字符串、文件甚至集成到网络传输或数据库存储中。5.1 字符串加解密的完整工具类为了方便使用我们可以创建一个静态工具类封装常用的字符串加解密操作并处理好编码问题。public static class Sm4Utility { // 使用固定的编码建议UTF-8 private static readonly Encoding TextEncoding Encoding.UTF8; /// summary /// 加密字符串CBC模式 /// /summary /// param name“plainText”明文/param /// param name“key”16字节密钥/param /// returnsBase64编码的字符串包含IV和密文/returns public static string EncryptCbcText(string plainText, byte[] key) { if (string.IsNullOrEmpty(plainText)) return string.Empty; byte[] plainBytes TextEncoding.GetBytes(plainText); byte[] encryptedPackage Sm4CryptoService.EncryptCbc(plainBytes, key); // 使用Base64编码便于在JSON、XML或URL中安全传输 return Convert.ToBase64String(encryptedPackage); } /// summary /// 解密字符串CBC模式 /// /summary /// param name“base64CipherText”Base64编码的密文含IV/param /// param name“key”16字节密钥/param /// returns解密后的原始字符串/returns public static string DecryptCbcText(string base64CipherText, byte[] key) { if (string.IsNullOrEmpty(base64CipherText)) return string.Empty; try { byte[] combinedBytes Convert.FromBase64String(base64CipherText); byte[] decryptedBytes Sm4CryptoService.DecryptCbc(combinedBytes, key); return TextEncoding.GetString(decryptedBytes); } catch (FormatException) { throw new ArgumentException(“输入的密文不是有效的Base64格式。”); } // 捕获解密失败异常如密钥错误可在此处进行统一处理或记录日志 catch (InvalidCipherTextException ex) { throw new ArgumentException(“解密失败请检查密钥或密文是否正确。”, ex); } } /// summary /// 一个更便捷的方法直接从密码字符串派生密钥并加解密 /// /summary public static string EncryptWithPassword(string plainText, string password) { byte[] key DeriveSm4KeyFromPassword(password); return EncryptCbcText(plainText, key); } public static string DecryptWithPassword(string base64CipherText, string password) { byte[] key DeriveSm4KeyFromPassword(password); return DecryptCbcText(base64CipherText, key); } }5.2 文件流式加密解密对于大文件一次性将全部内容读入内存可能会造成压力。我们可以使用流的方式分块处理。虽然BouncyCastle的IBufferedCipher本身支持流式操作但为了清晰展示过程这里演示一种结合CryptoStream的思路注意BouncyCastle的IBufferedCipher需要适配到.NET的ICryptoTransform接口过程稍复杂以下为简化概念流程。更实用的方式是直接使用上述的EncryptCbc方法处理整个文件的字节数组对于几百MB的文件在现代计算机内存中通常可以接受。如果文件巨大GB级别则需要专门实现流式处理生成随机IV写入输出文件流的前16字节。初始化IBufferedCipher为加密模式。循环读取文件块例如每次64KB调用cipher.ProcessBytes(byte[] input, int inOff, int length, byte[] output, int outOff)进行加密并将结果写入输出流。最后调用cipher.DoFinal(byte[] output, int outOff)处理最后一块并写入。解密过程类似先从文件流读取前16字节作为IV然后初始化解密器再循环处理后续的密文块。由于实现代码较长这里给出核心逻辑提示关键在于管理好ProcessBytes和DoFinal的调用以及输入输出缓冲区的偏移量。5.3 集成到数据库存储在需要将加密数据存入数据库如SQL Server, MySQL时通常将加密后的字节数组转换为Base64字符串或直接以二进制格式如VARBINARY类型存储。以Base64字符串存储文本字段如NVARCHARstring sensitiveData “用户的身份证号或手机号”; byte[] key GetKeyFromSecureStorage(); // 从安全的地方获取密钥 string encryptedBase64 Sm4Utility.EncryptCbcText(sensitiveData, key); // 将 encryptedBase64 存入数据库的某个字段以二进制数据存储二进制字段如VARBINARYbyte[] sensitiveDataBytes Encoding.UTF8.GetBytes(“敏感信息”); byte[] encryptedPackage Sm4CryptoService.EncryptCbc(sensitiveDataBytes, key); // 将 encryptedPackage 这个byte[]直接存入数据库的VARBINARY字段 // 使用像Dapper或Entity Framework这样的ORM时通常可以直接映射byte[]属性。从数据库读取后反向操作即可解密。务必注意密钥的管理必须独立于数据库最好使用硬件安全模块或操作系统提供的密钥保管机制。6. 调试技巧、常见问题与性能考量在实际开发中你肯定会遇到各种问题。下面是我在多个项目中总结出来的常见坑点和解决思路。6.1 常见异常与排查表异常信息/现象可能原因排查步骤与解决方案Org.BouncyCastle.Crypto.InvalidCipherTextException1. 密钥错误。2. 密文数据被损坏或篡改。3.CBC模式下IV不正确或丢失。4. 填充模式不匹配加密用PKCS7解密用NoPadding。1. 确认加解密使用的密钥完全一致打印Hex对比。2. 检查密文传输或存储过程是否有编码错误如Base64解码失败。3.重点检查CBC模式确保解密时使用的IV与加密时生成的IV完全相同。如果使用复合数据包确认拆分逻辑正确。4. 确认CipherUtilities.GetCipher中指定的填充模式一致。解密后得到乱码但未抛出异常1. 密钥正确但数据本身不是预期的明文。2. 可能使用了ECB模式且数据具有某种模式导致解密结果“看似合理”的乱码。3. 编码不一致加密用UTF-8解密用GBK。1. 确认加密前的明文和解密后的字节数组是否正确。可以尝试加密一个简单字符串如“test”进行验证。2.优先使用CBC模式ECB模式仅用于测试或特殊场景。3. 在整个流程中固定使用一种字符编码强烈推荐UTF-8。ArgumentException: “SM4密钥必须为16字节”传递给ParameterUtilities.CreateKeyParameter的密钥字节数组长度不是16。1. 检查密钥派生函数DeriveSm4KeyFromPassword的输出是否为16字节。2. 如果直接使用硬编码密钥确认字符串转换后是否为16字节如Encoding.ASCII.GetBytes(“0123456789ABCDEF”)正好是16字节。加密后的Base64字符串长度异常对加密后的字节数组进行Base64或Hex编码是正常的长度会增加。这是预期行为。Base64编码会使数据膨胀约33%。Hex编码两位字符表示一个字节会使长度翻倍。解密时需先进行对应的解码。大文件加密内存溢出一次性将整个文件读入byte[]。实现流式加密解密分块处理数据。参考5.2节提到的思路。6.2 调试与日志记录心得Hex编码是你的好朋友在调试阶段不要直接打印byte[]而是使用Hex.ToHexString(bytes)或BitConverter.ToString(bytes).Replace(“-“, “”)将其转换为十六进制字符串。这样可以清晰对比密钥、IV、明文块和密文块。Console.WriteLine($“Key: {Hex.ToHexString(key)}”); Console.WriteLine($“IV: {Hex.ToHexString(iv)}”); Console.WriteLine($“Cipher Block: {Hex.ToHexString(cipherBlock)}”);从最小单元测试开始不要一开始就处理复杂数据。先写一个单元测试用固定的密钥和IV加密一个已知的短字符串如“1234567890ABCDEF”正好16字节验证加解密是否能还原。这能快速排除算法调用层面的基本错误。隔离问题如果集成到系统后失败先剥离业务逻辑用相同的输入数据在控制台测试你的Sm4CryptoService类是否正常工作。以此判断问题是出在加密模块本身还是出在数据获取、传输或存储环节。6.3 性能与安全性考量性能SM4的软件实现速度与AES相当。在绝大多数应用场景中加解密操作不会是性能瓶颈。如果确实遇到性能问题如每秒需要处理GB级数据首先应检查是否是I/O文件读写、网络传输限制了速度然后再考虑使用平台固有的加速指令如Intel AES-NI但SM4的硬件加速支持不如AES广泛或异步操作来优化吞吐。密钥安全算法是公开的安全完全依赖于密钥。切勿在客户端代码中硬编码密钥。对于客户端-服务器模型应考虑使用非对称加密如SM2来安全传输对称密钥SM4密钥。服务端应将密钥存储在安全的配置中心或硬件安全模块中。模式与填充无脑选择CBC模式与PKCS7填充。对于需要认证的场景防止密文被篡改可以考虑使用GCM等认证加密模式但BouncyCastle对SM4-GCM的支持可能需要检查特定版本或自行组合HMAC。随机数生成SecureRandom是密码学安全的随机数生成器用于生成IV是合适的。在.NET中也可以使用System.Security.Cryptography.RandomNumberGenerator。7. 进阶话题与现有系统的兼容与测试当你需要与使用其他语言如Java、Python或不同库实现的SM4系统进行交互时兼容性就成了大问题。7.1 跨语言/跨平台交互要点算法、模式、填充三要素必须完全一致对方是“SM4/CBC/PKCS5Padding”还是“SM4/CBC/PKCS7Padding”注在分组密码中PKCS5Padding和PKCS7Padding在概念上基本等同。必须确认无误。密钥和IV的编码密钥和IV通常以字节数组形式参与运算。确保双方对字符串到字节数组的编码方式一致如UTF-8、ASCII。交互时最好直接传递Hex或Base64编码的密钥/IV字符串避免编码歧义。数据格式密文以及IV的拼接方式。是IV前置、后置还是单独传输传输时是用Base64还是Hex这些都需要在接口协议中明确定义。一个可靠的对接测试方法是双方先用一个公认的测试向量进行验证。例如找一组标准的SM4 ECB测试数据明文、密钥、密文确保各自的基础加密功能输出一致。然后再测试CBC模式约定好IV的处理方式。7.2 编写自动化测试为你的加密工具类编写单元测试至关重要这能保证代码修改后核心功能依然正确。using Xunit; // 使用xUnit框架示例 public class Sm4CryptoServiceTests { private readonly byte[] _testKey Hex.Decode(“0123456789ABCDEFFEDCBA9876543210”); // 128位测试密钥 private readonly byte[] _testIv Hex.Decode(“1234567890ABCDEF1234567890ABCDEF”); // 测试IV [Fact] public void EncryptDecryptCbc_ShouldReturnOriginalText() { // Arrange string originalText “Hello, SM4 CBC Mode!”; byte[] plainBytes Encoding.UTF8.GetBytes(originalText); // Act byte[] encrypted Sm4CryptoService.EncryptCbc(plainBytes, _testKey); byte[] decrypted Sm4CryptoService.DecryptCbc(encrypted, _testKey); string resultText Encoding.UTF8.GetString(decrypted); // Assert Assert.Equal(originalText, resultText); } [Fact] public void EncryptCbc_WithSameInput_ShouldProduceDifferentCiphertextDueToRandomIv() { // 多次加密相同明文由于IV随机密文应不同 byte[] plainBytes new byte[] { 0x01, 0x02, 0x03 }; byte[] cipher1 Sm4CryptoService.EncryptCbc(plainBytes, _testKey); byte[] cipher2 Sm4CryptoService.EncryptCbc(plainBytes, _testKey); // 断言两个密文不相同比较整个字节数组 Assert.NotEqual(cipher1, cipher2); // 但它们的长度应该相同IV长度密文长度 Assert.Equal(cipher1.Length, cipher2.Length); } [Fact] public void DecryptCbc_WithWrongKey_ShouldThrowInvalidCipherTextException() { byte[] plainBytes new byte[] { 0x01, 0x02, 0x03 }; byte[] encrypted Sm4CryptoService.EncryptCbc(plainBytes, _testKey); byte[] wrongKey new byte[16]; Array.Fill(wrongKey, (byte)0xFF); // 一个错误的密钥 Assert.ThrowsInvalidCipherTextException(() { Sm4CryptoService.DecryptCbc(encrypted, wrongKey); }); } }7.3 关于“在线SM4工具”的警示在开发调试时你可能会搜索到一些在线的SM4加密解密工具。它们可以用来快速验证一些想法但绝对不要用于处理任何真实的敏感数据。你无法确认这些网站是否会在后台记录你的明文和密钥。本地搭建一个测试环境或者使用自己编写的测试程序才是安全可靠的做法。最后再分享一个我自己的体会密码学是一个严谨的领域“差不多就行”的心态是万万要不得的。一个字母的错误比如ECB写成EBC、一个字节的偏差都会导致整个加解密过程的失败。在实现过程中保持耐心多用测试用例验证仔细比对每一个中间结果的Hex值问题总能被定位和解决。把本文提供的代码作为你的起点根据实际项目需求进行封装和扩展你就能在C#项目中稳健地驾驭SM4国密算法了。