一、前言

随着中美摩擦日益严重,美国对中国的技术封锁越来越激烈。在很多领域国产化替代方案必须提前布局,包括在一些基础领域也要提前考虑。一直在做运营商的项目,今年开始要进行国产化密码改造,为了公司的项目能平滑进行过度,结合前期 《前后端加解密经验分享前后端加解密经验分享》 演进,这里我进行了国产化的方案验证,本文重点使用了国产对称加密算法SM4。

二、前端加密工具

2.1 js获取

前端使用vue,可以用命令“npm install --save sm-crypto”进行安装,详细可以参考博客 《国密算法sm2、sm3和sm4的js版 及 IE兼容处理》 。这里为了快速验证,本地直接下载了sm4的js,下载路径 https://registry.npmjs.org/sm-crypto/-/sm-crypto-0.3.12.tgz 。下载完成后直接解压,为了自定义密钥和iv,我们使用了开发版本的js,具体见压缩文件内 [package\src\sm4\index.js],如果项目使用压缩版进行加工也是也可以的,压缩版的路径为 [package\dist\sm4.js]。

2.2 js扩展

这里为了程序方便,我们把 [package\src\sm4\index.js] 拷贝到项目中,直接修改名字sm4.js,我们在文件最后 "module.exports" 中增加代码如下:

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
module.exports = {
encrypt(inArray, key, options) {
return sm4(inArray, key, 1, options)
},
decrypt(inArray, key, options) {
return sm4(inArray, key, 0, options)
},
encryptByMode(inArray,mode) {
let key = '4c2f0a5bba948aeb862167ab952aeaac'
let iv = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
//mode 1:EBC 0:CBC
if(mode === 1){
return sm4(inArray, key, 1)
}else{
return sm4(inArray, key, 1, {mode:'cbc',iv: iv})
}
},
decryptByMode(inArray,mode) {
let key = '4c2f0a5bba948aeb862167ab952aeaac'
let iv = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
if(mode === 1){
return sm4(inArray, key, 0)
}else{
return sm4(inArray, key, 0, {mode:'cbc',iv: iv})
}
}
}

这里我们主要增加了 encryptByMode(inArray,mode) ,decryptByMode(inArray,mode) 这两个方法,方便程序使用。第一个参数 inArray 为需要处理的明文或者密文,第二个参数 mode 为目前加解密模式,0代表CBC,1代表EBC模式。注意这里的key和iv要和后端Java的对应。

三、后端加密工具

3.1 引入依赖

1
2
3
4
5
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.56</version>
</dependency>

3.2 sm4工具类

Sm4Util.java

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
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
import java.security.AlgorithmParameters;
import java.security.Key;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import org.bouncycastle.util.encoders.Hex;

public class Sm4Util {

static {
Security.addProvider(new BouncyCastleProvider());
}

private static final String ENCODING = "UTF-8";
public static final String ALGORITHM_NAME = "SM4";
// 加密算法/分组加密模式/分组填充方式
// PKCS5Padding-以8个字节为一组进行分组加密
// 定义分组加密模式使用:PKCS5Padding
public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";
// 定义分组加密模式使用:PKCS5Padding
public static final String ALGORITHM_NAME_CBC_PADDING = "SM4/CBC/PKCS5Padding";
// 128-32位16进制;256-64位16进制
public static final int DEFAULT_KEY_SIZE = 128;

/**
* 生成ECB暗号
*
* @param algorithmName 算法名称
* @param mode 模式
* @param key
* @return
* @throws Exception
* @explain ECB模式(电子密码本模式:Electronic codebook)
*/
private static Cipher generateEcbCipher(String algorithmName, int mode, byte[] key) throws Exception {
Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME);
cipher.init(mode, sm4Key);
return cipher;
}

/**
* 自动生成密钥
*
* @return
* @throws NoSuchAlgorithmException
* @throws NoSuchProviderException
* @explain
*/
public static String generateKey() throws Exception {
return new String(Hex.encode(generateKey(DEFAULT_KEY_SIZE)));
}

/**
* @param keySize
* @return
* @throws Exception
* @explain
*/
public static byte[] generateKey(int keySize) throws Exception {
KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
kg.init(keySize, new SecureRandom());
return kg.generateKey().getEncoded();
}

/**
* sm4加密
*
* @param hexKey 16进制密钥(忽略大小写)
* @param paramStr 待加密字符串
* @return 返回16进制的加密字符串
* @throws Exception
* @explain 加密模式:ECB
* 密文长度不固定,会随着被加密字符串长度的变化而变化
*/
public static String encryptEcb(String hexKey, String paramStr) throws Exception {
String cipherText = "";
// 16进制字符串-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// String-->byte[]
byte[] srcData = paramStr.getBytes(ENCODING);
// 加密后的数组
byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
// byte[]-->hexString
cipherText = ByteUtils.toHexString(cipherArray);
return cipherText;
}

/**
* 加密模式之Ecb
*
* @param key
* @param data
* @return
* @throws Exception
* @explain
*/
public static byte[] encrypt_Ecb_Padding(byte[] key, byte[] data) throws Exception {
Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(data);
}

/**
* sm4解密
*
* @param hexKey 16进制密钥
* @param cipherText 16进制的加密字符串(忽略大小写)
* @return 解密后的字符串
* @throws Exception
* @explain 解密模式:采用ECB
*/
public static String decryptEcb(String hexKey, String cipherText) throws Exception {
// 用于接收解密后的字符串
String decryptStr = "";
// hexString-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// hexString-->byte[]
byte[] cipherData = ByteUtils.fromHexString(cipherText);
// 解密
byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData);
// byte[]-->String
decryptStr = new String(srcData, ENCODING);
return decryptStr;
}

/**
* 解密
*
* @param key
* @param cipherText
* @return
* @throws Exception
*/
public static byte[] decrypt_Ecb_Padding(byte[] key, byte[] cipherText) throws Exception {
Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key);
return cipher.doFinal(cipherText);
}

/**
* 校验加密前后的字符串是否为同一数据
*
* @param hexKey 16进制密钥(忽略大小写)
* @param cipherText 16进制加密后的字符串
* @param paramStr 加密前的字符串
* @return 是否为同一数据
* @throws Exception
*/
public static boolean verifyEcb(String hexKey, String cipherText, String paramStr) throws Exception {
// 用于接收校验结果
boolean flag = false;
// hexString-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// 将16进制字符串转换成数组
byte[] cipherData = ByteUtils.fromHexString(cipherText);
// 解密
byte[] decryptData = decrypt_Ecb_Padding(keyData, cipherData);
// 将原字符串转换成byte[]
byte[] srcData = paramStr.getBytes(ENCODING);
// 判断2个数组是否一致
flag = Arrays.equals(decryptData, srcData);
return flag;
}

/**
* sm4加密
*
* @param hexKey 16进制密钥(忽略大小写)
* @param paramStr 待加密字符串
* @return 返回16进制的加密字符串
* @throws Exception
* @explain 加密模式:CBC
*/
public static String encrypt(String hexKey, byte[] iv, String paramStr) throws Exception {
String result = "";
// 16进制字符串-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// String-->byte[]
byte[] srcData = paramStr.getBytes(ENCODING);
// 加密后的数组
byte[] cipherArray = encryptCbcPadding(keyData, srcData, iv);

// byte[]-->hexString
result = ByteUtils.toHexString(cipherArray);
return result;
}

/**
* 加密模式之CBC
*
* @param key
* @param data
* @return
* @throws Exception
* @explain
*/
public static byte[] encryptCbcPadding(byte[] key, byte[] data, byte[] iv) throws Exception {
Cipher cipher = generateCbcCipher(ALGORITHM_NAME_CBC_PADDING, Cipher.ENCRYPT_MODE, key, iv);
return cipher.doFinal(data);
}

/**
* 加密模式之CBC
* @param algorithmName
* @param mode
* @param key
* @return
* @throws Exception
*/
private static Cipher generateCbcCipher(String algorithmName, int mode, byte[] key,byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME);
cipher.init(mode, sm4Key, generateIV(iv));
return cipher;
}

/**
* 生成iv
* @return
* @throws Exception
*/
public static AlgorithmParameters generateIV(byte[] iv) throws Exception {
//iv 为一个 16 字节的数组,这里采用和 iOS 端一样的构造方法,数据全为0
AlgorithmParameters params = AlgorithmParameters.getInstance(ALGORITHM_NAME);
params.init(new IvParameterSpec(iv));
return params;
}

/**
* sm4解密
*
* @param hexKey 16进制密钥
* @param text 16进制的加密字符串(忽略大小写)
* @return 解密后的字符串
* @throws Exception
* @explain 解密模式:采用CBC
*/
public static String decrypt(String hexKey, byte[] iv, String text) throws Exception {
// 用于接收解密后的字符串
String result = "";
// hexString-->byte[]
byte[] keyData = ByteUtils.fromHexString(hexKey);
// hexString-->byte[]
byte[] resultData = ByteUtils.fromHexString(text);
// 解密
byte[] srcData = decryptCbcPadding(keyData, iv, resultData);
// byte[]-->String
result = new String(srcData, ENCODING);
return result;
}

/**
* 解密
*
* @param key
* @param cipherText
* @return
* @throws Exception
* @explain
*/
public static byte[] decryptCbcPadding(byte[] key, byte[] iv, byte[] cipherText) throws Exception {
Cipher cipher = generateCbcCipher(ALGORITHM_NAME_CBC_PADDING, Cipher.DECRYPT_MODE, key, iv);
return cipher.doFinal(cipherText);
}

/**
* sm4加密
*
* @param text 明文
* @param mode 加密方式
* @return 返回16进制的加密字符串
* @throws Exception
* @explain 加密模式:加密方式 1:EBC 0:CBC
* 密文长度不固定,会随着被加密字符串长度的变化而变化
*/
public static String encryptByMode(String text, int mode) throws Exception {
String key = "4c2f0a5bba948aeb862167ab952aeaac";
byte[] iv = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
//mode 1:EBC 0:CBC
if(mode == 1) {
return encryptEcb(key, text);
}else {
return encrypt(key, iv, text);
}

}

/**
* sm4解密
*
* @param text 密文
* @param mode 解密方式
* @return 解密后的字符串
* @throws Exception
* @explain 加密模式:加密方式 1:EBC 0:CBC
*/
public static String decryptByMode(String text, int mode) throws Exception {
String key = "4c2f0a5bba948aeb862167ab952aeaac";
byte[] iv = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
if(mode == 1) {
return decryptEcb(key, text);
}else {
return decrypt(key, iv, text);
}
}

}

四、测试

Java加密使用: Sm4Util.encryptByMode(String text, int mode);
Java解密使用: Sm4Util.decryptByMode(String text, int mode);

Js加密使用: sm4.encryptByMode(inArray,mode);
Js解密使用: sm4.decryptByMode(inArray,mode);