> ## Documentation Index
> Fetch the complete documentation index at: https://docs-payment-merchant.keysecure.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Code Utilities

## 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.

```java theme={null}
/**
 * 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();
    }
}
```

<Note>
  * 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.
</Note>

## 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)                                                                                                   |

<Note>
  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**.
</Note>

### Utility Class Code

```java theme={null}
/**
 * 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**

```java theme={null}
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**

```java theme={null}
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**

```java theme={null}
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
```

### Related Error Codes

| 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 |
