最近支付公司和银联要求需要对PID上送格式做加密处理, 梳理几个重要逻辑帮助各服务商完成改造流程。
总的来说分为以下几步:
- 按要求整理待加密数据内容,该补位补位,注意左右,输出formatPidAndOrderStr;
- 形成的MAC ELEMEMENT BLOCK(MAB)按每 16 个字节做异或,如果最后不满 16 个字节,则添加 0x00
- 16 个字节(RESULT BLOCK)转换成 32 个 HEXDECIMAL,然后再分为两组(文档写的需要ASCII 字符, 我实际测试分组一不需要转换直接加密是正确的)
- 将组1进行SM4加密得到【结果】 & 【组2结果】 做异或得到 result2;
- result2再次SM4加密截取前8位即可。
完整代码( com.jeequan.jeepay.core.utils.JeepayPIDKit.java )
/*
* Copyright (c) 2021-2031, 河北计全科技有限公司 (https://www.jeequan.com & jeequan@126.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jeequan.jeepay.core.utils;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SmUtil;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.DecimalFormat;
import java.util.Arrays;
/***
* 根据pid等信息获取pidSct值
*
* @author terrfly#东南飞
* @site https://www.jeequan.com
* @date 2025/9/17 11:20
*/
public class JeepayPIDKit {
private final static String DES = "DES";
private final static String CIPHER_ALGORITHM = "DES/ECB/NoPadding";
public static void main(String[] args) {
String pid = "C1009999";
String payOrderId = "1220250321034502483875104";
Long amount = 123L;
String key = "F1774E4EDB4396B1647304EB4A6A4E3D";
System.out.println(JeepayPIDKit.calculatePidSct(pid, payOrderId, amount, key));
}
/**
* 功能描述:
生成加密字符串
* 在开展加密运算时,服务商 PID 代码长度为 11 位,左
* 对齐,右补空格;服务商订单编号长度为 40 位,左对齐,
* 右补空格;订单金额长度为 12 位,以分为单位,右对齐,
* 左补 0
* @param pid 服务商PID(11位)
* @param payOrderId 订单编号(40位)
* @param amount 订单金额(以分为单位)
* @return 格式化后的字符串
* @Author: terrfly#东南飞
* @Date: 2025/9/17 11:10
*/
public static String formatPidAndOrder(String pid, String payOrderId, Long amount) {
// 格式化PID(11位左对齐右补空格), 若超过11位则固定11位
String formattedPid = String.format("%-11s", pid.length() > 11 ? pid.substring(0, 11) : pid);
// 格式化订单编号(40位左对齐右补空格)
String formattedOrderId = String.format("%-40s", payOrderId.length() > 40 ? payOrderId.substring(0, 40) : payOrderId);
// 格式化金额(12位右对齐左补0)
DecimalFormat df = new DecimalFormat("000000000000");
String formattedAmount = df.format(amount);
return formattedPid + formattedOrderId + formattedAmount;
}
/**
* 处理MAB块:按16字节分组异或,不足补0x00
* @param data 输入数据
* @return 处理后的16字节结果
*/
public static byte[] processMAB(byte[] data) {
byte[] result = new byte[16];
Arrays.fill(result, (byte)0);
// 计算完整16字节块的数量
int fullBlocks = data.length / 16;
int remaining = data.length % 16;
// 处理完整块
for (int i = 0; i < fullBlocks; i++) {
int offset = i * 16;
for (int j = 0; j < 16; j++) {
result[j] ^= data[offset + j];
}
}
// 处理剩余不足16字节的部分
if (remaining > 0) {
int offset = fullBlocks * 16;
for (int j = 0; j < remaining; j++) {
result[j] ^= data[offset + j];
}
// 剩余部分补0x00
for (int j = remaining; j < 16; j++) {
result[j] ^= 0x00;
}
}
return result;
}
/**
* 功能描述:
* 将字节数组转换为十六进制字符串
* @Author: terrfly#东南飞
* @Date: 2025/9/16 17:11
*/
public static String hexToAsciiConverter(String hex) {
StringBuilder ascii = new StringBuilder();
for (int i = 0; i < hex.length(); i++) {
char c = hex.charAt(i);
ascii.append(Integer.toHexString((int) c));
}
return ascii.toString();
}
/**
* 功能描述:
生成pidSCT
* @param pid 服务商PID(11位)
* @param payOrderId 订单编号(40位)
* @param amount 订单金额(以分为单位)
* @param key 服务商秘钥
* @return 生成pidSCT
* @Author: terrfly#东南飞
* @Date: 2025/9/17 11:22
*/
public static String calculatePidSct(String pid, String payOrderId, Long amount, String key){
return JeepayPIDKit.calculatePidSct(JeepayPIDKit.formatPidAndOrder(pid, payOrderId, amount), key);
}
/**
* 功能描述:
生成pidSCT
* @param formatPidAndOrderStr 格式化pid和订单信息
* @param key 服务商秘钥
* @return 生成pidSCT
* @Author: terrfly#东南飞
* @Date: 2025/9/17 11:22
*/
public static String calculatePidSct(String formatPidAndOrderStr, String key){
byte[] result_block_hexdecimal_array = JeepayPIDKit.processMAB(formatPidAndOrderStr.getBytes());
String result_block_hexdecimal_str = JeepayPIDKit.bytesToHexString(result_block_hexdecimal_array);
String resultENC = SmUtil.sm4(HexUtil.decodeHex(key)).encryptHex(result_block_hexdecimal_str.substring(0, 16)).substring(0, 32).toUpperCase();
String resultXOR = HexUtil.encodeHexStr(bytesXOR(HexUtil.decodeHex(resultENC), HexUtil.decodeHex(hexToAsciiConverter(result_block_hexdecimal_str.substring(16))))).toUpperCase();
String result3 = SmUtil.sm4(HexUtil.decodeHex(key)).encryptHex(HexUtil.decodeHex((resultXOR))).substring(0, 32).toUpperCase();
return result3.substring(0, 8);
}
// 将字节数组转换为十六进制字符串的函数
public static String bcd2Str(byte[] b) {
char[] HEX_DIGITS = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
StringBuilder sb = new StringBuilder(b.length * 2);
for (int i = 0; i < b.length; ++i) {
sb.append(HEX_DIGITS[(b[i] & 240) >>> 4]);
sb.append(HEX_DIGITS[b[i] & 15]);
}
return sb.toString();
}
//将一个字符串转换为BCD码(二进码十进数)的字节数组。
//BCD码是一种用二进制编码十进制数字的编码方式,通常用4位二进制数表示一个十进制数字(0-9)
public static byte[] str2Bcd(String asc) {
int len = asc.length();
int mod = len % 2;
if (mod != 0) {
asc = "0" + asc;
len = asc.length();
}
byte[] abt = new byte[len];
if (len >= 2) {
len /= 2;
}
byte[] bbt = new byte[len];
abt = asc.getBytes();
for (int p = 0; p < asc.length() / 2; ++p) {
int j;
if (abt[2 * p] >= 97 && abt[2 * p] <= 122) {
j = abt[2 * p] - 97 + 10;
} else if (abt[2 * p] >= 65 && abt[2 * p] <= 90) {
j = abt[2 * p] - 65 + 10;
} else {
j = abt[2 * p] - 48;
}
int k;
if (abt[2 * p + 1] >= 97 && abt[2 * p + 1] <= 122) {
k = abt[2 * p + 1] - 97 + 10;
} else if (abt[2 * p + 1] >= 65 && abt[2 * p + 1] <= 90) {
k = abt[2 * p + 1] - 65 + 10;
} else {
k = abt[2 * p + 1] - 48;
}
int a = (j << 4) + k;
byte b = (byte) a;
bbt[p] = b;
}
return bbt;
}
/**
* 加密
*
* @param src 数据源
* @param key 密钥,长度必须是8的倍数
* @return 返回加密后的数据
*/
public static byte[] encrypt(byte[] src, byte[] key) {
SecureRandom sr = new SecureRandom();
try {
DESKeySpec dks = new DESKeySpec(key);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
SecretKey securekey = keyFactory.generateSecret(dks);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, securekey, sr);
return cipher.doFinal(src);
} catch (Exception e) {
}
return null;
}
/**
* 生成密钥
*
* @return
* @throws NoSuchAlgorithmException
*/
public static byte[] initKey() throws NoSuchAlgorithmException {
KeyGenerator kg = KeyGenerator.getInstance(DES);
kg.init(16);
SecretKey secretKey = kg.generateKey();
return secretKey.getEncoded();
}
/**
* 解密
*
* @param src 数据源
* @param key 密钥,长度必须是8的倍数
* @return 返回解密后的原始数据
*/
public static byte[] decrypt(byte[] src, byte[] key) {
SecureRandom sr = new SecureRandom();
try {
DESKeySpec dks = new DESKeySpec(key);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
SecretKey securekey = keyFactory.generateSecret(dks);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, securekey, sr);
return cipher.doFinal(src);
} catch (Exception e) {
}
return null;
}
/**
* mac计算
*
* @param key mac秘钥
* @param Input 待加密数据
*
*使用异或和DES加密生成MAC的过程。
* 函数的大致步骤:
*
* 对输入数据进行补位(填充至8的倍数)
*
* 将数据分成8字节的块
*
* 初始块与后续块依次异或,结果作为下一次异或的输入
*
* 将最终的8字节异或结果转换为16字节的十六进制字符串
*
* 取前8字节用密钥加密,然后与后8字节异或
*
* 对异或结果再用密钥加密,取结果的前8字节转换为十六进制,再取前8字节作为MAC
*
* 注意:代码中引用了JeepayPIDKit.encrypt和bytesToHexString,我们假设这些函数已正确实现。
* @return
*/
public static byte[] getMac(byte[] key, byte[] Input) {
int length = Input.length;
int x = length % 8;
// 需要补位的长度
int addLen = 0;
if (x != 0) {
addLen = 8 - length % 8;
}
int pos = 0;
// 原始数据补位后的数据
byte[] data = new byte[length + addLen];
System.arraycopy(Input, 0, data, 0, length);
byte[] oper1 = new byte[8];
System.arraycopy(data, pos, oper1, 0, 8);
pos += 8;
// 8字节异或
for (int i = 1; i < data.length / 8; i++) {
byte[] oper2 = new byte[8];
System.arraycopy(data, pos, oper2, 0, 8);
byte[] t = bytesXOR(oper1, oper2);
oper1 = t;
pos += 8;
}
// 将异或运算后的最后8个字节(RESULT BLOCK)转换成16个HEXDECIMAL:
byte[] resultBlock = bytesToHexString(oper1).getBytes();
// 取前8个字节MAK加密
byte[] front8 = new byte[8];
System.arraycopy(resultBlock, 0, front8, 0, 8);
byte[] behind8 = new byte[8];
System.arraycopy(resultBlock, 8, behind8, 0, 8);
byte[] desfront8 = JeepayPIDKit.encrypt(front8, key);
// 将加密后的结果与后8 个字节异或:
byte[] resultXOR = bytesXOR(desfront8, behind8);
// 用异或的结果TEMP BLOCK 再进行一次单倍长密钥算法运算
byte[] buff = JeepayPIDKit.encrypt(resultXOR, key);
// 将运算后的结果(ENC BLOCK2)转换成16 个HEXDECIMAL asc
byte[] retBuf = new byte[8];
// 取8个长度字节就是mac值
System.arraycopy(bytesToHexString(buff).getBytes(), 0, retBuf, 0, 8);
return retBuf;
}
/**
* 单字节异或
*
* @param src1
* @param src2
* @return
*/
public static byte byteXOR(byte src1, byte src2) {
return (byte) ((src1 & 0xFF) ^ (src2 & 0xFF));
}
/**
* 字节数组异或
*
* @param src1
* @param src2
* @return
*/
public static byte[] bytesXOR(byte[] src1, byte[] src2) {
int length = src1.length;
if (length != src2.length) {
return null;
}
byte[] result = new byte[length];
for (int i = 0; i < length; i++) {
result[i] = byteXOR(src1[i], src2[i]);
}
return result;
}
/**
* 字节数组转HEXDECIMAL
*
* @param bArray
* @return
*/
public static final String bytesToHexString(byte[] bArray) {
StringBuffer sb = new StringBuffer(bArray.length);
String sTemp;
for (int i = 0; i < bArray.length; i++) {
sTemp = Integer.toHexString(0xFF & bArray[i]);
if (sTemp.length() < 2)
sb.append(0);
sb.append(sTemp.toUpperCase());
}
return sb.toString();
}
}
文档更新时间: 2025-10-10 11:36 作者:马腾飞