跳转到主要内容

10.1 签名工具类(SignUtils)

平台回调商户时,请求头携带 TimestampSignatureSign-Version(V1)、Timezone(UTC+8)。 签名对 data 对象(JSON 字符串)计算,不含 顶层 idaction 字段。
/**
 * 签名工具类 
 * 算法:HMAC-SHA256,输出小写十六进制
 */
public class SignUtils {

    public static String sign(String appId, String timestamp, String body, String secretKey) throws Exception {
        return sign(appId, timestamp, buildBodyMap(body), secretKey);
    }

    public static String sign(String appId, String timestamp, Map<String, String> body, String secretKey) throws Exception {
        TreeMap<String, String> params = new TreeMap<>();
        params.put("appId", appId);
        params.put("timestamp", timestamp);
        if (body != null && !body.isEmpty()) {
            params.putAll(body);
        }
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : params.entrySet()) {
            if (entry.getValue() == null) continue;
            if (sb.length() > 0) sb.append("&");
            sb.append(entry.getKey()).append("=").append(entry.getValue());
        }
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        return bytesToHex(mac.doFinal(sb.toString().getBytes(StandardCharsets.UTF_8)));
    }

    public static Map<String, String> buildBodyMap(String body) {
        Map<String, String> bodyMap = new TreeMap<>();
        if (StringUtils.isBlank(body)) return bodyMap;
        JSONObject json = JSONObject.parseObject(body);
        for (Map.Entry<String, Object> e : json.entrySet()) {
            if (e.getValue() == null) continue;
            Object v = e.getValue();
            bodyMap.put(e.getKey(), v instanceof String ? (String) v : JSONObject.toJSONString(v));
        }
        return bodyMap;
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}
  • 参与签名的公共参数仅 appIdtimestampdata 内各字段;顶层 idaction 不参与签名。
  • buildBodyMappublic 方法,商户验签时可复用相同逻辑。

10.2 商户敏感数据加解密工具类

适用场景

接口方向说明
GET /card/{card_no}/private/info服务端 → 商户响应 data 为加密字符串,商户解密后得 JSON
POST /card/active商户 → 服务端请求 cvv 须为加密后的 Base64 密文
POST /card/pin/set商户 → 服务端请求 pin 须为加密后的 Base64 密文

算法说明

算法AES-256
模式/填充CBC / PKCS5Padding(每次加密随机 16 字节 IV)
密钥派生SHA-256(apiSecret UTF-8) → 32 字节 AES 密钥(apiSecret 为变长字符串,不可直接作为 AES Key)
密文格式Base64( IV[16字节] ‖ AES-CBC密文 )
字符集UTF-8(明文)
设置 PIN 明文须 6 位数字且不可含三位及以上相同或连续数字,否则返回 4016实体卡激活明文 CVV 须 3 位纯数字;解密失败返回 4015 / 4019

工具类代码

/**
 * 敏感字段加解密:PIN / CVV / 卡隐私 JSON
 */
public class MerchantEncryptUtil {

    private static final String KEY_ALGORITHM = "AES";

    private static final String CIPHER_ALGORITHM_CBC = "AES/CBC/PKCS5Padding";

    private static final int IV_LENGTH = 16;

    private static final SecureRandom SECURE_RANDOM = new SecureRandom();

    public static String encrypt(String data, String secretKey) {
        if (StringUtils.isAnyBlank(data, secretKey)) {
            log.error("MerchantEncryptUtil encrypt param is blank");
            return null;
        }
        try {
            byte[] iv = new byte[IV_LENGTH];
            SECURE_RANDOM.nextBytes(iv);
            IvParameterSpec ivSpec = new IvParameterSpec(iv);

            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM_CBC);
            cipher.init(Cipher.ENCRYPT_MODE, buildAesKey(secretKey), ivSpec);
            byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));

            byte[] combined = new byte[IV_LENGTH + encrypted.length];
            System.arraycopy(iv, 0, combined, 0, IV_LENGTH);
            System.arraycopy(encrypted, 0, combined, IV_LENGTH, encrypted.length);

            return Base64.encodeBase64String(combined);
        } catch (Exception e) {
            return null;
        }
    }

    public static String decrypt(String encryptedData, String secretKey) {
        if (StringUtils.isAnyBlank(encryptedData, secretKey)) {
            log.error("MerchantEncryptUtil decrypt param is blank");
            return null;
        }
        byte[] combined = Base64.decodeBase64(encryptedData);

        try {
            byte[] iv = Arrays.copyOfRange(combined, 0, IV_LENGTH);
            byte[] cipherBytes = Arrays.copyOfRange(combined, IV_LENGTH, combined.length);
            IvParameterSpec ivSpec = new IvParameterSpec(iv);

            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM_CBC);
            cipher.init(Cipher.DECRYPT_MODE, buildAesKey(secretKey), ivSpec);
            byte[] decrypted = cipher.doFinal(cipherBytes);
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception cbcEx) {
            log.warn("MerchantEncryptUtil CBC decrypt failed, fallback to ECB (legacy data): {}", cbcEx.getMessage());
        }
        return null;
    }

    private static SecretKeySpec buildAesKey(String secretKey) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] keyBytes = digest.digest(secretKey.getBytes(StandardCharsets.UTF_8));
            return new SecretKeySpec(keyBytes, KEY_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("SHA-256 not available", e);
        }
    }
}

调用示例

1)实体卡激活 — 商户加密 CVV 后上送
String apiSecret = "your_api_secret_plaintext";
String plainCvv = "123";
String encryptedCvv = MerchantEncryptUtil.encrypt(plainCvv, apiSecret);

// 请求体
// {
//   "card_no": "...",
//   "cardholder_no": "...",
//   "cvv": "<encryptedCvv>",
//   "card_last_no": "1234"
// }
2)设置实体卡 PIN — 商户加密后上送
String apiSecret = "your_api_secret_plaintext";
String plainPin = "123456";
String encryptedPin = MerchantEncryptUtil.encrypt(plainPin, apiSecret);

// 请求体
// { "card_no": "...", "cardholder_no": "...", "pin": "<encryptedPin>" }
3)获取卡隐私 JSON — 商户解密响应 data
String apiSecret = "your_api_secret_plaintext";
String encryptedData = response.getData(); // HTTP 响应中的 data 字段
String json = MerchantEncryptUtil.decrypt(encryptedData, apiSecret);
// json 为卡隐私信息 JSON 字符串

相关错误码

code中文描述
1013未开通 PCI 敏感卡展示能力
4015PIN 码解密失败,请检查加密方式与密钥
4018未找到可用的 API 凭证
4019CVV 解密失败,请检查加密方式与密钥