Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Luis Majano
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Luis Majano ( @lmajano )

Using AES / ECB / PKCS5Padding Encryption In ColdFusion And Decrypting Values In Node.js

By on

CREDIT: First things first, I can't take any credit for this post. This problem was solved by Josh Barber, our lead mobile engineer. I'm simply documenting the solution for my own future reference and understanding.

At InVision, we recently ran into an interesting issue with AES (Advanced Encryption Standard) encryption. We started out by encrypting and decrypting values in ColdFusion, which worked quite nicely. Then, for a number of unfortunate reasons, we had to start decrypting those same values in Node.js. What I assumed would have been an easy problem to solve ended up taking 3 developers, myself included, several hours to figure out.

The complexity in encrypting and decrypting values across different technology stacks is that the algorithms in question have to line up exactly. Otherwise, you end up playing a game of Telephone Operator in which the outputs have absolutely no resemblance to the inputs.

On the ColdFusion side, we simply chose to use "AES" encryption with a 128-bit key. What we didn't think about is that this "AES" algorithm name actually implied a number of additional defaults. Specifically, the "AES" encryption algorithm in ColdFusion defaults to using, "AES/ECB/PKCS5Padding". Or, AES with an Electronic Code Book (ECB) feedback mode using the PKCS5Padding padding method. Which looks something like this:

encrypt( input, key, "AES", "base64" )

What you may notice here is that we also omitted the optional salt / initialization vector (IV) in our encrypt() invocation. All together, this invocation leaves us with a set of default values on the ColdFusion side that we need to explicitly mirror on the Node.js side.

Before we look at the Node.js code, let's setup our ColdFusion test case:

<cfscript>

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

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

	// Put the input through the encryption and decryption life-cycle using the AES
	// algorithm and the default [AES], [IVorSalt], and [iterations] values.
	// --
	// NOTE: ColdFusion uses "AES/ECB/PKCS5Padding" as the default configuration which
	// we can demonstrate by using the default configuration to encrypt and then the
	// more explicit configuration to decrypt.
	encryptedInput = encrypt( input, encryptionKey, "AES", "base64" );
	decryptedInput = decrypt( encryptedInput, encryptionKey, "AES/ECB/PKCS5Padding", "base64" );

	// Output the all the values, including an input / output test.
	writeOutput( "Input: #input# <br />" );
	writeOutput( "Encrypted Input: #encryptedInput# <br />" );
	writeOutput( "Decrypted Input: #decryptedInput# <br />" );
	writeOutput( "Values Match: #( compare( input, decryptedInput ) eq 0 )#" );

</cfscript>

Here, we're generating a 128-bit secret key and using it to encrypt() and decrypt() a value using the "AES" algorithm without an explicit salt. As part of this control case, I'm trying to demonstrate that the "AES" algorithm name implies "AES/ECB/PKCS5Padding" by using both values during the encryption life-cycle. And, when we run this code, we get the following output:

Input: Get out back and hold the monkey!
Encrypted Input: TmiHZg7CvY+92iNxzp+nR6gX9ynpKmc5t6ZP1sZLU5UzCAN601RDfyOuGu3fq8jh
Decrypted Input: Get out back and hold the monkey!
Values Match: YES

As you can see, the inputs and outputs match. Now, on the Node.js side, we need to take the encrypted value, produced by ColdFusion, and decrypt it using the same secret key.

At first, we tried to use the crypto.createDecipher() method which doesn't include a salt. However, what we didn't realize at first was this method takes a "password" and not an "encryption key." The difference is that the password is used to derive the encryption key and salt. What Josh Barber finally figured out is that we needed to use the crypto.createDecipheriv() method and explicitly provide it with an empty initialization vector (IV) (since our ColdFusion code omitted a salt).

// Require the core node modules.
var crypto = require( "crypto" );


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

// In Node, we need to use the same AES secret key that we generated and used in
// our ColdFusion encryption algorithm.
// --
// Generated using generateSecretKey( "AES", 128 ).
var encryptionKey = "JZidBZLaYf27huVuM4MNTA==";

// I am the encrypted input value as generated by ColdFusion's encrypt() method.
// --
// NOTE: encrypt( input, encryptionKey, "AES", "base64" );
var coldfusionEncryptedValue = "TmiHZg7CvY+92iNxzp+nR6gX9ynpKmc5t6ZP1sZLU5UzCAN601RDfyOuGu3fq8jh";


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


// The CipherIV methods must take the inputs as a binary / buffer values.
var binaryEncryptionKey = new Buffer( encryptionKey, "base64" );
var binaryIV = new Buffer( 0 );

// It was the use of an empty IVorSalt value that really blocked us for a while in
// getting this to work. The biggest challenge when performing cryptography across
// different technologies is marking sure that the algorithms line-up exactly otherwise
// the values won't be correct. In ColdFusion, we didn't provide an initialization
// vector; as such, in Node, we have to do the same thing by EXPLICITLY providing an
// EMPTY IV value.
// --
// NOTE: If we had used the crypto.createCipher() method instead, the IV value would
// have been derived from the encryption key, which is definitely not what we wanted.
var cipher = crypto.createCipheriv( "AES-128-ECB", binaryEncryptionKey, binaryIV );

// When encrypting, we're converting the UTF-8 input to Base64 output.
var encryptedInput = (
	cipher.update( input, "utf8", "base64" ) +
	cipher.final( "base64" )
);


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


var decipher = crypto.createDecipheriv( "AES-128-ECB", binaryEncryptionKey, binaryIV );

// When decrypting we're converting the Base64 input to UTF-8 output.
var decryptedInput = (
	decipher.update( encryptedInput, "base64", "utf8" ) +
	decipher.final( "utf8" )
);


// Output the all the values, including an input / output test for ColdFusion and
// Node.js to see if the encrypted values match.
console.log( "Input:", input );
console.log( "Encrypted Input:", encryptedInput );
console.log( "Decrypted Input:", decryptedInput );
console.log( "Values Match:", ( input === decryptedInput ) );
console.log( "ColdFusion / Node Match:", ( coldfusionEncryptedValue === encryptedInput ) );

As you can see, while we omitted the salt on the ColdFusion side, we had to explicitly provide an empty salt - new Buffer( 0 ) - on the Node.js side. And, instead of just choosing "AES", we had to explicitly choose, "AES-128-ECB". That said, when we run the above code, we get the following output:

Input: Get out back and hold the monkey!
Encrypted Input: TmiHZg7CvY+92iNxzp+nR6gX9ynpKmc5t6ZP1sZLU5UzCAN601RDfyOuGu3fq8jh
Decrypted Input: Get out back and hold the monkey!
Values Match: true
ColdFusion / Node Match: true

As you can see, both ColdFusion and Node.js produce the same encrypted value. And, Node.js is able to decrypt that encrypted value in order to reveal the original input. The trick was taking the default and implied settings in ColdFusion and converting them into explicit configurations on the Node.js side.

According to Adobe, the ECB (Electronic Code Book) feedback mode is "fine for encrypting short strings of data, or for strings that do not contain any predictable or repeating groups of characters." Which is basically what we were doing. But, in retrospect, we may have opted for something stronger like CBC (Cipher Block Chaining), which is considered the strongest feedback mode. Of course, that would also require an initialization vector, which increases the complexity of the code. That said, it would definitely be a good follow-up exploration of cross-technology encryption.

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

Reader Comments

1 Comments

Interesting.

Good catch by Josh.

Don't you love it when these "simple" tasks spin out of control and take more time and resources than first thought.

:)

15,674 Comments

@Jeff,

Yeah, very true! I wish I really understood encryption better. I started a course on it at university; but, 2 weeks in, another course on Web Development was opened up, so I transferred. In retrospect, I still would have done it; but, I would have gone back and re-taken the cryptography class. All I remember is a lot of "bob" and "mary" need to talk and someone is sitting in between then :D

1 Comments

Thank you. This is exactly the information I was looking for. If CF's documentation on the encrypt function includes all those details about the default being "AES/ECB/PKCS5Padding", and what it does when no IV is specified, I couldn't find it there.

I've done a good portion of the work on an app called Kostizi since 2009, in ColdFusion with the ColdBox framework. Our CF app stores data in a MSSQL database; some of that data was encrypted using CF's encrypt function with "AES" as the algorithm, and no IVorSalt, exactly as you described above.

Now I'm creating an API service using node.js to serve up some of that data for standalone apps and other integrations. The node.js app retrieves the CF-encrypted string (along with its key) directly from the MSSQL database. I was able to adapt the code you provided to decrypt those values.

Incidentally, in the course of my research on this problem, I found out that Python uses null-byte padding by default (per nscdex, here: https://github.com/nodejs/node/issues/2794)

Your blog is a valuable public service.

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