1 如何啟用加密功能
客戶端發起請求時添加請求Header頭,decrypted=true 即可啟用接口加密功能。
2 加密算法說明
該功能涉及到三種國密算法,分別為非對稱加密SM2、對稱加密SM4及特征碼算法Hmac-SM3。
2.1 非對稱加密SM2
類似RSA的非對稱加密,用來加密客戶生成的對稱加密SM4密鑰。SM2公鑰在AI能力開放平臺(//ai.daliqc.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即為加密請求體,遵循鑒權邏輯調用接口即可,鑒權部分邏輯請參考鑒權相關說明文檔