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