1 如何启用加密功能
客户端发起请求时添加请求Header头,decrypted=true 即可启用接口加密功能。
2 加密算法说明
该功能涉及到三种国密算法,分别为非对称加密SM2、对称加密SM4及特征码算法Hmac-SM3。
2.1 非对称加密SM2
类似RSA的非对称加密,用来加密客户生成的对称加密SM4密钥。SM2公钥在AI能力开放平台(https://ai.ctyun.cn/console)中我的应用中应用详情下获取,控制台显示内容为经过Base64加密后的公钥字符串。算法标准约定:SM2算法采用BC包(BouncyCastle),sm2p256v1标准,格式为PKCS8,拼接方式C1C3C2,非压缩格式。
2.2 对称加密SM4
客户端自行生成128或者256位SM4密钥,采用BC包(BouncyCastle),格式为SM4/ECB/PKCS5Padding。
2.3 特征码算法Hmac-SM3
Hmac和SM3算法配合生成特征码串,防止接口被拦截篡改,生成特征码时的密钥需使用。
encryptedHashKey字段传递到服务端。
2.4 客户端加密步骤
1、从控制台获取公钥串,使用Base64解密获取公钥字节数组。
2、生成SM4对称密钥字节数组,使用第1步的公钥加密对称密钥,并使用Base64加密结果,填入ciphertextBlob字段。
3、使用第2步生成的对称密钥加密接口原版Body体JSON串,并使用Base64加密结果,填入encryptedBody字段。
4、生成Hmac-sm3哈希密钥,使用公钥进行加密,并使用Base64加密结果,填encryptedHashKey字段。
5、使用Hmac-sm3及第4步哈希密钥,提取ciphertextBlob字段特征码并使用Base64加密结果,填入ciphertextBlobHash字段。
6、使用Hmac-sm3及第4步哈希密钥,提取encryptedBody字段特征码并使用Base64加密结果,填入encryptedBodyHash字段。
2.5 请求体结构
入参结构体如下,下列五个参数都需要经过Base64加密处理:
{
"ciphertextBlob":"使用SM2公钥加密后的客户生成SM4对称密钥",
"encryptedBody":"使用对称密钥SM4加密后的接口原版Body体JSON串,编码UTF-8",
"encryptedHashKey":"使用公钥加密后的进行hmac-sm3哈希时使用的密钥",
"ciphertextBlobHash":"ciphertextBlob字段的hmac-sm3结果",
"encryptedBodyHash":"encryptedBody字段的hmac-sm3结果"
}
2.6 返回体结构
若接口返回非0错误,则不进行加密,结构同未使用加密功能时一致,客户端可根据返回体是否包含statusCode进行区分。
若接口请求成功且返回statusCode为0,则会进行加密处理,返回值如下,下列两个参数都经过Base64加密处理:
{
"encryptedResultHash": "encryptedResult字段的hmac-sm3结果,哈希密钥与客户端一致",
"encryptedResult": "SM4对称密钥加密后的接口响应结果"
}
客户端获取返回值后可选择性校验encryptedResultHash确保请求结果未被篡改。
获取encryptedResult字段后,使用请求时的sm4对称密钥进行解密,即可获取到未启用加密时接口的正常返回体内容(JSON串),编码UTF8。
2.7 加解密相关错误码说明
encryptedResult字段通过SM4对称密钥解密后可获取到不启用加密功能格式的返回值,相比未使用加密功能的请求方式,会有以下几种特殊错误码:
错误码 | 错误信息 | 错误描述 |
---|---|---|
AI_OP_40017 | 加密参数不符合要求 | 入参格式不符合加密功能要求 |
AI_OP_40018 | ciphertextBlob哈希值不匹配/encryptedBody哈希值不匹配 | 相关字段哈希值不匹配,存在被篡改可能 |
AI_OP_40019 | SM2解密失败 | SM2非对称解密出现异常 |
AI_OP_40020 | SM4加密失败/SM4解密失败 | SM4对称密钥加解密出现异常 |
3 参考代码(JAVA,jdk1.8及以上)
3.1 maven工程引入以下依赖
<!-- BC包,若jdk为1.8以下可替换对应版本及artifactId-->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<!--Base64功能包,推荐使用此包内Base64类进行加解密,生成加密结果不能包含换行-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.17.1</version>
</dependency>
3.2 SM2工具类
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
public class SM2Util {
static {
Security.addProvider(new BouncyCastleProvider());
}
// 生成SM2密钥对
public static KeyPair generateKeyPair() {
try{
KeyPairGenerator keyPairGenerator =
KeyPairGenerator.getInstance("EC", "BC");
keyPairGenerator.initialize(new ECGenParameterSpec("sm2p256v1"));
return keyPairGenerator.generateKeyPair();
} catch (Exception e) {
//TODO 做异常处理
}
}
// 加密
public static byte[] encrypt(byte[] publicKey, byte[] data) {
try{
PublicKey pubKey = KeyFactory.getInstance("EC", "BC")
.generatePublic(new X509EncodedKeySpec(publicKey));
Cipher cipher = Cipher.getInstance("SM2", "BC");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
return cipher.doFinal(data);
} catch (Exception e) {
//TODO 做异常处理
}
}
// 解密
public static byte[] decrypt(byte[] privateKey, byte[] encryptedData) {
try{
PrivateKey priKey = KeyFactory.getInstance("EC", "BC")
.generatePrivate(new PKCS8EncodedKeySpec(privateKey));
Cipher cipher = Cipher.getInstance("SM2", "BC");
cipher.init(Cipher.DECRYPT_MODE, priKey);
return cipher.doFinal(encryptedData);
} catch (Exception e) {
//TODO 做异常处理
}
}
public static void main(String[] args) {
KeyPair keyPair = generateKeyPair();
byte[] a = keyPair.getPublic().getEncoded();
byte[] b = keyPair.getPrivate().getEncoded();
}
}
3.3 SM4工具类
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.Security;
public class SM4Util {
static {
Security.addProvider(new BouncyCastleProvider());
}
// 生成SM4密钥
public static byte[] generateKey() {
try{
KeyGenerator keyGenerator = KeyGenerator.getInstance("SM4", "BC");
keyGenerator.init(128); // 可选 128 或 256
SecretKey secretKey = keyGenerator.generateKey();
return secretKey.getEncoded();
} catch (Exception e) {
//TODO 处理异常
}
}
// 加密
public static byte[] encrypt(byte[] keyBytes, byte[] data) {
try{
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "SM4");
Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
return cipher.doFinal(data);
} catch (Exception e) {
//TODO 处理异常
}
}
// 解密
public static byte[] decrypt(byte[] keyBytes, byte[] encryptedData) {
try{
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "SM4");
Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return cipher.doFinal(encryptedData);
} catch (Exception e) {
//TODO 处理异常
}
}
}
3.4 Hmac-sm3工具类
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import java.security.SecureRandom;
public class HmacSM3Util {
/**
* 计算 HMAC-SM3
*
* @param key 密钥
* @param data 数据
* @return HMAC-SM3 值
*/
public static byte[] hmacSM3(byte[] key, byte[] data) {
HMac hmac = new HMac(new SM3Digest());
hmac.init(new KeyParameter(key));
hmac.update(data, 0, data.length);
byte[] result = new byte[hmac.getMacSize()];
hmac.doFinal(result, 0);
return result;
}
/**
* 生成指定长度的随机密钥
*
* @param length 密钥长度(字节)
* @return 随机生成的密钥
*/
public static byte[] generateRandomKey(int length) {
SecureRandom random = new SecureRandom();
byte[] key = new byte[length];
random.nextBytes(key);
return key;
}
// 示例用法
public static void main(String[] args) {
// 生成随机密钥
byte[] key = generateRandomKey(16); // 16字节长度的随机密钥
// 待计算的数据
byte[] data = "Hello, HMAC-SM3!".getBytes();
// 计算 HMAC-SM3 值
byte[] hmacValue = hmacSM3(key, data);
// 打印结果
System.out.println("HMAC-SM3 值: " + Base64.encodeBase64String(hmacValue));
}
}
3.5 生成请求体
//未启用加密功能时的请求体json串,查询对应接口文档确定格式
String body = "{\"ImageData\":\"imagedatabase64xxxx\"}";
//控制台-应用详情下获取的公钥
String encKey = "xxxxxxx";
Map<String,String> requestBody = new HashMap<>();
//公钥转换为字节数组备用
byte[] publicKey = Base64.decodeBase64(encKey);
//生成ciphertextBlob字段
byte[] sm4Key = SM4Util.generateKey();
requestBody.put("ciphertextBlob", Base64.encodeBase64String(SM2Util.encrypt(publicKey, sm4Key)));
//生成encryptedBody字段
requestBody.put("encryptedBody", Base64.encodeBase64String(SM4Util.encrypt(sm4Key, body.getBytes())));
//生成Hmac-sm3密钥
byte[] hmacSm3Key = HmacSM3Util.generateRandomKey(16);
//生成encryptedHashKey字段
requestBody.put("encryptedHashKey", Base64.encodeBase64String(SM2Util.encrypt(publicKey, hmacSm3Key)));
//生成ciphertextBlobHash字段
requestBody.put("ciphertextBlobHash", Base64.encodeBase64String(HmacSM3Util.hmacSM3(hmacSm3Key, requestBody.get("ciphertextBlob").getBytes())));
//生成encryptedBodyHash字段
requestBody.put("encryptedBodyHash", Base64.encodeBase64String(HmacSM3Util.hmacSM3(hmacSm3Key, requestBody.get("encryptedBody").getBytes())));
//TODO requestBody即为加密请求体,遵循鉴权逻辑调用接口即可,鉴权部分逻辑请参考鉴权相关说明文档