最近支付公司和银联要求需要对PID上送格式做加密处理, 梳理几个重要逻辑帮助各服务商完成改造流程。

总的来说分为以下几步:

  1. 按要求整理待加密数据内容,该补位补位,注意左右,输出formatPidAndOrderStr;
  2. 形成的MAC ELEMEMENT BLOCK(MAB)按每 16 个字节做异或,如果最后不满 16 个字节,则添加 0x00
  3. 16 个字节(RESULT BLOCK)转换成 32 个 HEXDECIMAL,然后再分为两组(文档写的需要ASCII 字符, 我实际测试分组一不需要转换直接加密是正确的)
  4. 将组1进行SM4加密得到【结果】 & 【组2结果】 做异或得到 result2;
  5. 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   作者:马腾飞