Hex-Encoding A Binary Value / Byte Array In ColdFusion
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).
Want to use code from this post? Check out the license.
Reader Comments