4

I'm working on a project to decrypt an AES-128 encrypted string in ColdFusion that is passed as a URL parameter.

The vendor takes a pass phrase and converts it to a valid AES-128 key "using an algorithm equivalent to Microsoft's CryptDeriveKey using the SHA-1 hash function." I need to replicate this generatedKey in ColdFusion so I can use the value in my decrypt() call.

When using CryptDeriveKey you pass the encryption type, the Hash type, the block length and a 0 iv array and it returns the Hash. Source: Generating a Key from a Password

// generate an RC2 key
byte[] iv = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
byte[] key = cdk.CryptDeriveKey(“RC2”, “SHA1”, 128, iv);

In the vendor's test tool, the pass phrase "test1234" results in a Hash of:

 A6455C7A24BC5E869B0DDF647238F5DA

I found the genAESKeyFromPW() UDF, which seems to be the closest, but requires a salt which CryptDeriveKey does not use. I have also tried the code below. However, it is not working as the Hash() is not creating a valid AES-128 key:

<cfset generatedKey = Hash('test1234', 'SHA-1')>
<cfset decrypted=decrypt(encryptedString, generatedKey, 'AES/CBC/PKCS7Padding', 'Base64', '0')>

What steps do I need to replicate CryptDeriveKey function?

Update:

The vendor provided this C# example of the decryption:

public static byte[] AesDecryptBytes(byte[] cipherText, byte[] key)
{
    byte[] IV = new byte[16];

    AesManaged aes = new AesManaged();
    aes.Mode = CipherMode.CBC;
    aes.Padding = PaddingMode.PKCS7;
    ICryptoTransform decryptor = aes.CreateDecryptor(key, IV);

    byte[] plainBytes;

    using (MemoryStream memoryStream = new MemoryStream())
    {
        using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Write))
        {
            cryptoStream.Write(cipherText, 0, cipherText.Length);
        }

        plainBytes = memoryStream.ToArray();
    }

    return plainBytes;
}
Leigh
  • 28,765
  • 10
  • 55
  • 103
Stephen Sharpe
  • 167
  • 1
  • 8
  • Honestly, I would use Adobe ColdFusion's feature to invoke .NET assemblies for the key. Trying to achieve what `CryptDeriveKey` does will be a pain in the ... even with Java, so you are better of using what .NET already provides. – Alex Jun 15 '17 at 15:57
  • Probably. *RE: results in a Hash of 'A64...DA* Do you mean that is the result of CryptDeriveKey()? Since it looks like you are using dummy values, what is the test "encryptedString" value? Side note, CF/java use PKCS5Padding, not PKCS7Padding. – Leigh Jun 15 '17 at 18:40
  • Alex: thanks for the heads-up. I was not aware and if there is no CF way to replicate I will invoke .NET. – Stephen Sharpe Jun 15 '17 at 19:59
  • Leigh: Yes that is the result from putting 'test1234' into CryptDeriveKey to get an AES-128 generatedKey. the test encrypted string will be the next step but without the valid genrated key – Stephen Sharpe Jun 15 '17 at 20:04
  • **(Edit)** Are you certain about these values and/or the algorithms? You can duplicate that hash value, "A6455C7A..." in CF, but ... a few things seem fishy. AES/CBC requires a 16bit IV, but your example only has 8. So even hard coding the three values you provided (key, encrypted string and IV) encrypt is not going to work with "AES/CBC/PKCS5Padding". – Leigh Jun 15 '17 at 20:52
  • As far as I know, CryptDeriveKey uses a non-public algorithm. So you need to either call into the .NET one or all the way down to the Win32 version. – bartonjs Jun 16 '17 at 01:13
  • @bartonjs - CF supports tapping into .NET, so that would probably work. That said, it reads like the vendor is not actually using CryptDeriveKey, only something similar. (Also, I did not think CryptDeriveKey supported AES) I can replicate the sample key, but ... something still seems off as the sample key, iv and encrypted string do not align with what is expected for AES/CBC/128 ... – Leigh Jun 16 '17 at 01:26
  • @StephenSharpe - Personally, I would start with something simpler. Verify you can actually decrypt the data with the given algorithm using hard coded strings (key should be base64 obviously). Once that is working, move on to generating the same key. – Leigh Jun 16 '17 at 16:26
  • @Leigh thank you for your assistance/insight! I have a validator tool supplied by the vendor and I'm sharing values derived from that... but I'm unsure about the IV... their documentation states "For AES, there are three settings for decrypting the data (0 IV, CBC mode, PKCS7 padding)" – Stephen Sharpe Jun 16 '17 at 16:39
  • @bartonjs/@alex - I've moved forward with your suggestion and setup the CF .net integration... and ran a sample next up is something like this (unsure the .dll is correct looking at that as I type) – Stephen Sharpe Jun 16 '17 at 16:41
  • @bartonjs - the passphrase "will be converted to a key using an algorithm equivalent to Microsoft's CryptDeriveKey using the SHA-1 hash function." this generatedKey is used to decrypt the AES crypted string. – Stephen Sharpe Jun 16 '17 at 16:48
  • @StephenSharpe - Is this a public API? If so, could you post the URL? The IV should be a binary array of the algorithm block size. Based on the comment it should be all zeros (not good in practice, but okay for a test case). In CF the block size for AES is 16 (some api's support other sizes). The weird part is that even using a hard coded key and an array of all zeroes the sample value does not decrypt. That is why I am wondering if something is off. – Leigh Jun 16 '17 at 17:59
  • Unfortunately, it's not public... I've reached out to to the vendor for clarification... what I learn (that I'm able to share) I will add. Thank you for your assistance it has really helped me to ask better questions! – Stephen Sharpe Jun 16 '17 at 18:27
  • Okay, good. If they have any java, c# examples feel free to include them and we can translate. (BTW, too bad you do not have enough rep for chat yet. Think it is 25? ) – Leigh Jun 16 '17 at 19:18
  • In the test tool the keyphrase test1234 generates a hash of A6455C7A24BC5E869B0DDF647238F5DA then in a second step the hash is used to encrypt recordID=000001 to 1lqcm0Jiy4Rs29tz2jpuoQ== – Stephen Sharpe Jun 19 '17 at 15:21
  • @Leigh... I've added a sample of the .NET decrypt supplied by the vendor. – Stephen Sharpe Jun 19 '17 at 15:28
  • Okay, that explains it. The earlier encrypted string was wrong. "1lqcm0Jiy4Rs29tz2jpuoQ==" works when decrypted. – Leigh Jun 19 '17 at 17:24

1 Answers1

2

From what I have read in this documentation, it sounds like the function essentially hashes the password binary then does an XOR with two arrays. I am not 100%, but I do not think the method they described is the same as PBKDF1 or PBKDF2.

Let n be the required derived key length, in bytes. The derived key is the first n bytes of the hash value after the hash computation has been completed by CryptDeriveKey. If the hash is not a member of the SHA-2 family and the required key is for either 3DES or AES, the key is derived as follows:

  1. Form a 64-byte buffer by repeating the constant 0x36 64 times. Let k be the length of the hash value that is represented by the input parameter hBaseData. Set the first k bytes of the buffer to the result of an XOR operation of the first k bytes of the buffer with the hash value that is represented by the input parameter hBaseData.
  2. Form a 64-byte buffer by repeating the constant 0x5C 64 times. Set the first k bytes of the buffer to the result of an XOR operation of the first k bytes of the buffer with the hash value that is represented by the input parameter hBaseData.
  3. Hash the result of step 1 by using the same hash algorithm as that used to compute the hash value that is represented by the hBaseData parameter.
  4. Hash the result of step 2 by using the same hash algorithm as that used to compute the hash value that is represented by the hBaseData parameter.
  5. Concatenate the result of step 3 with the result of step 4.
  6. Use the first n bytes of the result of step 5 as the derived key.

CF Key Generation

Start by hashing the password and converting it into binary with binaryDecode:

hBaseData = binaryDecode(hash("test1234", "SHA1"), "hex");

Build and populate the two buffers with the specified constants:

// 0x36 (i.e. 54 decimal)
buff1 = listToArray(repeatString("54,", 64)); 
// 0x5C (i.e. 92 decimal)
buff2 = listToArray(repeatString("92,", 64));

Then do a bitwise XOR, storing the results in the buffer:

for (k = 1; k <= arrayLen(hBaseData); k++) {
    buff1[k] = BitXOR( buff1[k], hBaseData[k]);
    buff2[k] = BitXOR( buff2[k], hBaseData[k]);
}

Next hash() both buffers and concatenate the results:

hash1 = hash( javacast("byte[]", buff1), "SHA1");
hash2 = hash( javacast("byte[]", buff2), "SHA1");
combined = hash1 & hash2;

Finally, extract the first n bytes (16 == 128 bits / 8) as the new key. Since CF's hash() function returns hexadecimal (always two characters per byte) string functions can be used here.

keySize = 128 / 8;
newKey = left(combined, keySize *2);

Result: A6455C7A24BC5E869B0DDF647238F5DA


Decrypting in CF

Before you can decrypt, a few important notes:

  1. CF's encrypt/decrypt functions expect keys to be encoded as base64. The generated key above is in hexadecimal format. So it must be converted first:

  2. "PKCS7Padding" is not valid for CF/Java. Instead use PKCS5Padding.

  3. "CBC" mode always requires an IV. The IV is a binary array whose length is the same as the algorithm's block size (AES block size = 16 bytes). It must be "...the same value [used to encrypt] to successfully decrypt the data." Based on the descriptions in your API, your IV should be all zeros. (This is not good in practice, but is okay for a test case).

For more details, see Strong encryption in ColdFusion

Example:

encrypted = "1lqcm0Jiy4Rs29tz2jpuoQ==";
newKeyHex = "A6455C7A24BC5E869B0DDF647238F5DA";
keyBase64 = binaryEncode(binaryDecode(newKeyHex, "hex"), "base64");
iv = javacast("byte[]", [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
decrypted = decrypt(encrypted, keyBase64, "AES/CBC/PKCS5Padding", "Base64", iv);
writeOutput("<br>"& decrypted);

Result: recordID=000001

Leigh
  • 28,765
  • 10
  • 55
  • 103