news 2026/9/15 15:28:24

国密算法SM2/SM3/SM4的C#实现与工程实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
国密算法SM2/SM3/SM4的C#实现与工程实战解析

简介:国密算法是我国自主设计的商用密码标准,其中SM2为非对称加密算法,SM3为密码杂凑算法,SM4为分组加密算法。这份C#工程源码完整实现了三大国密算法,面向需要在.NET平台下集成国产密码能力的开发者。工程基于Visual Studio解决方案构建,包含源码、编译产物与依赖库,程序运行时支持对原始数据进行SM2加密、SM3摘要与SM4加解密操作,并输出完整的密文与还原数据,可直接运行验证算法正确性。资源共38个文件,以cs源码文件为主,辅以json配置文件、dll动态库、exe可执行程序及pdb调试符号等,压缩包整体仅1.37MB,轻量易部署。项目采用清晰的目录结构,便于开发者快速定位算法实现与调用入口,已有718人学习下载,适合正在推进信息系统国产化改造、需要参考成熟国密算法实现的软件工程师。

1. 这份 SM2/SM3/SM4 C# 源码包解决的不只是“加解密”

这份GmSm.rar里的东西,比我预想中完整:一个.sln解决方案,一个GmSm工程,binLib目录里放着封装好的 SM2、SM3、SM4 实现,Program.cs里跑了一段完整的“原始数据→SM2 加密→SM2 解密→还原”的演示。对正在做国密改造、又不想引入过重依赖的上位机项目来说,这套源码的价值在于能直接看到三类算法的输入输出格式:SM2 密文为什么是04开头、SM3 的 32 字节摘要如何参与校验、SM4 的 Key/IV 怎么用字节数组传。适合谁?需要对接等保密评、数据库字段加密或工控报文加密的 C# 开发。一个反直觉结论:SM2 加密结果每次都会变,同一个明文两次加密得到的两串 hex 完全不同,因为椭圆曲线点 C1 带入了随机性。

2. 国密算法选型与原理:SM2 非对称、SM3 杂凑、SM4 分组的内部构造

2.1 SM2:非对称加密与数字签名共用同一套椭圆曲线

SM2 基于有限域上的椭圆曲线,推荐参数使用 256 位素域,私钥是 256 位随机数,公钥是椭圆曲线上的点。和 RSA 不同,SM2 的加密过程中每次都会生成随机数k,同样的明文、同样的公钥,加密后 C1 不同,所以密文也不同。源码示例里048BDEE9...开头的那一长串,正是 SM2 加密结果的典型形态:第一个字节04表示 C1 是未压缩的椭圆曲线点,后面跟着 x 坐标和 y 坐标两个 32 字节大整数,然后才是 C3 摘要和 C2 密文。

在实际工程里,SM2 一般用 C1C3C2 的顺序输出,这是一种简洁的“加密即签名”做法:C2 是实际密文,C3 是用 SM3 对原文计算的哈希值,解密方先用私钥还原 C2,再对原文计算 SM3 并与 C3 比对,能同时发现密文被篡改。这也是 SM2 数字签名场景之外,常见的数据信封格式。做数据库国密测试时,经常遇到 SM2 密文长度跟着明文长度变化的情况,就是因为 C2 大小等于明文,而 C1 和 C3 固定占用额外字节。

2.2 SM3:256 位杂凑算法,P 置换带来的扩散效应

SM3 是密码杂凑算法,输出 256 位摘要,消息分组大小为 512 位。处理流程分三步:消息填充、迭代压缩、输出。填充时先补一个1,再补0,直到长度模 512 等于 448,最后附上原始消息长度的 64 位表示,这和 SHA-256 的填充思路一致。源码里如果需要拿 SM3 做 HMAC,要注意 SM3 本身没有内置 HMAC 结构,需要自己在外面套ipad/opad,否则直接用哈希键做完整性校验容易被长度扩展攻击击穿。

SM3 压缩函数里有一个值得注意的部件:P 置换。P(X) = X ^ (X <<< 9) ^ (X <<< 17),当输入差分有 1 比特时,经过循环左移和异或后,输出差分会扩散到多个位,所以它被称为线性置换。实际分析中,1 比特输入差分经过 P 置换会至少产生 3 比特输出差分,具体扩散位置取决于移位距离是否重叠。这就是 SM3 设计里“雪崩效应”的来源之一,也是同学们常问的“SM3 hash algorithm block diagram”里最核心的扩散层。

2.3 SM4:128 位分组密码,轮密钥生成顺序十分直白

SM4 是分组密码,明文、密文、密钥均为 128 位,内部采用 32 轮非线性迭代。每一轮把 128 位分成 4 个 32 位字,其中 3 个字参与非线性变换,再和轮密钥异或,最后循环移位。加密和解密的轮函数完全相同,只是解密时把 32 个轮密钥倒过来用,所以源码里只要写一套轮函数,再按正序或逆序取轮密钥即可。

轮密钥由主密钥经密钥扩展生成,先用固定参数继续异或,再进行 S 盒置换和移位。在一个 C# 工程里,我一般会这样封装密钥扩展:

private uint[] ExpandKey(byte[] key, uint[] ck) { uint[] mk = new uint[4]; for (int i = 0; i < 4; i++) { // BitConverter 在 .NET 里按小端读取,国密算法按大端约定 // 所以这里需要把字节序倒过来,否则轮密钥完全不同 byte[] b = { key[4 * i + 3], key[4 * i + 2], key[4 * i + 1], key[4 * i] }; mk[i] = BitConverter.ToUInt32(b, 0) ^ fk[i]; } for (int i = 0; i < 32; i++) { // 标准 SM4 密钥扩展:上一轮结果参与 S 盒查表 rk[i] = mk[i] ^ T(mk[i + 1] ^ mk[i + 2] ^ mk[i + 3] ^ ck[i]); mk[i + 4] = rk[i]; } return rk; }

这里ck是固定常数数组,fk是系统参数,两者都来自 SM4 标准。C# 端最容易出错的是字节序:SM4 标准文本里的示例向量按大端书写,而 BitConverter 在小端机器上会按相反顺序转换,所以mk[i]构造时必须手工翻转。源码里如果把 Key 和 IV 直接当成字节数组传给加密函数,通常不会暴露问题,但一旦要和其他语言联调,比如 Java 端SM4/CBC/PKCS7Padding解密你的数据,字节序不一致就会导致密钥完全对不上。

2.4 SM2、SM3、SM4 与 AES256 的适用边界

很多项目在选型时纠结“到底用 AES256 还是国密”。AES256 是国际标准的对称分组密码,性能通常比 SM4 更好,但国密改造要求特定场景必须使用 SM2/SM3/SM4。SM2 对应非对称体系,负责密钥交换和数字签名;SM3 负责完整性校验;SM4 负责批量数据的对称加密。下面的对比表可以直接给出初步判断。

算法类型密钥/输出长度主要用途典型问题
SM2非对称私钥 256 位,公钥点 512 位数字信封、签名、密钥交换密文比原文长很多
SM3杂凑摘要 256 位完整性校验、HMAC、数字签名摘要填充细节与 SHA 族不同
SM4分组对称密钥 128 位,分组 128 位大批量数据加解密密钥/IV 字节序易错
AES256分组对称密钥 256 位通用对称加密国密改造评审不被认可

选型还有一个不常被提到的点:SM2 加密后的数据长度随明文长度线性增长,而且每次加密结果都不同,这给数据库查询带来麻烦。AES256 配合关键字段可确定性加密或者哈希索引能很方便地做等值查询,而 SM2 基本只能做“整字段存储、全量解密”,这也是后面讲数据库国密测试时要单独处理的边界。

3. C# 工程源码解析:GmSm 解决方案的类库设计与核心实现

3.1 从 .sln 到 Lib,搞清楚源码包里各目录的作用

压缩包解开后,第一层是.vs目录和GmSm.sln.vs是 Visual Studio 的本地缓存,另一个机器上打开时可以删除。GmSm.sln是解决方案入口,GmSm目录下才是真正代码。GmSm.csproj决定目标框架,如果你的开发机是 VS2019 以上版本,而对方给的是旧版工程,打开时很可能弹出“需要安装某个 .NET Framework 版本”。常见做法是先看csproj里的TargetFrameworkVersion,新机器上重新选择目标框架再编译。

Lib目录通常是算法核心,Program.cs是调用示例。.NET自带的System.Security.Cryptography一直没内置国密算法,所以这颗Lib才这个包里最关键的部分。bin目录是编译输出,可以直接反编译确认实现,也可以直接引用编译后的 DLL。如果要把算法集成到自己的上位机,最好只拷贝Lib下相关源码或 DLL,避免把整个解决方案都拖进去。

3.2 SM2 的封装方式:公钥、密文都按字节数组传

一个容易接受的封装是把 SM2 类设计成无状态工具类,加解密方法接收字节数组和公钥/私钥参数:

public class Sm2Cipher { // 公钥是未压缩点,第一个字节固定为 0x04 public byte[] Encrypt(byte[] plaintext, byte[] publicKey) { // 1. 生成随机数 k // 2. 计算椭圆曲线点 C1 = k * G // 3. 派生出密钥流,异或得到 C2 // 4. 计算 C3 = SM3(明文) // 5. 按 C1 || C3 || C2 拼接 } public byte[] Decrypt(byte[] ciphertext, byte[] privateKey) { // 1. 分割 C1(65字节) || C3(32字节) || C2(剩余) // 2. 用私钥计算 C1 的椭圆曲线点,派生密钥流 // 3. C2 异或得到明文 // 4. 重新计算 SM3 并比对 C3 } }

这里最容易被忽略的是加密输出顺序。有些资料按 C1C2C3 排序,源码里如果按 C1C3C2 拼接,那么在线解密工具或异构语言解密前必须先明确顺序。判断方法很简单:取密文前 65 字节是04开头的点,再取最后一段做 SM3 比对,如果能对得上就是 C1C3C2,否则反过来试试。

3.3 SM3 的实现入口:输出 hex 还是 byte[]

SM3 的 C# 实现通常只有一个静态方法,输入任意字节数组,输出 32 字节摘要。调用代码和 SHA256 几乎一样:

using GmSm.Lib; byte[] data = Encoding.UTF8.GetBytes("这是国密SM2_SM3_SM4算法示例"); byte[] hash = Sm3Helper.ComputeHash(data); string hexHash = Convert.ToHexString(hash); Console.WriteLine(hexHash);

ComputeHash内部会先做消息填充和迭代压缩,返回的hash长度固定为 32 字节。因为 SM3 摘要没有可读字符,存储时基本都用 hex 或 Base64。这里有一个实用建议:做联调时,C# 端与 Java 端都用标准测试向量验证,比如空字符串和abc的 SM3 值,能通过再进入业务联调,否则后面所有的完整性校验失败都很难定位。

3.4 SM4 的加解密参数:Key、IV、Padding 三者必须同时对齐

SM4 的 C# 封装一般会提供 ECB 和 CBC 两种模式。ECB 简单但相同明文导致相同密文,字段级加密不推荐;CBC 需要 16 字节 IV,加解密两端一致。下面是 CBC 模式的典型调用:

using GmSm.Lib; byte[] key = Convert.FromHexString("0123456789ABCDEF0123456789ABCDEF"); byte[] iv = Convert.FromHexString("00000000000000000000000000000000"); byte[] plain = Encoding.UTF8.GetBytes("需要加密的字段内容"); byte[] encrypted = Sm4Helper.EncryptCbc(plain, key, iv); byte[] decrypted = Sm4Helper.DecryptCbc(encrypted, key, iv);

Key 必须是 16 字节,IV 也必须是 16 字节,源码里如果传入的字符串长度不是 32 位 hex,必然会在FromHexString抛异常。C# 这边常用的补齐方式是 PKCS7:明文不足 16 字节倍数时补上差值的字节,差值是 5 就补 5 个0x05。Java 端SM4/CBC/PKCS7Padding描述的就是这一组合。联调时最容易出的问题是:C# 端已经把 SM4 结果转成 Base64 字符串,Java 端却按 UTF-8 直接拿字节,导致填充字节被当成字符编码的一部分,最后解密报数据异常或得到乱码。

4. 加解密实战:复现“这是国密SM2_SM3_SM4算法示例”的完整流程

4.1 本地运行 GmSm 示例并了解编译环境

源码工程是 Visual Studio 解决方案,最简单的跑法是打开GmSm.sln,把GmSm设为启动项目,直接 F5。如果命令行环境具备.NET SDK,也可以切到GmSm目录执行:

dotnet run --project GmSm.csproj

使用明文的填空和密文顺序问题。输出里如果直接打印中文,需要在Console.OutputEncoding设置为 UTF-8,否则控制台会把字节按系统默认代码页显示成问号。不同 Visual Studio 版本打开工程时,如果报 “当前目标框架不受支持”,先修改TargetFrameworkVersion到本机已安装的版本即可。VS2019 开发的工程拿到 VS2015 上大概率无法直接打开,原因是新版csproj格式和 NuGet 包引用方式不一致,不要在这上面浪费时间,直接把Lib下的源文件加入新工程。

4.2 SM2 密文拆包:从 04 开头推断 C1、C3、C2 长度

示例密文:

048BDEE95960A436991765867726A5C8A2359D9AE0A67FCB10F66F3F06BD5F150 5CB09ADDD69A502BC501C78843E7BD5CAD0C13261DAAB1BA6C17453CD8FCD474 CFC4258A3845789C34FEC30465B910AE463FB258B027048264B58473ECA19D16 CB4C85853C987F99D8F9EB96F9335DD82967040EDF0462D57D94B820DEE0073A 448E9A7

第一字节04之后是 C1 的 x 和 y 坐标,各 32 字节,所以 C1 总共 65 字节,也就是 hex 字符串的 130 个字符。从第 131 个字符开始取 64 个字符是 C3,即 SM3 摘要。剩下的字符就是 C2 密文。用代码把这段密文拆开,然后直接调 SM2 解密:

string cipherHex = "048BDEE95960A436991765867726A5C8A2359D9AE0A67FCB10F66F3F06BD5F1505CB09ADDD69A502BC501C78843E7BD5CAD0C13261DAAB1BA6C17453CD8FCD474CFC4258A3845789C34FEC30465B910AE463FB258B027048264B58473ECA19D16CB4C85853C987F99D8F9EB96F9335DD82967040EDF0462D57D94B820DEE0073A448E9A7"; byte[] cipher = Convert.FromHexString(cipherHex); // 前65字节是C1,接着32字节是C3,剩余部分是C2 byte[] c1 = cipher[..65]; byte[] c3 = cipher[65..97]; byte[] c2 = cipher[97..]; byte[] plain = sm2.Decrypt(cipher, privateKey); Console.WriteLine(Encoding.UTF8.GetString(plain));

这里实际上不需要手动切分 C1、C2、C3,因为Decrypt内部会按固定偏移解析;但手动切分是验证实现正确性的好方法。如果解密结果乱码,优先检查私钥是否和公钥配对,其次检查密文在传输过程中是否被十六进制编码额外修改过。

4.3 SM3 完整性校验:解密后要比对 C3 摘要

SM2 的解密流程里已经包含 C3 比对,但仅仅“解密成功”不能证明你用的 SM3 实现正确。单独对原文做一次 SM3:

byte[] hash = Sm3Helper.ComputeHash(Encoding.UTF8.GetBytes("这是国密SM2_SM3_SM4算法示例")); string computedHashHex = Convert.ToHexString(hash); string c3Hex = cipherHex.Substring(130, 64); bool valid = computedHashHex.Equals(c3Hex, StringComparison.OrdinalIgnoreCase);

如果computedHashHexc3Hex不一致,问题几乎一定出在编码:源码里的“原始数据”到底是 UTF-8 还是 GBK,直接影响 SM3 的输入字节。中文环境下 C# 工程默认编码可能是 GB2312,而 Java 端默认用 UTF-8,两边的 SM3 值自然不同。遇到这种情况,不要急着查哈希算法,先确认所有参与方的Encoding.UTF8.GetString/GetBytes是否发生在同一个字节序列上。

4.4 高频异常与排查:NoSuchAlgorithmException、字段乱码、长度对齐

Java 端NoSuchAlgorithmException: No such algorithm: SM4/CBC/PKCS7Padding通常不是算法本身缺失,而是使用的 Provider 没有注册国密 SM4。这里先明确一点:有多个已知算法常数,而 Java 默认的SunJCE并不包含 SM4,所以需要在代码里显式引入包含 SM4 实现的 JCE Provider。换成 C# 侧,Cipher类的概念不存在,常见的错误是直接写SM4.Create(),结果发现System.Security.Cryptography里根本没有这个工厂方法。源码包里自己实现的Sm4Helper就是为了解决这一点。

还有一个很多人踩的坑:数据库字段直接显示一条记录数据时,看到的是 hex 字符串或 Base64,而有些人会把它当成文本字段直接Encoding.UTF8.GetString解密,结果得到一串乱码。正确做法是先Convert.FromHexString(字段值),还原成原始密文字节,再进行 SM4 解密。严格来说,加密结果本身是二进制,不应使用 UTF-8 编解码。

5. 进阶:C# 上位机集成国密算法时的高频问题与优化技巧

5.1 大批量数据加解密时避免阻塞 UI

上位机在循环采集数据时,如果每采集一帧就在 UI 线程里做 SM4 加密,界面会明显卡顿。这个问题不是 SM4 本身慢,而是同步计算占用了消息循环。常见做法是把加解密放到后台任务:

byte[] encrypted = await Task.Run(() => Sm4Helper.EncryptCbc(frame, key, iv)); Dispatcher.Invoke(() => UpdateUi(encrypted));

这里Task.Run把耗时计算移到线程池,Dispatcher.Invoke再把结果送回 UI 线程,避免跨线程访问控件。如果采集频率很高,可以进一步用生产者消费者队列把待加密的帧缓存起来,批量处理后再统一刷新,而不是一帧一次await

5.2 数据库字段国密测试的落地步骤

做数据库国密测试时,先分清“传输加密”和“字段加密”。SM2 适合做数字信封,不直接用于数据库字段;SM4 适合整字段加密。落地建议如下:

  1. 对敏感字段值取 UTF-8 字节,用 SM4-CBC 加密。
  2. 把密文字节转 hex 存入varchar,避免数据库字符集转换破坏字节。
  3. 查询时读取 hex,先转字节,再解密得到原文。
  4. 需要在数据库侧做等值查询时,用确定性加密 SM4-ECB 固定 IV 或额外存 SM3 哈希值,但哈希值不能直接用于唯一索引,因为同一个值对应同一个哈希,存在字典攻击风险。

与 OceanBase 这类数据库的国密能力搭配时,应用层字段加密依然有效,但要注意不要把密文直接当成排序字段,因为密文顺序与明文顺序无关。

5.3 用标准测试向量校验三个算法

把这三个函数的输出和标准测试向量比对。例如 SM3 对abc的摘要应为66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0,SM4 在 ECB 模式下用固定密钥加密固定明文的结果也可以从标准文档中取出一组已知值。把这个哈希值放进单元测试里,比直接猜密文格式更靠谱。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 15:27:10

Wazuh安装实战:从部署到Agent接入的避坑指南

Wazuh这套东西&#xff0c;说好装也好装&#xff0c;一条官方脚本跑完就能看到仪表板&#xff1b;说难装也难装&#xff0c;生产环境里拆成三个组件部署时&#xff0c;每个环节都能把你卡上半天。群里天天有人问“为什么dashboard打不开”“为什么agent一直显示Disconnected”&…

作者头像 李华
网站建设 2026/9/15 15:26:37

STM32与ESP8266串口通信:USART2 DMA接收+IDLE中断实战解析

简介&#xff1a;面向STM32嵌入式开发者与物联网初学者的STM32ESP8266驱动集成资源包&#xff0c;聚焦两者通过UART/SPI通信、AT指令集控制、TCP/IP协议栈对接等关键环节&#xff0c;可帮助读者快速搭建Wi-Fi联网应用。压缩包共85个文件&#xff0c;以37个C源文件与36个H头文件…

作者头像 李华
网站建设 2026/9/15 15:26:01

GUI智能体UI-TARS实操指南:让它替你点击与输入

GUI智能体UI-TARS实操指南&#xff1a;让它替你点击与输入 【免费下载链接】UI-TARS Pioneering Automated GUI Interaction with Native Agents 项目地址: https://gitcode.com/GitHub_Trending/ui/UI-TARS 想让电脑替你点鼠标填表单&#xff0c;却被自动化脚本劝退&am…

作者头像 李华
网站建设 2026/9/15 15:25:02

EIP-7792 可验证日志:用链上日志累积器让 eth_getLogs 响应可验证

EIP-7792 可验证日志&#xff1a;用链上日志累积器让 eth_getLogs 响应可验证 【免费下载链接】EIPs The Ethereum Improvement Proposal repository 项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs 本指南以仓库中的 EIPS/eip-7792.md 为蓝本&#xff0c;系统…

作者头像 李华