C#国密算法实战:SM2、SM3、SM4集成与混合加密实现
1. 项目概述:为什么要在C#里搞国密算法?
最近在做一个对接某特定行业系统的项目,对方明确要求通信和数据存储必须使用国密算法。一开始我也头大,毕竟平时用AES、RSA用惯了,对国密那一套SM2、SM3、SM4确实不熟。但需求就是命令,硬着头皮研究了一圈,发现其实在C#里集成国密算法并没有想象中那么复杂,核心就是找到靠谱的库,然后理解国密算法的“脾气”。
简单来说,这个“C#使用国密算法实现非对称加密、对称加密、消息摘要”项目,就是要在.NET环境中,用国密标准替代我们熟悉的那些国际通用算法。非对称加密用SM2替代RSA/ECC,对称加密用SM4替代AES/DES,消息摘要用SM3替代SHA-256/MD5。这不仅仅是简单的算法替换,更涉及到密钥格式、签名验签流程、加密模式等一系列适配工作。适合正在或即将面临国密改造需求的.NET开发者、需要与国内金融、政务、物联网等强合规领域对接的系统架构师,以及所有对密码学应用感兴趣,想了解国产密码体系实现的同行。
2. 国密算法核心三剑客:SM2, SM3, SM4深度解析
在动手写代码之前,我们必须先搞清楚我们要用的这三个“工具”到底是怎么回事。国密算法是一套完整的密码体系,各有分工,不能乱用。
2.1 SM2:基于椭圆曲线的非对称加密与签名
SM2本质上是一种椭圆曲线密码(ECC)算法。你可以把它理解为国产的、参数特定的ECC。和国际上常用的secp256k1(比特币在用)等曲线不同,SM2使用一条由国家密码管理局指定的椭圆曲线参数。
它的核心用途有两个:
- 非对称加密/解密:一方用公钥加密,另一方用对应的私钥解密。常用于加密会话密钥。
- 数字签名/验签:发送方用私钥对消息摘要签名,接收方用公钥验证签名,确保消息的完整性和不可否认性。
注意:SM2的签名算法与国际标准的ECDSA有所不同,它采用了更复杂的计算流程,增强了安全性。这意味着你不能直接用.NET自带的
ECDsa类去兼容SM2签名,必须使用实现了SM2完整标准的库。
一个关键点是密钥格式。SM2的公钥通常以04||X||Y的未压缩格式表示(04是标识位,后面跟着64字节的X和Y坐标),私钥就是一个大的随机整数。在代码中处理时,需要确保库能正确生成和解析这种格式。
2.2 SM3:密码杂凑算法(消息摘要)
SM3可以看作是国产的SHA-256。它接收任意长度的输入,生成一个固定长度(256位,即32字节)的哈希值。它的设计结构和SHA-256类似,都是Merkle–Damgård结构,但具体的压缩函数和常量经过了重新设计,安全性有保障。
它的用途很纯粹:
- 完整性校验:计算文件或数据的摘要,比对是否被篡改。
- 数字签名的一部分:通常先对消息用SM3做摘要,再对摘要用SM2私钥签名。
- 密钥派生:在某些协议中用于从主密钥派生出子密钥。
在实际使用中,SM3的API通常最简单,就是ComputeHash。但要注意,有些场景下要求“加盐”哈希,这就需要我们手动在数据前后拼接盐值再计算。
2.3 SM4:分组对称加密算法
SM4是一种分组密码,分组长度为128位,密钥长度也是128位。它对标的是AES-128。但和AES支持128/192/256多种密钥长度不同,SM4固定使用128位密钥。
SM4支持多种工作模式,最常用的是:
- ECB (Electronic Codebook):最简单,但不安全,相同的明文块会加密成相同的密文块,不建议用于加密有意义的数据。
- CBC (Cipher Block Chaining):最常用的模式,需要一个初始化向量(IV),安全性好。这是我们项目中的首选模式。
- 其他模式:如CTR, GCM等,部分库也可能支持。GCM模式还能同时提供加密和认证,但实现相对复杂。
这里有个重要的实操心得:SM4的CBC模式,其IV也需要是128位(16字节)。这个IV不需要保密,但必须是随机的且不可预测,通常每次加密都生成一个新的随机IV,并和密文一起传输。解密时使用同样的IV。
3. 工具选型与项目环境搭建
在C#里使用国密算法,主要有三条路:纯托管实现、调用C++库的P/Invoke、使用现成的NuGet包。经过对比和踩坑,我强烈推荐第三种。
3.1 主流国密算法库对比
| 库/方案 | 类型 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
| BouncyCastle | 纯C#托管库 | 老牌密码学库,功能极其全面,支持国密算法。社区活跃,文档相对较多。 | API设计较为底层和复杂,对新手不友好。需要从源码编译或寻找包含国密扩展的版本。 | ★★★★☆ |
Portable.BouncyCastle | NuGet包 | BouncyCastle的便携版,可通过NuGet直接安装。 | 同样存在API复杂的问题。部分版本对国密算法的支持可能不完整或需要额外配置。 | ★★★★☆ |
GMSSL的C#封装 | P/Invoke | 基于著名的C国密库GMSSL,性能可能较好。 | 需要处理本地库的部署(dll/so),跨平台麻烦,增加项目复杂度。 | ★★☆☆☆ |
SKIT系列库 | NuGet包 | 国内开发者维护,专门针对常用场景封装,API设计更符合C#开发者习惯,开箱即用。 | 可能不如BouncyCastle那样功能全面到覆盖所有边缘场景。 | ★★★★★ |
对于大多数应用场景,尤其是快速开发和集成,我推荐使用SKIT的库。例如,SKIT.Crypto或SKIT.BouncyCastle(后者是对BouncyCastle的友好封装)。它们让代码写起来更像是在用.NET原生的Aes、RSA类,学习成本低。
3.2 项目环境准备
假设我们使用SKIT.BouncyCastle这个NuGet包,因为它提供了对BouncyCastle国密算法的友好API封装。
- 创建项目:创建一个新的C#控制台应用或类库项目(.NET 6+ 或 .NET Framework 4.6.1+均可)。
- 安装NuGet包:通过Visual Studio的NuGet包管理器或命令行安装。
dotnet add package SKIT.BouncyCastle - 引入命名空间:在代码文件顶部添加引用。
using Org.BouncyCastle.Crypto; // 核心密码学接口 using Org.BouncyCastle.Crypto.Engines; // 算法引擎 using Org.BouncyCastle.Crypto.Modes; // 工作模式 using Org.BouncyCastle.Crypto.Paddings; // 填充模式 using Org.BouncyCastle.Crypto.Parameters; // 参数 using Org.BouncyCastle.Security; // 安全随机数等工具 // SKIT的封装可能提供更简洁的API,具体看其文档
提示:如果你找不到
SKIT.BouncyCastle,或者项目环境限制不能使用第三方NuGet,那么直接使用Portable.BouncyCastle也是完全可行的,只是后续的代码示例在API调用上会稍显繁琐。本文后续的核心原理和步骤是完全通用的。
4. 核心环节实现:从生成密钥到加解密实战
环境搭好,库也引了,现在我们来真刀真枪地实现三大功能。我会按照实际开发中的典型流程来讲解:密钥生成 -> 加密/签名 -> 解密/验签。
4.1 SM2非对称加密与解密实现
SM2的非对称加密过程,通常用于加密一个随机的对称密钥(比如一个SM4的密钥),而不是直接加密大量业务数据。
4.1.1 生成SM2密钥对
首先,我们需要一对公私钥。
using Org.BouncyCastle.Asn1.GM; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; // 1. 创建SM2椭圆曲线参数(使用国密标准参数) var ecParams = GMNamedCurves.GetByName("sm2p256v1"); var domainParams = new ECDomainParameters(ecParams.Curve, ecParams.G, ecParams.N, ecParams.H); // 2. 生成密钥对生成器 var generator = new ECKeyPairGenerator(); generator.Init(new ECKeyGenerationParameters(domainParams, new SecureRandom())); // 3. 生成密钥对 AsymmetricCipherKeyPair keyPair = generator.GenerateKeyPair(); ECPrivateKeyParameters privateKeyParams = (ECPrivateKeyParameters)keyPair.Private; ECPublicKeyParameters publicKeyParams = (ECPublicKeyParameters)keyPair.Public; // 4. 将密钥转换为字节数组,方便存储或传输 // 私钥:一个大整数D的字节数组 byte[] privateKeyBytes = privateKeyParams.D.ToByteArrayUnsigned(); // 通常32字节 // 公钥:未压缩格式 (04 || X || Y) byte[] publicKeyBytes = publicKeyParams.Q.GetEncoded(false); // 通常65字节,04开头 Console.WriteLine($"私钥长度: {privateKeyBytes.Length}"); Console.WriteLine($"公钥长度: {publicKeyBytes.Length}");实操心得:
ToByteArrayUnsigned()非常重要,它能确保我们得到的私钥字节数组是标准的无符号大整数表示,避免了因为符号位导致的密钥长度不一致问题,这在后续导入密钥时是常见的坑点。
4.1.2 使用公钥加密数据
假设我们现在要加密一个随机的16字节SM4密钥。
using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Encodings; // 待加密的数据(例如一个SM4密钥) byte[] dataToEncrypt = new byte[16]; // 128位密钥 new SecureRandom().NextBytes(dataToEncrypt); Console.WriteLine($"原始SM4密钥: {BitConverter.ToString(dataToEncrypt)}"); // 1. 创建SM2加密引擎 var sm2Engine = new SM2Engine(); // 2. 初始化引擎为加密模式,使用公钥参数 sm2Engine.Init(true, publicKeyParams); // true 表示加密 // 3. 执行加密 byte[] encryptedData = sm2Engine.ProcessBlock(dataToEncrypt, 0, dataToEncrypt.Length); Console.WriteLine($"加密后数据长度: {encryptedData.Length}"); Console.WriteLine($"加密后数据: {BitConverter.ToString(encryptedData)}");SM2加密后的数据长度会比原文长不少,因为它包含了椭圆曲线加密过程中产生的点坐标等信息。
4.1.3 使用私钥解密数据
接收方拿到加密数据后,用自己的私钥解密。
// 假设我们拿到了加密后的数据 encryptedData 和私钥参数 privateKeyParams // 1. 创建SM2加密引擎 var sm2EngineDecrypt = new SM2Engine(); // 2. 初始化引擎为解密模式,使用私钥参数 sm2EngineDecrypt.Init(false, privateKeyParams); // false 表示解密 // 3. 执行解密 byte[] decryptedData = sm2EngineDecrypt.ProcessBlock(encryptedData, 0, encryptedData.Length); Console.WriteLine($"解密后SM4密钥: {BitConverter.ToString(decryptedData)}"); Console.WriteLine($"解密是否成功: {dataToEncrypt.SequenceEqual(decryptedData)}");4.2 SM4对称加密与解密实现
我们用上面解密得到的SM4密钥,来加密一段实际的业务数据。这里采用最常用的CBC模式,并处理PKCS7填充。
4.2.1 CBC模式加密
using Org.BouncyCastle.Crypto.Paddings; using Org.BouncyCastle.Crypto.Modes; // 业务数据 string plainText = "这是一段需要加密的敏感业务数据,比如订单号20240520001。"; byte[] plainData = Encoding.UTF8.GetBytes(plainText); // SM4密钥和IV(初始化向量) byte[] sm4Key = decryptedData; // 使用上面解密出来的密钥,确保是16字节 byte[] iv = new byte[16]; // IV必须是16字节 new SecureRandom().NextBytes(iv); // 生成随机IV // 1. 创建SM4引擎 var sm4Engine = new SM4Engine(); // 2. 创建CBC模式包装器 var cbcBlockCipher = new CbcBlockCipher(sm4Engine); // 3. 创建PKCS7填充器(因为CBC是块加密,需要处理最后一块不足位的问题) var paddedCipher = new PaddedBufferedBlockCipher(cbcBlockCipher, new Pkcs7Padding()); // 4. 初始化加密器 paddedCipher.Init(true, new ParametersWithIV(new KeyParameter(sm4Key), iv)); // true 表示加密 // 5. 执行加密 byte[] outputBuffer = new byte[paddedCipher.GetOutputSize(plainData.Length)]; int length = paddedCipher.ProcessBytes(plainData, 0, plainData.Length, outputBuffer, 0); length += paddedCipher.DoFinal(outputBuffer, length); // 处理最后一块并应用填充 byte[] cipherData = new byte[length]; Array.Copy(outputBuffer, 0, cipherData, 0, length); Console.WriteLine($"IV: {BitConverter.ToString(iv)}"); Console.WriteLine($"密文: {Convert.ToBase64String(cipherData)}");关键点:IV必须随密文一起保存或传输给解密方。它本身不是秘密,但必须唯一且随机。常见的做法是将IV拼接在密文前面:
最终数据 = IV + 密文。
4.2.2 CBC模式解密
解密方需要拥有相同的SM4密钥、IV和密文。
// 假设我们收到了 iv 和 cipherData // 1. 创建同样的SM4引擎、CBC模式、填充器 var sm4EngineDec = new SM4Engine(); var cbcBlockCipherDec = new CbcBlockCipher(sm4EngineDec); var paddedCipherDec = new PaddedBufferedBlockCipher(cbcBlockCipherDec, new Pkcs7Padding()); // 2. 初始化解密器 paddedCipherDec.Init(false, new ParametersWithIV(new KeyParameter(sm4Key), iv)); // false 表示解密 // 3. 执行解密 byte[] decOutputBuffer = new byte[paddedCipherDec.GetOutputSize(cipherData.Length)]; int decLength = paddedCipherDec.ProcessBytes(cipherData, 0, cipherData.Length, decOutputBuffer, 0); decLength += paddedCipherDec.DoFinal(decOutputBuffer, decLength); byte[] decryptedPlainData = new byte[decLength]; Array.Copy(decOutputBuffer, 0, decryptedPlainData, 0, decLength); string decryptedText = Encoding.UTF8.GetString(decryptedPlainData); Console.WriteLine($"解密后明文: {decryptedText}");4.3 SM3消息摘要实现
SM3的使用相对直接,类似于计算MD5或SHA256。
4.3.1 计算数据的SM3哈希值
using Org.BouncyCastle.Crypto.Digests; // 待计算摘要的数据 byte[] dataForHash = Encoding.UTF8.GetBytes(plainText); // 使用之前的明文 // 1. 创建SM3摘要计算器 var sm3Digest = new SM3Digest(); // 2. 输入数据 sm3Digest.BlockUpdate(dataForHash, 0, dataForHash.Length); // 3. 获取摘要结果 byte[] hashResult = new byte[sm3Digest.GetDigestSize()]; // SM3是32字节 sm3Digest.DoFinal(hashResult, 0); Console.WriteLine($"SM3哈希值: {BitConverter.ToString(hashResult).Replace("-", "").ToLower()}");4.3.2 验证数据完整性
验证时,重新计算收到数据的SM3哈希值,与发送方附带的原始哈希值进行比对。如果一致,则数据未被篡改。
// 假设发送方发送了 data 和其 hashResult // 接收方收到 dataReceived byte[] dataReceived = dataForHash; // 模拟接收到的数据 byte[] hashReceived = hashResult; // 模拟接收到的哈希值 // 接收方自己计算哈希 var sm3DigestVerify = new SM3Digest(); sm3DigestVerify.BlockUpdate(dataReceived, 0, dataReceived.Length); byte[] hashCalculated = new byte[sm3DigestVerify.GetDigestSize()]; sm3DigestVerify.DoFinal(hashCalculated, 0); bool isIntegrityOk = hashReceived.SequenceEqual(hashCalculated); Console.WriteLine($"数据完整性验证: {isIntegrityOk}");5. 典型应用场景与代码封装实践
了解了基础用法,我们来看看在实际项目中如何组织这些代码。一个常见的场景是“混合加密系统”:用SM2加密随机的SM4密钥,再用该SM4密钥加密业务数据,最后用SM3验证数据完整性并可能用SM2做签名。
5.1 构建一个简单的国密混合加密工具类
下面是一个高度简化的工具类示例,展示了如何将上述流程封装起来。在实际项目中,你需要添加更完善的错误处理、密钥管理、序列化等逻辑。
using System.Text; using Org.BouncyCastle.Asn1.GM; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Modes; using Org.BouncyCastle.Crypto.Paddings; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; public class SM2CryptoHelper { // 生成SM2密钥对 public static (byte[] publicKey, byte[] privateKey) GenerateSm2KeyPair() { var ecParams = GMNamedCurves.GetByName("sm2p256v1"); var domainParams = new ECDomainParameters(ecParams.Curve, ecParams.G, ecParams.N, ecParams.H); var generator = new ECKeyPairGenerator(); generator.Init(new ECKeyGenerationParameters(domainParams, new SecureRandom())); var keyPair = generator.GenerateKeyPair(); var privateKeyParams = (ECPrivateKeyParameters)keyPair.Private; var publicKeyParams = (ECPublicKeyParameters)keyPair.Public; return (publicKeyParams.Q.GetEncoded(false), privateKeyParams.D.ToByteArrayUnsigned()); } // 从字节数组还原公钥参数 (简化示例,实际需处理更多格式) private static ECPublicKeyParameters RestorePublicKey(byte[] publicKeyBytes) { var ecParams = GMNamedCurves.GetByName("sm2p256v1"); var curve = ecParams.Curve; var point = curve.DecodePoint(publicKeyBytes); // 解码公钥点 return new ECPublicKeyParameters(point, new ECDomainParameters(curve, ecParams.G, ecParams.N, ecParams.H)); } // 从字节数组还原私钥参数 private static ECPrivateKeyParameters RestorePrivateKey(byte[] privateKeyBytes) { var ecParams = GMNamedCurves.GetByName("sm2p256v1"); var d = new Org.BouncyCastle.Math.BigInteger(1, privateKeyBytes); // 注意这里的1表示正数 return new ECPrivateKeyParameters(d, new ECDomainParameters(ecParams.Curve, ecParams.G, ecParams.N, ecParams.H)); } // SM2加密 public static byte[] Sm2Encrypt(byte[] publicKeyBytes, byte[] data) { var publicKey = RestorePublicKey(publicKeyBytes); var engine = new SM2Engine(); engine.Init(true, publicKey); return engine.ProcessBlock(data, 0, data.Length); } // SM2解密 public static byte[] Sm2Decrypt(byte[] privateKeyBytes, byte[] encryptedData) { var privateKey = RestorePrivateKey(privateKeyBytes); var engine = new SM2Engine(); engine.Init(false, privateKey); return engine.ProcessBlock(encryptedData, 0, encryptedData.Length); } } public class SM4CryptoHelper { // SM4 CBC加密 public static (byte[] iv, byte[] cipher) Sm4CbcEncrypt(byte[] key, byte[] plainData) { if (key.Length != 16) throw new ArgumentException("SM4 key must be 16 bytes."); byte[] iv = new byte[16]; new SecureRandom().NextBytes(iv); var engine = new SM4Engine(); var blockCipher = new CbcBlockCipher(engine); var cipher = new PaddedBufferedBlockCipher(blockCipher, new Pkcs7Padding()); cipher.Init(true, new ParametersWithIV(new KeyParameter(key), iv)); byte[] output = new byte[cipher.GetOutputSize(plainData.Length)]; int len = cipher.ProcessBytes(plainData, 0, plainData.Length, output, 0); len += cipher.DoFinal(output, len); byte[] cipherData = new byte[len]; Array.Copy(output, 0, cipherData, 0, len); return (iv, cipherData); } // SM4 CBC解密 public static byte[] Sm4CbcDecrypt(byte[] key, byte[] iv, byte[] cipherData) { if (key.Length != 16) throw new ArgumentException("SM4 key must be 16 bytes."); var engine = new SM4Engine(); var blockCipher = new CbcBlockCipher(engine); var cipher = new PaddedBufferedBlockCipher(blockCipher, new Pkcs7Padding()); cipher.Init(false, new ParametersWithIV(new KeyParameter(key), iv)); byte[] output = new byte[cipher.GetOutputSize(cipherData.Length)]; int len = cipher.ProcessBytes(cipherData, 0, cipherData.Length, output, 0); len += cipher.DoFinal(output, len); byte[] plainData = new byte[len]; Array.Copy(output, 0, plainData, 0, len); return plainData; } } public class SM3Helper { // 计算SM3哈希 public static byte[] ComputeSm3Hash(byte[] data) { var digest = new SM3Digest(); digest.BlockUpdate(data, 0, data.Length); byte[] result = new byte[digest.GetDigestSize()]; digest.DoFinal(result, 0); return result; } }5.2 混合加密流程示例
使用上面封装的工具类,模拟一个完整的发送-接收流程:
// ========== 发送方 ========== // 1. 生成或拥有接收方的SM2公钥 var (receiverPublicKey, receiverPrivateKey) = SM2CryptoHelper.GenerateSm2KeyPair(); // 2. 生成一个随机的SM4会话密钥 byte[] sessionKey = new byte[16]; new SecureRandom().NextBytes(sessionKey); // 3. 用接收方的SM2公钥加密会话密钥 byte[] encryptedSessionKey = SM2CryptoHelper.Sm2Encrypt(receiverPublicKey, sessionKey); // 4. 用会话密钥加密业务数据 string businessData = "订单金额:1000元,用户ID:12345"; byte[] plainData = Encoding.UTF8.GetBytes(businessData); var (iv, encryptedData) = SM4CryptoHelper.Sm4CbcEncrypt(sessionKey, plainData); // 5. 计算业务数据的SM3摘要(可选,用于完整性校验) byte[] dataHash = SM3Helper.ComputeSm3Hash(plainData); // 6. 将加密后的会话密钥、IV、密文、哈希值等打包发送给接收方 // 模拟传输:encryptedSessionKey, iv, encryptedData, dataHash // ========== 接收方 ========== // 1. 用自己的SM2私钥解密会话密钥 byte[] decryptedSessionKey = SM2CryptoHelper.Sm2Decrypt(receiverPrivateKey, encryptedSessionKey); // 2. 用解密出的会话密钥和收到的IV解密业务数据 byte[] decryptedPlainData = SM4CryptoHelper.Sm4CbcDecrypt(decryptedSessionKey, iv, encryptedData); string receivedData = Encoding.UTF8.GetString(decryptedPlainData); Console.WriteLine($"接收方解密数据: {receivedData}"); // 3. (可选)验证数据完整性:重新计算解密后数据的哈希,与收到的dataHash比对 byte[] recalculatedHash = SM3Helper.ComputeSm3Hash(decryptedPlainData); if (dataHash.SequenceEqual(recalculatedHash)) { Console.WriteLine("数据完整性验证通过!"); } else { Console.WriteLine("警告:数据可能被篡改!"); }6. 常见问题、避坑指南与性能考量
在实际集成和开发过程中,你会遇到各种各样的问题。下面是我踩过的一些坑和总结的经验。
6.1 密钥管理与格式问题
这是最常见的问题来源。
问题1:SM2公钥导入失败,提示“无效的点编码”或类似错误。
- 原因:公钥字节数组的格式不对。SM2公钥标准格式是未压缩的
04||X||Y(65字节)。有时从其他系统(如Java、OpenSSL)传来的公钥可能是压缩格式或其他编码(如Base64后的ASN.1 DER结构)。 - 解决:
- 确认对方提供的公钥格式。如果是Base64字符串,先解码。
- 如果是65字节且以
0x04开头,直接使用。 - 如果是其他格式(如ASN.1 DER序列),你需要解析这个结构,提取出X和Y坐标,再重新组装成
04||X||Y。BouncyCastle提供了Asn1Object和X9ECParameters等类来解析这些结构,但这部分代码比较繁琐。
- 建议:在系统间约定好公钥的交换格式(如纯X||Y坐标的64字节,或标准的65字节04格式),并编写统一的密钥导入导出工具函数。
- 原因:公钥字节数组的格式不对。SM2公钥标准格式是未压缩的
问题2:SM2私钥导入失败,解密或签名时出错。
- 原因:私钥字节数组可能包含了额外的信息(如ASN.1包装),或者长度不是32字节。
- 解决:确保你传递给
ECPrivateKeyParameters构造函数的BigInteger是基于正确的私钥字节数组创建的。使用new BigInteger(1, privateKeyBytes)来确保它被解释为正数。如果私钥是ASN.1格式,同样需要先解析。
问题3:SM4密钥长度错误。
- 原因:SM4密钥必须是16字节(128位)。误用了其他长度的密钥(如从SM3哈希截取的32字节)。
- 解决:严格检查密钥来源。如果是派生出来的,确保最终长度是16字节。
6.2 加密解密过程中的异常
问题:SM4 CBC解密时抛出“无效的填充”异常。
- 原因:
- 密钥错误:加密和解密使用的密钥不一致。
- IV错误:解密时使用的IV与加密时使用的IV不一致。
- 密文被篡改:传输或存储过程中密文发生了损坏。
- 填充模式不匹配:加密用了PKCS7,解密用了其他填充或无填充。
- 排查步骤:
- 打印并比对加密和解密两端的密钥和IV的Hex值。
- 确保密文在传输过程中没有经过不必要的编码/解码(如Base64编解码要配对使用)。
- 确认两端使用的填充模式完全相同。
- 原因:
问题:SM2加密后的数据长度不固定。
- 原因:这是正常的。SM2加密结果包含椭圆曲线点坐标等信息,其长度会有几个字节的浮动,但通常在一个固定范围内(如对于
sm2p256v1曲线,密文长度可能在120字节左右)。 - 注意:不要假设SM2密文是固定长度的。在存储或传输时,直接处理整个字节数组即可。
- 原因:这是正常的。SM2加密结果包含椭圆曲线点坐标等信息,其长度会有几个字节的浮动,但通常在一个固定范围内(如对于
6.3 性能与最佳实践
- 性能:SM2的非对称加密解密运算比RSA快,但依然远慢于SM4对称加密。这就是为什么混合加密是标准做法——用SM2保护一个随机的SM4密钥,再用SM4加密实际数据。SM3哈希计算速度很快。
- 随机数安全:密钥生成、IV生成、SM2加密中的随机数
k,都必须使用密码学安全的随机数生成器(CSPRNG)。在C#中,务必使用System.Security.Cryptography.RandomNumberGenerator或BouncyCastle的SecureRandom,绝对不要使用System.Random。 - 错误处理:密码学操作必须进行细致的异常处理(
try-catch),并记录日志。但要注意,不要将具体的密码学错误信息(如“填充错误”)直接暴露给最终用户,以免泄露系统信息,应转换为通用的“处理失败”提示。 - 算法标识:在实际通信协议中,除了传输加密数据,最好还附带一个标识符,指明使用了哪种国密算法和模式(如“SM2-SM4-CBC-SM3”),方便接收方进行解析。
6.4 国密算法与标准体系
- 标准符合性:如果你做的项目需要过密评(密码应用安全性评估),那么你使用的国密算法实现必须是通过国家密码管理局认证的。并非所有开源实现都符合认证要求。
BouncyCastle是一个优秀的密码学库,但其国密实现是否可用于过密评的商用系统,需要你向供应商或评测机构确认。在严格要求合规的场景下,可能需要采购商用的、经过认证的密码模块(如硬件加密卡或经过认证的软件库)。 - 算法组合:GB/T 32918等国家标准定义了SM2、SM3、SM4如何组合使用(如SM2签名验签的流程、SM2加密的流程)。在实现时,应尽量参考这些标准文档,确保与其他系统的互操作性。
最后,再分享一个调试小技巧:在开发初期,可以先用固定的测试向量(Test Vector)来验证你的加密解密流程是否正确。网上可以找到国密算法的标准测试数据,用这些已知的明文、密钥、密文来验证你的代码,能快速定位是密钥处理问题、加密逻辑问题还是编码问题。当你确认基础流程无误后,再切换到随机密钥和数据进行集成测试。