Skip to main content

10.1 Signature Utility Class (SignUtils)

When the platform calls back the merchant, the request headers carry Timestamp, Signature, Sign-Version (V1), and Timezone (UTC+8). The signature is calculated on the data object (JSON string), excluding the top-level id and action fields.
/**
 * Signature utility class
 * Algorithm: HMAC-SHA256, output in lowercase hexadecimal
 */
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();
    }
}
  • Parameters involved in signature: appId, timestamp, and all fields in data; top-level id and action are not involved in signature.
  • buildBodyMap is a public method that can be reused by merchants during signature verification.

10.2 Merchant Sensitive Data Encryption/Decryption Utility (MerchantEncryptUtil)

Use Cases

InterfaceDirectionDescription
GET /card/{card_no}/private/infoServer → MerchantResponse data is encrypted string, merchant decrypts to get JSON
POST /card/activeMerchant → ServerRequest cvv must be encrypted Base64 ciphertext
POST /card/pin/setMerchant → ServerRequest pin must be encrypted Base64 ciphertext

Algorithm Description

ItemValue
AlgorithmAES-256
Mode/PaddingCBC / PKCS5Padding (16-byte random IV per encryption)
Key DerivationSHA-256(apiSecret UTF-8) → 32-byte AES key (apiSecret is variable-length, cannot be used directly as AES Key)
Ciphertext FormatBase64( IV[16 bytes] ‖ AES-CBC ciphertext )
CharsetUTF-8 (plaintext)
PIN plaintext must be 6 digits and cannot contain three or more identical or consecutive digits, otherwise returns 4016.Physical card activation plaintext CVV must be 3 pure digits; decryption failure returns 4015 / 4019.

Utility Class Code

/**
 * Sensitive field encryption/decryption: PIN / CVV / Card privacy 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);
        }
    }
}

Usage Examples

1) Physical Card Activation — Merchant Encrypts CVV Before Submission
String apiSecret = "your_api_secret_plaintext";
String plainCvv = "123";
String encryptedCvv = MerchantEncryptUtil.encrypt(plainCvv, apiSecret);

// Request body
// {
//   "card_no": "...",
//   "cardholder_no": "...",
//   "cvv": "<encryptedCvv>",
//   "card_last_no": "1234"
// }
2) Set Physical Card PIN — Merchant Encrypts Before Submission
String apiSecret = "your_api_secret_plaintext";
String plainPin = "123456";
String encryptedPin = MerchantEncryptUtil.encrypt(plainPin, apiSecret);

// Request body
// { "card_no": "...", "cardholder_no": "...", "pin": "<encryptedPin>" }
3) Get Card Privacy JSON — Merchant Decrypts Response Data
String apiSecret = "your_api_secret_plaintext";
String encryptedData = response.getData(); // data field from HTTP response
String json = MerchantEncryptUtil.decrypt(encryptedData, apiSecret);
// json is card privacy information JSON string
codeDescription
1013PCI sensitive card display is not enabled
4015PIN decryption failed, check encryption method and key
4018No available API credential found
4019CVV decryption failed, check encryption method and key