Set PIN - [Non-PCI DSS]

This document illustrates the sequence of API calls required to set card PIN for financial institutions that are non-compliant and can't store ZPK (Zone PIN Key).

Those flows apply to any card type or product.

Option 1:

Option 1 is about using Network mobile SDK, in which Network SDK will send the request directly to Network middleware

Set PIN [Non-PCI DSS]

Set PIN [Non-PCI DSS]

Option 1

User
User
Issuer App
Issuer App
Issuer App
NI SDK
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, set PIN Call SDK Set PIN Prompt PIN set form Enter the PIN GET sdk/v2/security/pin_certificate Return response HTTP 200 POST sdk/v2/cards/lookup Return response HTTP 200 Generte PIN Block Encrypted under Public Key POST sdk/v2/security/set_pin Return response HTTP 200 Return response Return response

Set PIN Flow

This guide explains the process of setting a PIN for a card using a Java-based implementation. The flow involves generating a JWT token, performing a card lookup (if needed), obtaining a PIN certificate, encrypting the PIN, and submitting the encrypted PIN to the server. Below, we break down each step in detail with code examples and explanations.

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).
  • Constants: A Constants class is assumed to contain values like securedSvcUrl, financialId, channelId, clientId, clientSecret, etc.
  • Network Access: The code makes HTTPS requests to external APIs (e.g., https://apiuat.za.network.global/v1/tokenkc/generate).

Overview of the Set 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 PIN Certificate: Fetch a public certificate to encrypt the PIN.
  4. Encrypt the PIN: Generate a PIN block and encrypt it using the certificate's public key.
  5. Set the PIN: Submit the encrypted PIN to the server.

Here’s the main entry point of the flow:

public void execPinSetFlow() {
    String jwtToken = TokenUtils.generateToken(); // Step 1
    String pan = identifierType.equals("EXID") ? cardLookup(cardId, jwtToken) : cardId; // Step 2
    PinCertificateResponse pinCertificateResponse = pinCertificate(jwtToken); // Step 3
    X509Certificate sdkCert = parseCertificateString(pinCertificateResponse.getCertificate());
    String pinBlock = PINBlockUtils.generatePinBlock(newPin, pan);
    String encryptedPinBlock = RsaUtil.encryptDataUnderPublicKey(sdkCert, pinBlock); // Step 4
    pinSet(cardId, encryptedPinBlock, 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 cardLookup method retrieves the PAN using the provided cardId.

private String cardLookup(String cardId, String jwtToken) {
    KeyPair keyPair = RsaUtil.generateKeyPair();
    X509Certificate x509Certificate = RsaUtil.generateSelfSignedCertificate(keyPair);
    String certString = new String(Base64.getEncoder().encode(x509Certificate.getEncoded()));
    PrivateKey privateKey = keyPair.getPrivate();

    CardLookupRequest lookupRequest = new CardLookupRequest(certString, "EXID", cardId);
    String body = objectMapper.writeValueAsString(lookupRequest);
    URI uri = new URI("https", Constants.securedSvcUrl, "/sdk/v2/cards/lookup", null, null);

    HttpRequest request = HttpRequest.newBuilder()
        .uri(uri)
        .header("Content-Type", "application/json")
        .header("Authorization", "Bearer " + jwtToken)
        .header("Financial-Id", Constants.financialId)
        .header("Channel-Id", Constants.channelId)
        .header("Unique-Reference-Code", String.valueOf(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)))
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();

    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    CardLookupResposne cardLookupResposne = objectMapper.readValue(response.body(), CardLookupResposne.class);
    String pan = decodeHexToAscii(RsaUtil.decryptWithPrivateKey(cardLookupResposne.getCardIdentifierId(), privateKey));
    return pan;
}
  • Purpose: Maps an external ID (EXID) to a PAN.
  • Key Steps:
    1. Generate an RSA key pair and a self-signed certificate.
    2. Send the certificate and cardId to the /sdk/v2/cards/lookup endpoint.
    3. Decrypt the response’s cardIdentifierId using the private key to obtain the PAN.
  • Endpoint: https://{Constants.securedSvcUrl}/sdk/v2/cards/lookup.
  • 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 PIN Certificate

The pinCertificate method fetches a public certificate used to encrypt the PIN.

private PinCertificateResponse pinCertificate(String jwtToken) {
    URI uri = new URI("https", Constants.securedSvcUrl, "/sdk/v2/security/pin_certificate", null, null);
    HttpRequest request = HttpRequest.newBuilder()
        .uri(uri)
        .header("Content-Type", "application/json")
        .header("Authorization", "Bearer " + jwtToken)
        .header("Financial-Id", Constants.financialId)
        .header("Channel-Id", Constants.channelId)
        .header("Unique-Reference-Code", String.valueOf(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)))
        .GET()
        .build();

    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    return objectMapper.readValue(response.body(), PinCertificateResponse.class);
}
  • Purpose: Retrieves an X.509 certificate for PIN encryption.
  • Endpoint: https://{Constants.securedSvcUrl}/sdk/v2/security/pin_certificate.
  • Headers: Similar to the card lookup request.
  • Output: A PinCertificateResponse object containing a Base64-encoded certificate string.

The certificate is parsed into an X509Certificate object:

public X509Certificate parseCertificateString(String certificateString) throws CertificateException {
    byte[] decoded = Base64.getDecoder().decode(certificateString);
    return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(decoded));
}

Step 4: Encrypt the PIN

The 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 encryptDataUnderPublicKey(final X509Certificate certificate, final String data) {
    Cipher encryptCipher = Cipher.getInstance("RSA");
    encryptCipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
    byte[] secretMessageBytes = ISOUtil.hex2byte(data);
    byte[] encryptedMessageBytes = encryptCipher.doFinal(secretMessageBytes);
    return ISOUtil.byte2hex(encryptedMessageBytes);
}
  • Input: The PIN block and the X509Certificate.
  • Process: RSA encryption with the certificate’s public key.
  • Output: An encrypted hex string (e.g., "1A2B3C4D...").

Step 5: Set the PIN

The pinSet method submits the encrypted PIN to the server.

private void pinSet(String cardId, String encryptedPin, String jwtToken) {
    SetPinRequest pinSetRequest = new SetPinRequest(identifierType, cardId, encryptedPin);
    String body = objectMapper.writeValueAsString(pinSetRequest);
    URI uri = new URI("https", Constants.securedSvcUrl, "/sdk/v2/security/set_pin", null, null);

    HttpRequest request = HttpRequest.newBuilder()
        .uri(uri)
        .header("Content-Type", "application/json")
        .header("Authorization", "Bearer " + jwtToken)
        .header("Financial-Id", Constants.financialId)
        .header("Channel-Id", Constants.channelId)
        .header("Unique-Reference-Code", String.valueOf(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)))
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();

    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    if (response.statusCode() != 200) {
        throw new RuntimeException("Set Pin returned unexpected status code: " + response.statusCode());
    }
}
  • Purpose: Submits the encrypted PIN to set it for the card.
  • Endpoint: https://{Constants.securedSvcUrl}/sdk/v2/security/set_pin.
  • Body: JSON payload with identifierType, cardId, and encryptedPin.
  • Headers: Includes JWT token and other identifiers.
  • Output: A successful response (HTTP 200) indicates the PIN is set.

Running the Code

To execute the flow, use the main method:

public static void main(String[] args) {
    SetPinDemo setPinDemo = new SetPinDemo();
    setPinDemo.execPinSetFlow();
}
  • Execution: Instantiates SetPinDemo and runs execPinSetFlow().

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.