Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Eric Betts
Ben Nadel at InVision In Real Life (IRL) 2018 (Hollywood, CA) with: Eric Betts

Considering Encrypting Passwords At Rest In ColdFusion

By
Published Comments (16)

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() and decrypt() 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:

  1. Base64Url( initializationVector ) + .
  2. 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() and decode() instead of encrypt() and decrypt(), respectively, so as not to collide with ColdFusion's built-in function names.

When we run this ColdFusion code, we get the following output:

Screenshot of a CFDump showing that the original password can be read from the encrypted token.

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

6 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:

<cfscript>

encryptionKey = "JAidBZLaYf37huVuM4MNTA==";  //our AES key
signingKey = "pickSomethingBetter";  //let's use a different key for signing

// A valid url.secret should now contain <CIPHERTEXT>-<HMAC> 
// If there's a "secret" URL parameter, we'll attempt to decrypt it - ONLY IF the HMAC is valid
if (isDefined("url.secret")) { 

// Does the "secret" contain two parts - presumably the ciphertext and an HHAMC?     
     secretParts = listToArray(url.secret,"-");
     if (len(secretParts) != 2) {
        writeOutput("nope");
        cfabort();
     } 

// Is the HMAC valid?
     if (hmac(secretParts[1], signingKey, "HMACSHA256") != secretParts[2]) { 
        writeOutput("nope");
        cfabort();
     } 

     decryptedInput = decrypt( secretParts[1], encryptionKey, "AES/CBC/PKCS5Padding", "hex" );
     writeOutput( "<b>Decrypted Stuff:</b><br> #decryptedInput# <br>" );
}

// if we don't have a "secret" URL parameter, just output valid ciphertext with an HMAC
// you can then use this as a value to validate and decrypt, passed in url.secret
else { 

     input = "Here is the super secret stuff we want to encrypt.";
     encryptedInput = encrypt( input, encryptionKey, "AES/CBC/PKCS5Padding", "hex" );
     hmac = hmac(encryptedInput, signingKey, "HMACSHA256"); 
     writeOutput( "<b>Encrypted Stuff:</b><br> #encryptedInput#-#hmac# <br>" );
}

</cfscript>

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. 😀

15,902 Comments

@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.

6 Comments

@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. 🤓

15,902 Comments

@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:

Base64Url( expiration ) + "." +
Base64Url( list_of_ids ) + "." +
Base64Url( hmac( expiration, list_of_ids, key, algo ) )

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:

  1. User is prompted for password to access "thing" with ID 4.
  2. If password is validated against encrypted password in DB, they can access ID 4.
  3. I then add ID 4 so the signed cookie using the hmac() jazz.
  4. When they then try to access "thing" with ID 4, I look in their cookie to see if it exists (and that the value has not been tampered with).

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.

26 Comments

@Ben,

May have missed it in previous discussions…have you thought about encoding the password into the URL for the incident? As in, you create the incident with a password, and the encrypted password is the ID (or combo of id + salted hashed password):
id={guid}&hash={hashedValue}

When visiting the first time, a different user would be presented with a password field, when submitted, the server takes the password, id, and salt and hashes that and compares against the hash URL parameter, then the incident is shown and the user's session/cookie/localStorage is set.

Not being an encryption expert, so not sure about any issues with exposing the hashed value in the URL.

15,902 Comments

@Danilo,

That's more-or-less what I'm trying to do, only I'm trying to keep the authy-bits out of the URL and in the cookie. Honestly, I don't really know if it makes any difference at all. But, I figure if the user wants some added assurances about security, that I wanted to keep it out of the URL in order to reduce the places it would be logged.

I mean, it's possible that this is just "security theater" and that I'm making things harder than they have to be. 😜

15,902 Comments

@Gregory,

I'm curious to hear about what you're building - we can't have too many good discussions in the CF community 🙌

5 Comments

@Ben Nadel,
Hi Ben,
I plan to release the new version of Galaxie Blog, which will support Lucee and have extensive CMS support, but I keep finding new things to introduce and bugs to fix, and it is taking forever. In the meantime, my blog posting is suffering, but I wanted to write a quick post about securing passwords with hashing. I will still write it and refer to your posts like this one.
Thanks for your educational posts Ben!

15,902 Comments

@Gregory,

To be fair, I'm encrypting the password and it sounds like you're going to be hashing it (I'm assuming with something like bCrypt). So, I wouldn't even sweat it—we're operating in two different gestures anyway.

Exciting to hear about the Galaxy blog release! Isn't it fun to build products! At first, it's so freaking overwhelming. But then, you do one task at a time, and sooner or later you start to see real progress. Rock on!

5 Comments

@Ben Nadel, Thanks Ben! Yes, I have never worked harder for free! You're right about being overwhelming- I find myself coding like crazy, trying to remember where all the pieces fit in, and eventually, I am done, and it is bittersweet! I am amazed at how you can blog, write a book or podcast, and develop at the same time! I don't have the energy and often stop blogging when I am in a rhythm writing code.
Take care Ben,

15,902 Comments

@Gregory,

I wish I could insert the "I have no idea what I'm doing dog" MEME 😆 a big part of why I blog is that I'm desperately trying to cram things into my head and hope that they stick. It also gives me something to refer to later when I inevitably forget the details.

I wish you luck! I've spent more time this past year actually building stuff and not just writing about building stuff. After I do some more experimentation on this Incident Commander jazz, I want to start working on a small site for writing poetry. There's something very differently satisfying about building a real thing (vs. just writing about lower-level concepts). And, I'm craving that feeling now.

But, there's only so many hours in the days.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel