View PIN [Non-PCI DSS]
This document illustrates the sequence of API calls required to view 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
View PIN [Non-PCI DSS]
Option 1
View PIN Flow
This guide explains the process of viewing a PIN for a card using a Java-based implementation. The flow involves generating a JWT token, calls the View PIN endpoint, receives a PIN block encrypted under certificate, then decrypts it locally using 3DES/ECB/NoPadding and extracts the PIN. 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 Viewing PIN Flow
The flow consists of the following steps:
- Generate a JWT Token: Authenticate with the server to obtain a token for subsequent requests.
- Generate an RSA key pair locally
- Request View PIN: Send the public key in the View PIN request, PIN response encrypted under it
- Decrypt & Extract PIN: locally decrypt and derive PIN from PIN block (ISO-0).
Here’s the main entry point of the flow:
public void execPinViewFlow() {
String jwtToken = TokenUtils.generateToken(); //Step 1
KeyPair keyPair = RsaUtil.generateKeyPair(); //Step 2
String publicKeyB64 = RsaUtil.publicKeyToBase64(keyPair.getPublic()); // send this
PrivateKey privateKey = keyPair.getPrivate();
ViewPinResponse resp = viewPin(cardId, publicKeyB64, jwtToken);//Step 3
byte[] clearBytes = RsaUtil.decryptBase64(resp.getEncryptedPinData(), privateKey);//Step 4
String pin = new String(clearBytes, StandardCharsets.UTF_8);
}
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: Generate Private/Public Key + Encode Public Key
The generateKeyPair generates key pair public and private so that we can use it to encrypt/decrypt pin.
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
return kpg.generateKeyPair();
}
- Purpose: Create a one-time asymmetric key pair for secure “View PIN” response encryption.
- Output: KeyPair { PublicKey, PrivateKey }
Step 3: View PIN
The viewPin method fetches the PIN encrypted under public key sent in request.
- 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
private ViewPinResponse viewPin(String pan, String publicKeyB64, String jwtToken) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
HttpClient client = HttpClient.newHttpClient();
ViewPinRequest req = new ViewPinRequest("PAN", pan, publicKeyB64);
String body = objectMapper.writeValueAsString(req);
URI uri = new URI("https", Constants.securedSvcUrl, "/sdk/v2/security/view_pin", null, null); // replace if needed
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("View PIN returned unexpected status code: " + response.statusCode());
}
return objectMapper.readValue(response.body(), ViewPinResponse.class);
}
- Purpose: Fetches the encrypted PIN to a specific card.
- Endpoint:
https://{Constants.securedSvcUrl}/sdk/v2/security/view_pin. - Body: JSON payload with
publicKey, andcardId. - Headers: Includes JWT token and other identifiers.
- Output:
encryptedPinData.
Step 4: Decrypt with Private Key
The decryptBase64 method accepts the encrypted PIN and decrypt it using private key.
public static byte[] decryptBase64(String encryptedB64, PrivateKey privateKey) throws Exception {
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedB64);
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
// If your API uses PKCS1 v1.5 instead, change to: "RSA/ECB/PKCS1Padding"
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encryptedBytes);
}
- Purpose: Decrypt the server response locally (PIN never travels in clear).
- Input: Encrypted PIN received from API
encryptedB64, andprivateKey. - Output: Decrypted bytes (either PIN directly, or PIN block hex.
Running the Code
To execute the flow, use the main method:
public static void main(String[] args) throws Exception {
ViewPinDemo viewPinDemo = new ViewPinDemo();
viewPinDemo.execPinViewFlow();
}
- Execution: Instantiates
ViewPinDemoand runsexecPinViewFlow().
Troubleshooting
- HTTP Errors: Check the response status codes and logs for details.
- Certificate Issues: Ensure the PIN certificate is valid and properly Base64-encoded.
- Dependencies: Verify all required libraries are included in your project.
- Wrong PIN output: Ensure PAN used for extraction matches the same PAN used by the server (especially if Step 2 is involved).
Updated 3 months ago