Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Austin Shelton
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Austin Shelton

Using AES / CBC / PKCS5Padding Encryption With An Auto-Generated Initialization Vector In ColdFusion

By
Published in Comments (2)

In the last couple of posts, I've looked at using AES / ECB encryption and AES / CBC encryption in ColdFusion and then, subsequently, decrypting those values in Node.js using the Crypto module. And, as I was reading up on ColdFusion's encryption features in Adobe's white-paper on Strong Encryption, something caught my eye - if you don't provide an explicit initialization vector (IV) for the encrypt() function, ColdFusion will auto-generate one and prepend it to the encrypted result. Since I've been thinking a lot about producing and consuming encrypted values across platforms, I wanted to see how I might tease-out the auto-generated IV value for cross-platform encryption.

Here's the quote from the white-paper that I was referring to:

When ColdFusion creates an IV automatically, it generates a secure, random IV and prepends this to the encrypted data. When ColdFusion decrypts the data, this IV is recovered and used. It is cryptologically important that the IV varies between encryptions. This is why the encrypted value changes when you repeatedly encrypt the same string with an algorithm that uses an IV, like DES/CBC/PKCS5Padding. Unlike the encryption key, it is not necessary for the IV to be kept secret.

I'm not a security expert by any means. But, from what I understand, the beauty of this is that a different salt value (or "initialization vector" in this case) is stored with each encrypted value. And, while the salt itself is not private - it would be compromised right along with the encrypted value if stolen - a unique salt prevents one cracked value from matching others in a compromised set of data. And, what's more, the hacker would have to know that a salt is even being used. If nothing else, this helps defend against the use of so-called "rainbow table attacks" as the rainbow table would be unlikely to contain precomputed values that include the right salts.

That said, I wanted to see if I could encrypt() a value in ColdFusion without an explicit initialization vector (IV); and then, parse the resultant value, tease out the IV, and decrypt() the same value, this time with an explicitly provided IV.

Because we are using the CBC, or "Cipher Block Chaining" feedback mode, we know that each block of encrypted data is being used to encrypt the next block of data. And, that the initialization vector has to be the same size as the block in order to seed the first block encryption. Since the AES block size is 16-bytes, we can conclude that the initialization vector is the first 16-bytes of the encrypted value. Let's see if we can splice out those 16-bytes and use them to decrypt the input.

<cfscript>

	// Generated using generateSecretKey( "AES", 128 ).
	encryptionKey = "Glu0r6o0GzBZIe0Qsrh2FA==";

	// I am the value that we will be encrypted and decrypting.
	input = "Get out back and hold the monkey!";

	// Here, we're using the "CBC" feedback mode, or "Cipher Block Chaining", but we're
	// not providing an initialization vector (IV). As such, ColdFusion will randomly
	// enerate the initialization vector and prepend it to the encrypted input (as part
	// of the result) so that it can be recovered during decryption.
	encryptedValue = encrypt(
		input,
		encryptionKey,
		"AES/CBC/PKCS5Padding",
		"base64"
	);

	// At this point, the encrypted value is philosophically : "#IV##encryptedInput#".
	// To test this, let's see if we can take the result, parse out the automatically
	// generated initialization vector, and then use to explicitly decrypt the input.

	// First, we need to convert the result back to a binary value.
	encryptedBytes = binaryDecode( encryptedValue, "base64" );

	// Now, we have to tease out the initialization vector (IV). When using a block-based
	// encryption algorithm like AES, the IV value has to be the same size as the
	// algorithm's block size. For AES, this is 16-bytes. Which means that the IV will
	// be the first 16-bytes of the encrypted value. We can splice the result using
	// simple array indices.
	randomIV = binarySlice( encryptedBytes, 1, 16 );
	encyptedInput = binarySlice( encryptedBytes, 17 );

	// Now that we have our explicit values, we can try to decrypt the input.
	decryptedInput = decrypt(
		binaryEncode( encyptedInput, "base64" ),
		encryptionKey,
		"AES/CBC/PKCS5Padding",
		"base64",
		randomIV
	);

	// Output the results.
	writeOutput( "Input: #input# <br />" );
	writeOutput( "Encrypted Input: #encryptedValue# <br />" );
	writeOutput( "Decrypted Input: #decryptedInput# <br />" );
	writeOutput( "Values Match: #( compare( input, decryptedInput ) eq 0 )#" );


	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //


	/**
	* I slice a portion of the given binary value and return it.
	*
	* @input I am the binary value being sliced.
	* @index I am the 1-based index used to start the slice.
	* @length I am the optional length of the slice (if omitted, will slice to end).
	* @output false
	*/
	public binary function binarySlice(
		required binary input,
		required numeric index,
		numeric length = 0
		) {

		var result = length
			? arraySlice( input, index, length )
			: arraySlice( input, index )
		;

		return( javaCast( "byte[]", result ) );

	}

</cfscript>

As you can see, our call to encrypt() omits an IV value, which will cause ColdFusion to auto-generate it and prepend it to the result. We then take that result, splice out the first 16-bytes and then feed the spliced IV and the spliced encrypted value back into the decrypt() function. And, when we run this, we get the following output:

Input: Get out back and hold the monkey!
Encrypted Input: ffTQiMvZs2H9MzuH49sRdmo4EfJDrhlbT7YmUtZM7Ef/qQ5Diups8zX2GVEpucDYF6iVvas6tWmdDDLwLHmMYQ==
Decrypted Input: Get out back and hold the monkey!
Values Match: YES

Woot! It worked like a charm!

Of course, if you're using ColdFusion for both the encryption and the decryption portions of the workflow, you don't need to jump through these hoops. If you omit an IV value in your decrypt() invocation, ColdFusion will attempt to parse the IV value out of the encrypted value (just like we did). You only need to do this yourself if you were encrypting the value in ColdFusion and then decrypting it on another platform, like Node.js.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel