Enable Message-Level Encryption
IMPORTANT
This feature is in the pilot phase. To use message-level encryption, contact your sales
representative.
There are additional tasks you must complete before you can enable message-level encryption. For
more information, see Prerequisites for Message-Level Encryption.
Message-Level Encryption (MLE) enables you to store information or communicate with other
parties while helping to prevent uninvolved parties from understanding the stored
information. MLE is optional and supported only for payments services.
MLE provides enhanced security for message payload by using an asymmetric encryption
technique (public-key cryptography). The message encryption is implemented with
symmetric encryption using Advanced Encryption Standard (AES), Galois Counter Mode (GCM)
with 256-bit key size. The encryption of keys is supported using RSA Optimal Asymmetric
Encryption Padding (OAEP) with 2048-bit key size. The encryption service is based on
JSON Web Encryption (JWE), works on top of SSL and requires separate key-pairs for
request and response legs of the transaction.
MLE is required for APIs that primarily deal with sensitive transaction data, both
financial and non-financial. These are the types of sensitive transaction data:
- Personal identification information (PII)
- Personal account number (PAN)
- Personal account information (PAI)
MLE is supported when using JSON web tokens. For more information, see Message-Level Encryption Using JSON Web Tokens.
Each of these authentication schemes uses an encrypted payload, called the
JWE
. A
JWE token has these five components, with each component separated by a period (.): - JOSE header containing four elements:"alg": "RSA-OAEP-256", //The algorithm used to encrypt the CEK "enc": "A256GCM", //The algorithm used to encrypt the message "iat": "1702493653" //The current timestamp in milliseconds "kid": "keyId" //The serial number of shared public cert for encryption of CEK
- JWE encrypted key
- JWE initialization vector
- JWE additional authentication data (AAD)
- JWE ciphertext and authentication tag
Prerequisites for Message-Level Encryption
Before enabling message-level encryption (MLE), you must complete these requirements:
- Sign the pilot agreement for using MLE.
- Confirm that the APIs you are integrating to support MLE.
- Retrieve theVisa Acceptance Solutionspublic key from either the Account Manager or Client Executive services in theBusiness Center.
- Ensure that client-side systems are modified to read the public key and encrypt the API payload.
Message-Level Encryption Using JSON Web Tokens
To use message-level encryption (MLE) with JSON Web Tokens (JWT), you must generate
the JWT and send it as part of the HTTP header. The payload is encrypted with the
dynamic Content Encryption key (CEK) that is generated for each transaction. The
serialized encrypted payload, the JWE, is passed as the request body.
- Use the required Maven dependency:<dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.0</version> </dependency>
- Prepare the API request payload. This example is hard-coded for demonstration.String jsonMsg = "{\"clientReferenceInformation\":{\"code\":\"TC50171_3\"},\"processingInformation\":{\"commerceIndicator\":\"internet\"},\"aggregatorInformation\":{\"subMerchant\":{\"cardAcceptorID\":\"1234567890\",\"country\":\"US\",\"phoneNumber\":\"650-432-0000\",\"address1\":\"900MetroCenter\",\"postalCode\":\"94404-2775\",\"locality\":\"FosterCity\",\"name\":\"VisaInc\",\"administrativeArea\":\"CA\",\"region\":\"PEN\",\"email\":\[email protected]\},\"name\":\"V-Internatio\",\"aggregatorID\":\"123456789\"},\"orderInformation\":{\"billTo\":{\"country\":\"US\",\"lastName\":\"VDP\",\"address2\":\"Address2\",\"address1\":\"201S.DivisionSt.\",\"postalCode\":\"48104-2201\",\"locality\":\"AnnArbor\",\"administrativeArea\":\"MI\",\"firstName\":\"RTS\",\"phoneNumber\":\"999999999\",\"district\":\"MI\",\"buildingNumber\":\"123\",\"company\":\"Visa\",\"email\":\[email protected]\},\"amountDetails\":{\"totalAmount\":\"102.21\",\"currency\":\"USD\"}},\"paymentInformation\":{\"card\":{\"expirationYear\":\"2031\",\"number\":\"5555555555554444\",\"securityCode\":\"123\",\"expirationMonth\":\"12\",\"type\":\"002\"}}}";
- Read the merchantp12file.The P12 file should have been created when you set up your test account. See Create a P12 File.ClassLoader classLoader = Main.class.getClassLoader(); KeyStore merchantKeyStore = KeyStore.getInstance("PKCS12", new BouncyCastleProvider()); merchantKeyStore.load(classLoader.getResourceAsStream("test_merchant.p12"), "test_merchant".toCharArray()); String merchantKeyAlias = null; Enumeration enumKeyStore = merchantKeyStore.aliases(); RSAPrivateKey rsaPrivateKey = null; RSAPrivateKey rsaPrivateKey_SJC = null; X509Certificate x509Certificate = null; X509Certificate x509Certificate_SJC = null;
- Loop through the Java KeyStore to extract the private key from merchantp12file and extract the public key forVisa Acceptance Solutions. You must use theVisa Acceptance SolutionsSJC (CyberSource_SJC_US) to encrypt the payload.while (enumKeyStore.hasMoreElements()) { merchantKeyAlias = (String) enumKeyStore.nextElement(); if (merchantKeyAlias.contains("test_merchant")) { KeyStore.PrivateKeyEntry keyEntry = (KeyStore.PrivateKeyEntry) merchantKeyStore.getEntry( merchantKeyAlias, new KeyStore.PasswordProtection( "test_merchant".toCharArray())); //Extract the merchant certificate to sign the payload. x509Certificate = (X509Certificate) keyEntry.getCertificate(); rsaPrivateKey = (RSAPrivateKey) keyEntry.getPrivateKey(); //Extract the merchant certificate to encrypt the payload. } else if (merchantKeyAlias.contains("CyberSource_SJC_US")) { KeyStore.PrivateKeyEntry keyEntry = (KeyStore.PrivateKeyEntry) merchantKeyStore.getEntry( merchantKeyAlias, new KeyStore.PasswordProtection( "test_merchant".toCharArray())); //Store the public key from the certificate. x509Certificate_SJC = (X509Certificate) keyEntry.getCertificate(); //rsaPrivateKey_SJC = (RSAPrivateKey) keyEntry.getPrivateKey(); } }
- Update the custom headers to include"iat"with the current timestamp:Map<String, Object> customHeaders = new HashMap<String, Object>(); customHeaders.put("iat", Instant.now().getEpochSecond());
- Generate the JWE token (the encrypted payload) using the supported algorithm and theVisa Acceptance Solutionspublic certificate. Include the JSON payload as the input.String jweToken = encryptAttributeWithAlgo(jsonMsg, x509Certificate_SJC, JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM, customHeaders);public static String encryptAttributeWithAlgo(String content, X509Certificate x509Certificate, JWEAlgorithm algo, EncryptionMethod encryptionMethod, Map<String, Object> customHeaders) { if (isNullOrEmpty(content)) { System.out.println("empty or null content"); return null; } else if ( x509Certificate == null) { System.out.println("public certificate is null"); return null; } String serialNumber = extractSerialNumberFromDN(x509Certificate); JWEObject jweObject = new JWEObject( new JWEHeader.Builder(algo, encryptionMethod) .contentType("JWT") // required to signal nested JWT .keyID(serialNumber) .customParams(customHeaders) .build(), new Payload(content)); jweObject = encrypt(jweObject, x509Certificate); return jweObject == null ? null : serializeToken(jweObject); } public static boolean isNullOrEmpty(String string) { return (string == null || string.trim().length() == 0); }
- Build the JSON request body for calling theVisa Acceptance SolutionsAPI.String jsonBody = createJsonString(jweToken);private static String createJsonString(String jweToken) { String message; JSONObject json = new JSONObject(); json.put("encryptedRequest", jweToken); return json.toString(); }
- Generate the body digest to validate that the payload has not been compromised.String bodyDigest = createBodyDigest(jsonBody);public static String createBodyDigest(String jsonBody) { MessageDigest messageDigest = null; try { messageDigest = MessageDigest.getInstance(DEFAULT_HASH_ALG); } catch (NoSuchAlgorithmException e) { System.out.println("Couldn't instantiate SHA-256 digest " + e.getMessage()); return null; } byte[] bodyDigestBytes = messageDigest.digest(jsonBody.getBytes()); return java.util.Base64.getEncoder().encodeToString(bodyDigestBytes); }
- Prepare the JWT payload for signature.JWTPayload jwtPayload = createJWTPayloadClass(bodyDigest); Map<String, Object> customHeader = new HashMap<String, Object>(); customHeader.put("v-c-merchant-id", "test_merchant");private static JWTPayload createJWTPayloadClass(String bodyDigest) throws NoSuchAlgorithmException { JWTPayload jwtPayload = new JWTPayload(); jwtPayload.setDigest(bodyDigest); jwtPayload.setDigestAlgorithm("SHA-256"); jwtPayload.setIat(String.valueOf(System.currentTimeMillis())); return jwtPayload; }
- Sign the payload and create the JWT token that is passed in the request header.String jwsSignature = sign(Json.encode(jwtPayload), rsaPrivateKey, x509Certificate, customHeader);public static String sign(String content, PrivateKey privateKey, X509Certificate x509Certificate, Map<String, ? extends Object> customHeaders) { return serializeToken(signPayload(content, privateKey, x509Certificate, customHeaders)); } protected static JOSEObject signPayload(String content, PrivateKey privateKey, X509Certificate x509Certificate, Map<String, ? extends Object> customHeaders) { return signPayload(content, privateKey, x509Certificate, customHeaders, true); } protected static JOSEObject signPayload(String content, PrivateKey privateKey, X509Certificate x509Certificate, Map<String, ? extends Object> customHeaders, boolean includeKid) { if (isNullOrEmpty(content) || x509Certificate == null || privateKey == null) { System.out.println("empty or null content or Private key or public certificate is null"); return null; } String serialNumber = extractSerialNumberFromDN(x509Certificate); List<Base64> x5cBase64List = addCertificateToBase64List(x509Certificate); if (x5cBase64List.isEmpty()) return null; RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) privateKey; Payload payload = new Payload(content); JWSHeader jwsHeader; if ( includeKid ) { jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256) .customParams((Map<String, Object>) customHeaders) .keyID(serialNumber) .x509CertChain(x5cBase64List) .build(); } else { jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256) .customParams((Map<String, Object>) customHeaders) .x509CertChain(x5cBase64List) .build(); } JWSObject jwsObject = new JWSObject(jwsHeader, payload); try { RSASSASigner signer = new RSASSASigner(rsaPrivateKey, true); jwsObject.sign(signer); if (!jwsObject.getState().equals(JWSObject.State.SIGNED)) { System.out.println("Payload signing failed."); return null; } } catch (JOSEException joseException) { System.out.println("ERROR_SIGN_AND_ENCRYPT_THE_PAYLOAD" + " " + joseException); return null; } return jwsObject; } protected static String extractSerialNumberFromDN(X509Certificate x509Certificate) { String serialNumber = null; String serialNumberPrefix = "SERIALNUMBER="; String principal = x509Certificate.getSubjectDN().getName().toUpperCase(); int beg = principal.indexOf(serialNumberPrefix); if (beg >= 0) { int end = principal.indexOf(",", beg); if (end == -1) end = principal.length(); serialNumber = principal.substring(beg + serialNumberPrefix.length(), end); } else serialNumber = x509Certificate.getSerialNumber().toString(); return serialNumber; }
- Send the JWT as the Bearer token in the header and send the JWE as the body.HTTP Request Header: Content-Type: application/json Authorization: Bearer eyJ2LWMtbWVyY2hhbnQtaWQiOiJtcG9zX3BheW1lbnRlY2giLCJhbGciOiJSUzI1NiIsImtpZCI6Im1wb3NfcGF5bWVudGVjaCIsIng1YyI6WyJNSUlDWmpDQ0FjK2dBd0lCQWdJV05qZ3hNek0wTkRNMk1qTXdNREU0TVRNNU5EY3lNekFOQmdrcWhraUc5dzBCQVFzRkFEQWVNUnd3R2dZRFZRUUREQk5EZVdKbGNsTnZkWEpqWlVObGNuUkJkWFJvTUI0WERUSXpNRFF4TWpJeE1qQXpObG9YRFRJMk1EUXhNakl4TWpBek5sb3dPekVZTUJZR0ExVUVBd3dQYlhCdmMxOXdZWGx0Wlc1MFpXTm9NUjh3SFFZRFZRUUZFeFkyT0RFek16UTBNell5TXpBd01UZ3hNemswTnpJek1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdmdiWFRIS0pHaXNFM3JQVHZySjZvMXJIb1R0UDJPQWlhOEtPSEhSZ21uMFR1cU1MTjVndGNacVVtTGJVeWlIWGhrZGFqbUtVMG5tUGJpWWRuQW4yc2pDbDVvVUgrSGViXC9LVFE1NW1BVEJnY0ZOY0ZvNVdMREN4VzNEeFZvM1Q3U3BhTHRrU0ZtZWV1bUZpRzFnZ1hcL2h1SFI1QU5BK3dyS2R4VTk1WjFEZHBudUpTWkpjckg2cWRWXC9LdHlRNmxTRUJWUUhibXBvVmdtREVVdVNvUExDc0RJNVhSa0ZHOHZQRXZqRHNIQWdOeHpoMCt1RzJxN3p5SGRoTWFnS3IzOGdRandFZVBWZXY4c0ExYkJcL1VzaDNzSU5kdFhKREhBUFlPZWJTS25iTWh4a1dUZlg3TnRKRE1xaEQrXC9DK0hNWDRyTGxVQXZKSjVYMmJYWEtsUUdPeFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0R0JBQ0s3aWR5QWFQZTJGbDFITFhMaEo3RFBDMEtTTjFlRldZQUk4VDBqYmJmYWVHWXhkdXlOVitsRXlaUjF5MllWRWVjbU1xUllFN3RuV0JIYitoS0I1ODFTeEU4Z21ZQTVIaHA0VzdlczM2aDl1SUFvQ1JqU1pFWGJcL09wUG5VN0JkVDBMTlZ0N0hyR21JcnVtSlFmSDRvMitRbUY4T3BtM2thMnArRDRaaDNtTiJdfQ.ewogICAgICAgICAgICAiZGlnZXN0IjoiclU5ZUlUTHVTZzFpUEJlQW5WSGdNMmN4clBPQ1Rzais3cEhENWhIZFI2bz0iLAogICAgICAgICAgICAiZGlnZXN0QWxnb3JpdGhtIjoiU0hBLTI1NiIsCiAgICAgICAgICAgICJpYXQiOiIyMDIzLTA2LTA4VDIwOjQxOjI1LjYxNVoiCn0gCgo.RSOMFXouXVP-6_o_x0r6QRzMDfwr2wb0DG3EE48uKemG5oiHOmHQY_sN78bCbuFFgy1C1R8QZA-jphx0dH6jzuJxoerkgz3VSMuDt9mDJtnlDnisSTf35NWh6u4TeJqGr3E8oOOJSX6N32r6XovCXyJyaDm4h2fJOeLZc8HcvfSC5SpMUsC2vQMRbrRJXEfMjCiEbPplSuusfsmX3xbFTLnP1jlNY_hi2TZazFBb6rvetPpDM5kibRS7OpJoGWievmmDI6LyaelJhkghZ-_OwElAYZfOnZ9FphLQZWnwZ3mku0C6gysv6ISMrI9BlCpWaPbmDeuWBSLexC_U01cblg HTTP Request Body: {"encryptedRequest":"eyJ2LWMtbWVyY2hhbnQtaWQiOiJtcG9zX3BheW1lbnRlY2giLCJ4NWMiOlsiTUlJRE5UQ0NBaDJnQXdJQkFnSVdNVFkwT1RFNU1ERXhNREEwTkRBd056Y3lOakF6T0RBTkJna3Foa2lHOXcwQkFRc0ZBREJwTVFzd0NRWURWUVFHRXdKVlV6RU5NQXNHQTFVRUNnd0VWbWx6WVRFVU1CSUdBMVVFQ3d3TFEzbGlaWEpUYjNWeVkyVXhOVEF6QmdOVkJBTU1MRU41WW1WeVUyOTFjbU5sSUZSeVlXNXpZV04wYVc5dVlXd2dUbTl1VUhKdlpDQkpjM04xYVc1bklFTkJNQjRYRFRJeU1EUXdOVEl3TWpFMU1Wb1hEVEkxTURRd05USXdNakUxTVZvd1BqRWJNQmtHQTFVRUF3d1NRM2xpWlhKVGIzVnlZMlZmVTBwRFgxVlRNUjh3SFFZRFZRUUZFeFl4TmpRNU1Ua3dNVEV3TURRME1EQTNOekkyTURNNE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeE53S1F5UXFJV2tCVWpLd041WEZyazgxdm5BSk9FOGVlUE80UENmckdETDdGeFJwS0NCWXN3MTk2MTB3MzlVMlFUU3hib3grMVhERUcxSVd3SGtTckJMeDVSVTQ3T2ZBeDEySHdcL3lNNCttUUVENERXSWFCVld6NmZRZDBJTG5tWTB5VDRkMjVWUTZ6dDRRYU1pcXMwM2x5d21WdGFvWEx3SDNuRjJzRzhXV1pEUjRqZ1J2ZkNnZzZVUWN6aVNQMzNRVENsMmxqUmYrTmtWRFJYNGFEOWFQblwvVWh2Qzlna1JBVVlXMUZCb0RMVVA0TTdrQ29WakVsZDgyZnVnMmNyNzNFTVlRbmtKUnpjT0xVcXk4T3lrQXNYWDBsTktKVVFINW1OTUlTWTVjXC8yVSs1UVVFSWRJalp1SFB0UkVxUklpb2tjdVlzRGlvUVRtQUxWSCtWdGR3SURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFDY2t0VytIczdXTGVDNGhBQTVvNmR6WXJvSXpcL2RnYXl0MWlDOVltUnNGRng1d3ZWV0pqeGM4aElDaXJHXC93QURWNjRxMDg3Y1ZobWJoM1wvZG52UEJwR1V6TWhnMUFsM1VBcFRDZTN0KzJhdENGR0lvWDZabk1aUEtKNnAxNEp5TTFUN2F2eEY0a3ZXaklQYVllbmJxWk5qcFBuTm1DREdselhZdjhwRXQ4ZWRZbGorT1QzS3N0VVV1eDZoRXI5MnJUekFGbVFXZlZxaURjYklZUG12XC9UWU0yNGxkUlwvMldUd3NpbzZwWUsxMk4yQUxxemJcL3U5MVE5MGEyN3NHbFNFV0RZcUo4aVwvM3Jidys5cDNkNXFlZzlXRmJ6N2FUUWVpOFJyU2pBRU1SRllDNnREUWEweStpUUgxZlNLaEg1YmJuOGdDbWJpbjNGaXJqWXFKVnVOZFplIl0sImtpZCI6IjE2NDkxOTAxMTAwNDQwMDc3MjYwMzgiLCJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiaWF0IjoxNjg2MjU2NjkyLCJhbGciOiJSU0EtT0FFUCJ9.YyXUcX_fzTGwhfAon6gT62rrZWIybdgAH2FLhtJxQin0hu0OaWvYYT34bLguXqbEOxzXxYRcOo-GCEaFs15Ul_BrtlhQTn9aKjX_-rbYxM-ZXJlbpg6CsyAqy63-MkYPP2BNXjFfP3yUSxes76zHlMaJG0gp681QY85AqGq6mCSrDqWE7NUTWifseRtKMv5u9pMHMxddkz9Xvp6Q5TbiEjGZbvD-xKhhgs0-IupvPDKhxdJSNVPaDiTnFVnYtyOuLZLOFO4Fq2bfj86iGHRjfh9zq91Gp4uN36kmRHzkLN4Wrr5R6D79Z-FC5bLU4BUrilGQtVSWCWtcxYAIQOhz1w.tuv-9Xtl0uNoPxXV.RRGnkA1chplnGQf-SlXaXEntzGJrEF4EJU-F6PEx6H3us1APoWAR-26aHdWctNFoGSalNt1ZzidRi3TA-iwpSFkEonSVbe7aVLJeAKgqCHnVXT-eWb89gqTVkQFZiSZCHtIjDUtOMy95sU4MRcCvtrfAPDnIMudVVA5YtAsCZpta_ATl_iS6oLBMI57R0Ra7pO3MxFdLTrk-FkLSd4JbGokm_JXpH8lI1V11vaMAtyEqGrzllrQv408zUGbvtvSirF31iiGITEF7QG5rbVn7oTWF4wWzKEkpSZ7J4LpIdjCG6sojeld4XkK9dHHL1r0-vFVfa-ua4uh4PNcVK0o3ke4TOqLnVcnaEtYW1AS2wIu_tHxW_hdkyPmDI8ceSBqmloRxV3q8xOS5u-2GNQ9p5pm2_NjkqVB8RYup9NFZWBBjriLRaMTp41W5T_gOOQkH8Xt2JaKAxwevrtg0UST3rryjt5U9y074DJqDZAS1OOjoCHaQga4S34L5gJTPIPRe94G_jBU1o9SmGtrHMDTkxL5-RzJ0mOwPF2MZzQ318z78IyAGXotYT4QXGJZhnyDMNgHjyyGX7IZtGPRYDpxc10Kko9DLM_r6fWoDLemRhFbi8prnlJpQZbUh98TLRwCndmH9lIdyH6v3YUfFde_ncxYPMbkDqEdoi26bvm1pWRFm8EI6kXGZV_H9bCcse2x3JZFzUg0xp5yv0bfSAxdjKOqmRqIR.5ooQUzqPfFtPjGCefMZ_6A"}