SM4国密算法C#与Java跨语言加解密实战与避坑指南

发布时间:2026/9/3 2:25:13
SM4国密算法C#与Java跨语言加解密实战与避坑指南 简介国密SM4作为国家商用密码算法在政务、金融等对数据安全要求较高的行业中应用日益广泛越来越多的前端项目需要在其业务逻辑中加入国密加密支持。这份资源提供的解决方案定位清晰面向需要快速实现加密解密功能的前端开发者适用于H5、Vue、Angular以及jQuery等多种常见项目环境即使对算法原理了解不深也能通过简洁的调用方式实现安全传输。压缩包整体仅3KB大小内部共包含一个JavaScript文件核心逻辑集中、无额外依赖引入项目后即可直接使用后续维护及二次封装也非常方便。该资源上传至今已有3594人学习下载实测运行稳定能够有效降低国密算法在浏览器端的集成门槛。借助封装好的接口开发者可以轻松完成SM4数据的加密与解密操作既可直接应用于业务系统也可作为前端适配国密算法的参考模板帮助团队快速落地相关需求并提升整体数据传输安全性。 最近在做系统国密改造有个常规需求是把我这边服务里用AES加密的接口报文全部换成SM4。本来以为只是换一个算法的事结果真动手才发现坑不少尤其是C#端和Java端对同一段明文加密出来的密文对不上光排查这个问题就花了小半天。这篇就把SM4加密解密过程中真正用得上的东西整理一下包括算法参数怎么定、C#端怎么落地、跨语言联调怎么对齐、以及SpringBoot项目里用SM4保护数据库密码的思路希望帮到正在做同类改造的朋友。1. SM4算法是什么为什么到处都在提“国密改造”1.1 SM4的技术定位与基础参数SM4是我国发布的商用密码分组算法最早在2012年公开2016年成为国家标准GB/T 32907-2016。它是一个典型的分组密码分组长度128位密钥长度128位整体结构是32轮迭代的Feistel网络。这个参数和AES-128正好对上所以在很多业务场景里SM4可以作为AES的平替。但SM4绝不是AES的简单复制。它的S盒、线性变换、密钥扩展算法都是独立设计的。比如它的非线性变换使用了固定的S盒32轮迭代中每一轮的轮密钥都由上一轮生成加解密过程高度对称解密时只需要把轮密钥的顺序倒过来就行。这种结构的好处是硬件实现非常简洁对资源受限的物联网设备特别友好这也是为什么除了金融政务之外车联网、智能电表这类场景也大量用SM4。从安全性角度看SM4的128位密钥在当前计算能力下是足够的。很多人会问“AES不也挺好吗为什么要费劲换”。实际原因通常不是技术上的而是合规和信任问题。在金融、政务、能源这类行业里合作方要求必须使用国密算法没有讨价还价的余地。所以“国密改造”这件事实质就是把系统里原有的AES、3DES、RSA、SHA等国际算法替换成SM2、SM3、SM4系列国密算法并且要保证业务不中断、数据可互通。1.2 典型应用场景我梳理了一下SM4在真实项目里最常见的几个应用位置数据库敏感字段加密用户密码、身份证号、手机号、银行卡号API接口报文加密前后端加密传输或者系统间接口数据保护配置文件脱敏比如数据库连接密码用Jasypt集成SM4来加密文件加密金融对账单、批量数据文件的落地加密数据库TDE透明数据加密部分国产数据库原生支持国密加密配合SM2、SM3组成完整密码体系SM2做密钥交换和数字签名SM3做完整性校验SM4做数据加密其中接口报文加解密和数据库密码保护是开发同学最常遇到的两类后面我会重点讲这两个方向的具体实现。2. SM4加解密前必须先搞懂的四个关键参数2.1 密钥、IV、模式、填充一个都不能错SM4本身只是一个分组算法要真正用于数据加密还需要约定四个外围参数。这些参数在C#和Java等多语言对接时任何一处不一致都会导致解密失败。密钥长度固定是128位也就是16字节。但实际业务中接口文档里常见的是32位十六进制字符串比如“0123456789ABCDEF0123456789ABCDEF”。这东西拿到代码里怎么处理很关键一定要把它从十六进制解码成16字节而不是用UTF-8直接转成字节数组。用UTF-8转32个字符会得到32字节密钥长度直接翻倍初始化就会报错。这几乎是新手必踩的坑。IV初始化向量只用在CBC这类有链接模式的算法里长度也是16字节。IV不要求保密但它不能被重复使用而且加解密两端必须一致。一般的做法是固定写死一组16字节的值或者用随机数生成后拼在密文前面传给对方。模式方面ECB和CBC占到了日常需求的九成。ECB模式下同样的明文加密后得到同样的密文结构简单方便调试但安全性弱一些易受模式攻击。CBC模式每个明文块先和上一个密文块做异或再加密安全性更好是目前的主流推荐。如果接口文档没有特别指定默认选CBC。填充方式上SM4分组长度16字节当明文不是16字节整数倍时必须有填充。PKCS7最常用它按缺多少字节就填多少个相同值的规则补位。例如还差5字节就补5个0x05刚好凑满16字节时再补整整16字节。这个概念看着绕但实现库里都是现成的真正要注意的是C#和Java两边选的填充类是否一致。2.2 编码和密文输出格式同样重要还有一个容易被忽略的点字符串转字节数组时用什么编码。全文统一用UTF-8不要出现一端UTF-8、另一端GBK的情况。密文落地传输时一般用Base64或Hex两种格式。Base64更短适合接口传输Hex可读性好适合调试比对。跨语言联调时建议先在调试阶段都用Hex输出中间结果方便肉眼比对。3. C#端SM4加密解密完整实操3.1 引入BouncyCastle.NET本身没有内置SM4实现我用的方案是BouncyCastle它覆盖了SM2、SM3、SM4全套国密算法社区活跃接口稳定。NuGet直接搜BouncyCastle.Cryptography老项目也可以选Portable.BouncyCastle。装好之后核心的类都在Org.BouncyCastle.Crypto命名空间下。3.2 ECB模式实现代码using System; using System.Text; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Modes; using Org.BouncyCastle.Crypto.Paddings; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Utilities.Encoders; public class Sm4Helper { public static string EncryptEcb(string plainText, string keyHex) { byte[] keyBytes Hex.Decode(keyHex); byte[] input Encoding.UTF8.GetBytes(plainText); var cipher new PaddedBufferedBlockCipher( new EcbBlockCipher(new SM4Engine()), new Pkcs7Padding()); cipher.Init(true, new KeyParameter(keyBytes)); byte[] output new byte[cipher.GetOutputSize(input.Length)]; int length cipher.ProcessBytes(input, 0, input.Length, output, 0); cipher.DoFinal(output, length); return Convert.ToBase64String(output); } public static string DecryptEcb(string cipherBase64, string keyHex) { byte[] keyBytes Hex.Decode(keyHex); byte[] input Convert.FromBase64String(cipherBase64); var cipher new PaddedBufferedBlockCipher( new EcbBlockCipher(new SM4Engine()), new Pkcs7Padding()); cipher.Init(false, new KeyParameter(keyBytes)); byte[] output new byte[cipher.GetOutputSize(input.Length)]; int length cipher.ProcessBytes(input, 0, input.Length, output, 0); cipher.DoFinal(output, length); return Encoding.UTF8.GetString(output); } }这里有几个细节值得单独说一下。GetOutputSize(input.Length)是BouncyCastle用来计算输出缓冲区大小的核心方法。因为PKCS7填充会额外增加1到16字节所以输出数组必须比原始输入长。很多人在这一步图省事直接用new byte[input.Length]遇到明文长度正好是16的倍数时填充块无处安放直接抛异常。解密时输出长度等于输入长度因为填充内容会在DoFinal阶段被去掉。ProcessBytes负责处理所有完整的数据块DoFinal负责处理最后剩下的数据块和填充所以返回值要累加。这段逻辑如果从AES迁移过来基本框架不变只是把引擎类换成SM4Engine而已。3.3 CBC模式实现代码public static string EncryptCbc(string plainText, string keyHex, string ivHex) { byte[] keyBytes Hex.Decode(keyHex); byte[] ivBytes Hex.Decode(ivHex); byte[] input Encoding.UTF8.GetBytes(plainText); var cipher new PaddedBufferedBlockCipher( new CbcBlockCipher(new SM4Engine()), new Pkcs7Padding()); cipher.Init(true, new ParametersWithIV(new KeyParameter(keyBytes), ivBytes)); byte[] output new byte[cipher.GetOutputSize(input.Length)]; int length cipher.ProcessBytes(input, 0, input.Length, output, 0); cipher.DoFinal(output, length); return Convert.ToBase64String(output); } public static string DecryptCbc(string cipherBase64, string keyHex, string ivHex) { byte[] keyBytes Hex.Decode(keyHex); byte[] ivBytes Hex.Decode(ivHex); byte[] input Convert.FromBase64String(cipherBase64); var cipher new PaddedBufferedBlockCipher( new CbcBlockCipher(new SM4Engine()), new Pkcs7Padding()); cipher.Init(false, new ParametersWithIV(new KeyParameter(keyBytes), ivBytes)); byte[] output new byte[cipher.GetOutputSize(input.Length)]; int length cipher.ProcessBytes(input, 0, input.Length, output, 0); cipher.DoFinal(output, length); return Encoding.UTF8.GetString(output); }CBC模式和ECB的区别就两处一是EcbBlockCipher换成CbcBlockCipher二是初始化参数从KeyParameter换成ParametersWithIV把密钥和IV一起传进去。如果IV长度不是16字节BC会直接抛异常这个约束其实也是个天然的参数校验手段。3.4 自测用例分享一个我常用的自测向量调试代码时可以用来确认环境是否正常密钥Hex0123456789ABCDEF0123456789ABCDEFIVHex00000000000000000000000000000000明文hello sm4CBC加密后Base64密文2NU6uDx9n0kU8b0O1sIIhA这个结果是用ECB还是CBC要区分清楚我在项目里一般先用CBC做联调基线这个值我记得比较牢。4. C#与Java加密结果不一致的排查实录4.1 三个最容易踩的坑很多团队在做国密改造时C#端和Java端是两拨人开发的联调阶段经常出现“我这边加密你那边解不开”的情况。我总结下来九成以上是下面三个原因。第一个坑是密钥处理方式不同。Java端如果直接用Hutool的SmUtil.sm4(0123456789ABCDEF0123456789ABCDEF)它底层可能会自动做Hex解码但C#端如果手写Encoding.UTF8.GetBytes拿到的就是32字节两边密钥完全对不上。解决办法是C#端统一用Hex.Decode解析密钥字符串。第二个坑是填充模式名称不同。Java的Cipher.getInstance里填“PKCS5Padding”C#的BC里用的却是“Pkcs7Padding”。很多人一看名字不一样就慌了其实在分组长度16字节的算法里PKCS5和PKCS7的填充规则完全一致可以放心对齐。第三个坑是默认模式不同。Hutool的SmUtil.sm4()默认走的是“SM4/ECB/PKCS5Padding”而部分C#同事手写代码时习惯直接抄个CBC例子两边一个ECB一个CBC密文当然对不上。联调前第一件事就是互相确认模式、IV、编码这三个点。4.2 Java端对照实现用Hutool做Java端的实现非常省事加解密代码可以控制在十行以内import cn.hutool.crypto.symmetric.SM4; import cn.hutool.crypto.SmUtil; import java.nio.charset.StandardCharsets; public class Sm4Demo { public static void main(String[] args) { String keyHex 0123456789ABCDEF0123456789ABCDEF; String plainText hello sm4; SM4 sm4 SmUtil.sm4(keyHex.getBytes(StandardCharsets.UTF_8)); String cipherBase64 sm4.encryptBase64(plainText); System.out.println(cipherBase64); String decrypted sm4.decryptStr(cipherBase64); System.out.println(decrypted); } }这里有个细节Hutool的SmUtil.sm4()虽然接收byte[]但它内部把传入的字节数组当作“密钥原始字节”不会自动做Hex解码。所以如果想和C#端的Hex.Decode对齐Java端也可以先做一次Hex解码再传进去或者直接用SmUtil.sm4配合HexUtil来转换。最简单的对齐方式就是双方都拿同一串Hex字符串各自转成16字节再喂给算法库。4.3 排查思路遇到结果不一致时我建议按这个顺序排查先确认两端的密钥字节序列是否完全一致用Hex打印出来逐字节比对再确认模式ECB还是CBC接着确认CBC模式的IV是否一致最后确认填充方式。还有一个更狠的调试办法直接把明文字节、密钥字节、IV字节先固定死然后在C#和Java里分别只调用最底层的SM4Engine处理一个16字节的明文块对比中间输出。这一步如果能对上说明算法本身没问题剩下的就是参数对齐问题了。5. 常见问题速查表与开发建议5.1 实际问题速查表我把这段时间在项目里和网友交流中收集到的典型问题整理成一个速查表希望对排查问题有帮助。现象可能原因解决方案初始化算法时抛异常“invalid key length”密钥长度不是16字节通常是直接用UTF-8字符串转成了32字节用Hex.Decode把32位十六进制字符串解码为16字节C#加密后Java解不开报“final block not properly padded”多数是模式不一致比如C#用CBC、Java用ECB确认两侧模式、IV、填充完全一致解密后中文乱码编码不一致一侧UTF-8一侧GBK统一使用UTF-8加密后密文长度忽长忽短没走PKCS7填充或BC底层用了NoPadding在PaddedBufferedBlockCipher里明确指定Pkcs7Padding同一个明文每次加密结果都不同正常CBC模式加随机IV后密文不同按业务约定保存或拼接随机IVDoFinal时缓冲区溢出输出数组长度不够少了填充块字节数用GetOutputSize而不是直接用输入长度5.2 密钥管理别太随意从安全角度讲把密钥硬编码在代码里是最差的做法代码仓库泄露等于密钥泄露。我现在的项目里SM4密钥存在配置中心通过环境变量注入配置中心本身再加密一层。实际工作中如果条件有限至少要确保密钥不提交到Git写入.gitignore并走部署机环境变量。密钥轮换也要提前想好线上系统如果密钥变更存量密文全部无法解密所以需要预留双密钥过渡期老密钥解密、新密钥加密。5.3 SpringBoot中集成Jasypt做数据库密码加密关于SpringBoot项目里对数据库用户密码做SM4加密并在Jasypt中生效的需求这里给一个实现思路。Jasypt默认的加密算法不是SM4但可以通过自定义StringEncryptor来替换。import cn.hutool.crypto.symmetric.SM4; import cn.hutool.crypto.SmUtil; import org.jasypt.encryption.StringEncryptor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; Component(sm4JasyptEncryptor) public class Sm4JasyptEncryptor implements StringEncryptor { private final SM4 sm4; public Sm4JasyptEncryptor(Value(${sm4.encrypt.key}) String keyHex) { this.sm4 SmUtil.sm4(keyHex.getBytes(StandardCharsets.UTF_8)); } Override public String encrypt(String message) { return sm4.encryptBase64(message); } Override public String decrypt(String encryptedMessage) { return sm4.decryptStr(encryptedMessage); } }然后配置文件里指定beanjasypt: encryptor: bean: sm4JasyptEncryptor例如数据源密码配置为ENC(密文)SpringBoot启动时Jasypt就会自动通过这个自定义加密器解密。注意实现里密钥还是要从配置读取不要写死。有一点必须提醒数据库密码本身通常比较短如果直接加密很容易被字典攻击。建议在业务层先对原文拼接盐值再加密或者用SM3先做一次摘要处理增加破解成本。加盐规则必须全局统一否则换环境后解不开数据。我在实际项目中还有一个体会国密改造本身并不复杂真正花时间的是把各种参数对齐。只要密钥、模式、IV、填充、编码这五件事在联调前就形成一个正式文档C#和Java两边都按文档实现基本不会出大问题。SM4作为一个对称算法学起来门槛不高一次跑通之后后面就是重复劳动了。这里再分享一个小技巧写一个简单的命令行自测工具输入明文和密钥一键输出ECB/CBC两种模式的密文联调时让对面拿这个工具生成样例数据能省掉很多沟通成本。本文还有配套的精品资源点击获取