How to integrate with Visa Services in Java?

In this article, we will see how to integrate with the Visa services or exchange data with the them in Java.

Integrate with Visa services 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.

Visa two way encryption example in Java

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;
    }
}