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]
Option 1
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
Constantsclass is assumed to contain values likesecuredSvcUrl,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:
- Generate a JWT Token: Authenticate with the server to obtain a token for subsequent requests.
- Card Lookup (Optional): If using an external ID (
EXID), retrieve the Primary Account Number (PAN). - Obtain PIN Certificate: Fetch a public certificate to encrypt the PIN.
- Encrypt the PIN: Generate a PIN block and encrypt it using the certificate's public key.
- 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_tokenusing 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, andgrant_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:
- Generate an RSA key pair and a self-signed certificate.
- Send the certificate and
cardIdto the/sdk/v2/cards/lookupendpoint. - Decrypt the response’s
cardIdentifierIdusing 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
PinCertificateResponseobject 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:
- Generate a PIN Block: Combine the PIN with the PAN using the ISO-0 PIN block format.
- 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:
- Format the PIN block:
"0+ PIN length + PIN + padding F’s"(e.g.,"041234FFFFFFFFFF"`). - Extract the last 12 digits of the PAN plus 4 leading zeros (e.g.,
"0000567890123456"). - XOR the two hexadecimal strings.
- Format the PIN block:
- 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, andencryptedPin. - 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
SetPinDemoand runsexecPinSetFlow().
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
generatePinBlockwill throw an exception. - Dependencies: Verify all required libraries are included in your project.
Updated 3 months ago