Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Kurt Wiersma and Peter Farrell
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Kurt Wiersma and Peter Farrell

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

By Ben Nadel on

The other day, I talked about the problems that we ran into when using default AES (Advanced Encryption Standard) encryption settings in ColdFusion and then trying to decrypt those values in Node.js. It was a bit sticky trying to figure out what the default feedback mode was (it was ECB) and how to deal with an empty initialization vector (IV). The whole thing would have been more straightforward - and self-documenting - had we explicitly defined all of the encryption setting. So, I wanted to do a really quick follow-up post that does just that.


 
 
 

 
 
 
 
 

The ECB (Electronic Code Book) feedback mode is fine for short inputs that aren't likely to contain repetition. But, the CBC (Cipher Block Chaining) feedback mode is considered the fastest and most secure feedback mode; it uses the result of each encrypted block as an input to the next block encryption.

In this demo, we're going to use CBC, which means that we also have to supply an initialization vector (IV) to seed the encryption of the first block. With block-based algorithms, like AES, the IV value needs to be same size as the encryption block; which, in the case of AES, is 16-bytes. To keep things simple, we'll generate 16 random Java-compatible bytes using the randRange() function and the SHA1PRNG algorithm.

First, let's generate the cryptographically secure secret key and initialization value:

  • <cfscript>
  •  
  • // Generated cryptographically secure 128-bit key.
  • encryptionKey = generateSecretKey( "AES", 128 );
  •  
  • // When generating the initialization vector (IV) for the AES algorithm, we have to
  • // use a value that is the same size as the algorithm block size, which in the case
  • // of AES is 16-bytes. In order to create a cryptographically secure value, we'll
  • // use the "SHA1PRNG" algorithms in the randRange() function, which I BELIEVE is
  • // provided as part of the Java Cryptography Extension (JCE).
  • randomIntegers = [];
  •  
  • for ( i = 1 ; i <= 16 ; i++ ) {
  •  
  • arrayAppend( randomIntegers, randRange( -128, 127, "SHA1PRNG" ) );
  •  
  • }
  •  
  • initializationVector = javaCast( "byte[]", randomIntegers );
  •  
  • // Output the keys for use in the ColdFusion and Node.js demos.
  • writeOutput( "Key: #encryptionKey# <br />" );
  • writeOutput( "IV: #binaryEncode( initializationVector, 'base64' )# <br />" );
  •  
  • </cfscript>

As you can see, we're generating an AES-compatible 128-bit secret key and randomly generated initialization vector. Now, let's try to encrypt and decrypt an input value in ColdFusion using these settings (I am hard-coding the output of the previous code as the keys in the following code):

  • <cfscript>
  •  
  • // Generated using generateSecretKey( "AES", 128 ).
  • encryptionKey = "Glu0r6o0GzBZIe0Qsrh2FA==";
  •  
  • // Generated using randRange() with "SHA1PRNG" algorithm.
  • initializationVector = "53IQ1tPX3aHxzqV4AyePDg==";
  •  
  • // 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". In this
  • // mode, each block of encrypted data is fed back into the encryption algorithm for
  • // the encryption of the next block. As such, even an input that contains repeating
  • // values will not result in an output with similar repetition.
  • encryptedInput = encrypt(
  • input,
  • encryptionKey,
  • "AES/CBC/PKCS5Padding",
  • "base64",
  • binaryDecode( initializationVector, "base64" )
  • );
  •  
  • decryptedInput = decrypt(
  • encryptedInput,
  • encryptionKey,
  • "AES/CBC/PKCS5Padding",
  • "base64",
  • binaryDecode( initializationVector, "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>

As you can see, we are explicitly using the "AES" algorithm with the "CBC" feedback mode. And, when we run this code, we get the following output:

Input: Get out back and hold the monkey!
Encrypted Input: 73vtXZp/0BQNi2NhTxyfmJLham1tiWoAmP8sNjdIc/CO5A1/PySOgxvFhQugYkaC
Decrypted Input: Get out back and hold the monkey!
Values Match: YES

Now, let's try the same thing in Node.js, using the same secret key and initialization vector, and see if the outputs line up with what ColdFusion generated:

  • // 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!";
  •  
  • // I am the encryption inputs generated and used in the ColdFusion code.
  • // --
  • // NOTE: Generated using generateSecretKey( "AES", 128 ) and randRange( "SHA1PRNG" ).
  • var encryptionKey = "Glu0r6o0GzBZIe0Qsrh2FA==";
  • var initializationVector = "53IQ1tPX3aHxzqV4AyePDg==";
  •  
  • // I am the encrypted input value as generated by ColdFusion's encrypt() method.
  • // --
  • // NOTE: encrypt( input, key, "AES/CBC/PKCS5Padding", "base64", IV );
  • var coldfusionEncryptedValue = "73vtXZp/0BQNi2NhTxyfmJLham1tiWoAmP8sNjdIc/CO5A1/PySOgxvFhQugYkaC";
  •  
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  •  
  • // The CipherIV methods must take the inputs as a binary / buffer values.
  • var binaryEncryptionKey = new Buffer( encryptionKey, "base64" );
  • var binaryIV = new Buffer( initializationVector, "base64" );
  •  
  • // When creating the cipher in Node, we have to make sure we use the exact same
  • // algorithm and inputs that we used in ColdFusion. In this case, we're using the "AES"
  • // (Advanced Encryption Standard) with an "CBC" (Cipher Block Chaining) feedback mode
  • // and 128-bit key.
  • var cipher = crypto.createCipheriv( "AES-128-CBC", 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-CBC", 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 ) );

This time, all of the encryption settings are explicitly spelled out in both the ColdFusion and Node.js scripts, which make this code easier to reason about. And, when we run this code through Node, we can see that all values line-up properly:

Input: Get out back and hold the monkey!
Encrypted Input: 73vtXZp/0BQNi2NhTxyfmJLham1tiWoAmP8sNjdIc/CO5A1/PySOgxvFhQugYkaC
Decrypted Input: Get out back and hold the monkey!
Values Match: true
ColdFusion / Node Match: true

As you can see, the encrypted value, produced by Node.js, matches the encrypted value produced by ColdFusion. And, since Node.js was able to encrypt and then decrypt the value, it demonstrates that the input can be encrypted in ColdFusion and decrypted in Node.js. While the general algorithm in this post is really the same as it was in my previous post, I think having all the settings explicitly defined makes the code easier to understand.




Reader Comments

@All,

Another interesting feature of the encrypt() and decrypt() functions is that you can completely omit the IV (initialization vector) value. In doing so, ColdFusion will auto-generate the IV value for each call to encrypt() and prepend it to the resultant, encrypted value:

http://www.bennadel.com/blog/3122-using-aes-cbc-pkcs5padding-encryption-with-an-auto-generated-initialization-vector-in-coldfusion.htm

The decrypt() function can then parse the input and splice out the auto-generated IV value. In the above post, I look at doing it explicitly so as to enable cross-platform encryption.

Reply to this Comment

@Lily,

It sounds like ECB is frowned upon, except for small values that are not repeating. But, I agree - from the things I have read lately, it seems that CBC is a recommended approach.

Reply to this Comment

Very lame tutorial.you could ve at least shown how to set padding in this fucking thing

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.