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
| Interface | Direction | Description |
|---|
GET /card/{card_no}/private/info | Server → Merchant | Response data is encrypted string, merchant decrypts to get JSON |
POST /card/active | Merchant → Server | Request cvv must be encrypted Base64 ciphertext |
POST /card/pin/set | Merchant → Server | Request pin must be encrypted Base64 ciphertext |
Algorithm Description
| Item | Value |
|---|
| Algorithm | AES-256 |
| Mode/Padding | CBC / PKCS5Padding (16-byte random IV per encryption) |
| Key Derivation | SHA-256(apiSecret UTF-8) → 32-byte AES key (apiSecret is variable-length, cannot be used directly as AES Key) |
| Ciphertext Format | Base64( IV[16 bytes] ‖ AES-CBC ciphertext ) |
| Charset | UTF-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
| code | Description |
|---|
| 1013 | PCI sensitive card display is not enabled |
| 4015 | PIN decryption failed, check encryption method and key |
| 4018 | No available API credential found |
| 4019 | CVV decryption failed, check encryption method and key |