In this article, we will see how to integrate with the Visa services or exchange data with the them in Java.
Visa is a payment and card issuing corporation and if you are reading this article, you are already into banking or fintech and looking for a way to integrate with the VISA and utilize their services to exchange data.
Visa uses two-way encryption to exchange data through their services.
Once you are onboarded on the Visa developer portal and have exchanged your public-private RSA keys with them, you will receive
- Shared secret
- API key
- encryption kid
- signing kid
- encryption certificate
- signing certificate
Using these, we can perform the encryption and decryption of the request and response payloads while interacting with the Visa.
Encryption the payload of VISA service in Java
For encryption, we have to first import the RSA keys in Java from the environment variables or the spring properties files.
Once imported we can create the encryption and decryption methods, that will accept the payloads and perform the operations.
@Component @Slf4j public class VisaRSAEncryption { @Autowired @Qualifier("visaEncKid") private String encKid; @Autowired @Qualifier("visaSignKid") private String signKid; @Autowired @Qualifier("visaEncCertificate") private String visaEncCertificate; @Autowired @Qualifier("visaSignCertificate") private String visaSignCertificate; @Autowired VisaCertificateUtils visaCertificateUtils; /** * Create JWE using RSA Public Key * * @param data - Plain Text * @return JWE String in compact serialization format * @throws Exception */ public String createJwe(String data) throws GeneralSecurityException, IOException, JOSEException { RSAPublicKey rsaPubKey = visaCertificateUtils.loadPublicKeyFromFile(visaEncCertificate); String currentTime = String.valueOf((new Date()).getTime() / 1000L); JWEHeader updatedHeader = (new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)) .keyID(encKid).type(JOSEObjectType.JOSE).customParam("iat", currentTime).build(); log.info("JWE object {}", updatedHeader); JWEObject jweObject = new JWEObject(updatedHeader, new Payload(data)); RSAEncrypter encrypter = new RSAEncrypter(rsaPubKey); jweObject.encrypt(encrypter); String serializedData = jweObject.serialize(); return serializedData; } /** * Create JWS Using RSA PKI * * @param jwe - JWE Payload * @return JWS Compact Serialization Format * @throws JOSEException */ public String createJws(String jwe) throws JOSEException, GeneralSecurityException, IOException { PrivateKey rsaPrivateKey = visaCertificateUtils.loadPrivateKeyFromFile(visaSignCertificate); JWSObject jwsObject = new JWSObject((new com.nimbusds.jose.JWSHeader.Builder(JWSAlgorithm.PS256)) .type(JOSEObjectType.JOSE).keyID(signKid).contentType("JWE").build(), new Payload(jwe)); log.info("JWS object {}", jwsObject.getHeader()); JWSSigner signer = new RSASSASigner(rsaPrivateKey); jwsObject.sign(signer); String serializedData = jwsObject.serialize(); return serializedData; } public String encryptPayload(String data) throws GeneralSecurityException, IOException, JOSEException { String jwe = createJwe(data); String jws = createJws(jwe); return jws; } /** * Decrypt JWE Using RSA PKI * * @param jwe - JWE String in compact serialization format * @param privateKeyPem - RSA Private Key in PEM Format * @return Plain Text * @throws GeneralSecurityException * @throws ParseException */ public static final String decryptJwe(String jwe, String privateKeyPem) throws GeneralSecurityException, ParseException { String privateKey = privateKeyPem .replace("-----BEGIN PRIVATE KEY-----", "") .replaceAll(System.lineSeparator(), "") .replace("-----END PRIVATE KEY-----", ""); byte[] encodedPrivateKey = Base64.getDecoder().decode(privateKey); String plainText; JWEObject jweObject = JWEObject.parse(jwe); JWEHeader header = jweObject.getHeader(); JWEAlgorithm jweAlgorithm = header.getAlgorithm(); try { if (JWEAlgorithm.RSA1_5.equals(jweAlgorithm) || JWEAlgorithm.RSA_OAEP_256.equals(jweAlgorithm)) { RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(encodedPrivateKey)); RSADecrypter decrypter = new RSADecrypter(rsaPrivateKey); jweObject.decrypt(decrypter); plainText = jweObject.getPayload().toString(); } else { JWEDecrypter decrypterForAES = new AESDecrypter(encodedPrivateKey); jweObject.decrypt(decrypterForAES); plainText = jweObject.getPayload().toString(); } } catch (JOSEException e) { throw new GeneralSecurityException("JOSEException has encountered.", e); } return plainText; } }
In the two way encryption we have to send the JWS encrypted payload to the VISA, thus the JSON payload is first encrypted as JWE using Encryption certificate (Visa’s public key) and then the JWE is encrypted to JWS using Signing certificate (your’s private key).
For decryption you will use your’s public key and Visa’s private key.
Along with the encrypted payload, Visa also expects a x-pay-token along with x-api-key in the request header.
x-pay-token is a combination of resource-path, query-parameters, raw request payload, and share secret.
@Component @Slf4j public class VisaXPayToken { public String generateXpaytoken(String resourcePath, String queryString, String requestBody, String shredScretKey) throws SignatureException { String timestamp = timeStamp(); String beforeHash = timestamp + resourcePath + queryString + requestBody; String hash = hmacSha256Digest(beforeHash.trim(), shredScretKey); return "xv2:" + timestamp + ":" + hash; } private static String timeStamp() { return String.valueOf(System.currentTimeMillis() / 1000L); } private static String hmacSha256Digest(String data, String shredScretKey) throws SignatureException { return getDigest("HmacSHA256", shredScretKey, data, true); } private static String getDigest(String algorithm, String sharedSecret, String data, boolean toLower) throws SignatureException { try { Mac sha256HMAC = Mac.getInstance(algorithm); SecretKeySpec secretKey = new SecretKeySpec(sharedSecret.getBytes(StandardCharsets.UTF_8), algorithm); sha256HMAC.init(secretKey); byte[] hashByte = sha256HMAC.doFinal(data.getBytes("UTF-8")); // String hashString = toHex(hashByte); return Hex.encodeHexString(hashByte); } catch (Exception e) { throw new SignatureException(e); } } }
and x-api-key passes the API-key that we had received.
Once you have all these in place, you make the request to the Visa services.
@Service @Slf4j public class VisaGateway{ private static final ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);; @Autowired private VisaRSAEncryption visaRSAEncryption; @Autowired private VisaXPayToken visaXPayToken; @Autowired @Qualifier("visaBaseUrl") private String baseUrl; @Autowired @Qualifier("visaApiKey") private String apiKey; @Autowired @Qualifier("visaSharedSecret") private String sharedSecret; private Object fetchCardDetails(String cardPan) throws Exception { try { String path = VISA_FETCH_TOKEN_PATH; String queryParam = "apiKey=" + apiKey; //form the URL UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(baseUrl + path) .queryParam(queryParam); String url = builder.build().toUri().toString(); String json = mapper.writeValueAsString("request-payload"); //encrpyt the json payload String encryptedData = visaRSAEncryption.encryptPayload(json); //pass the encrypted data as payload to the request body Map<String, String> encryptedPayload = new HashMap<String, String>(); encryptedPayload.put("encryptedData", encryptedData); //convert the encryptedData to payload String encryptedPayloadJson = mapper.writeValueAsString(encryptedPayload); //set the header HttpHeaders headers = getHeaders(path, encryptedPayloadJson, queryParam, sharedSecret); //execute the request ResponseEntity<Object> responseEntity = createAndExecuteNetworkRequest(url, HttpMethod.POST, headers, encryptedPayloadJson); return responseEntity.getBody(); } catch (RestClientException | JOSEException | IOException | GeneralSecurityException e) { log.error("Error while fetching visa token details {}", e.getMessage()); throw new Exception(HttpStatus.INTERNAL_SERVER_ERROR); } } private @Nullable ResponseEntity createAndExecuteNetworkRequest(String url, HttpMethod httpMethod, HttpHeaders headers, Object body) { try { HttpEntity<Object> httpEntity = new HttpEntity<>(body, headers); ResponseEntity<Object> responseEntity = restTemplate.exchange(url, httpMethod, httpEntity, Object.class); return responseEntity; } catch (RestClientException e) { log.error("Error calling visa apis " + e.getMessage()); throw new RestClientException(e.getMessage()); } } private HttpHeaders getHeaders(String resourcePath, String requestBody, String queryParameters, String sharedSecret) throws SignatureException { HttpHeaders headers = new HttpHeaders(); String uniqueID = UUID.randomUUID().toString(); headers.set("x-request-id", uniqueID); headers.set("Accept", "application/json"); headers.set("Content-Type", "application/json"); headers.set("X-API-KEY", apiKey); String xPayToken = visaXPayToken.generateXpaytoken(resourcePath, queryParameters, requestBody, sharedSecret); log.info("X pay Token {}", xPayToken); headers.set("x-pay-token", xPayToken); return headers; } }