最近公司开展新的业务,需要调用银行新的接口,使用原来AES的方式不能调用,需要使用国密的方式调用,这里做个记录。着急调用接口的家人们可以直接看“三、工具类”那一块,改造过的工具类和调用示例都放在那里了。
一般会把你拉到一个对接群,先问对接人员要相关的测试环境,一般是Excel表格。
然后下载示例代码
到示例代码哪里有一个链接,点击进入到下载页面
PS:原来AES方式调用接口的示例代码都被删除了。
或者直接点击下载链接进入
https://openbiz.cmbchina.com/developer/UI/Business/CloudDirectConnect/Public/DownLoadCenter/DownloadTransfer.aspx
不添加白名单的话,在调用接口时会报白名单的错误。这里是测试环境,所以在测试网银中添加
进入之后将自己的ip添加到ip白名单设置那里,过一会就可以了。
在“云直连-对接开发”的后面全是招商银行的接口,找到想要调用的接口
比如我要调用“支付转账-企银支付单笔经办BB1PAYOP”这个接口
在调用接口之前先使用Excel表中的银行公钥、私钥、对称秘钥、网银用户号参数,替换示例代码“ApiDemo”中对应的参数
按照文档中的示例定义好请求json,这是企银支付单笔经办BB1PAYOP的请求json示例
替换示例代码中的请求参数就可以了,主要是替换“ApiDemo”类中的这两个参数
国密示例代码有两个类,一个是“ApiDemo”,代码如下:
package cmb;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Security;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
/**
* 招商银行银企直联国密免前置/SaaS对接示例,本示例仅供参考,不保证各种异常场景运行,请勿直接使用,如有错漏请联系对接人员。运行时,请使用所获取的测试资源替换 用户编号、公私钥、对称密钥、服务商编号等信息。
*
* @author cmb.firmbank
* @date 2023/7/20
*/
public class ApiDemo {
private static final int BOUND_START = 1000000;
private static final int BOUND_END = 9000000;
// 测试地址,生产需要替换
private static String url = "http://cdctest.cmburl.cn:80/cdcserver/api/v2";
// 生产地址
//private static String url = "https://cdc.cmbchina.com/cdcserver/api/v2";
// 银行公钥
private static String publicKey = "BNsIe9U0x8IeSe4h/dxUzVEz9pie0hDSfMRINRXc7s1UIXfkExnYECF4QqJ2SnHxLv3z/99gsfDQrQ6dzN5lZj0=";
// 客户私钥,生产需要替换
private static String privateKey = "NBtl7WnuUtA2v5FaebEkU0/Jj1IodLGT6lQqwkzmd2E=";
// 对称密钥,生产需要替换
private static String symKey = "VuAzSWQhsoNqzn0K";
// 企业网银用户号,生产需要替换
private static String uid = "N003261207";
private static Random random = new Random();
private ApiDemo() {
}
public static void main(String[] args) throws GeneralSecurityException, IOException, CryptoException {
// 装载BC库,必须在应用的启动类中调用此函数
Security.addProvider(new BouncyCastleProvider());
// 业务接口名,这里是查询业务模式接口,生产请替换为对应接口名
String funcode = "DCLISMOD";
// 准备接口数据,生产请替换为具体接口请求报文,包含所需的请求字段
String currentDatetime = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String reqid = new SimpleDateFormat("yyyyMMddHHmmssSSSS").format(new Date()) + (BOUND_START + random.nextInt(BOUND_END));
String data = "{\"request\":{\"body\":{\"buscod\":\"N02030\"},\"head\":{\"funcode\":\"" + funcode + "\",\"userid\":\"" + uid + "\",\"reqid"
+ "\":\"" + reqid + "\"}},\"signature\":{\"sigdat\":\"__signature_sigdat__\",\"sigtim\":\"" + currentDatetime + "\"}}";
DcHelper dchelper = new DcHelper(url, uid, privateKey, publicKey, symKey);
String response = dchelper.sendRequest(data, funcode);
process("Api请求1成功,响应报文:\r\n" + response);
String response2 = dchelper.sendRequest(data, funcode);
process("Api请求2成功,响应报文:\r\n" + response2);
}
private static void process(String response) {
}
}
另外一个是“DcHelper”,代码如下:
package cmb;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithID;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.math.ec.ECPoint;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeSet;
/**
* 招商银行银企直联国密免前置/SaaS对接示例,本示例仅供参考,不保证各种异常场景运行,请勿直接使用,如有错漏请联系对接人员。运行时,请使用所获取的测试资源替换 用户编号、公私钥、对称密钥、服务商编号等信息。
*
* @author cmb.firmbank
* @date 2023/7/20
*/
public class DcHelper {
private static final int LENGTH_32 = 32;
private static final int USERID_LEN = 16;
private static final int CONNECT_TIMEOUT = 15000;
private static final int READ_TIMEOUT = 60000;
private static final int STATUS_OK = 200;
private static Base64.Encoder encoder = Base64.getEncoder();
private static Base64.Decoder decoder = Base64.getDecoder();
// 请求URL
private final String url;
// 企业网银用户号
private final String uid;
// 国密算法向量,根据用户号生成
private final byte[] userId;
// 算法,固定为国密算法
private final String alg;
// 客户私钥
private final byte[] privateKey;
// 银行公钥
private final byte[] publicKey;
// 协商的对称密钥
private final byte[] symKey;
public DcHelper(String url, String uid, String privateKey, String publicKey, String symKey) {
this.url = url;
this.uid = uid;
this.userId = getUserId(uid);
this.alg = "SM";
this.privateKey = decoder.decode(privateKey);
this.publicKey = decoder.decode(publicKey);
this.symKey = symKey.getBytes(StandardCharsets.UTF_8);
}
public String sendRequest(String data, String funcode) throws IOException, CryptoException, GeneralSecurityException {
// 对请求报文做排序
JsonObject requestJson = new Gson().fromJson(data, JsonObject.class);
String source = recursiveKeySort(requestJson);
// 生成签名
byte[] signature = cmbSM2SignWithSM3(userId, privateKey, source.getBytes(StandardCharsets.UTF_8));
// 替换签名字段
requestJson.getAsJsonObject("signature").addProperty("sigdat", new String(encoder.encode(signature), StandardCharsets.UTF_8));
// 对数据进行对称加密
String request = requestJson.toString();
byte[] encryptRequest = cmbSM4Crypt(symKey, userId, request.getBytes(StandardCharsets.UTF_8), 1);
String encryptedRequest = new String(encoder.encode(encryptRequest), StandardCharsets.UTF_8);
// 发送请求
HashMap<String, String> map = new HashMap<>();
map.put("UID", uid);
map.put("ALG", alg);
map.put("DATA", URLEncoder.encode(encryptedRequest, StandardCharsets.UTF_8.displayName()));
map.put("FUNCODE", funcode);
String response = httpPost(url, map);
if (response.startsWith("CDCServer:")) {
throw new IOException("访问目标地址 " + url + " 失败:" + response);
}
// 返回结果解密
response = new String((cmbSM4Crypt(symKey, userId, decoder.decode(response), 2)), StandardCharsets.UTF_8);
// 验证签名是否正确
JsonObject responseJson = new Gson().fromJson(response, JsonObject.class);
JsonObject signatureJson = responseJson.getAsJsonObject("signature");
String responseSignature = signatureJson.get("sigdat").getAsString();
signatureJson.addProperty("sigdat", "__signature_sigdat__");
responseJson.add("signature", signatureJson);
String responseSorted = recursiveKeySort(responseJson);
boolean verify = cmbSM2VerifyWithSM3(userId, publicKey, responseSorted.getBytes(StandardCharsets.UTF_8), decoder.decode(responseSignature));
if (!verify) {
throw new IOException("响应报文的签名无效");
}
return response;
}
private static String recursiveKeySort(JsonObject json) {
StringBuilder appender = new StringBuilder();
appender.append("{");
Iterator<String> keys = new TreeSet<>(json.keySet()).iterator();
boolean isFirstEle = true;
while (keys.hasNext()) {
if (!isFirstEle) {
appender.append(",");
}
String key = keys.next();
Object val = json.get(key);
if (val instanceof JsonObject) {
appender.append("\"").append(key).append("\":");
appender.append(recursiveKeySort((JsonObject)val));
} else if (val instanceof JsonArray) {
JsonArray jarray = (JsonArray)val;
appender.append("\"").append(key).append("\":[");
boolean isFirstArrEle = true;
for (int i = 0; i < jarray.size(); i++) {
if (!isFirstArrEle) {
appender.append(",");
}
Object obj = jarray.get(i);
if (obj instanceof JsonObject) {
appender.append(recursiveKeySort((JsonObject)obj));
} else {
appender.append(obj.toString());
}
isFirstArrEle = false;
}
appender.append("]");
} else {
String value = val.toString();
appender.append("\"").append(key).append("\":").append(value);
}
isFirstEle = false;
}
appender.append("}");
return appender.toString();
}
private static byte[] getUserId(String uid) {
return (uid + "0000000000000000").substring(0, USERID_LEN).getBytes();
}
private static String httpPost(String httpUrl, Map<String, String> param) throws IOException, GeneralSecurityException {
HttpURLConnection connection = null;
String result;
try {
URL url = new URL(httpUrl);
SSLContext sslcontext;
sslcontext = SSLContext.getInstance("SSL");
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore)null);
X509TrustManager defaultTm = null;
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
defaultTm = (X509TrustManager)tm;
break;
}
}
sslcontext.init(null, new TrustManager[]{defaultTm}, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sslcontext.getSocketFactory());
connection = (HttpURLConnection)url.openConnection();
connection.setRequestMethod("POST");
connection.setConnectTimeout(CONNECT_TIMEOUT);
connection.setReadTimeout(READ_TIMEOUT);
connection.setInstanceFollowRedirects(true);
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
try (OutputStream os = connection.getOutputStream()) {
os.write(createLinkString(param).getBytes());
if (connection.getResponseCode() != STATUS_OK) {
InputStream is = connection.getErrorStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
StringBuilder sbf = new StringBuilder();
String temp;
while ((temp = br.readLine()) != null) {
sbf.append(temp);
sbf.append("\r\n");
}
result = sbf.toString();
br.close();
is.close();
} else {
InputStream is = connection.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
StringBuilder sbf = new StringBuilder();
String temp;
while ((temp = br.readLine()) != null) {
sbf.append(temp);
}
result = sbf.toString();
br.close();
is.close();
}
}
} finally {
if (connection != null) {
connection.disconnect();
}
}
return result;
}
private static String createLinkString(Map<String, String> params) {
ArrayList<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
StringBuilder prestr = new StringBuilder();
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
if (i == keys.size() - 1) {
prestr.append(key).append("=").append(value);
} else {
prestr.append(key).append("=").append(value).append("&");
}
}
return prestr.toString();
}
// 以下是加解密相关的函数
private static byte[] cmbSM2SignWithSM3(byte[] id, byte[] privkey, byte[] msg) throws IOException, CryptoException {
if (privkey == null || msg == null) {
throw new CryptoException("CMBSM2SignWithSM3 input error");
}
ECPrivateKeyParameters privateKey = encodePrivateKey(privkey);
SM2Signer signer = new SM2Signer();
ParametersWithID parameters = new ParametersWithID(privateKey, id);
signer.init(true, parameters);
signer.update(msg, 0, msg.length);
return decodeDERSignature(signer.generateSignature());
}
private static ECPrivateKeyParameters encodePrivateKey(byte[] value) {
BigInteger d = new BigInteger(1, value);
ECParameterSpec spec = ECNamedCurveTable.getParameterSpec("sm2p256v1");
ECDomainParameters ecParameters = new ECDomainParameters(spec.getCurve(), spec.getG(), spec.getN(), spec.getH(), spec.getSeed());
return new ECPrivateKeyParameters(d, ecParameters);
}
private static byte[] decodeDERSignature(byte[] signature) throws IOException {
ASN1InputStream stream = new ASN1InputStream(new ByteArrayInputStream(signature));
ASN1Sequence primitive = (ASN1Sequence)stream.readObject();
Enumeration<ASN1Integer> enumeration = primitive.getObjects();
BigInteger intR = enumeration.nextElement().getValue();
BigInteger intS = enumeration.nextElement().getValue();
byte[] bytes = new byte[LENGTH_32 * 2];
byte[] r = format(intR.toByteArray());
byte[] s = format(intS.toByteArray());
System.arraycopy(r, 0, bytes, 0, LENGTH_32);
System.arraycopy(s, 0, bytes, LENGTH_32, LENGTH_32);
return bytes;
}
private static byte[] format(byte[] value) {
if (value.length == LENGTH_32) {
return value;
} else {
byte[] bytes = new byte[LENGTH_32];
if (value.length > LENGTH_32) {
System.arraycopy(value, value.length - LENGTH_32, bytes, 0, LENGTH_32);
} else {
System.arraycopy(value, 0, bytes, LENGTH_32 - value.length, value.length);
}
return bytes;
}
}
private static byte[] cmbSM4Crypt(byte[] key, byte[] iv, byte[] input, int mode) throws GeneralSecurityException {
SecretKeySpec spec = new SecretKeySpec(key, "SM4");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", BouncyCastleProvider.PROVIDER_NAME);
cipher.init(mode, spec, ivParameterSpec);
return cipher.doFinal(input);
}
private static boolean cmbSM2VerifyWithSM3(byte[] id, byte[] pubkey, byte[] msg, byte[] signature) throws IOException {
if (pubkey == null || msg == null || signature == null) {
throw new IllegalArgumentException("CMBSM2VerifyWithSM3 input error");
}
ECPublicKeyParameters publicKey = encodePublicKey(pubkey);
SM2Signer signer = new SM2Signer();
ParametersWithID parameters = new ParametersWithID(publicKey, id);
signer.init(false, parameters);
signer.update(msg, 0, msg.length);
return signer.verifySignature(encodeDERSignature(signature));
}
private static ECPublicKeyParameters encodePublicKey(byte[] value) {
byte[] x = new byte[LENGTH_32];
byte[] y = new byte[LENGTH_32];
System.arraycopy(value, 1, x, 0, LENGTH_32);
System.arraycopy(value, LENGTH_32 + 1, y, 0, LENGTH_32);
BigInteger intX = new BigInteger(1, x);
BigInteger intY = new BigInteger(1, y);
ECParameterSpec spec = ECNamedCurveTable.getParameterSpec("sm2p256v1");
ECPoint intQ = spec.getCurve().createPoint(intX, intY);
ECDomainParameters ecParameters = new ECDomainParameters(spec.getCurve(), spec.getG(), spec.getN(), spec.getH(), spec.getSeed());
return new ECPublicKeyParameters(intQ, ecParameters);
}
private static byte[] encodeDERSignature(byte[] signature) throws IOException {
byte[] r = new byte[LENGTH_32];
byte[] s = new byte[LENGTH_32];
System.arraycopy(signature, 0, r, 0, LENGTH_32);
System.arraycopy(signature, LENGTH_32, s, 0, LENGTH_32);
ASN1EncodableVector vector = new ASN1EncodableVector();
vector.add(new ASN1Integer(new BigInteger(1, r)));
vector.add(new ASN1Integer(new BigInteger(1, s)));
return (new DERSequence(vector)).getEncoded();
}
}
感觉银行示例代码调接口太麻烦了,就自己做了些修改,这里只是调用接口的代码片段,完整代码放在工具类那里,还是企银支付单笔经办BB1PAYOP接口为例
public static void main(String[] args) throws Exception {
BankSMConfigDto bankSMConfig = new BankSMConfigDto();
bankSMConfig.setPublicKey("你的银行公钥");
bankSMConfig.setPrivateKey("你的银行私钥");
bankSMConfig.setSymKey("你的对称秘钥");
bankSMConfig.setUID("你的网银用户号");
bankSMConfig.setBbknbr("75");
bankSMConfig.setPayacc("你的结算户");
bankSMConfig.setBusmod("N02030");
bankSMConfig.setGrtflg("N");
bankSMConfig.setURL("http://cdctest.cmburl.cn/cdcserver/api/v2");
//============================企银支付单笔经办 接口测试================================
BB1PAYOPOneParam bb1PAYOPOneParam = new BB1PAYOPOneParam();
bb1PAYOPOneParam.setBusMod("S100B");
bb1PAYOPOneParam.setBusCod("N02030");
List<BB1PAYOPOneParam> bb1paybmx1 = Arrays.asList(bb1PAYOPOneParam);
BB1PAYOPTwoParam bb1PAYOPTwoParam = new BB1PAYOPTwoParam();
bb1PAYOPTwoParam.setDbtAcc("你的转账账号");
bb1PAYOPTwoParam.setCrtAcc("你的收款账号");
bb1PAYOPTwoParam.setCrtNam("你的收款账号户名");
bb1PAYOPTwoParam.setCcyNbr("10");
bb1PAYOPTwoParam.setTrsAmt("55.00");
bb1PAYOPTwoParam.setNusAge("测试转账");
bb1PAYOPTwoParam.setCrtBnk("深圳分行");
bb1PAYOPTwoParam.setCrtAdr("a");
bb1PAYOPTwoParam.setYurRef("1712676447073546252");
List<BB1PAYOPTwoParam> bb1payopx1 = Arrays.asList(bb1PAYOPTwoParam);
Map<String, Object> body = new HashMap<>();
body.put("bb1paybmx1", bb1paybmx1);
body.put("bb1payopx1", bb1payopx1);
List<String> BB1PAYOPEmptyFiledList = Arrays.asList("dmaNbr", "crtBnk", "crtAdr", "brdNbr", "bnkFlg", "eptDat", "eptTim", "stlChn", "crtSqn", "busNar", "ntfCh1", "ntfCh2", "trsTyp", "rcvChk", "drpFlg");
JSONObject BB1PAYOPResponse = commonRequestMethod(bankSMConfig, CMBTransAction.ProxyPay.getCode(), body, BB1PAYOPEmptyFiledList, null);
System.out.println("BB1PAYOPResponse " + BB1PAYOPResponse);
}
响应json解密后如下:
错误代码“resultcode”为“SUC0000”,表示请求成功被银行接收,即此接口调用成功了。
这一块放的是调用接口会用到的工具类,调用接口的完整示例放在“4、调用接口的工具类”里面。
package org.jeecg.modules.cmb.enums;
/**
* 招商银行的方法枚举类
*
* @author:user
* @date: 2022-03-24 11:43
*/
public enum CMBTransAction {
//企银支付单笔经办
ProxyPay("企银支付单笔经办","BB1PAYOP" );
// 支付申请类型名称
private String name;
// 支付申请类型编码
private String code;
CMBTransAction(String name, String code) {
this.name = name;
this.code = code;
}
public static CMBTransAction getTransType(String code) throws RuntimeException {
for (CMBTransAction type : CMBTransAction.values()) {
if (type.getCode().equals(code)) {
return type;
}
}
throw new RuntimeException("系统异常!未知银行接口码[" + code + "]");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}
package org.jeecg.modules.cmb.dto;
import lombok.Data;
/**
* 银行国密配置dto
*
* @author:user
* @date: 2023-10-13 17:51
*/
@Data
public class BankSMConfigDto {
//公钥
private String publicKey;
//私钥
private String privateKey;
//对称密钥
private String symKey;
//用户id
private String UID;
//访问地址
private String URL;
//=============以下参数并不是所有的银行接口都需要,所以是非必填的
//付方账号
private String payacc;
//模式编号
private String busmod;
//网银审批标志 可选;Y 直连经办,客户端审批
private String grtflg;
//分行号
private String bbknbr;
}
这里以企银支付单笔经办BB1PAYOP相关请求参数为例,可以根据自己需要添加,这是企银支付单笔经办BB1PAYOP 请求参数 “bb1paybmx1”的请求参数dto
package org.jeecg.modules.cmb.dto;
import lombok.Data;
/**
* 企银支付单笔经办 BB1PAYOP - 参数 - bb1paybmx1
*
* @author:gan
* @date: 2023-10-13 15:14
*/
@Data
public class BB1PAYOPOneParam {
/*
业务模式(模式编号)
可通过“可经办业务模式查询(DCLISMOD)”接口获得,也可通过前置机获得。
*/
private String busMod;
/*
业务类型(业务代码)
N02030:支付
*/
private String busCod;
}
这是企银支付单笔经办BB1PAYOP 请求参数 “bb1payopx1”的请求参数dto
package org.jeecg.modules.cmb.dto;
import lombok.Data;
/**
* 企银支付单笔经办 BB1PAYOP - 参数 - bb1payopx1
*
* @author:gan
* @date: 2023-10-13 15:17
*/
@Data
public class BB1PAYOPTwoParam {
/*
转出帐号
*/
private String dbtAcc;
/*
收方帐号
*/
private String crtAcc;
/*
收方户名
最长100汉字
*/
private String crtNam;
/*
币种
只支持10人民币
*/
private String ccyNbr;
/*
交易金额
*/
private String trsAmt;
/*
用途
展示在回单,最长100汉字
*/
private String nusAge;
/*
业务参考号
业务参考号,必须唯一。
业务参考号(YURREF)是每笔交易的唯一编号,是防止重复提交的重要手段,如需重复发送请求,请务必保证同一笔交易的业务参考号不变,否则会存在重复提交风险。
*/
private String yurRef;
//=================以下字段为非必填=================
/*
记账子单元编号
目前生产上记账子单元编号为10位
*/
private String dmaNbr;
/*
收方开户行名称
最长30汉字
1、非标准银联卡,必须要填行名称或者联行号;其他情况可空,但优先使用客户传入;
2、若设置了收方限制,则行名联行号都必传;
3、若同时输入行名和联行号,则以联行号为准进行汇出;
4、若收方非招行户,请尽量补充完整账户信息以提高支付成功率;
*/
private String crtBnk;
/*
收方开户行地址
最长30汉字
以下任意情况,收方开户行地址可不传:
a.收方为招行账户;
b.已输入收方开户行名称或收方开户行联行号(若客户未填写开户地,计费将根据行名/行号判断开户地进行同城异地判断,若行名中不含明确的地址信息,可能存在同城异地的误判,请知悉);
*/
private String crtAdr;
/*
收方行联行号
1、非标准银联卡,必须要填行名称或者联行号;其他情况可空,但优先使用客户传入;
2、若设置了收方限制,则行名联行号都必传;
3、若同时输入行名和联行号,则以联行号为准进行汇出;
4、若收方非招行户,请尽量补充完整账户信息以提高支付成功率;
若客户未填写收方开户行联行号,计费将根据系统识别的行号进行开户地判断,可能存在同城异地的误判,请知悉
*/
private String brdNbr;
/*
系统内标志
收方为招行户:传空或Y;
收方为他行户:传N;
*/
private String bnkFlg;
/*
期望日
*/
private String eptDat;
/*
期望时间
*/
private String eptTim;
/*
结算通道
G 普通
Q 快速
R 实时-超网
I 智能路由
输入空时默认为Q快速;
注意:
1.R实时走超网通道,支持收方为行内户,但是手续费不同,收方为行外的是按照跨行标准收费的,收方为行内的是按照行内标准收费的。
2.超网通道人行限额100万。
3.结算方式选择“R 实时”:
收方为招行账户(含个人及对公账户),则转账最少要素为:收方账号+收方户名
收方为非招行账户,且为标准银联卡,则转账最少要素为:收方账号+收方户名
收方为非招行账户,且为非标准银联卡(如对公账户等),则转账最少要素为:收方账号+收方户名+收方行名/总行行号;
4.结算方式选择“G普通”、“Q快速”:
收方为招行账户(含个人及对公账户),则转账最少要素为:收方账号+收方户名
收方为非招行账户,则转账最少要素为:收方账号+收方户名+收方行名/行号
5.结算方式选择“I 智能路由模式 ”:
根据输入的收方信息择优选择人行大额/小额/超级网银/同行支付系统进行汇出,无需客户手动选择支付汇路,规则如下:
a. 客户需输入账号、户名、行名或行号,其中行名或行号无需区分总行或支行(标准银联卡只需输入账号、户名);
b. 若收方开户行支持超网入账,则优先通过超网渠道进行汇出,保障收方入账效率;
c. 当选择非直汇模式(drpFlg字段传A)时,若未输入行号且输入行名不规范,则交易可能落地人工处理,影响入账时效;
d. 智能路由仅用于辅助识别汇路,请尽可能完善支付要素,提升支付成功率;
*/
private String stlChn;
/*
收方编号
*/
private String crtSqn;
/*
业务摘要
*/
private String busNar;
/*
通知方式一(邮箱)
*/
private String ntfCh1;
/*
通知方式二(手机号)
*/
private String ntfCh2;
/*
业务种类
100001 普通汇兑 (默认值)
101001 慈善捐款
101002 其他
注:只有结算通道为“G 普通”或者“Q 快速”时,才支持101001、101002方式优惠手续费。
*/
private String trsTyp;
/*
行内收方账号户名校验
1:校验
空或者其他值:不校验
如果为1,行内收方账号与户名不相符则支付经办失败。
*/
private String rcvChk;
/*
直汇普通标志
1:校验
空时为A; A-普通 B-直汇(失败后不落人工处理)。
注:只有结算通道为“G 普通”或者“Q 快速”时,才支持。
*/
private String drpFlg;
}
其中“VerifyUtil”是校验空参的工具类,放在这篇博客里面了,也可以用自己的方法校验。
package org.jeecg.modules.cmb.utils;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.bouncycastle.asn1.*;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithID;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.math.ec.ECPoint;
import org.jeecg.common.util.VerifyUtil;
import org.jeecg.modules.cmb.dto.*;
import org.jeecg.modules.cmb.enums.CMBTransAction;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.*;
import java.io.*;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.Security;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* 招商银行国密工具类
*
* @author:gan
* @date: 2023-10-13 17:50
*/
public class CMBSMUtils {
private static final int LENGTH_32 = 32;
private static final int USERID_LEN = 16;
private static final int CONNECT_TIMEOUT = 15000;
private static final int READ_TIMEOUT = 60000;
private static final int STATUS_OK = 200;
private static Base64.Encoder encoder = Base64.getEncoder();
private static Base64.Decoder decoder = Base64.getDecoder();
// 请求URL
private static String url;
// 企业网银用户号
private static String uid;
// 国密算法向量,根据用户号生成
private static byte[] userId;
// 算法,固定为国密算法
private static String alg;
// 客户私钥
private static byte[] privateKey;
// 银行公钥
private static byte[] publicKey;
// 协商的对称密钥
private static byte[] symKey;
public static void main(String[] args) throws Exception {
BankSMConfigDto bankSMConfig = new BankSMConfigDto();
bankSMConfig.setPublicKey("你的银行公钥");
bankSMConfig.setPrivateKey("你的银行私钥");
bankSMConfig.setSymKey("你的对称秘钥");
bankSMConfig.setUID("你的网银用户号");
bankSMConfig.setBbknbr("75");
bankSMConfig.setPayacc("你的结算户");
bankSMConfig.setBusmod("N02030");
bankSMConfig.setGrtflg("N");
bankSMConfig.setURL("http://cdctest.cmburl.cn/cdcserver/api/v2");
//============================企银支付单笔经办 接口测试================================
BB1PAYOPOneParam bb1PAYOPOneParam = new BB1PAYOPOneParam();
bb1PAYOPOneParam.setBusMod("S100B");
bb1PAYOPOneParam.setBusCod("N02030");
List<BB1PAYOPOneParam> bb1paybmx1 = Arrays.asList(bb1PAYOPOneParam);
BB1PAYOPTwoParam bb1PAYOPTwoParam = new BB1PAYOPTwoParam();
bb1PAYOPTwoParam.setDbtAcc("你的转账账号");
bb1PAYOPTwoParam.setCrtAcc("你的收款账号");
bb1PAYOPTwoParam.setCrtNam("你的收款账号户名");
bb1PAYOPTwoParam.setCcyNbr("10");
bb1PAYOPTwoParam.setTrsAmt("55.00");
bb1PAYOPTwoParam.setNusAge("测试转账");
bb1PAYOPTwoParam.setCrtBnk("深圳分行");
bb1PAYOPTwoParam.setCrtAdr("a");
bb1PAYOPTwoParam.setYurRef("1712676447073546252");
List<BB1PAYOPTwoParam> bb1payopx1 = Arrays.asList(bb1PAYOPTwoParam);
Map<String, Object> body = new HashMap<>();
body.put("bb1paybmx1", bb1paybmx1);
body.put("bb1payopx1", bb1payopx1);
List<String> BB1PAYOPEmptyFiledList = Arrays.asList("dmaNbr", "crtBnk", "crtAdr", "brdNbr", "bnkFlg", "eptDat", "eptTim", "stlChn", "crtSqn", "busNar", "ntfCh1", "ntfCh2", "trsTyp", "rcvChk", "drpFlg");
JSONObject BB1PAYOPResponse = commonRequestMethod(bankSMConfig, CMBTransAction.ProxyPay.getCode(), body, BB1PAYOPEmptyFiledList, null);
System.out.println("BB1PAYOPResponse " + BB1PAYOPResponse);
}
/**
* 初始化银行配置
* @param bankSMConfig
*/
private static void init(BankSMConfigDto bankSMConfig) {
// 装载BC库,必须在应用的启动类中调用此函数
Security.addProvider(new BouncyCastleProvider());
url = bankSMConfig.getURL();
uid = bankSMConfig.getUID();
userId = getUserId(uid);
alg = "SM";
privateKey = decoder.decode(bankSMConfig.getPrivateKey());
publicKey = decoder.decode(bankSMConfig.getPublicKey());
symKey = bankSMConfig.getSymKey().getBytes(StandardCharsets.UTF_8);
}
/**
* 银行接口公共请求方法
* @param bankSMConfig 银行相关配置
* @param funcode 银行接口的funcode
* @param body 银行接口的body属性值,目前只有Object和Map两种类型
* @param emptyFiledList body属性中可以为空的属性值,没有传null
* @param message 在为空的基础上,前面添加的提示信息,没有传null
* @param
* @return
* @throws Exception
*/
public static <T> JSONObject commonRequestMethod(BankSMConfigDto bankSMConfig, String funcode, T body, List<String> emptyFiledList, String message) throws Exception {
JSONObject requestJson = getRequestJson(bankSMConfig, funcode, body, emptyFiledList, message);
System.out.println("请求参数:" + requestJson);
init(bankSMConfig);
//对请求报文做排序
String source = recursiveKeySort(requestJson);
// 生成签名
byte[] signature = cmbSM2SignWithSM3(userId, privateKey, source.getBytes(StandardCharsets.UTF_8));
//请求体的signature
JSONObject reqSignatureJson = requestJson.getJSONObject("signature");
// 替换签名字段
reqSignatureJson.put("sigdat", new String(encoder.encode(signature), StandardCharsets.UTF_8));
System.out.println("处理请求参数:" + requestJson);
// 对数据进行对称加密
String request = requestJson.toString();
byte[] encryptRequest = cmbSM4Crypt(symKey, userId, request.getBytes(StandardCharsets.UTF_8), 1);
String encryptedRequest = new String(encoder.encode(encryptRequest), StandardCharsets.UTF_8);
// 发送请求
HashMap<String, String> map = new HashMap<>();
map.put("UID", uid);
map.put("ALG", alg);
map.put("DATA", URLEncoder.encode(encryptedRequest, StandardCharsets.UTF_8.displayName()));
map.put("FUNCODE", funcode);
System.out.println("完整请求参数:" + map);
String response = httpPost(url, map);
if (response.startsWith("CDCServer:")) {
throw new IOException("访问目标地址 " + url + " 失败:" + response);
}
// 返回结果解密
response = new String((cmbSM4Crypt(symKey, userId, decoder.decode(response), 2)), StandardCharsets.UTF_8);
// 验证签名是否正确
JSONObject responseJson = JSONObject.parseObject(response);
System.out.println("响应参数:" + requestJson);
JSONObject resSignatureJson = responseJson.getJSONObject("signature");
String responseSignature = resSignatureJson.getString("sigdat");
resSignatureJson.put("sigdat", "__signature_sigdat__");
responseJson.put("signature", resSignatureJson);
String responseSorted = recursiveKeySort(responseJson);
boolean verify = cmbSM2VerifyWithSM3(userId, publicKey, responseSorted.getBytes(StandardCharsets.UTF_8), decoder.decode(responseSignature));
if (!verify) {
throw new IOException("响应报文的签名无效");
}
System.out.println("解密响应参数:" + responseJson);
return responseJson;
}
/**
* 获取请求参数json
* @param bankSMConfig 银行接口参数,这里用来校验是否为空,这个方法只使用到了UID
* @param funcode 接口code
* @param body 请求json中的参数body,泛型类型
* @return
*/
private static <T> JSONObject getRequestJson(BankSMConfigDto bankSMConfig, String funcode, T body, List<String> emptyFiledList, String message) {
List<String> noEmptyFieldList = Arrays.asList("publicKey", "privateKey", "symKey", "UID", "URL");
VerifyUtil.checkBeanByNonEmptyFiledList(bankSMConfig, noEmptyFieldList, "银行配置");
VerifyUtil.checkParam(funcode, "funcode不能为空!");
//校验body参数
checkBody(body, emptyFiledList, message);
String data = "{\"request\":{\"body\":" + JSONObject.toJSONString(body) + ",\"head\":{\"funcode\":\"" + funcode + "\",\"reqid\":\"" + getReqId() + "\",\"userid\":\"" + bankSMConfig.getUID() + "\"}},\"signature\":{\"sigdat\":\"__signature_sigdat__\",\"sigtim\":\"" + getCurrentDatetime() + "\"}}";
return JSONObject.parseObject(data);
}
/**
* 校验银行body参数
* @param body body参数
* @param emptyFiledList 可以为空的属性集合,为JavaBean的属性,没有可以为空的传null
* @param message 在为空的基础上,前面添加的提示信息,没有传null
*/
public static <T> void checkBody(T body, List<String> emptyFiledList, String message) {
if (body instanceof Map) {
Map<String, Object> map = (Map<String, Object>) body;
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
List list = null;
if (value instanceof List) {
list = (List) value;
} else {
throw new RuntimeException("map集合中key为【" + key + "】的value值非法!应为List!");
}
checkList(list, emptyFiledList, message);
}
} else {
VerifyUtil.checkBeanByEmptyFiledList(body, emptyFiledList, message);
}
}
/**
* 校验参数集合
* @param list 参数集合
* @param emptyFiledList 可以为空的属性集合,为JavaBean的属性,没有可以为空的传null
* @param message 在为空的基础上,前面添加的提示信息,没有传null
*/
public static <T> void checkList(List<T> list, List<String> emptyFiledList, String message) {
VerifyUtil.checkParam(list, (StringUtils.isBlank(message) ? "集合" : message) + "为空!");
for (int i = 0; i < list.size(); i++) {
T t = list.get(i);
VerifyUtil.checkBean(t, emptyFiledList, true, (StringUtils.isBlank(message) ? "集合的" : message + "的") + "第【" + i + "】条记录");
}
}
//成功字符串
private static final String SUC0000 = "SUC0000";
//响应参数字符串
private static final String RESPONSE = "response";
//响应参数中head参数字符串
private static final String HEAD = "head";
//响应参数中body参数字符串
private static final String BODY = "body";
/**
* 判断银行调用结果是否成功
* @param responseJson 银行响应结果json
* @return
*/
public static Boolean isSuccess(JSONObject responseJson) {
return SUC0000.equals(getCmbResponseParam(responseJson, HEAD, "resultcode"));
}
/**
* 获取调用银行接口的响应参数值,不能通过该方法获取signature参数下的值
* @param responseJson 银行响应结果json
* @param param1 要获取的结果参数名 (一级或者二级)
* @param param2 要获取的结果参数名 (三级,没有可以为空,如果是body下的参数则一定要写正确)
* @return
*/
public static String getCmbResponseParam(JSONObject responseJson, String param1, String param2) {
JSONObject response = (JSONObject) responseJson.get(RESPONSE);
VerifyUtil.checkParam(response, "返回结果中【response】为空或传入param1有误!");
JSONObject head = (JSONObject) response.get(HEAD);
VerifyUtil.checkParam(head, "返回结果中【head】为空或传入param1有误!");
JSONObject body = (JSONObject) response.get(BODY);
VerifyUtil.checkParam(body, "返回结果中【body】为空或传入param1有误!");
//head下的参数
List<String> param2List = Arrays.asList("bizcode", "funcode", "reqid", "resultcode", "resultmsg", "rspid", "userid");
String result = null;
if (RESPONSE.equals(param1)) {
result = response.toString();
} else if (HEAD.equals(param1)) {
result = VerifyUtil.isNotEmpty(param2) && param2List.contains(param2) ? head.get(param2).toString() : head.toString();
} else if (BODY.equals(param1)) {
result = VerifyUtil.isNotEmpty(param2) ? body.get(param2).toString() : body.toString();
} else {
throw new RuntimeException("传入param2时param1不能为空!");
}
return result;
}
private static final int BOUND_START = 1000000;
private static final int BOUND_END = 9000000;
//reqid时间格式
private static final String reqIdFormat = "yyyyMMddHHmmssSSSS";
//当前时间格式
private static final String currentDatetimeFormat = "yyyyMMddHHmmss";
private static Random random = new Random();
/**
* 获取请求id
* @return
*/
private static String getReqId() {
return getTargetTime(reqIdFormat) + (BOUND_START + random.nextInt(BOUND_END));
}
/**
* 获取当前时间字符
* @return
*/
private static String getCurrentDatetime() {
return getTargetTime(currentDatetimeFormat);
}
/**
* 根据时间格式返回指定时间字符串
* @param pattern 时间格式
* @return
*/
private static String getTargetTime(String pattern) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT+08:00"));
return simpleDateFormat.format(new Date());
}
/**
* 对请求报文做排序
* @param json
* @return 注意返回类型不能再转化为JSONObject,不然会打乱原有的排序
*/
private static String recursiveKeySort(JSONObject json) {
StringBuilder appender = new StringBuilder();
appender.append("{");
Iterator<String> keys = new TreeSet<>(json.keySet()).iterator();
boolean isFirstEle = true;
while (keys.hasNext()) {
if (!isFirstEle) {
appender.append(",");
}
String key = keys.next();
Object val = json.get(key);
if (val instanceof JSONObject) {
appender.append("\"").append(key).append("\":");
appender.append(recursiveKeySort((JSONObject) val));
} else if (val instanceof JSONArray) {
JSONArray jarray = (JSONArray) val;
appender.append("\"").append(key).append("\":[");
boolean isFirstArrEle = true;
for (int i = 0; i < jarray.size(); i++) {
if (!isFirstArrEle) {
appender.append(",");
}
Object obj = jarray.get(i);
if (obj instanceof JSONObject) {
appender.append(recursiveKeySort((JSONObject) obj));
} else {
appender.append(obj.toString());
}
isFirstArrEle = false;
}
appender.append("]");
} else {
String value = val.toString();
appender.append("\"").append(key).append("\":\"").append(value).append("\"");
}
isFirstEle = false;
}
appender.append("}");
return appender.toString();
}
private static byte[] getUserId(String uid) {
return (uid + "0000000000000000").substring(0, USERID_LEN).getBytes();
}
private static String httpPost(String httpUrl, Map<String, String> param) throws IOException, GeneralSecurityException {
HttpURLConnection connection = null;
String result;
try {
URL url = new URL(httpUrl);
SSLContext sslcontext;
sslcontext = SSLContext.getInstance("SSL");
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore)null);
X509TrustManager defaultTm = null;
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
defaultTm = (X509TrustManager)tm;
break;
}
}
sslcontext.init(null, new TrustManager[]{defaultTm}, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sslcontext.getSocketFactory());
connection = (HttpURLConnection)url.openConnection();
connection.setRequestMethod("POST");
connection.setConnectTimeout(CONNECT_TIMEOUT);
connection.setReadTimeout(READ_TIMEOUT);
connection.setInstanceFollowRedirects(true);
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
try (OutputStream os = connection.getOutputStream()) {
os.write(createLinkString(param).getBytes());
if (connection.getResponseCode() != STATUS_OK) {
InputStream is = connection.getErrorStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
StringBuilder sbf = new StringBuilder();
String temp;
while ((temp = br.readLine()) != null) {
sbf.append(temp);
sbf.append("\r\n");
}
result = sbf.toString();
br.close();
is.close();
} else {
InputStream is = connection.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
StringBuilder sbf = new StringBuilder();
String temp;
while ((temp = br.readLine()) != null) {
sbf.append(temp);
}
result = sbf.toString();
br.close();
is.close();
}
}
} finally {
if (connection != null) {
connection.disconnect();
}
}
return result;
}
private static String createLinkString(Map<String, String> params) {
ArrayList<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
StringBuilder prestr = new StringBuilder();
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = params.get(key);
if (i == keys.size() - 1) {
prestr.append(key).append("=").append(value);
} else {
prestr.append(key).append("=").append(value).append("&");
}
}
return prestr.toString();
}
// 以下是加解密相关的函数
private static byte[] cmbSM2SignWithSM3(byte[] id, byte[] privkey, byte[] msg) throws IOException, CryptoException {
if (privkey == null || msg == null) {
throw new CryptoException("CMBSM2SignWithSM3 input error");
}
ECPrivateKeyParameters privateKey = encodePrivateKey(privkey);
SM2Signer signer = new SM2Signer();
ParametersWithID parameters = new ParametersWithID(privateKey, id);
signer.init(true, parameters);
signer.update(msg, 0, msg.length);
return decodeDERSignature(signer.generateSignature());
}
private static ECPrivateKeyParameters encodePrivateKey(byte[] value) {
BigInteger d = new BigInteger(1, value);
ECParameterSpec spec = ECNamedCurveTable.getParameterSpec("sm2p256v1");
ECDomainParameters ecParameters = new ECDomainParameters(spec.getCurve(), spec.getG(), spec.getN(), spec.getH(), spec.getSeed());
return new ECPrivateKeyParameters(d, ecParameters);
}
private static byte[] decodeDERSignature(byte[] signature) throws IOException {
ASN1InputStream stream = new ASN1InputStream(new ByteArrayInputStream(signature));
ASN1Sequence primitive = (ASN1Sequence)stream.readObject();
Enumeration<ASN1Integer> enumeration = primitive.getObjects();
BigInteger intR = enumeration.nextElement().getValue();
BigInteger intS = enumeration.nextElement().getValue();
byte[] bytes = new byte[LENGTH_32 * 2];
byte[] r = format(intR.toByteArray());
byte[] s = format(intS.toByteArray());
System.arraycopy(r, 0, bytes, 0, LENGTH_32);
System.arraycopy(s, 0, bytes, LENGTH_32, LENGTH_32);
return bytes;
}
private static byte[] format(byte[] value) {
if (value.length == LENGTH_32) {
return value;
} else {
byte[] bytes = new byte[LENGTH_32];
if (value.length > LENGTH_32) {
System.arraycopy(value, value.length - LENGTH_32, bytes, 0, LENGTH_32);
} else {
System.arraycopy(value, 0, bytes, LENGTH_32 - value.length, value.length);
}
return bytes;
}
}
private static byte[] cmbSM4Crypt(byte[] key, byte[] iv, byte[] input, int mode) throws GeneralSecurityException {
SecretKeySpec spec = new SecretKeySpec(key, "SM4");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", BouncyCastleProvider.PROVIDER_NAME);
cipher.init(mode, spec, ivParameterSpec);
return cipher.doFinal(input);
}
private static boolean cmbSM2VerifyWithSM3(byte[] id, byte[] pubkey, byte[] msg, byte[] signature) throws IOException {
if (pubkey == null || msg == null || signature == null) {
throw new IllegalArgumentException("CMBSM2VerifyWithSM3 input error");
}
ECPublicKeyParameters publicKey = encodePublicKey(pubkey);
SM2Signer signer = new SM2Signer();
ParametersWithID parameters = new ParametersWithID(publicKey, id);
signer.init(false, parameters);
signer.update(msg, 0, msg.length);
return signer.verifySignature(encodeDERSignature(signature));
}
private static ECPublicKeyParameters encodePublicKey(byte[] value) {
byte[] x = new byte[LENGTH_32];
byte[] y = new byte[LENGTH_32];
System.arraycopy(value, 1, x, 0, LENGTH_32);
System.arraycopy(value, LENGTH_32 + 1, y, 0, LENGTH_32);
BigInteger intX = new BigInteger(1, x);
BigInteger intY = new BigInteger(1, y);
ECParameterSpec spec = ECNamedCurveTable.getParameterSpec("sm2p256v1");
ECPoint intQ = spec.getCurve().createPoint(intX, intY);
ECDomainParameters ecParameters = new ECDomainParameters(spec.getCurve(), spec.getG(), spec.getN(), spec.getH(), spec.getSeed());
return new ECPublicKeyParameters(intQ, ecParameters);
}
private static byte[] encodeDERSignature(byte[] signature) throws IOException {
byte[] r = new byte[LENGTH_32];
byte[] s = new byte[LENGTH_32];
System.arraycopy(signature, 0, r, 0, LENGTH_32);
System.arraycopy(signature, LENGTH_32, s, 0, LENGTH_32);
ASN1EncodableVector vector = new ASN1EncodableVector();
vector.add(new ASN1Integer(new BigInteger(1, r)));
vector.add(new ASN1Integer(new BigInteger(1, s)));
return (new DERSequence(vector)).getEncoded();
}
}