6

I am getting information back from Visa Checkout in an encrypted format. The guide on their site provides these instructions:

First, you must decrypt the dynamic key (encKey), then use the decrypted dynamic key value to decrypt the payment data payload (encPaymentData).

Follow these four steps to decrypt the encKey:

  1. Base64-decode the encKey.
  2. Remove the first 32 bytes of the decoded value. This is the HMAC (Hash Message Authentication Code). Calculate a SHA-256 HMAC of the
    rest of the decoded data using your API Shared Secret and compare it
    to the HMAC from the first 32 bytes.
  3. The next 16 bytes should be removed and used as the IV (Initialization Vector) for the decryption algorithm.
  4. Decrypt the remaining data using AES-256-CBC, the IV from step 3, and the SHA-256 hash of your API Shared Secret.

Follow these four steps to decrypt the encPaymentData using the decrypted encKey:

  1. Base64-decode the encPaymentData.
  2. Remove the first 32 bytes of the decoded value. This is the HMAC. Calculate a SHA-256 HMAC of the rest of the decoded data using the decrypted encKey and compare it with the HMAC from the first 32 bytes.
  3. The next 16 bytes should be removed and used as the IV for the decryption algorithm.
  4. Decrypt the rest of the encPaymentData payload using AES-256-CBC, the IV from step 3, and the SHA256-hash of the decrypted encKey.

I tried using ColdFusion but I am lost somewhat with the encryption issues, and am unable to fix the code. Below I have what is required. I am stuck on the step 3 & 4 where they say compare it and then decrypt it. Can someone guide what could be done to fix it?

enckey:

2M2WWOD4wANsGwWTmPqQIQYdz9WPwgiR0ntJHGaxm23b5a1sWUtCBcUQUMMtc9hvcYavJ6yqPgETOGZgDOdd9qjDwIb2aV9DLZT1iIcB3zNN5Ddhxd9iiui6TAlJxU/O

encPaymentData:

X2TXp0ZmwHrtfzSP5TPjUOjdZb0rjsHeDSqr8TwIF/VR8sMQhWN5hP4IRhQxWT CZcQhxZoUHP 0g/E/ot sjREAJ8YQf7c7jSzKsXRH/wrew5rQit2wJBlVSSZ YoLeIHbLrTz CfIoFv09hixl7ff27u0YCyV0zjP5vNfCBEIwfqyriqwXK2J QEOxQiKzDUW4br3o1t31aymCQC9eBBpoVKjFfSKlNXM9QEdNZBcLMZ8Wlv8lF/ua bnwshbM9u7Uhudqvut94RZEW NzkRD8MfBo12e/XhnL35qxGpHeQNPClC4EQDK6U/HmegeOj BZLbIIYBs6t9E8Q3AKBwfiPOFgB gSVnhXKnd3nKvllaG BaGrQJtk 7QAtnHMHxQAO5rdiS9465HCdiHa8zlv7SkvWh8EwcKCiT4qiZSM6QuYAeRSzDpPS1gsZ54Q9LizUnueH7yyzSd47cLPd0VlOQxobKtNN2LrsRb3IwOfzuwGnSRf2cNp49hBmmGP1b0BC hhB6UpCqP2ixTPvui NwMYzqZUe336bF1mfnKzEbEZIvIrPyx3uMiLDAns2g7S80gMNnHb/09i49xbfY3V7oudeiHV99FCh67DuG3uHE3/HzIZbcnxJwVJoJj6/3DuzK/Kw1JqSorE0M1qxUqoNkJC4aNCBrqfTlR7/eErrvB554TUZwcyQXqKCwrKv4NJEw6S0n3W1VASkfA0atbJQX2aLgx9kqnhYdDbaU8UcFIoeA45 yEuQ9vXzo2ILQhvamsAAFQd3i4mEOZ KNtMu25dDFlORn5C/oTZ1t1dzJoYMvq44gejp6L3IK e7JCugGchr963a2kd8NFa3wctRDHF8ChHxawVlU0aY7nasrVireMFLiM 9XIb4abfDtct/j1Q8IGN0hRkgCHO6dlnOrAaaQDYYH3axaMDp5Onb04whULYqGbn/XSR8Sn8gnoFbYqVJbR5aCp5Pe9TpfwxlEvV3z8ZfsASqW2y So9gpwg2y16K/FX3Io6kqeqUlAxbTRDfN/ofDIJaO H PUu2teqjvwvCkjPciGOQdXT5JxqoCiHqRwD0zeZPcG3b9Nfrq3Caf6zjwhf /CMeGc3dNHhSkXox R50MP8BlLWk/bXZRScTE/HSrVxE n073utHAnbVOM3gVha0Hr8TmoV8z1vBg5fY253so6kQX61ZIfHneCAZo0qeKRgDgLUryVPmUNc5 yKP8DxtmHl/0YUztpgyEx5njsrn1L 3EHMMUhui8d LQdNZoEpZ9U1Xb7XVsV5gnwR/mOITNOKJQsine4zMMHBcomHclrM0CuI58YrKPqworCmK6CYfzSc8UmXxXUe5dzND/DS9XgqDttQic2/OqTSAK63ynnrNqzr3D56VpDBeDeQjk3mc/0zmuFAPEXoAQoQKfD6HEuajvWJebQ6QIPgA TshqsnPlktbpftr4lsuB1tHS/W8D7SYVFMC/Kxy9QuYWs0cmRTtzfWEKIRHeDElOTQCX5JB5PgzVhhi5kYTi488Ba8j4zvNUw55hEoMxONYO7eMjJosmNjULsT492LGw3EfAgmgx9h3yFLQRZgfylg0h4PfLlcPOAdsnVX9/yLElD xu7Atwc4S7pBWTHvwue7PpRvWpTeqkU5sqiX4KcV5x8rk mBtxm48a8fsmp GNf 4IjwXu9cQaU9WLipiEnkqFsYo7/aAsmmKWBETyQg9BFXYK 165vrzSX8WTsv6ZZDnVjcE1n4Ov8Jl2cnAigoQbB0ROPpIRzZ3zH2diUv1vzlSuh9gbEJf3uQRKlYRVUbpboC0RbQ/7jgznfJAWyLykyDQ0EB8fVEOtbP1l4JEz39QwAU18ph3btnWWuKEV4 ghYvNG4m1DYntSF57s2ajRS6rPtR oYvGjrJL9zbHBhKHlfkIPC0TKotOCi96mqpikbBEfIZSomHxYgDwYCSvt60zaDIjlBxZ1UBdK JL0554Wia9W3Wg91bmYS9Q4SXMT8r4xGYB7OutEV24n7p088rVm/w2SZSiqlLqai539k6WGkzEQf19ytPtIE81a N z7aijTjy 7FCuVPF90svI5/NoGpSINqv84HUcMU71BvXUIT53Ea6CCpiWvvOPpo/XZar44emlIG0UgeB kfP6C6sis=

Secret code:

zRf7WZ3nM7ON{U0E6J5S}KpVm@k2ReDyq#1lG9go

CF Code:

<cfset str = "2M2WWOD4wANsGwWTmPqQIQYdz9WPwgiR0ntJHGaxm23b5a1sWUtCBcUQUMMtc9hvcYavJ6yqPgETOGZgDOdd9qjDwIb2aV9DLZT1iIcB3zNN5Ddhxd9iiui6TAlJxU/O">
<cfset tobas = tobase64(str)>
<cfset getFirst32bytes = Left(tobas,32)>
<cfset tobas2 = RemoveChars(tobas,1,32)>
<cfdump var="#tobas2#">
<cfset key = "zRf7WZ3nM7ON{U0E6J5S}KpVm@k2ReDyq##1lG9go">
<cfset x = hmac("#tobas2#","#key#","HMACSHA256")>
<cfset y = hmac("#getFirst32bytes#","#key#","HMACSHA256")>
<cfset decalgo = Left(x,16)>
<cfset decremainingData = RemoveChars(x,1,16)>
<cfset getDec = Decrypt(decalgo,"#key#","AES")>
<cfdump var="#x#"><br>
<cfdump var="#y#"><br>
<cfdump var="#decalgo#">
<cfdump var="#decremainingData#">
<cfdump var="#getDec#">

This is the java example they have on their site:

private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String HASH_ALGORITHM = "SHA-256";
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final int IV_LENGTH = 16, HMAC_LENGTH = 32;
private static final Charset utf8 = Charset.forName("UTF-8");
private static final Provider bcProvider;
static {
   bcProvider = new BouncyCastleProvider();
   if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
    Security.addProvider(bcProvider);
   }
}

private static byte[] decrypt(byte[] key, byte[] data) throws GeneralSecurityException {
   byte[] decodedData = Base64.decode(data);
   if (decodedData == null || decodedData.length <= IV_LENGTH) {
    throw new RuntimeException("Bad input data.");
   }
   byte[] hmac = new byte[HMAC_LENGTH];
   System.arraycopy(decodedData, 0, hmac, 0, HMAC_LENGTH);
   if (!Arrays.equals(hmac,
     hmac(key, decodedData, HMAC_LENGTH, decodedData.length– HMAC_LENGTH))) {
    throw new RuntimeException("HMAC validation failed.");
   }
   byte[] iv = new byte[IV_LENGTH];
   System.arraycopy(decodedData, HMAC_LENGTH, iv, 0, IV_LENGTH);
   Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM, bcProvider);
   cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(hash(key), "AES"),
    new IvParameterSpec(iv));
   return cipher.doFinal(decodedData, HMAC_LENGTH + IV_LENGTH,
    decodedData.length– HMAC_LENGTH– IV_LENGTH);
}

private static byte[] hash(byte[] key) throws NoSuchAlgorithmException {
   MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM);
   md.update(key);
   return md.digest();
}

private static byte[] hmac(byte[] key, byte[] data, int offset, int length)
throws GeneralSecurityException {
   Mac mac = Mac.getInstance(HMAC_ALGORITHM, bcProvider);
   mac.init(new SecretKeySpec(key, HMAC_ALGORITHM));
   mac.update(data, offset, length);
   return mac.doFinal();
}
Leigh
  • 28,765
  • 10
  • 55
  • 103
  • In Visa's example, they are using Bouncy Castle. You may want to consider that route. That's what I eventually had to do to get all my encryption and parsing done. (After lots of banging my head.) But that was 3 years ago... maybe things are better today in Cold Fusion land... but I'd suspect not. Best of luck. :) – abraxascarab Feb 08 '17 at 15:06
  • 1
    It would be wise not to expose in public the encryption key and secret code . At least you didn't expose your name. But the employer may find you one day. – Raymond Chenon Feb 20 '17 at 00:16

1 Answers1

2

An important thing to understand about the sample code is that it refers to bytes. Your CF code is using characters. That might seem like a trivial distinction, but they are totally different things, which will produce very, very different results. In order to decrypt successfully, you need to work with the bytes (or binary) of the given strings - not characters.

Although it is possible to manipulate binary arrays using core CF functions, like arraySlice(), the syntax gets a little bulky/clunky at times. The reason is that binary arrays are a different type of object than your standard CF array, i.e. byte[] versus java.util.List. So depending on which functions are used, you may need javacast to coerce variables into the expected type. With that in mind ..

Part I - Decrypt the encKey

  1. Base64-decode the encKey.
  2. Remove the first 32 bytes of the decoded value. This is the HMAC (Hash Message Authentication Code). Calculate a SHA-256 HMAC of the rest of the decoded data using your API Shared Secret and compare it to the HMAC from the first 32 bytes.

First convert the base64 string into binary using binaryDecode. Then extract the appropriate number of bytes from the returned array. This is the expected HMAC value:

hmacSize = 32;
binaryToDecrypt  = binaryDecode(encryptedKey, "base64");
expectedHMAC = binaryEncode( javacast("byte[]", arraySlice(binaryToDecrypt, 1, hmacSize))
                          , "hex" );

Next, extract all of the remaining bytes, and use them to calculate the actual HMAC. Verify it against the expected value. If the two do not match, something went wrong.

remainData = arraySlice(binaryToDecrypt, hmacSize + 1);
actualHMAC = hmac( javacast("byte[]", remainData ), sharedSecret, "HMACSHA256");

if (compare(actualHMAC, expectedHMAC) != 0) {
    throw("ERROR: Invalid HMAC ["& actualHMAC &"]. Expected ["& expectedHMAC &"]");
}
  1. The next 16 bytes should be removed and used as the IV (Initialization Vector) for the decryption algorithm.

The remaining bytes contains an IV, followed by the encrypted value. Before you can decrypt the latter, you need to extract and separate the two:

ivSize = 16;
ivValue = javacast("byte[]", arraySlice(remainData, 1, ivSize));
encryptedValue = javacast("byte[]", arraySlice(remainData, ivSize + 1));
  1. Decrypt the remaining data using AES-256-CBC, the IV from step 3, and the SHA-256 hash of your API Shared Secret.

The last step before you can decrypt is to generate the decryption key, by hashing the shared secret. Unfortunately, CF's hash() function always returns a hex string. So it must be converted into base64 format to be compatible with the decryption function.

keyHex = hash(sharedSecret, "SHA-256", "utf-8");
keyBase64 = binaryEncode(binaryDecode(keyHex, "hex"), "base64");

Finally, use all three values to decrypt. The returned binary will contain the encryption key used in part II.

decryptedKeyBinary = decryptBinary( encryptedValue
                           , keyBase64
                           , "AES/CBC/PKCS5Padding"
                           , ivValue);

Part II - Decrypt the encPaymentData

Use the exact same process as in Part I, just swap the variables:

  • Use encPaymentData instead of encryptedKey
  • Use decryptedKeyBinary instead of sharedSecret.

The final, decrypted result will be binary. Use charsetEncode to convert it back into a human readable string:

result = charsetEncode(decryptedResult, "utf-8");

NB: The sample values you posted appear to be broken, as they do not even work with the java example. The steps above do produce the correct result when used valid values (key, data, etcetera).

Community
  • 1
  • 1
Leigh
  • 28,765
  • 10
  • 55
  • 103