国密算法

简介

国密算法,即国家商用密码算法。是由国家密码管理局认定和公布的密码算法标准及其应用规范,其中部分密码算法已经成为国际标准。

  • SM1:是一种分组加密算法,算法不公开,仅允许通过硬件方式使用。
  • SM2:是一种非对称加密算法,基于椭圆曲线密码的公钥密码算法标准。用于替换 RSA 等国际算法。
  • SM3:是一种密码杂凑算法,用于替代MD5/SHA-1/SHA-2等国际算法。基于 SHA256 改进的。
  • SM4:是一种分组加密算法,算法公开,用于替换 DES/AES 等国际算法。

其他还有 SM7 和 SM9 这里不做介绍。常用有 SM2 和 SM4。

下面给出两种语言的实现和使用。

相关依赖

在 Go 语言中,使用该库 https://github.com/tjfoc/gmsm

在 Java 语言中,使用如下库:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk18on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.79</version>
</dependency>

实现

在 Go 语言的依赖库中,SM4 的封装使用较为方便,所以不在进一步封装。Java 语言中则对两种算法都进行了封装使用。以下是代码示例。

⚠️注意:针对证书本文使用 PEM 格式编码。并且指定了标题等信息,不喜欢可自行使用其他方式。

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package sm2x

import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"fmt"
"github.com/tjfoc/gmsm/sm2"
"github.com/tjfoc/gmsm/x509"
)

type CertDataFrom int

const (
FromBase64 CertDataFrom = iota
FromPEM
FromHex
)

// GenerateSM2PEMKeyPair 生成 sm2 证书对
// 使用 pem 格式存储
func GenerateSM2PEMKeyPair() (privateKey, publicKey string, err error) {
privateKeyObj, err := sm2.GenerateKey(rand.Reader)
if err != nil {
return "", "", err
}

publicKeyObj := &privateKeyObj.PublicKey

// 序列化私钥证书转为 DER 编码字节流
// 注意这里使用的是 pkcs8 无加密方式序列化,解析时必须使用 pkcs8 无加密方式反序列化 DER 数据
privateKeyBytes, err := x509.MarshalSm2UnecryptedPrivateKey(privateKeyObj)
if err != nil {
return "", "", err
}

// 使用 pem 格式编码
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "SM2 PRIVATE KEY",
Bytes: privateKeyBytes,
})

// 序列化公钥证书转为 DER 编码字节流
publicKeyBytes, err := x509.MarshalSm2PublicKey(publicKeyObj)
if err != nil {
return "", "", err
}
// 使用 pem 格式编码
publicKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "SM2 PUBLIC KEY",
Bytes: publicKeyBytes,
})
return string(privateKeyPEM), string(publicKeyPEM), nil
}

// NewSM2PrivateKey 加载私钥
func NewSM2PrivateKey(data string, from CertDataFrom) (*sm2.PrivateKey, error) {
// 解析出来的证书数据为 DER 数据
keyBytes, err := decodeCertData(data, from)
if err != nil {
return nil, err
}
// 本模块在创建证书时,使用的无加密 pkcs8 方式,这里使用对应的方式解密
return x509.ParsePKCS8UnecryptedPrivateKey(keyBytes)
}

// NewSM2PublicKey 加载公钥
func NewSM2PublicKey(data string, from CertDataFrom) (*sm2.PublicKey, error) {
keyBytes, err := decodeCertData(data, from)
if err != nil {
return nil, err
}
return x509.ParseSm2PublicKey(keyBytes)
}

// decodeCertData 解码证书数据
// 还原各种途径的证书内容为 DER 数据
func decodeCertData(data string, from CertDataFrom) ([]byte, error) {
var keyBytes []byte
var err error

switch from {
case FromBase64:
keyBytes, err = base64.StdEncoding.DecodeString(data)
case FromPEM:
block, _ := pem.Decode([]byte(data))
if block == nil {
return nil, fmt.Errorf("pem 解码证书失败,请传入正确的 pem 证书内容")
}
keyBytes = block.Bytes
case FromHex:
keyBytes, err = hex.DecodeString(data)
default:
return nil, fmt.Errorf("不支持的证书数据源")
}

if err != nil {
return nil, fmt.Errorf("解码证书失败: %v", err)
}

return keyBytes, nil
}

// Encrypt 加密数据
// 使用 公钥 加密数据,模式为默认的 C1C3C2
func Encrypt(data []byte, publicKey *sm2.PublicKey) ([]byte, error) {
return sm2.Encrypt(publicKey, data, rand.Reader, sm2.C1C3C2)
}

// Decrypt 解密数据
// 使用 私钥 解密数据,模式为默认的 C1C3C2
func Decrypt(data []byte, privateKey *sm2.PrivateKey) ([]byte, error) {
// mod sm2.C1C3C2 为默认
return sm2.Decrypt(privateKey, data, sm2.C1C3C2)
}

// Sign 签名数据
func Sign(data []byte, privateKey *sm2.PrivateKey) ([]byte, error) {
return privateKey.Sign(rand.Reader, data, nil)
}

// SignVerify 签名验证(验签)
func SignVerify(data []byte, publicKey *sm2.PublicKey, signature []byte) bool {
return publicKey.Verify(data, signature)
}

Java

首先是 SM4 的简单封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package org.example;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.Security;

public class SM4X {

private static final String ALGORITHM_NAME = "SM4";
private static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";

static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}

// 生成 Cipher 指定使用算法
private static Cipher generateEcbCipher(int mode, byte[] key) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM_NAME_ECB_PADDING, BouncyCastleProvider.PROVIDER_NAME);
Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME);
cipher.init(mode, sm4Key);
return cipher;
}

// 解密
public static byte[] decrypt(byte[] key, byte[] data) throws Exception {
Cipher cipher = generateEcbCipher(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(data);
}

// 加密
public static byte[] encrypt(byte[] key, byte[] data) throws Exception {
Cipher cipher = generateEcbCipher(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(data);
}

public static void test() throws Exception {
String key = "WW6WWmli8DFppdAc";
String data = "Hello World";
byte[] encryptData = SM4X.encrypt(key.getBytes(), data.getBytes());
String encryptDataHex = DatatypeConverter.printHexBinary(encryptData);
System.out.println("加密之后(hex): " + encryptDataHex);
byte[] decryptData = SM4X.decrypt(key.getBytes(), encryptData);
System.out.println("解密之后: " + new String(decryptData, StandardCharsets.UTF_8));
}
}

下面是 SM2 的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
package org.example;

import org.bouncycastle.asn1.gm.GMObjectIdentifiers;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.xml.bind.DatatypeConverter;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class SM2X {
private PrivateKey privateKey;
private PublicKey publicKey;

private static final String EC_STD_NAME = "sm2p256v1";
private static final String privatePemTitle = "SM2 PRIVATE KEY";
private static final String publicPemTitle = "SM2 PUBLIC KEY";

// 注册 BouncyCastle 提供者
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}

// 获取公钥证书的 pem 编码内容
public String getPublicKeyPem() {
return encodeToPem(publicKey, publicPemTitle);
}

// 获取私钥证书的 pem 编码内容
public String getPrivateKeyPem() {
return encodeToPem(privateKey, privatePemTitle);
}

// 设置公钥
public void setPublicKey(PublicKey key) {
this.publicKey = key;
}

// 设置私钥
public void setPrivateKey(PrivateKey key) {
this.privateKey = key;
}

// 从公钥 pem 证书内容中加载公钥
public void setPublicKeyPem(String pem) {
this.publicKey = decodePublicKeyFromPem(pem);
}

// 从私钥 pem 证书内容中加载私钥
public void setPrivateKeyPem(String pem) {
this.privateKey = decodePrivateKeyFromPem(pem);
}

// 解码 PEM 格式的公钥
private PublicKey decodePublicKeyFromPem(String pem) throws IllegalArgumentException {
byte[] der = decodeFromPem(pem, publicPemTitle);
try {
// 使用 KeyFactory 和 X509EncodedKeySpec 还原公钥
KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(der);
return keyFactory.generatePublic(keySpec);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to decode public key from PEM", e);
}
}

// 解码 PEM 格式的私钥
private PrivateKey decodePrivateKeyFromPem(String pem) throws IllegalArgumentException {
byte[] der = decodeFromPem(pem, privatePemTitle);
try {
// 使用 KeyFactory 和 PKCS8EncodedKeySpec 还原私钥
KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(der);
return keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to decode private key from PEM", e);
}
}

// 编码证书的 DER 数据为 pem 格式
private String encodeToPem(Key key, String keyType) {
// Base64 编码 DER 数据
// key.getEncoded() 默认就是 pkcs#8 无加密方式序列化证书
String base64Str = Base64.getEncoder().encodeToString(key.getEncoded());

// 每 64 个字符换行
base64Str = base64Str.replaceAll("(.{64})", "$1\n");

// 添加 PEM 头部和尾部
String startTitle = "-----BEGIN " + keyType + "-----\n";
String endTitle = "-----END " + keyType + "-----\n";
return startTitle + base64Str + "\n" + endTitle;
}

// 解码 pem 证书返回 DER 数据
// DER 数据为 pkcs#8 无加密
private byte[] decodeFromPem(String pem, String keyType) {

if (pem == null || keyType == null) {
throw new IllegalArgumentException("PEM string and key type cannot be null");
}

// 移除 PEM 文件中的头部和尾部标记
String startTitle = "-----BEGIN " + keyType + "-----";
String endTitle = "-----END " + keyType + "-----";

if (!pem.contains(startTitle) || !pem.contains(endTitle)) {
throw new IllegalArgumentException("Invalid PEM format: missing BEGIN or END tag");
}

String base64Content = pem.replace(startTitle, "")
.replace(endTitle, "")
.replaceAll("\\s", ""); // 移除所有空白字符

// Base64 解码
return Base64.getDecoder().decode(base64Content);
}

// 生成证书对
public static KeyPair generate() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
final ECGenParameterSpec sm2Spec = new ECGenParameterSpec(EC_STD_NAME);
// 获取一个椭圆曲线类型的密钥对生成器
final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
// 产生随机数
SecureRandom secureRandom = new SecureRandom();
// 使用SM2参数初始化生成器
kpg.initialize(sm2Spec, secureRandom);
// 返回密钥对
return kpg.generateKeyPair();
}

// 加密数据
public byte[] encrypt(byte[] data) throws Exception {
if (data == null || data.length == 0) {
throw new IllegalArgumentException("Data cannot be null or empty");
}
ParametersWithRandom publicKeyParameters = new ParametersWithRandom(
ECUtil.generatePublicKeyParameter(this.publicKey),
new SecureRandom()
);
SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
sm2Engine.init(true, publicKeyParameters);
return sm2Engine.processBlock(data, 0, data.length);
}

// 解密数据
public byte[] decrypt(byte[] data) throws Exception {
if (data == null || data.length == 0) {
throw new IllegalArgumentException("Data cannot be null or empty");
}
AsymmetricKeyParameter privateKeyParameter = ECUtil.generatePrivateKeyParameter(this.privateKey);
SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
sm2Engine.init(false, privateKeyParameter);
return sm2Engine.processBlock(data, 0, data.length);
}

// 签名
public byte[] sign(byte[] data) throws Exception {
if (data == null || data.length == 0) {
throw new IllegalArgumentException("Data cannot be null or empty");
}
Signature sig = Signature.getInstance(
GMObjectIdentifiers.sm2sign_with_sm3.toString(),
BouncyCastleProvider.PROVIDER_NAME
);
sig.initSign(this.privateKey);
sig.update(data);
return sig.sign();
}

// 验签
public boolean signVerify(byte[] data, byte[] signature) throws Exception {
if (data == null || data.length == 0) {
throw new IllegalArgumentException("Data cannot be null or empty");
}
Signature sig = Signature.getInstance(
GMObjectIdentifiers.sm2sign_with_sm3.toString(),
BouncyCastleProvider.PROVIDER_NAME
);
sig.initVerify(this.publicKey);
sig.update(data);
return sig.verify(signature);
}

// 测试加密是否正常
public static void test() throws Exception {
// 测试证书对由 Golang 生成, pkcs8 无加密序列化 pem 编码
// 加密模式为 C1C3C2
String sm2PrivateKeyPem = "-----BEGIN SM2 PRIVATE KEY-----\n" +
"MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgVAicT/TL156MhtxJ\n" +
"UUxYmSMrqCLWJl2QEGF6GAPoA86gCgYIKoEcz1UBgi2hRANCAARFldvKuEOl3cqG\n" +
"uUUD98xKkCKhj6OML+n7/EJT1GzVhJzejJSlnn8PtH8jRcHr1S8QkxAYAATOtwO/\n" +
"ROmDFHUW\n" +
"-----END SM2 PRIVATE KEY-----";
String sm2PublicKeyPem = "-----BEGIN SM2 PUBLIC KEY-----\n" +
"MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAERZXbyrhDpd3KhrlFA/fMSpAioY+j\n" +
"jC/p+/xCU9Rs1YSc3oyUpZ5/D7R/I0XB69UvEJMQGAAEzrcDv0TpgxR1Fg==\n" +
"-----END SM2 PUBLIC KEY-----";
// 证书加载测试
SM2X sm2Obj = new SM2X();
sm2Obj.setPrivateKeyPem(sm2PrivateKeyPem);
sm2Obj.setPublicKeyPem(sm2PublicKeyPem);

// 加密测试
String data = "Hello World";
System.out.println("原始数据: " + data);
byte[] encryptData = sm2Obj.encrypt(data.getBytes());
String encryptDataHex = DatatypeConverter.printHexBinary(encryptData);
System.out.println("加密之后(hex): " + encryptDataHex);
byte[] decryptData = sm2Obj.decrypt(encryptData);
System.out.println("解密之后: " + new String(decryptData, StandardCharsets.UTF_8));

// 签名测试
byte[] signData = sm2Obj.sign(data.getBytes());
System.out.println("签名数据(hex): " + DatatypeConverter.printHexBinary(signData));
if (sm2Obj.signVerify(data.getBytes(), signData)) {
System.out.println("签名验证成功");
} else {
System.out.println("签名验证失败");
}

// 证书生成测试
KeyPair keyPair = SM2X.generate();
SM2X newObj = new SM2X();
newObj.setPrivateKey(keyPair.getPrivate());
newObj.setPublicKey(keyPair.getPublic());

System.out.println(newObj.getPrivateKeyPem());
System.out.println(newObj.getPublicKeyPem());

// 测试 golang 加密,这里解密
// 加密值为: Hello World
String golangEncryptDataHex = "040e6d178f4c2e64768301a52c34d555887852c5ff2a3d05c2f385e3fdc5e098a1b12896d353dba517b125e9d27cd061cf93d6728ffe54e9f25119e7d54e0204f8494f4898545c28d6440b1cfdd46b4ecf061ce63eb8800be34621c0670c0e9d2a65a302cc268faefeb44271";
byte[] golangEncryptData = DatatypeConverter.parseHexBinary(golangEncryptDataHex);
byte[] golangDecryptData = sm2Obj.decrypt(golangEncryptData);
System.out.println("Golang 数据解密之后: " + new String(golangDecryptData, StandardCharsets.UTF_8));
}
}