手把手教你用C#和BouncyCastle实现IC卡SM4国密算法(含密钥分散与MAC计算)
实战指南:C#与BouncyCastle实现IC卡SM4国密算法全流程
金融IC卡和交通卡系统中,数据安全始终是核心诉求。国密SM4算法作为我国自主设计的商用密码标准,凭借其高效安全的特性,已成为各类智能卡应用的首选加密方案。本文将带您从零开始,使用C#和BouncyCastle库完整实现SM4算法的三大核心功能:密钥分散、数据加解密和MAC计算,解决实际开发中的典型痛点。
1. 环境准备与基础配置
1.1 开发环境搭建
首先通过NuGet安装必要的BouncyCastle库:
Install-Package BouncyCastle -Version 1.8.9创建.NET Core控制台项目后,添加以下命名空间引用:
using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Paddings; using Org.BouncyCastle.Crypto.Modes;1.2 SM4算法基础类封装
构建基础工具类SM4Util,封装核心操作方法:
public class SM4Util { private const int BlockSize = 16; // SM4分组长度(字节) private readonly bool _forEncryption; private readonly IBlockCipher _engine; public SM4Util(byte[] key, bool forEncryption, string mode = "ECB") { _engine = new SM4Engine(); _forEncryption = forEncryption; var keyParam = new KeyParameter(key); if (mode == "CBC") _engine = new CbcBlockCipher(_engine); _engine.Init(_forEncryption, keyParam); } }注意:实际项目中应将密钥存储在安全区域,避免硬编码
2. 密钥分散实现详解
2.1 分散算法原理
密钥分散(Diversify)是IC卡系统的关键安全机制,通过主密钥(MK)和分散因子生成衍生密钥(DK)。SM4标准分散流程包含三个核心步骤:
- 取分散因子前8字节作为输入数据左半部分
- 将分散因子前8字节按位取反作为右半部分
- 用MK对组合后的16字节数据进行SM4加密
2.2 C#实现代码
public static byte[] KeyDiversify(byte[] masterKey, byte[] diversifyData) { if (masterKey.Length != 16 || diversifyData.Length < 8) throw new ArgumentException("Invalid key or diversify data"); // 构造分散输入块 byte[] inputBlock = new byte[16]; Array.Copy(diversifyData, 0, inputBlock, 0, 8); // 左半部分 // 右半部分取反 for (int i = 0; i < 8; i++) inputBlock[8 + i] = (byte)~diversifyData[i]; // SM4加密处理 var sm4 = new SM4Util(masterKey, true); return sm4.Process(inputBlock); }典型应用场景示例:
byte[] masterKey = Encoding.ASCII.GetBytes("0123456789ABCDEF"); byte[] cardNo = Encoding.ASCII.GetBytes("62148501"); // 卡号作为分散因子 byte[] sessionKey = KeyDiversify(masterKey, cardNo);3. 数据加解密实战
3.1 填充方案选择
SM4作为分组密码需要处理数据填充,IC卡系统常用以下两种方式:
| 填充类型 | 规则 | 适用场景 |
|---|---|---|
| ISO7816-4 | 追加0x80后补0x00 | PBOC金融标准 |
| PKCS7 | 每个填充字节为填充长度值 | 通用系统 |
实现ISO7816-4填充的辅助方法:
private static byte[] Iso7816Pad(byte[] input) { int padLength = 16 - (input.Length % 16); byte[] output = new byte[input.Length + (padLength == 0 ? 16 : padLength)]; Array.Copy(input, output, input.Length); output[input.Length] = 0x80; return output; }3.2 完整加解密流程
封装支持ECB/CBC模式的加解密类:
public byte[] Process(byte[] input, byte[] iv = null) { if (_engine is CbcBlockCipher cbc) { if (iv == null || iv.Length != BlockSize) throw new ArgumentException("IV必须为16字节"); return ProcessCbc(input, iv); } return ProcessEcb(input); } private byte[] ProcessEcb(byte[] input) { byte[] output = new byte[input.Length]; for (int i = 0; i < input.Length; i += BlockSize) { _engine.ProcessBlock(input, i, output, i); } return output; } private byte[] ProcessCbc(byte[] input, byte[] iv) { byte[] output = new byte[input.Length]; byte[] block = new byte[BlockSize]; Array.Copy(iv, block, BlockSize); for (int i = 0; i < input.Length; i += BlockSize) { // CBC模式需要异或处理 for (int j = 0; j < BlockSize; j++) block[j] ^= input[i + j]; _engine.ProcessBlock(block, 0, output, i); Array.Copy(output, i, block, 0, BlockSize); } return output; }4. MAC计算关键实现
4.1 算法流程分解
IC卡MAC计算采用CBC模式,典型流程包含:
- 获取随机数并补全到16字节
- 数据分组处理
- 末尾追加0x80并补零
- 逐块进行CBC加密
- 取最终结果前4字节作为MAC值
4.2 代码实现与优化
public static byte[] CalculateMac(byte[] key, byte[] iv, byte[] input) { // 初始化向量处理 if (iv == null) iv = new byte[16]; else if (iv.Length != 16) iv = PadData(iv, 16); // 数据填充处理 byte[] paddedData = PadData(input, 16, true); // CBC模式加密 var sm4 = new SM4Util(key, true, "CBC"); byte[] result = sm4.Process(paddedData, iv); // 返回前4字节MAC byte[] mac = new byte[4]; Array.Copy(result, result.Length - 16, mac, 0, 4); return mac; } private static byte[] PadData(byte[] data, int blockSize, bool iso7816 = false) { int padLength = blockSize - (data.Length % blockSize); byte[] output = new byte[data.Length + (padLength == blockSize ? 0 : padLength)]; Array.Copy(data, output, data.Length); if (iso7816) { if (padLength > 0) { output[data.Length] = 0x80; for (int i = data.Length + 1; i < output.Length; i++) output[i] = 0x00; } } else { for (int i = data.Length; i < output.Length; i++) output[i] = 0x00; } return output; }5. 调试技巧与性能优化
5.1 常见问题排查
开发过程中可能遇到的典型问题及解决方案:
- 密钥长度错误:确保所有密钥均为16字节
- IV未初始化:CBC模式必须提供16字节初始化向量
- 填充不一致:加解密双方必须使用相同填充方案
- 字节序问题:处理多字节数据时注意大小端转换
5.2 性能优化建议
对于高频交易场景,可采用以下优化策略:
- 预初始化SM4引擎实例
- 重用密钥参数对象
- 使用ArrayPool减少内存分配
- 并行处理独立数据块
优化后的处理示例:
public class SM4OptimizedProcessor : IDisposable { private readonly SM4Engine _engine; private readonly KeyParameter _keyParam; public SM4OptimizedProcessor(byte[] key) { _engine = new SM4Engine(); _keyParam = new KeyParameter(key); } public void ProcessBlocks(Span<byte> input, Span<byte> output, bool encrypt) { _engine.Init(encrypt, _keyParam); for (int i = 0; i < input.Length; i += BlockSize) { _engine.ProcessBlock(input.Slice(i, BlockSize), output.Slice(i, BlockSize)); } } public void Dispose() => _engine.Reset(); }在金融IC卡项目实践中,我们发现密钥分散阶段的数据验证尤为关键。建议在调试时输出各中间步骤的十六进制值,比照标准测试向量进行验证。对于MAC计算,特别注意初始向量的处理方式可能因具体规范而异,交通部标准与金融PBOC3.0就存在细微差异。
