Hex-Encoding A Binary Value / Byte Array In ColdFusion
Posted August 16, 2012 at 10:18 AM by Ben Nadel
When you start dealing with 3rd-party APIs, hex-encoding a byte array becomes a surprisingly common task. This is because 3rd-party APIs often require you to "sign" the request using your secret Key to produce a Hashed Message Authentication Code (Hmac). EmailYak uses the MD5 hashing algorithm; Pusher uses the Sha-256 hashing algorithm; and Twilio uses the Sha-1 hashing algorithm. Each of these hashes is returned as a byte array that has to be Hex-encoded before it can be attached to the API request. I've used a number of Hex-encoding approaches; but yesterday, I (re-)discovered that ColdFusion provides a super simple, built-in method for Hex-encoding binary values: binaryEncode().
NOTE: ColdFusion 10 now provides a built-in hmac() function for creating Hashed Message Authentication Codes; but that is beyond the scope of this post.
ColdFusion has so many built-in functions that if you don't use them on a regular basis, you can quickly forget that they exist. Such is the case with binaryEncode(). I don't believe I've used in about 6 years. As it turns out, though, it makes hex-encoding a binary value a one-line task.
Having re-discovered this yesterday, I wanted to quickly perform a functional comparison of binaryEncode() to some of the other hex-encoding approaches that I have used in the past. In the following code, I'm using a manual approach, a BigInteger approach, and a binaryEncode() approach to hex-encode a single byte array. This way, I can make sure that they all produce the same output:
- <cfscript>
-
-
- // I encode the binary value / byte array as a HEX value by
- // manually looping over the array, encoding each byte
- // individually.
- function encodeManually( Any bytes ){
-
- // Create a buffer to hold each encoded byte.
- var hexBuffer = [];
-
- // Get the length of the array so we don't have to keep
- // evaluating it for each loop iteration.
- var byteCount = arrayLen( bytes );
-
- // Use an index loop rather than a for-in loop because
- // ColdFusion seems to have some trouble with navigating a
- // non-stanard array.
- for (var i = 1 ; i <= byteCount ; i++){
-
- // Strip off any sign information - let's work with
- // only the last 8 bits of information.
- var unsignedByte = bitAnd( bytes[ i ], 255 );
-
- var hexValue = formatBaseN( unsignedByte, 16 );
-
- // Of any byte value less than or equal to 15, we only
- // need one-digit of HEX encoding; however, we want all
- // our encoding values to be 2-digits.
- if (unsignedByte <= 15){
-
- hexValue = ("0" & hexValue);
-
- }
-
- arrayAppend( hexBuffer, hexValue );
-
- }
-
- // Join the hex buffer and return the full hex-encoding.
- return(
- arrayToList( hexBuffer, "" )
- );
-
- }
-
-
- // ------------------------------------------------------ //
- // ------------------------------------------------------ //
-
-
- // I encode the binary value / byte array as a HEX value using
- // the BigInteger class.
- function encodeWithBigInt( Any bytes ){
-
- // Let's pretend that our byte array represents the bytes of
- // enormous integer value.
- var bigInt = createObject( "java", "java.math.BigInteger" ).init(
- javaCast( "int", 1 ),
- bytes
- );
-
- // Cast our enormous integer to a base16 string.
- var hexEncoding = bigInt.toString( javaCast( "int", 16 ) );
-
- // When converting to a HEX value, BigInteger will strip off
- // the leading zero. However, we want all of our HEX values to
- // be represented as a 2-digit hex. If the first byte is less
- // than or equal to 15, let's prepend the leading zero.
- var firstByte = bitAnd( bytes[ 1 ], 255 );
-
- if (firstByte <= 15){
-
- hexEncoding = ("0" & hexEncoding);
-
- }
-
- // Return the hex-encoding.
- return( hexEncoding );
-
- }
-
-
- // ------------------------------------------------------ //
- // ------------------------------------------------------ //
-
-
- // I encode the binary value / byte array as a HEX value using
- // the built-in binaryEncode() ColdFusion metho.
- function encodeWithBinaryEncode( Any bytes ){
-
- // Convert the binary to a hex-encoded string.
- var hexEncoding = binaryEncode( bytes, "hex" );
-
- // Lower-case the HEX value since none of the other methods
- // above use upper-case value (while ColdFusion does).
- return(
- lcase( hexEncoding )
- );
-
- }
-
-
- // ------------------------------------------------------ //
- // ------------------------------------------------------ //
- // ------------------------------------------------------ //
- // ------------------------------------------------------ //
-
-
- // I return a byte array. The byte array is built using values
- // at the low end of the ASCII table. This is done to ensure that
- // we run into a situation where our first HEX value is less than
- // 16. This will test the leading-zero situation.
- function getBytes(){
-
- // Let's built up an array of characters that will be
- // converted to binary.
- var charBuffer = [];
-
- // Build the array up using ASCII decimal values.
- for (var i = 9 ; i <= 32 ; i++){
-
- arrayAppend( charBuffer, chr( i ) );
-
- }
-
- // Collapse the character array.
- var textValue = arrayToList( charBuffer, "" );
-
- // Return the string as binary data.
- return(
- toBinary( toBase64( textValue ) )
- );
-
- }
-
-
- // ------------------------------------------------------ //
- // ------------------------------------------------------ //
-
-
- bytes = getBytes();
-
- // Test the various encoding approaches.
- writeOutput( "Hex: " & encodeManually( bytes ) );
- writeOutput( "<br />" );
- writeOutput( "Hex: " & encodeWithBigInt( bytes ) );
- writeOutput( "<br />" );
- writeOutput( "Hex: " & encodeWithBinaryEncode( bytes ) );
-
-
- </cfscript>
As you can see, each of these approaches is less complex than the one before it, with binaryEncode() being the only one-liner. When we run the three algorithms in sequence, we get the following output:
Hex: 090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20
Hex: 090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20
Hex: 090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20
In this case, I am using lcase() to convert the binaryEncode() value to lower-case. While case does not matter for HEX values, the first two approaches use lower-case characters. As such, I used lcase() simply to make them all look the same - it was easier to compare the outputs.
As you can see above, all three approaches produce the same Hex-encoding. And, obviously, ColdFusion's built-in binaryEncode() is by far the most straightforward approach. It's a shame that I forgot this function existed - it will definitely make dealing with 3rd party APIs much easier (when not using ColdFusion 10).



