☰
C#基于BouncyCastle实现国密SM4加解密完整指南
2026/10/12 10:46:36 网站建设 项目流程

简介:这是一份面向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的情况,所以这个模式也留着备用。但说句实在话,如果对方没有强制要求,优先选CBC;ECB模式下相同密文块会暴露明文的重复模式,安全性确实弱一截。

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字节<1ms<1ms通过
15字节(不满一块)15字节<1ms<1ms通过
16字节(刚好一块)16字节<1ms<1ms通过
17字节(超过一块)17字节<1ms<1ms通过
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”,加密结果是多少,直接写进单元测试里。这样以后不管谁改了代码,回归测试一跑就知道加解密逻辑有没有被破坏。这个习惯帮我挡掉过不止一次因为“顺手优化”导致的不兼容问题,你可以试试看。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询