ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C#基于BouncyCastle实现国密SM4加解密完整指南

C#基于BouncyCastle实现国密SM4加解密完整指南 简介这是一份面向Web前端开发者的国密SM4加密解密工具包适用于H5页面以及Angular、Vue、jQuery等主流项目场景。SM4是我国商用密码算法常用于敏感数据传输的加密保护该JS文件封装了加解密核心逻辑引入项目后即可快速调用。压缩包整体仅3KB内部只包含1个JS文件无多余依赖与配置成本可直接嵌入现有工程对初次接触国密算法的开发者也很友好。资源发布至今已有3594人学习或下载实用性已经过验证。拿到后可直接参考封装好的接口实现数据加解密省去自行研究算法与调试的时间为前后端安全通信提供一套轻量方案无论是满足合规要求的信息系统还是希望增强数据安全性的中小型项目都可以便捷集成使用。 我最近在做一个系统对接的项目对方明确要求数据加密必须用国密SM4而且接口文档里只丢了一句“使用SM4加密密钥自行协商”。我当时脑袋一懵网上一搜资料确实不少但大多是片段式的——要么只贴一段代码不给依赖要么填充模式跟你对接方对不上要么跑起来直接报错。折腾了两天才弄利索。这篇就把我亲测可用的SM4实现方案、完整代码、还有那些网上没人细说的坑一次性整理出来。1. 服务端和客户端都在用SM4到底是什么来头先说点背景免得你对接的时候被对方一句“我们要求国密算法”问住。SM4是国密算法里专门做对称加密的分组长度128位即16字节密钥长度也是128位即16字节轮数32轮。它最核心的特点就是加解密用的是同一把密钥速度非常快适合大数据量的加密传输。对称加密的世界里AES是国际通用标准SM4是国密标准。两者在算法结构上其实思路接近但SM4的设计细节是自主实现的密钥调度、轮函数都不同。对普通开发者来说最大的感受就是AES的Key长度可以是16/24/32字节而SM4固定16字节没有更多选择。这就带来了一个常见问题很多人照搬AES的习惯用32字节的密钥去初始化SM4直接抛异常。那为什么服务端强制要求用SM4而不是AES多半是等保合规、金融或者政务类项目的要求属于硬性规定没得商量。我在实际对接中遇到最多的场景是客户端和服务端分别用不同编程语言开发但加密解密的模式、填充方式、编码格式必须完全对齐否则你这边加密的数据对方那边解出来就是乱码或者直接解密失败。另外MD5和SM4完全是两回事。MD5是哈希算法只能单向校验数据完整性不可逆网上说“MD5加密解密”本身就是个使用误区。真正要加解密数据用的就是SM4、AES这类对称算法。2. 动手写代码前先把“模式-填充-编码”这三件套对齐很多人SM4写失败问题不在算法本身而在你没搞明白这三个基础概念。这三件套就像合同条款不先对齐后面全是纠纷。第一个是分组模式。SM4和AES一样是分组密码默认一次只处理16字节的数据块。如果明文长度不是16的倍数就需要填充如果明文超过16字节就需要模式来串联各个分组。最常用的两种模式是ECB和CBC。ECB简单粗暴每块独立加密同样的明文块会产生同样的密文块安全性相对弱CBC会引入一个初始向量IV每个密文块依赖前一个密文块同样明文在不同位置会产生不同密文安全性更好。能用CBC就用CBC只有对接方死规定ECB时才用ECB。第二个是填充方式。因为分组密码要求明文长度对齐16字节所以最后一块要填充。最常见的是PKCS7也就是缺几个字节就补几个值为几的字节比如缺5个字节就补5个值为0x05的字节。BouncyCastle默认用的就是类似机制这块坑不大但要注意ECB和CBC是算法模式PKCS7是填充方案两者不是一类东西别混为一谈。第三个是编码格式。加密完的密文是二进制字节直接存数据库或者传接口肯定不行所以要么转Base64字符串要么转十六进制Hex字符串。这里最容易扯皮你转的是Base64对方按Hex解析必然失败。所以联调之前先问清楚对方要哪种格式。我个人习惯统一用Base64字符串短一些传参方便。还有明文用UTF-8还是GBK编码也要确定下来我见过有项目用默认编码改服务器环境后解密就乱码这种坑能提前定死就提前定死。把这三点确定了代码层面的实现就清晰了。3. C#侧基于BouncyCastle实现SM4核心代码拆解C#本身没有内置SM4算法我实测下来最省事的方案是引入BouncyCastle的NuGet包。它是老牌密码学库覆盖面广SM4、SM3、SM2都有支持而且跨平台运行稳定。选了它还有一个理由生态成熟有问题随便一搜就有答案不像小众库遇到报错根本找不到人问。包名现在的版本是BouncyCastle.Cryptography老项目里也可能见到Portable.BouncyCastle。功能一样引包的时候留意一下版本就行。我用的是.NET 6引包之后直接开写。3.1 CBC模式加解密核心代码CBC模式需要密钥和IV都是16字节。我用示例来说明你实际使用的时候可以通过配置中心下发别写死在代码里。using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Modes; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; using System.Text; public static class Sm4CbcHelper { private static readonly Encoding DefaultEncoding Encoding.UTF8; public static string Encrypt(string plainText, string key, string iv) { byte[] keyBytes Encoding.UTF8.GetBytes(key); byte[] ivBytes Encoding.UTF8.GetBytes(iv); // 1. 创建SM4引擎 var engine new SM4Engine(); // 2. 包装成CBC模式 var cbcCipher new CbcBlockCipher(engine); // 3. 加上PKCS7填充 var cipher new PaddedBufferedBlockCipher(cbcCipher); // 4. 初始化true表示加密 ICipherParameters parameters new ParametersWithIV(new KeyParameter(keyBytes), ivBytes); cipher.Init(true, parameters); byte[] inputBytes DefaultEncoding.GetBytes(plainText); byte[] outputBytes new byte[cipher.GetOutputSize(inputBytes.Length)]; int length cipher.ProcessBytes(inputBytes, 0, inputBytes.Length, outputBytes, 0); length cipher.DoFinal(outputBytes, length); return Convert.ToBase64String(outputBytes, 0, length); } public static string Decrypt(string cipherText, string key, string iv) { byte[] keyBytes Encoding.UTF8.GetBytes(key); byte[] ivBytes Encoding.UTF8.GetBytes(iv); var engine new SM4Engine(); var cbcCipher new CbcBlockCipher(engine); var cipher new PaddedBufferedBlockCipher(cbcCipher); ICipherParameters parameters new ParametersWithIV(new KeyParameter(keyBytes), ivBytes); cipher.Init(false, parameters); byte[] inputBytes Convert.FromBase64String(cipherText); byte[] outputBytes new byte[cipher.GetOutputSize(inputBytes.Length)]; int length cipher.ProcessBytes(inputBytes, 0, inputBytes.Length, outputBytes, 0); length cipher.DoFinal(outputBytes, length); return DefaultEncoding.GetString(outputBytes, 0, length); } }这套代码的核心流程可以记成四步创建引擎、包装模式、追加填充、初始化并处理。加密和解密只有Init的第一个参数和输入输出编码不同其他完全对称。很多从AES转过来的朋友会问为什么还要自己动手包一下CbcBlockCipher和PaddedBufferedBlockCipher因为BouncyCastle把底层引擎和上层模式、填充拆开了这种设计的好处是你可以自由组合想用ECB不填充也可以自己改灵活性更高。3.2 ECB模式加解密核心代码ECB模式不需要IV代码比CBC更简单。我在对接一些老系统时遇到过对方坚持用ECB的情况所以这个模式也留着备用。但说句实在话如果对方没有强制要求优先选CBCECB模式下相同密文块会暴露明文的重复模式安全性确实弱一截。public static class Sm4EcbHelper { private static readonly Encoding DefaultEncoding Encoding.UTF8; public static string Encrypt(string plainText, string key) { byte[] keyBytes Encoding.UTF8.GetBytes(key); var engine new SM4Engine(); var cipher new PaddedBufferedBlockCipher(engine); cipher.Init(true, new KeyParameter(keyBytes)); byte[] inputBytes DefaultEncoding.GetBytes(plainText); byte[] outputBytes new byte[cipher.GetOutputSize(inputBytes.Length)]; int length cipher.ProcessBytes(inputBytes, 0, inputBytes.Length, outputBytes, 0); length cipher.DoFinal(outputBytes, length); return Convert.ToBase64String(outputBytes, 0, length); } public static string Decrypt(string cipherText, string key) { byte[] keyBytes Encoding.UTF8.GetBytes(key); var engine new SM4Engine(); var cipher new PaddedBufferedBlockCipher(engine); cipher.Init(false, new KeyParameter(keyBytes)); byte[] inputBytes Convert.FromBase64String(cipherText); byte[] outputBytes new byte[cipher.GetOutputSize(inputBytes.Length)]; int length cipher.ProcessBytes(inputBytes, 0, inputBytes.Length, outputBytes, 0); length cipher.DoFinal(outputBytes, length); return DefaultEncoding.GetString(outputBytes, 0, length); } }注意看ECB只用了KeyParameter没有ParametersWithIV这一层。如果你用ECB还硬塞一个IV进去BouncyCastle会抛InvalidOperationException提示你“not initialized”之类原因就在这里。CBC就反过来不传IV会直接报参数错误因为CBC模式缺了IV整个算法就跑不起来。4. 写一个完整的SM4Util工具类拿过去直接用上面两套代码虽然直接可用但每次调用都要传key、传iv函数多了之后很容易传串参数。我习惯封装成一个工具类统一管理配置对外只暴露Encrypt和Decrypt两个方法调用方根本不需要关心底层是ECB还是CBC、填充是PKCS7还是别的。using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Modes; using Org.BouncyCastle.Crypto.Parameters; using System.Text; public class Sm4Util { private readonly byte[] _key; private readonly byte[] _iv; private readonly bool _useCbc; public Sm4Util(string key, string iv null, bool useCbc true) { if (string.IsNullOrEmpty(key)) throw new ArgumentException(密钥不能为空); _key Encoding.UTF8.GetBytes(key); if (_key.Length ! 16) throw new ArgumentException(SM4密钥长度必须为16字节); _useCbc useCbc; if (_useCbc) { if (string.IsNullOrEmpty(iv)) throw new ArgumentException(CBC模式必须提供16字节的IV); _iv Encoding.UTF8.GetBytes(iv); if (_iv.Length ! 16) throw new ArgumentException(SM4的IV长度必须为16字节); } else { _iv null; } } public string Encrypt(string plainText) { if (string.IsNullOrEmpty(plainText)) return string.Empty; var cipher CreateCipher(true); byte[] inputBytes Encoding.UTF8.GetBytes(plainText); byte[] outputBytes new byte[cipher.GetOutputSize(inputBytes.Length)]; int length cipher.ProcessBytes(inputBytes, 0, inputBytes.Length, outputBytes, 0); length cipher.DoFinal(outputBytes, length); return Convert.ToBase64String(outputBytes, 0, length); } public string Decrypt(string cipherText) { if (string.IsNullOrEmpty(cipherText)) return string.Empty; var cipher CreateCipher(false); byte[] inputBytes Convert.FromBase64String(cipherText); byte[] outputBytes new byte[cipher.GetOutputSize(inputBytes.Length)]; int length cipher.ProcessBytes(inputBytes, 0, inputBytes.Length, outputBytes, 0); length cipher.DoFinal(outputBytes, length); return Encoding.UTF8.GetString(outputBytes, 0, length); } private BufferedBlockCipher CreateCipher(bool forEncryption) { var engine new SM4Engine(); BufferedBlockCipher cipher; if (_useCbc) { cipher new PaddedBufferedBlockCipher(new CbcBlockCipher(engine)); ICipherParameters parameters new ParametersWithIV(new KeyParameter(_key), _iv); cipher.Init(forEncryption, parameters); } else { cipher new PaddedBufferedBlockCipher(engine); cipher.Init(forEncryption, new KeyParameter(_key)); } return cipher; } }这个工具类在构造时就帮你校验了密钥长度和IV长度如果密钥不是16字节直接抛异常等于是把最容易犯的错误挡在了门外。调用方式很简洁var sm4 new Sm4Util(1234567890abcdef, 1234567890abcdef, true); string encrypted sm4.Encrypt(你好SM4加密测试); Console.WriteLine(encrypted); string decrypted sm4.Decrypt(encrypted); Console.WriteLine(decrypted);我在实际项目中还加了一个小细节明文为空时直接返回空字符串不参与加密。这个不是标准要求是我自己的习惯因为很多业务场景里可空字段很常见空值直接透传比加密一坨无意义的数据更合理但前提是你和对接方约定一致。5. 亲测结果和性能数据以及我踩过的三个大坑代码写完了得跑起来验证。我用.NET 6写了个控制台测试程序做了三件事1万次CBC模式加解密往返测试、边界长度明文测试、特殊字符测试。结果如下测试项明文长度加密耗时解密耗时结果空字符串0字节直接返回直接返回通过短文本4字节1ms1ms通过15字节不满一块15字节1ms1ms通过16字节刚好一块16字节1ms1ms通过17字节超过一块17字节1ms1ms通过1万次往返压力测试200字节约820ms约830ms无失败16字节这个测试很关键很多人以为分组正好16字节就不需要填充了但PKCS7依然会追加一个完整块解密时再剔除。我最开始自测时没注意这个问题硬编码去对比密文长度结果发现加密一次多出16字节困惑了半天后来才反应过来这是PKCS7的标准行为。5.1 坑一密钥和IV长度不对抛异常抛到怀疑人生这个坑我踩过两回都是团队其他人写的代码。有的直接把密钥用成32字节有的全是ASCII字符但数错了位数。BouncyCastle遇到长度不对的KeyParameter不会给你一个中文说明直接抛出英文异常字面意思是“无效的密钥长度”。如果你看到这个错先别急着去翻算法代码第一件事就是数密钥字节数用UTF-8编码后是不是正好16字节。注意中文或者特殊字符一个都不行因为一个中文字在UTF-8下占3字节16个中文字符就是48字节肯定报错。5.2 坑二接口文档里写“Hex字符串”我转成了Base64联调时对方说加密结果用“字符串”传回来我默认用Base64处理了。结果对方解密出来全是乱码排查了两个小时才发现对方说的“字符串”是指十六进制Hex字符串。这东西不加约定根本说不清因为打印出来都是可见字符。后来我在所有接口文档里都会明确写明“密文编码格式Base64”联调之前先跟对方交换一组加密结果同一份明文两边加密出来的密文一致再继续往下走。这个校验动作看着简单但能省掉后面一整天的沟通成本。5.3 坑三密钥是动态下发的新旧版本切换时缓存没清干净还有一个隐蔽问题密钥通过接口动态下发每次调用都从远端拿一次新密钥但是工具类是单例的旧密钥缓存在内存里。第一次加密用A密钥后端更新成B密钥后前端还在用A密钥加密出来的数据后端解不开。这个问题的排查链路比较长因为代码层面完全正常加解密在本地测试都通过只有联调环境才暴露。后来我在密钥刷新逻辑里打印了一个日志记录每次加密用的密钥指纹才把问题定位出来。建议如果你做的是长期运行的服务密钥更新后一定要把使用旧密钥的缓存对象全部释放。6. 顺手聊聊Rabbit流密码和SM4的选择问题搜索热词里还有个“rabbit加密解密”我在BouncyCastle里也见过Rabbit这个算法。它其实是一种流密码和SM4这种分组密码的思路完全不同。流密码逐个字节加密天然不需要填充速度也快但应用远不如SM4广泛。我目前的判断是如果你的项目没有明确约束优先选SM4理由很简单——对接兼容性广、资料多、大多数人都会。Rabbit在特定协议里可能会有出场机会但作为通用方案它不如SM4稳妥。至于SM2和SM3那是另外两个国密算法的范畴。SM2是非对称加密和签名SM3是哈希摘要。它们经常和SM4一起出现在“国密改造”的项目里算是一条龙服务。但如果你是第一次接触国密先把SM4用好其他的后面遇到再说。我从实际经验出发的体会是国密算法的难点不在算法本身而在方案对接把模式、填充、编码格式这三件事和对方对齐后面的实现都是体力活。最后分享一个我自己的习惯每次写完SM4相关代码我都会保存一组固定的“测试向量”比如明文“test”密钥“1234567890abcdef”IV“1234567890abcdef”加密结果是多少直接写进单元测试里。这样以后不管谁改了代码回归测试一跑就知道加解密逻辑有没有被破坏。这个习惯帮我挡掉过不止一次因为“顺手优化”导致的不兼容问题你可以试试看。本文还有配套的精品资源点击获取
返回列表