Password Hashing in CFML
As we all know, or at least should know, if you are storing passwords in a database, they should only ever be stored as hashes and NEVER as plain text or using reversible encryption.
What is hashing?
By hashing the password, you are not storing the original password, only a calculated representation of the password, that given you know certain information about how the hash of the password was created, you can recreate the same hash from the password and compare the stored version to the newly hashed version for verification.
Not all hashing is equal
There are many different types of hashing from MD5 to Argon2 (at the time of writing) and lots in between, however, some, like MD5 are a lot less “crackable than others, like Argon2, which won the most recent Password Hash Competition. There are also additional methods that you can employ to improve the “crackability” of your stored passwords even further.
What is wrong with MD5 hashing?
Fundamentally, there is nothing particularly wrong with MD5 hashing, for none cryptographic uses, like for checksums to verify data integrity, however, it suffers from extensive vulnerabilities for cryptographic uses.
MD5 hashing is particularly quick to compute, for example, an NVIDIA GeForce 8800 Ultra GPU can calculate more than 200 million MD5 hashes per second. It also suffers from extensive collisions making it particularly vulnerable to collision attacks where two or more different inputs can produce the same hash. Due to this, if an attacker has accessed your database and retrieved the hashes you have stored, they do not need to find the actual password to create the same hash, they just need to find a string that produces the same hash and they would be able to use that as the password and as MD5 hashes can be calculated relatively “cheaply”, e.g. millions per second on consumer hardware, it is relatively “cheap” to find a password that would work.
Some people think that they are getting around this by performing the hash multiple times (key stretching) or by adding a salt, however, neither of these really help due to the collision issue.
So, if you are using the hash()
function in your CFML application to hash passwords to store in a database, you are probably using MD5, for example, hash(password)
, as MD5 is the default used by this function. This function also has an algorithm argument that allows the use of SHA, SHA-256, SHA-384 & SHA-512 as well. Also from Adobe ColdFusion (ACF) 7 there is an additional argument called additionalIterations
which allows you to tell it to perform additional iterations on the hash. Lucee also has a similar argument called numIterations
which defaults to 1 and allows you to specify the number of iterations you would like to perform. Note the difference between these two, with ACF it is the number of additional iterations to perform and for Lucee it is the total number of iterations you wish to perform. So, if you wish to perform 1000 iterations to generate the hash, on ACF you would specify additionalIterations=999
and on Lucee it would be numIterations=1000
to produce the same hash value.
How about encryption?
Both Adobe ColdFusion and Lucee have an encrypt()
function that offers lots of different algorithms, adds a salt and allows for iterations, however, when it comes to storing passwords in a database, you do not want or need them to be reversible, e.g. you do not want to be able to recover the original input from the output. Encryption, however, is designed to be reversible so it is not what we are looking for when storing passwords.
What hashing should I be using then?
Hashing is, therefore, the way to go and both Adobe ColdFusion (ACF) and Lucee offer support for Password-Based Key Derivation Function 2 (PBKDF2). RFC 8018, published in 2017 recommends PBKDF2 for password hashing. At the time of writing this, neither ACF or Lucee support Argon2 natively, however, you could use it via a third-party Java library should you wish, but we will be using PBKDF2 as it is natively available in both.
How do I do that in CFML?
The generatePBKDFKey()
function supports the PBKDF2WithHmacSHA1
algorithm in both ACF and Lucee. ACF Enterprise Edition also supports additional algorithms, however, for this post, I will only use the PBKDF2WithHmacSHA1
algorithm as it is supported by all versions of ACF and Lucee.
The function syntax is as follows:
string generatePBKDFKey(algorithm, passphrase, salt [, iterations, keySize]);
Where:
- algorithm =
PBKDF2WithHmacSHA1
- passphrase = the string you wish to hash, i.e. the password
- salt = A salt to add to the passphrase before hashing
- iterations = The number of iterations to perform (default 4096)
- keySize = The length of the generated hash in bytes (default 128)
The function returns a string, which is the hash of the password.
A simple implementation of this would be to have the salt
and iterations
stored in your source code as constants, however, this means all your passwords are hashed with the same salt and the same number of iterations. This means if your code and database are compromised by an attacker, they can now try and brute force your passwords with a smaller number of attempts as they only need to produce hashes for a known single salt and iterations. Whilst this is still going to require a large amount of computing power and time to do, it is not as good as it could be.
Random Salt and Iterations
To improve things from using a single salt and number of iterations for all passwords, we are going to use a random salt and number of iterations which we will then concatenate with the generated hash and store as a single string in the database.
To generate a random salt, we are going to use the generateSecretKey()
function and we are going to do that using the AES algorithm and at 256 bits:
var salt = generateSecretKey( 'AES' , 256 );
To generate a random number of iterations, we are going to use the randRange()
function and for a good number of iterations without taking too long to generate, we are going to select a number between 50,000 and 100,000:
var iterations = randRange( 50000 , 100000 , 'SHA1PRNG' );
Now that we have both of these we can feed them into our generatePBKDFKey()
call and get ourselves a hash:
var hash = generatePBKDFKey('PBKDF2WithHmacSHA1', password, salt, iterations);
Then to get the string we are going to store in the database, we need to concatenate the three things together and separate them with a delimiter, for which I use a colon (:):
var stringToStore = iterations & ':' & salt & ':' & hash;
Which will give you an output something like:
97684:zlZo4aq3nYxiQvTKiRqdYU5CtYVHgjBeNFPY5kmuk2I=:vsjLquSVFvZK9v/QZjmuNQ==
We can then store this in the database and when it comes to checking it for a user login, we retrieve this from the database, extract the iterations and salt from it, and use these along with the password entered by the user and generate the hash again and compare the two hashes and if they match, the user entered the correct password.
Salt but no pepper?
So, you seasoned your user’s password with a little salt, but no pepper? Well, let’s fix that and improve things further by adding a little pepper. A pepper is a constant value defined in your codebase that is added to your salt before you generate the hash. This means that even if an attacker gains access to your database and retrieves your stored strings, whilst they would know the number of iterations and the salt, they wouldn’t know your pepper, or even that you have used one, without also gaining access to your codebase.
Head on over to trycf.com and use the following code to generate yourself a secret key:
writeDump( generateSecretKey( 'AES' , 256 ) );
You should get an output that looks like this, but a different value:
Crq7aX0yEORgx0OFhPUNIBHGDoOx95qcjqjhEFbyhiQ=
Then, take this value and put it into an application scope variable that is defined in your onApplicationStart
function:
application.pepper = 'Crq7aX0yEORgx0OFhPUNIBHGDoOx95qcjqjhEFbyhiQ=';
Lets now adjust our hash generator function call to use both the salt and the pepper:
var hash = generatePBKDFKey( 'PBKDF2WithHmacSHA1' , password , salt & application.pepper , iterations );
Putting it all together
Let’s now put this all together into two functions that you can use to create the password hash and to check a password against a hash:
function generateHash(
required string password,
string salt = GenerateSecretKey( 'AES' , '256' ),
numeric iterations = randRange( 50000 , 100000 , 'SHA1PRNG' )
) {
return arguments.iterations & ':' & arguments.salt & ':' & GeneratePBKDFkey( 'PBKDF2WithHmacSHA1' , arguments.password , arguments.salt & application.pepper , arguments.iterations );
}
function checkPassword(
required string password,
required string hash
) {
var iterations = ListGetAt(arguments.hash, 1, ':');
var salt = ListGetAt(arguments.hash, 2, ':');
return ( generateHash ( arguments.password , salt , iterations ) EQ arguments.hash );
}
There we have it. Everything you need to generate and check a password hash that you can safely store in your database.
UPDATE (Sept 2020): Argon2 is now available in SNAPSHOT builds of Lucee.
Well written and explained.
Thanks Hugh
Great, helped me a lot. Thanks for that!
And I find your final code example quite elegant.
Thanks Christian
Hi Andrew, I just tried using your code to hash a password for my test application. It’s great that you have your codes here for me to learn BUT I noticed on your generateHash function you added application.pepper which is great BUT on the checkPassword function, you did not include the application.pepper value. So when I used your code to test mine (without the pepper) I got a NO instead of Yes in the value returned by the check password function.
How should I include the application.pepper in the checkPassword function, Thank you
There is no need to include the “pepper” in the
checkPassword
function as it uses thegenerateHash
function to generate the hash again and compare it to the stored hash. so the pepper is added automatically.Also, if you are doing something new and using Lucee, then you probably want to use the Argon2 algorithm, see https://www.andrewdixon.co.uk/2020/09/19/using-argon2-in-lucee-cfml/
Hi,
Thank you for this very very interesting post!
Kind regards,
Christophe