由于开放平台“快速接入”产品API的接口都是在互联网进行访问,这意味着接口调用方在调用接口时上送的请求参数都将暴露在互联网,虽然开放平台接口使用了“HTTPS”协议保证在传输层对网络连接进行加密,但接口调用方仍面临着在接口调用中敏感参数泄露的风险(例如身份证号、手机号、银行卡号等)。基于安全考虑,开放平台制定了请求报文体参数加密规则。
对于备注中有特殊说明的请求报文体参数,在进行接口调用时,传递的值应该是加密后的信息,如果传递加密后的信息为空或未传递该参数,接口将返回错误应答码(respCd):9918,应答信息(respMsg)为“数据解密失败,解密值为空”。
目前请求报文体参数加密规则支持RSA非对称加密算法、AES对称加密算法和3DES对称加密算法。用户根据请求报文体参数字段对应的加密算法对参数进行加密,在接口调用时,开放平台会对其进行解密,获取真实参数。通过这样的方式可以保证用户在调用接口时传递的请求报文体参数不包含明文的敏感信息,降低信息泄露的安全风险。
对于备注中说明需RSA加密的请求报文体参数,用户可以登录开放平台后,查看“我的信息”页面中的RSA公钥,使用该公钥对参数进行加密。在接口调用时,开放平台会使用该用户公钥所对应的的私钥对请求参数进行解密,获取真实参数。
可进入后台“我的信息”查看,页面地址:
https://open.unionpay.com/tjweb/user/info
RSA加密示例代码如下:
package com.unionpay.upapi.client.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
public class RSAUtils {
/**
* 加密算法RSA
*/
private static final String RSA_ALGORITHM = "RSA";
private static final String RSA_ECB_OAEP_PADDING = "RSA/ECB/OAEPPadding";
private static final String BC_PROVIDER = "BC";
/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 214;
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* @param data 源数据
* @param publicKey 公钥(BASE64编码)
* @return 加密后byte数组
* @throws Exception 异常
*/
private static byte[] encryptByPublicKey2048(byte[] data, String publicKey) throws Exception {
byte[] keyBytes = Base64.decodeBase64(publicKey);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM, BC_PROVIDER);
Key publicK = keyFactory.generatePublic(x509KeySpec);
// 对数据加密
Cipher cipher = Cipher.getInstance(RSA_ECB_OAEP_PADDING, BC_PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, publicK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}
public static void main(String[] args) {
try {
String publicKeyStr = "publicKey";
String jsonStr = "{\"key1\":\"value1\",\"key2\":\"value2\"}";
JSONObject jsonObj = JSON.parseObject(jsonStr);
// 仅对key1的值value1进行加密
String oldValue = jsonObj.getString("key1");
byte[] encryped = RSAUtils.encryptByPublicKey2048(oldValue.getBytes(), publicKeyStr);
jsonObj.put("key1", new String(Base64.encodeBase64(encryped)));
} catch (Exception e) {
e.printStackTrace();
}
}
}
对于备注中说明需AES加密的请求报文体参数,用户可以登录开放平台后,查看“我的信息”页面中的AES密钥,使用该密钥对参数进行加密。在接口调用时,开放平台会使用该密钥对请求参数进行解密,获取真实参数。
可进入后台“我的信息”查看,页面地址:
https://open.unionpay.com/tjweb/user/info
AES加密示例代码如下:
package com.unionpay.upapi.client.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class EncryptAESUtils {
/**
* 加密算法AES
*/
public static String encryptAES(String value, String key) throws Exception {
if (null == value || "".equals(value)) {
return "";
}
byte[] valueByte = value.getBytes();
byte[] sl = encryptAES(valueByte, EncryptAESUtils.hexToBytes(key));
String result = Base64.encodeBase64String(sl);
return result;
}
public static byte[] encryptAES(byte[] input, byte[] key) throws Exception {
Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding");
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"));
return c.doFinal(input);
}
/**
* 将16进制的字符串转换成bytes
*
* @param hex
* @return 转化后的byte数组
*/
public static byte[] hexToBytes(String hex) {
return hexToBytes(hex.toCharArray());
}
/**
* 将16进制的字符数组转换成byte数组
*
* @param hex
* @return 转换后的byte数组
*/
public static byte[] hexToBytes(char[] hex) {
int length = hex.length / 2;
byte[] raw = new byte[length];
for (int i = 0; i < length; i++) {
int high = Character.digit(hex[i * 2], 16);
int low = Character.digit(hex[i * 2 + 1], 16);
int value = (high << 4) | low;
if (value > 127) {
value -= 256;
}
raw[i] = (byte) value;
}
return raw;
}
public static void main(String[] args) {
try {
String key_AES = "AESKey";
String jsonStr = "{\"key1\":\"value1\",\"key2\":\"value2\"}";
JSONObject jsonObj = JSON.parseObject(jsonStr);
// 仅对key1的值value1进行加密
String oldValue = jsonObj.getString("key1");
String encryptedValue = EncryptAESUtils.encryptAES(oldValue, key_AES);
jsonObj.put("key1", encryptedValue);
} catch (Exception e) {
e.printStackTrace();
}
}
}
对于备注中说明需3DES加密的请求报文体参数,用户可以登录开放平台后,查看“我的信息”页面中的3DES密钥,使用该密钥对参数进行加密。在接口调用时,开放平台会使用该密钥对请求参数进行解密,获取真实参数。
可进入后台“我的信息”查看,页面地址:
https://open.unionpay.com/tjweb/user/info
3DES加密示例代码如下:
package com.unionpay.upapi.client.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class Encrypt3DESUtils {
/**
* 加密算法3DES
*/
public static String encrypt3DES(String value, String key) throws Exception {
if (null == value || "".equals(value)) {
return "";
}
byte[] valueByte = value.getBytes();
byte[] sl = encrypt3DES(valueByte, Encrypt3DESUtils.hexToBytes(key));
String result = Base64.encodeBase64String(sl);
return result;
}
public static byte[] encrypt3DES(byte[] input, byte[] key) throws Exception {
Cipher c = Cipher.getInstance("DESede/ECB/PKCS5Padding");
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "DESede"));
return c.doFinal(input);
}
/**
* 将16进制的字符串转换成bytes
*
* @param hex
* @return 转化后的byte数组
*/
public static byte[] hexToBytes(String hex) {
return hexToBytes(hex.toCharArray());
}
/**
* 将16进制的字符数组转换成byte数组
*
* @param hex
* @return 转换后的byte数组
*/
public static byte[] hexToBytes(char[] hex) {
int length = hex.length / 2;
byte[] raw = new byte[length];
for (int i = 0; i < length; i++) {
int high = Character.digit(hex[i * 2], 16);
int low = Character.digit(hex[i * 2 + 1], 16);
int value = (high << 4) | low;
if (value > 127) {
value -= 256;
}
raw[i] = (byte) value;
}
return raw;
}
public static void main(String[] args) {
try {
String key_3DES = "3DESKey";
String jsonStr = "{\"key1\":\"value1\",\"key2\":\"value2\"}";
JSONObject jsonObj = JSON.parseObject(jsonStr);
// 仅对key1的值value1进行加密
String oldValue = jsonObj.getString("key1");
String encryptedValue = Encrypt3DESUtils.encrypt3DES(oldValue, key_3DES);
jsonObj.put("key1", encryptedValue);
} catch (Exception e) {
e.printStackTrace();
}
}
}
对于备注中说明需SM4加密的请求报文体参数,用户可以登录开放平台后,查看“我的信息”页面中的SM4密钥,使用该密钥对参数进行加密。在接口调用时,开放平台会使用该密钥对请求参数进行解密,获取真实参数。
可进入后台“我的信息”查看,页面地址:
https://open.unionpay.com/tjweb/user/info
SM4加密示例代码如下:
package com.magpie.common.utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class BytesUtil {
/**
* 将16进制的字符串转换成bytes
*
* @param hex
* @return 转化后的byte数组
*/
public static byte[] hexToBytes(String hex) {
return hexToBytes(hex.toCharArray());
}
/**
* 将16进制的字符数组转换成byte数组
*
* @param hex
* @return 转换后的byte数组
*/
public static byte[] hexToBytes(char[] hex) {
int length = hex.length / 2;
byte[] raw = new byte[length];
for (int i = 0; i < length; i++) {
int high = Character.digit(hex[i * 2], 16);
int low = Character.digit(hex[i * 2 + 1], 16);
int value = (high << 4) | low;
if (value > 127) {
value -= 256;
}
raw[i] = (byte) value;
}
return raw;
}
/**
* 将byte数组转换成16进制字符串
*
* @param bytes
* @return 16进制字符串
*/
public static String bytesToHex(byte[] bytes) {
String hexArray = "0123456789abcdef";
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
int bi = b & 0xff;
sb.append(hexArray.charAt(bi >> 4));
sb.append(hexArray.charAt(bi & 0xf));
}
return sb.toString();
}
public static byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
for (int i = in.read(); i != -1; i = in.read()) {
bout.write(i);
}
return bout.toByteArray();
}
}
package com.magpie.common.utils;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.Security;
import java.util.Arrays;
public class SM4Utils {
// 固定填充byte数组
private static String ivStr = "0123456789123456";
//SM4-PADDING-CBC 运算方式
public static final String SM4_CBC_PKCS7PADDING = "SM4/CBC/PKCS7Padding";
static {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
} else {
Security.removeProvider("BC");
Security.addProvider(new BouncyCastleProvider());
}
}
/**
* SM4加密算法
* @param value 加密字符
* @param key 密钥
* @return
* @throws Exception
*/
public static String encryptSM4(String value, String key) throws Exception {
if (null == value || "".equals(value)) {
return "";
}
byte[] valueByte = value.getBytes();
byte[] result =
sm4EncryptCBC(BytesUtil.hexToBytes(key),valueByte,ivStr.getBytes(),
SM4_CBC_PKCS7PADDING);
return Base64.encodeBase64String(result);
}
public static byte[] sm4EncryptCBC(byte[] keyBytes, byte[] data, byte[] iv, String algo){
try {
Key key = new SecretKeySpec(keyBytes, "SM4");
Cipher in = Cipher.getInstance(algo, "BC"); //
if(iv == null) iv = zeroIv(algo);
in.init(Cipher.ENCRYPT_MODE, key, getIV(iv));
return in.doFinal(data);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static byte[] zeroIv(final String algo) {
try {
Cipher cipher = Cipher.getInstance(algo);
int blockSize = cipher.getBlockSize();
byte[] iv = new byte[blockSize];
Arrays.fill(iv, (byte)0);
return iv;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static IvParameterSpec getIV(byte[] iv){
return new IvParameterSpec(iv);
}
public static void main(String[] args) {
try {
String encryptData =
encryptSM4("刘鹏", "fd994eeddff7b436b14be1c5f285d4d7");
System.out.println(encryptData);
} catch (Exception e) {
e.printStackTrace();
}
}
}
对于需AES-GCM加密的请求报文体参数,用户可以登录开放平台后,查看“我的信息”页面中的AES密钥,使用该密钥对参数进行加密。在接口调用时,开放平台会使用该密钥对请求参数进行解密,获取真实参数。(此加密模式请与业务方沟通确认后使用)
可进入后台“个人信息”-“API认证账号”详情页查看,页面地址:
https://open.unionpay.com/tjweb/user/gateway/list
AES-GCM加密示例代码如下:
package com.unionpay.upapi.client.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
public class AESAndHmacTest{
private static final char[] DIGITS_LOWER = new char[] {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
private static final char[] DIGITS_UPPER = new char[] {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
public static void main(String args[]) throws Exception {
String data = "hello world, what are you doing now";
System.out.println("data: " + data);
String aesKey = "E0C9874AD320A3F53D9D58FD82DE5CF0";
System.out.println("aesKey: " + aesKey);
String enData = encryptAesGcm(data, aesKey);
System.out.println("enData: " + enData);
}
private static byte[] genSecureRandomByte(int byteSize) {
SecureRandom sr = new SecureRandom();
byte[] bytes = new byte[byteSize];
sr.nextBytes(bytes);
return bytes;
}
private static String byte2HexStr(byte[] array) {
return array == null ? null : new String(encodeHex(array, false));
}
private static char[] encodeHex(byte[] data, boolean toLowerCase) {
return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
}
private static char[] encodeHex(byte[] data, char[] toDigits) {
int l = data.length;
char[] out = new char[l << 1];
int i = 0;
for (int var5 = 0; i < l; ++i) {
out[var5++] = toDigits[(240 & data[i]) >>> 4];
out[var5++] = toDigits[15 & data[i]];
}
return out;
}
private static String encryptAesGcm(String plainText, String aesKey)
throws GeneralSecurityException, IllegalArgumentException {
return encryptAes(plainText, hexStr2Byte(aesKey), genSecureRandomByte(12), "AES_GCM");
}
private static String encryptAes(String plainKey, byte[] key, byte[] salt, String aesType)
throws GeneralSecurityException {
if (plainKey != null && key != null && salt != null) {
if (key.length < 16) {
throw new IllegalArgumentException("key length must be more than 128bit.");
} else {
byte[] cipherKey = encryptAes(plainKey.getBytes(DEFAULT_CHARSET), key, salt, aesType);
String composeKey = byte2HexStr(salt) + ':' + byte2HexStr(cipherKey);
return composeKey;
}
} else {
return null;
}
}
private static byte[] encryptAes(byte[] content, byte[] secret, byte[] salt, String aesType)
throws GeneralSecurityException {
SecretKeySpec skeySpec = new SecretKeySpec(secret, "AES");
Cipher cipher = genAesEncryptCipher(skeySpec, new GCMParameterSpec(128, Arrays.copyOf(salt, 12)),
"AES/GCM/NoPadding");
return cipher.doFinal(content);
}
private static Cipher genAesEncryptCipher(SecretKeySpec skeySpec, GCMParameterSpec algorithmParaSpec,String cipherType)
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException,
InvalidKeyException {
return genAesCipher(1, skeySpec, algorithmParaSpec, cipherType);
}
private static Cipher genAesCipher(int cipherMode, SecretKeySpec skeySpec, AlgorithmParameterSpec algorithmParaSpec,
String cipherType) throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException,
NoSuchPaddingException {
Cipher cipher = Cipher.getInstance(cipherType);
cipher.init(cipherMode, skeySpec, algorithmParaSpec);
return cipher;
}
private static byte[] hexStr2Byte(String hexStr) throws IllegalArgumentException {
return hexStr == null ? new byte[0] : decodeHex(hexStr.toCharArray());
}
private static byte[] decodeHex(char[] data) {
int len = data.length;
if ((len & 1) != 0) {
throw new IllegalArgumentException("Number of characters illegal.");
} else {
byte[] out = new byte[len >> 1];
int i = 0;
for (int j = 0; j < len; ++i) {
int f = toDigit(data[j], j) << 4;
++j;
f |= toDigit(data[j], j);
++j;
out[i] = (byte) (f & 255);
}
return out;
}
}
private static int toDigit(char ch, int index) throws IllegalArgumentException {
int digit = Character.digit(ch, 16);
if (digit == -1) {
throw new IllegalArgumentException("Illegal hexadecimal character " + ch + " at index " + index);
} else {
return digit;
}
}
}