How to integrate NPCI (Rupay) services in Java

Integrate with NPCI (Rupay) services in Java

National Payments Corporation of India, is the parent organization of the Rupay and the UPI. If you are into the banking and fintech within India then you must be dealing with the integration of NPCI services with your services.

Here we are going to see the NPCI (Rupay) services integration with Java spring boot, but you can follow the steps for any other programming languages.

Prerequisite for NPCI (rupay) integration with Java

NPCI takes security very seriously for sensitive information exchange, request, and response data are encrypted using two-way encryption.

Assuming that you have already been onboarded to the NPCI, you must already exchanged your public RSA key with them and they must have shared their public key with you.

NPCI also whitelists the URL and shares an SSL certificate that you need to have in your application while making the request.

You can add the SSL certificate in your JVM using the following command.

 $JAVA_HOME/bin/keytool -import -file /path-to/RUPAY_SSL_CERTIFICATE.cer -alias mycertrupay -keystore $JAVA_HOME/jre/lib/security/cacerts -trustcacerts -storepass changeit -noprompt

Encrypting and decrypting the NPCI payload

NPCI expects JWE encryption for exchanging the data. The request payload data is encrypted using the public key shared by the NCPI and it is decrypted at their end using their private key.

Similarly, the NPCI uses our shared public key for encrypting the response payload and we can decrypt the response using our private key.

To encrypt the data we are going to use the JOSE library. First, import the public and private RSA keys in Java.

Then we can create this util to perform the encryption and decryption.

@Component
@Slf4j
public class RupayEncryption {
    @Autowired
    @Qualifier("rupayPublicKey")
    private String rupayPublicKey;

    @Autowired
    @Qualifier("rupayPrivateKey")
    private String rupayPrivateKey;

    @Autowired
    private EncryptionLibrary utils;

    @Autowired
    private CertificateUtils certificateUtils;

    public String JWEEncrpyt(String payload) throws GeneralSecurityException, JoseException, IOException {
        RSAPublicKey rsaPubKey = certificateUtils.loadPublicKeyFromFile(rupayPublicKey);

        JsonWebEncryption jwe = new JsonWebEncryption();

        // RSA256 for key wrap
        jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.RSA1_5);

        // A256GCM for content encryption
        jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256);

        // the key (from above)
        jwe.setKey(rsaPubKey);

        // whatever content you want to encrypt
        jwe.setPayload(payload);

        // Produce the JWE compact serialization, which is where the actual encryption is done.
        // The JWE compact serialization consists of five base64url encoded parts
        // combined with a dot ('.') character in the general format of
        // <header>.<encrypted key>.<initialization vector>.<ciphertext>.<authentication tag>
        String serializedJwe = jwe.getCompactSerialization();

        return serializedJwe;
    }

    public String JWEDecrypt(String serializedJwe) throws GeneralSecurityException, JoseException, IOException {

        Key rsaPrivateKey = certificateUtils.loadPrivateKeyFromFile(rupayPrivateKey);

        JsonWebEncryption jwe = new JsonWebEncryption();

        // Set the compact serialization on new Json Web Encryption object
        jwe.setCompactSerialization(serializedJwe);

        // Symmetric encryption, like we are doing here, requires that both parties have the same key.
        // The key will have had to have been securely exchanged out-of-band somehow.
        jwe.setKey(rsaPrivateKey);

        // Get the message that was encrypted in the JWE. This step performs the actual decryption steps.
        String plaintext = jwe.getPlaintextString();

        return plaintext;
    }
}

Now using this encryption and decryption utils, we can encrypt and decrypt the request and response payloads in the services.

private Object fetchTokenDetails(String cardPan, String cardType) throws Exception {
    try {
        String url = baseUrl + RUPAY_FETCH_TOKEN_PATH;

        Map<String, String> rPan = new HashMap<>();
        rPan.put("rpan", cardPan);
        String rPanJson = String.valueOf(new JSONObject(rPan));

        //create payload
        Map<String, String> payload = new HashMap<>();
        payload.put("conversationIdentifier", UUID.randomUUID().toString());

        //encrypt rPan
        String encryptedRPANData = rupayEncryption.JWEEncrpyt(rPanJson);
        payload.put("rpanData", encryptedRPANData);

        //set headers
        HttpHeaders headers = getHeaders(cardType);

        ResponseEntity<Object> responseEntity = createAndExecuteNetworkRequest(url, HttpMethod.POST, headers, payload);

        return responseEntity.getBody();
    } catch (NoSuchAlgorithmException | JoseException e) {
        throw new Exception(HttpStatus.INTERNAL_SERVER_ERROR,  "Exception occured while calling rupay api:");
    } 
}

 private @Nullable ResponseEntity createAndExecuteNetworkRequest(String url, HttpMethod httpMethod, HttpHeaders headers, Object body) throws RestClientException {

    ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
    String jsonPayload = null;
    try {
        jsonPayload = ow.writeValueAsString(body);
    } catch (JsonProcessingException e) {
        log.error(e.getMessage());
    }

    try {
        HttpEntity<Object> httpEntity = new HttpEntity<>(jsonPayload, headers);

        ResponseEntity response =  restTemplate.exchange(url, httpMethod, httpEntity, Object.class);
        return response;

    } catch (RestClientException e) {
        log.error("Error calling rupay apis " + e.getMessage());
        throw new RestClientException("Error calling rupay apis ");
    }
}

private HttpHeaders getHeaders(String cardType) throws Exception {
    HttpHeaders headers = new HttpHeaders();

    String uniqueID = UUID();
    headers.set("X-REQUEST-ID", uniqueID);

    String clientCertNumber = cardType.equalsIgnoreCase(RUPAY_DEBIT_CARD_TYPE) ? rupayDebitClientCertNumber : rupayCreditClientCertNumber;
    headers.set(RUPAY_CLIENT_CERT_NUMBER_KEY, clientCertNumber);
    
    return headers;
}

NPCI also expects us to send the clientCertNumber which is a unique identifier for each vendor in the request along with a unique identifier X-REQUEST-ID to trace the request in case it fails.