Considering Encrypting Passwords At Rest In ColdFusion
Now that I've rebuilt my Incident Commander triage app in ColdFusion, I'm looking at ways to make it more security-minded. Right now, it uses a large 64-byte alpha-numeric URL-based token to prevent brute-force attacks. But, I'd like to give users the option of including an additional non-URL-based authentication mechanism. To this end, I'm exploring the idea of a session password. Only, unlike a traditional password, which can leverage a one-way hash (think bCrypt, sCrypt, and Argon2), I need to be able to render this password in the application experience. To do this securely, I need to store the password in an encrypted state.
Normally, I would immediately dismiss the idea of storing a password (as opposed to the hash of a password); but, in the context of an incident triage effort, people are already maximally stressed. Asking the triage team—and all of the peripheral stakeholders—to remember a password will do nothing but pour fuel on the fire.
It's important to remember that security is just an accumulation of trade-offs. Rendering the password in the application interface and in the shareable Slack message is a user experience (UX) compromise. But, having a non-URL-based authentication mechanism is still a value-add. This is compromise that I'm willing to make.
That said, we still want to follow as many best practices as we can. I'm not a security expert; but, I did attend Justin Scott's "Advanced Cryptography in ColdFusion" session at ColdFusion Summit West. In that talk, he covered some encryption essentials:
Use the AES (Advanced Encryption Standard) algorithm with CBC mode. It's the best option that we have available; and, has recently been made the default algorithm in ColdFusion's
encrypt()
anddecrypt()
functions (AES/CBC/PKCS5Padding
).Use an initialization vector. For us non-Security experts, an initialization vector (IV) is much like the salt used in a password hashing algorithm. It's a non-secure value, stored alongside the encrypted payload, that helps to create a unique output regardless of the inputs. Unlike a salt, the IV also affects the underlying encryption process, but that's beyond my understanding.
Use at 256-bit key for quantum readiness. Quantum computers will be able to reduce the complexity of brute-force attacks. By using a 256-bit key, it helps to keep the encryption effective even in our Sci-Fi future.
Use the
generateSecretKey()
method to produce secure random-bit keys. Then, store those keys securely. In this case, "store securely" can mean many different things (at many different cost levels). At the very least, the key should not be committed to your source control.
Regarding the key size, the ColdFusion documentation for generateSecretKey()
states:
The AES algorithm keys are limited to 128 bits unless the Java Unlimited Strength Jurisdiction Policy Files are installed.
I've tested a 256-bit key in both my local development environment and in my production environment and it seems to work fine in both places (no error is thrown). As such, I'm not sure how much I should be worried about this constraint.
Update, 2024-11-22: Justin Scott left a comment letting me know that the 256-bit key constraint is no longer an issue and that the Adobe ColdFusion documentation is out-of-date (regarding this limitation).
After considering a few database storage approaches, I've decided to take inspiration from the bCrypt algorithm. When you hash a password using bCrypt, the generated hash is actually a compound token that contains four parts: the version, the cost factor, the salt, and the hash. When I encrypt a password, I'm going to generate a two-part compound token with:
Base64Url( initializationVector )
+.
Base64Url( Encrypt( password ) )
This way, I can store the encrypted password value and its unique initialization vector in a single database field.
Aside: Encryption keys, API keys, and other private keys are all usually rotated on an ongoing basis. In that vein, it would make sense for me to store a version number in the encryption payload as well. But, this goes beyond what I'm able to think about at this time. Once I get the passwords encrypted and stored securely, I can evolve the notion of a key-version into the application over time, likely storing the key-version in the compound token.
To simplify the handling of passwords, I'm encapsulating my encrypt()
and decrypt()
calls inside a ColdFusion component named PasswordEncoder.cfc
. This allows the calling context to remain free of all the low-level AES details. Here's an example of this component being used - in this demo, I'm taking a password, encrypting it, decrypted it, and then comparing the inputs and outputs:
<cfscript>
passwordEncoder = new PasswordEncoder();
// Note: normally this key would be generated once and stored securely outside the
// code. For the sake of the demo, I'm just creating a new secret key on each request.
secretKey = passwordEncoder.generateKeyForAlgorithm();
// This is the password that we want to encrypt at rest.
password = "MyNameIsOzymandiasKingOfKings";
encoding = passwordEncoder.encode( password, secretKey );
decoding = passwordEncoder.decode( encoding, secretKey );
writeDump([
input: password,
encrypted: encoding,
decrypted: decoding,
isMatch: ( password == decoding )
]);
</cfscript>
Aside: I'm calling my methods
encode()
anddecode()
instead ofencrypt()
anddecrypt()
, respectively, so as not to collide with ColdFusion's built-in function names.
When we run this ColdFusion code, we get the following output:
As you can see, the "encrypted password" is actually a two-part, dot-delimited token in which the first part is the initialization vector (IV) and the second part is AES-encrypted password. And, I was able to take this encrypted token and extract the original, plain-text password.
In this demo, I'm generating a new secret key on every request (using the PasswordEncoder.cfc
utility method). In reality, this secret key would be generated once and then stored securely for reuse.
Here's my implementation of the PasswordEncoder.cfc
ColdFusion component. It doesn't do much other then encapsulate the complexity of calling the encrypt()
and decrypt()
functions.
component
output = false
hint = "I provide methods for encrypting passwords at rest."
{
/**
* I initialize the password encoder.
*/
public void function init() {
// For more aesthetically pleasing data storage strings.
variables.base64UrlEncoder = new Base64UrlEncoder();
// [AES] = Advanced Encryption Standard. This is the new default as of the latest
// ColdFusion update; but, I'm defining it explicitly for clarity.
// [CBC] = Cipher Block Chaining. This uses a single thread to process one block
// of data at a time, using the results of the previous block as the key used to
// encrypt the next block.
variables.algorithm = "AES/CBC/PKCS5Padding";
// Note: the ColdFusion documentation states that the AES algorithm is limited to
// 128 bits unless the Java Unlimited Strength Jurisdiction Policy Files are
// installed. It's unclear to me if that is the common case or an outlier case.
// Regardless, it seems to work for me in my development environment no problem.
variables.keySize = 256;
// The initialize vector (IV) size is tied to the block size of the AES algorithm,
// which uses a fixed-block size of 16-bytes (128 bits). This has nothing to do
// with the size of the encryption key that we will use.
variables.ivSize = 128;
}
// ---
// PUBLIC METHODS.
// ---
/**
* I decode the given encrypted password.
*/
public string function decode(
required string input,
required string key
) {
// The encoded password is actually a compound token that includes both the unique
// initialization vector and the encrypted password. To decrypt the password, we
// have to break the token apart.
var parts = input.listToArray( "." );
var initializationVector = base64UrlEncoder.decode( parts[ 1 ] );
var encodedPassword = base64UrlEncoder.decodeToBase64( parts[ 2 ] );
return decrypt(
encodedPassword,
key,
algorithm,
"base64",
initializationVector
);
}
/**
* I encode the given plain-text password.
*/
public string function encode(
required string input,
required string key
) {
// The initialization vector works much like the salt in a hashing routine. It
// ensures that similar inputs always produce a different output, thereby making
// it harder for a malicious actor to brute-force a decryption routine.
var initializationVector = generateInitializationVectorForAlgorithm();
var encodedPassword = encrypt(
input,
key,
algorithm,
"base64",
initializationVector
);
// Much like the bCrypt hashing, we're going to return a compound token value that
// contains both the "salt" and the encrypted payload. This way, the token can be
// stored and passed around a single unit.
// --
// Note: there's no technical reason that I have to encode these values using the
// Base64url format - this is just for aesthetics (I rather dislike those padding
// characters at the end, seems entirely unnecessary).
return (
base64UrlEncoder.encode( initializationVector ) &
"." &
base64UrlEncoder.encodeFromBase64( encodedPassword )
);
}
/**
* I generate a Base64-encoded key for use with the AES encryption algorithm.
*
* Note: this is a utility function to help generate one-off keys to be stored securely
* outsize of the ColdFusion code.
*/
public string function generateKeyForAlgorithm() {
return generateSecretKey( "AES", keySize );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I generate a Base64-encoded initialization vector for the AES algorithm.
*/
private binary function generateInitializationVectorForAlgorithm() {
return binaryDecode( generateSecretKey( "AES", ivSize ), "base64" );
}
}
And, for completeness, here's my implementation of the Base64UrlEncoder.cfc
. I also shared this ColdFusion component yesterday in my post on a JWT-inspired encoding algorithm; but, I refactored it a bit for this demo due to the fact that generateSecretKey()
give me a Base64-encoded value (not a binary or a plain-text value).
component
output = false
hint = "I provide methods for encoding and decoding values as Base64Url."
{
/**
* I decode the given Base64Url input into a binary value.
*/
public binary function decode( required string input ) {
return binaryDecode( decodeToBase64( input ), "base64" );
}
/**
* I decode the given Base64Url input into a Base64 value.
*/
public string function decodeToBase64( required string input ) {
var unpadded = input
.replace( "-", "+", "all" )
.replace( "_", "/", "all" )
;
// When we generate the URL-safe value, we strip out the padding characters at the
// end. When we decode the input, we then need to append the padding characters
// back on the end.
var paddingLength = ( 4 - ( unpadded.len() % 4 ) );
var padding = repeatString( "=", paddingLength );
return ( unpadded & padding );
}
/**
* I decode the given Base64Url input into a string value with the given encoding.
*/
public string function decodeToString(
required string input,
string encoding = "utf-8"
) {
return charsetEncode( decode( input ), encoding );
}
/**
* I encode the given binary input into a Base64Url value.
*/
public string function encode( required binary input ) {
return encodeFromBase64( binaryEncode( input, "base64" ) );
}
/**
* I encode the given Base64 input into a Base64Url value.
*/
public string function encodeFromBase64( required string input ) {
return input
.replace( "+", "-", "all" )
.replace( "/", "_", "all" )
.replace( "=", "", "all" )
;
}
/**
* I encode the given string input with given encoding into a Base64Url value.
*/
public string function encodeFromString(
required string input,
string encoding = "utf-8"
) {
return encode( charsetDecode( input, encoding ) );
}
/**
* I encode the given HEX input into a Base64Url value.
*/
public string function encodeFromHex( required string input ) {
return encode( binaryDecode( input, "hex" ) );
}
}
Again, I'll stress that I'm not a security expert. But, running through these experiments is helping me think deeper about this kind of work. If you see any fatal flaws in my approach, please let me know!
Want to use code from this post? Check out the license.
Reader Comments
AES operating in CBC mode is the best algorithm to chose... but you need to be aware of padding oracle attacks when using block ciphers in CBC mode. Padding oracle attacks are a side-channel cryptanalysis attack where an attacker can decrypt and encrypt arbitrary data without knowledge of the key, by sending specially-crafted ciphertext and instrumenting against the responses. There's a great overview here: https://robertheaton.com/2013/07/29/padding-oracle-attack/ and I've written about it with respect to CFML here: https://www.hoyahaxa.com/2023/07/on-coldfusion-aes-and-padding-oracle.html
User-controller ciphertext is still user-controlled input, and you want to validate it before you pass it to sensitive actions (such as trying to decrypt it). The best approach is to add some type of integrity check, such as an HMAC, that you can validate prior to decryption to ensure the ciphertext hasn't been modified or created by the user. If the integrity check fails, don't even attempt decryption.
I go into more detail at the link above, but sample code could look something like:
CFMX_COMPAT is very insecure and you don't want to use it, but I'm also wondering if we'll see an uptick in padding oracle vulnerabilities in ColdFusion applications, now that AES-CBC is the default encryption algorithm. 😀
@Brian,
Ok, I don't immediately understand anything you just said 😆 let me go look at those other links and then return with more understanding. I can't quite tell if you're saying that this only applies in cases where the user is supplying the already encrypted value; or, if this applies even in cases where the user is only supplying the plain text value? Hold that thought.
@Ben Nadel,
This applies to when the user is supplying the already encrypted value. I thought that's what your code was doing (decrypting something passed in a URL parameter or cookie), but maybe I got that wrong.
It's admittedly complex stuff and almost like magic when you see it work. Happy to try and explain any part of it more clearly. 🤓
@Brian,
I just read your blog post and I have a better understanding of what you're talking about now. It's actually very relevant to the post I had yesterday about doing something more in the JWT-space. In today's post, the user is giving me a password to store; and I need to encrypt it so that I can later show it in the app (and in Slack ... it's all about trade-offs). So, the user won't ever give me something untrusted to decrypt—at most, they'll give me a plain-text value that I'll have to
compare()
against the encrypted value in the database.That said, I say this is timely with yesterday's post because JWT and my bastardization of it uses the
hmac()
concept you layout as well. Essentially, I will be storing a list of IDs in a signed cookie like:And, when I parse that cookie, I'm doing exactly what you're talking about - making sure the that
hmac()
matches before I even attempt to parse the user-provided portions of the cookie.But, I'm entering into that workflow understanding that the list of IDs is not secret, only signed.
Overall, the workflow for the app is gonna be something like this:
hmac()
jazz.I don't have all the moving parts yet; but when I do, I will link to them (they are all in GitHub).
Thanks for all this great feedback—thinking deeply about this stuff is so freakin' important.
@Ben Nadel,
Gotcha - thank you for the clarification! An informative and interesting read, as usual.
This stuff is certainly fun (and nerve-racking) to think about 😄
Regarding the "Java Unlimited Strength Jurisdiction Policy Files" the Adobe documentation is out of date. Oracle now includes these by default with JDK 9 and later, and JDK 6, 7, and 8 include them by default after updates 6u181, 7u171, and 8u161 respectively. See https://www.oracle.com/java/technologies/javase-jce-all-downloads.html for details.
@Justin,
Great to know, I will update the post 🙌
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →