Change PIN [PCI DSS]

This document illustrates the sequence of API calls required to change card PIN for financial institutions that can store ZPK.

This flow applies to any card type or product.

Change PIN [PCI DSS]

Change PIN [PCI DSS]

User
User
Issuer App
Issuer App
Issuer Middleware
Issuer Middleware
Network
Network
Login Get list of cards GET /cards/search Return response HTTP 200 Return response Show user list of cards Select card, change PIN Enter old and new PIN Forward request Generate PIN block for old and new PIN Encrypted under ZPK POST /cards/{id}/change-pin Return response HTTP 200 Return response Return response

Change PIN Flow (ZPK + 3DES)

This guide explains setting a PIN using a ZPK (Zone PIN Key) and Triple DES. The flow generates a JWT, optionally looks up the PAN, obtains the ZPK (e.g., from Vault/keystore/config), builds the ISO-9564-0 PIN block, encrypts it with 3DES/ECB/NoPadding, and submits it.

Prerequisites

  • Java Environment: Ensure you have a JDK installed (e.g., JDK 11 or higher).
  • Dependencies: The code relies on libraries such as Jackson (com.fasterxml.jackson.databind), Bouncy Castle (org.bouncycastle), SLF4J for logging, and the Java HTTP Client (java.net.http).
  • Crypto: Uses the JDK’s javax.crypto (DESede/ECB/NoPadding)—no extra crypto deps needed.
  • Constants: A Constants class is assumed to contain values like securedSvcUrl, financialId, channelId, clientId, clientSecret, zpk, etc.
  • Network Access: The code makes HTTPS requests to external APIs (e.g., https://apiuat.za.network.global/v1/tokenkc/generate).

Overview of the Change PIN Flow

The flow consists of the following steps:

  1. Generate a JWT Token: Authenticate with the server to obtain a token for subsequent requests.
  2. Card Lookup (Optional): If using an external ID (EXID), retrieve the Primary Account Number (PAN).
  3. Obtain ZPK: From Vault/keystore/config.
  4. Encrypt the PIN under ZPK: Generate a PIN block and encrypt it using (3DES/ECB/NoPadding) algorithm.
  5. Change the PINs: Submit the encrypted PINs to the server.

Here’s the main entry point of the flow:

public void execPinChangeFlow() {
    String jwtToken = TokenUtils.generateToken(); // Step 1
    String pan = identifierType.equals("EXID") ? cardLookup(cardId, jwtToken) : cardId; // Step 2
    String zpk = getZpkHex(); // Step 3
  	String oldPinBlock = PINBlockUtils.generatePinBlock(oldPin, pan);
		String newPinBlock = PINBlockUtils.generatePinBlock(newPin, pan);
  	String encryptedOldPinBlock = encryptPinBlockUnderZpk(oldPinBlock, zpk); // Step 4
		String encryptedNewPinBlock = encryptPinBlockUnderZpk(newPinBlock, zpk);
    pinChange(cardId, encryptedOldPinBlock, encryptedNewPinBlock, jwtToken); // Step 5
}

Step-by-Step Explanation

Step 1: Generate a JWT Token

The TokenUtils.generateToken() method authenticates with an external API to obtain a JWT token, which is used for authorization in subsequent requests.

public static String generateToken() throws URISyntaxException {
    HttpClient client = HttpClient.newHttpClient();
    Map<String, String> parameters = new HashMap<>();
    parameters.put("client_id", Constants.clientId);
    parameters.put("client_secret", Constants.clientSecret);
    parameters.put("grant_type", "client_credentials");

    String form = parameters.entrySet()
        .stream()
        .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
        .collect(Collectors.joining("&"));

    URI uri = new URI("https", Constants.basePath, "/v1/tokenkc/generate", null, null);

    HttpRequest request = HttpRequest.newBuilder()
        .uri(uri)
        .header("Content-Type", "application/x-www-form-urlencoded")
        .header("NI-apiuat_za_network_global", "qWyQt3D44Upner1T")
        .POST(HttpRequest.BodyPublishers.ofString(form))
        .build();

    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() != 200) {
        throw new RuntimeException("Unexpected status code: " + response.statusCode());
    }
    return JsonPath.parse(response.body()).read("$.access_token");
}
  • Purpose: Obtains an OAuth 2.0 access_token using client credentials.
  • Endpoint: https://{Constants.basePath}/v1/tokenkc/generate.
  • Headers: Includes a custom Akamai header for routing/security.
  • Body: URL-encoded form data with client_id, client_secret, and grant_type.
  • Output: A JWT token (e.g., eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...).

Step 2: Card Lookup (Optional)

If the identifierType is "EXID", the data endpoint retrieves the PAN using the provided cardId.

private String cardLookup(String cardId, String jwtToken) throws Exception {
    HttpClient client = HttpClient.newHttpClient();

    String path = "/api/v1/cards/" + cardId + "/data?fields=pan";
    URI uri = new URI("https", Constants.basePath, path, null, null);

    HttpRequest request = HttpRequest.newBuilder()
        .uri(uri)
        .GET()
        .header("accept", "application/json")
        .header("Authorization", "Bearer " + jwtToken)
        .build();

    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() != 200) {
        throw new RuntimeException("Card lookup failed: " + response.statusCode());
    }

    // Example response: { "pan": "1234567890123456" }
    JsonNode json = new ObjectMapper().readTree(response.body());
    return json.get("pan").asText();
}
  • Purpose: Maps an external ID (EXID) to a PAN.
  • Endpoint: https://{Constants.basePath}/api/v1/cards/{cardId}/data.
  • Headers: Includes JWT token, financial/channel IDs, and a unique reference code.
  • Output: The decrypted PAN (e.g., 1234567890123456).

Note: If identifierType is "CONTRACT_NUMBER", the cardId is assumed to be the PAN, and this step is skipped.

Step 3: Obtain ZPK

The getZpkHex method should fetch ZPK that used to encrypt the PIN, How you fetch is environment-specific; stubbed here:

public static String getZpkHex() {
  // TODO: Pull from Vault/keystore/KMS or env/config. Must return 32 hex chars (double-length).
  return Constants.zpkHex; // e.g., "0123456789ABCDEFFEDCBA9876543210"
}
  • Purpose: Retrieves 16 bytes (32 hex chars) key to be used on encryption of pinBlock.

Step 4: Encrypt the PINs

Generate two ISO-0 PIN blocks using the same PAN, Each PIN is encrypted in two stages:

  1. Generate a PIN Block: Combine the PIN with the PAN using the ISO-0 PIN block format.
  2. Encrypt the PIN Block: Use the public key from the PIN certificate.

Generate PIN Block

public static String generatePinBlock(String pin, String cardNumber) throws Exception {
    if (pin.length() < 4 || pin.length() > 6) {
        throw new Exception("PIN must be 4-6 digits");
    }
    String pinBlock = String.format("%s%d%s", "0", pin.length(), pin);
    while (pinBlock.length() != 16) {
        pinBlock += "F";
    }
    int cardLen = cardNumber.length();
    String pan = "0000" + cardNumber.substring(cardLen - 13, cardLen - 1);
    return xorHex(pinBlock, pan);
}
  • Input: PIN (e.g., "4321") and PAN (e.g., "1234567890123456").
  • Process:
    1. Format the PIN block: "0 + PIN length + PIN + padding F’s"(e.g.,"041234FFFFFFFFFF"`).
    2. Extract the last 12 digits of the PAN plus 4 leading zeros (e.g., "0000567890123456").
    3. XOR the two hexadecimal strings.
  • Output: A 16-character hex string (e.g., "041DABCD12345678").

Encrypt PIN Block

public static String encryptPinBlockUnderZpk(String pinBlockHex, String zpkHex) {
        byte[] clearBlock = hexToBytes(pinBlockHex);
        byte[] keyBytes   = Hex.decodeStrict(prepareDesKey(zpkHex)); // 24 bytes = K1||K2||K1

        try {
            SecretKey key = new SecretKeySpec(keyBytes, "DESede");
            Cipher cipher = Cipher.getInstance("DESede/ECB/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] ct = cipher.doFinal(clearBlock);
            return Hex.toHexString(ct);
        } catch (Exception e) {
            throw new RuntimeException("3DES encryption failed", e);
        }
}

// ---- Helpers ----

    // ZPK expected as 32 hex chars (16 bytes). Build 24-byte 3DES key = K1||K2||K1.
    private String prepareDesKey(String desKey) {
        desKey = desKey.length() == 32 ? desKey : desKey.substring(1);
        return desKey.substring(0, 16) + desKey.substring(16, 32) + desKey.substring(0, 16);
    }

    private static byte[] hexToBytes(String hex) {
        if (hex.length() % 2 != 0) throw new IllegalArgumentException("Odd hex length.");
        int len = hex.length() / 2;
        byte[] out = new byte[len];
        for (int i = 0; i < len; i++) {
            int hi = Character.digit(hex.charAt(2*i),   16);
            int lo = Character.digit(hex.charAt(2*i+1), 16);
            if (hi < 0 || lo < 0) throw new IllegalArgumentException("Invalid hex.");
            out[i] = (byte)((hi << 4) + lo);
        }
        return out;
    }


}

  • Input: The PIN block and the ZPK.
  • Process: (3DES/ECB/NoPadding) encryption with the ZPK.
  • Output: An encrypted hex string (e.g., "1A2B3C4D...").

Step 5: Change the PIN

The pinChange method submits the encrypted PIN to the server.

private void pinChange(String cardId, String encryptedOldPinBlock, String encryptedNewPinBlock, String jwtToken) throws Exception {
    HttpClient client = HttpClient.newHttpClient();

    String path = "/api/v1/cards/" + cardId + "/change-pin";
    URI uri = new URI("https", Constants.basePath, path, null, null);

  String body = "{ \"pinBlock\": \"" + encryptedNewPinBlock + "\"currentPinBlock\": \"" + encryptedOldPinBlock + "\"}";

    HttpRequest request = HttpRequest.newBuilder()
        .uri(uri)
        .header("accept", "*/*")
        .header("accept-language", "en-US,en;q=0.9")
        .header("content-type", "application/json")
        .header("Authorization", "Bearer " + jwtToken)
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();

    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() != 200) {
        throw new RuntimeException("Change PIN failed: " + response.statusCode());
    }
}
  • Purpose: Submits the encrypted PINs to change it for the card.
  • Endpoint: https://{Constants.basePath}/api/v1/cards/{cardId}/change-pin.
  • Body: JSON payload with pinBlock which is the encrypted pinBlock.
  • Headers: Includes JWT token and other identifiers.
  • Output: A successful response (HTTP 200) indicates the PIN is change.

Running the Code

To execute the flow, use the main method:

public static void main(String[] args) {
    ChangePinDemo changePinDemo = new ChangePinDemo();
    changePinDemo.execPinChangeFlow();
}
  • Execution: Instantiates ChangePinDemo and runs execPinChangeFlow().

Troubleshooting

  • HTTP Errors: Check the response status codes and logs for details.
  • Certificate Issues: Ensure the PIN certificate is valid and properly Base64-encoded.
  • PIN Length: The PIN must be 4-6 digits, or generatePinBlock will throw an exception.
  • Dependencies: Verify all required libraries are included in your project.