Using Password4j And The BCrypt, SCrypt, And Argon2 Password Hashing Algorithms In Lucee CFML 5.3.7.47
At InVision, we use BCrypt to perform a one-way, cryptographic hash of user passwords. Which we've been doing for as long as I can remember. In a conversation the other day, however, I mentioned to Adam Tuttle that I thought there was now a more "modern" hashing algorithm; but, that I didn't know what it was. So, I wanted to take a minute to freshen-up my understanding of password hashing best practices (and how they can be applied in ColdFusion and Lucee CFML).
To be clear, I am not a security expert by any stretch of the imagination. When it comes to security, I generally do whatever OWASP (Open Web Application Security Project) recommends. And, as luck would have it, they have a Password Storage Cheat Sheet, which lists out the accepted password hashing algorithms along with their minimum security characteristics. Reproducing the OWASP recommendations from their cheat sheet:
Use Argon2id with a minimum configuration of 15 MiB of memory, an iteration count of 2, and 1 degree of parallelism.
If Argon2id is not available, use BCrypt with a work factor of 10 or more and with a password limit of 72 bytes.
For legacy systems using SCrypt, use a minimum CPU/memory cost parameter of (2^16), a minimum block size of 8 (1024 bytes), and a parallelization parameter of 1.
If FIPS-140 compliance is required, use PBKDF2 with a work factor of 310,000 or more and set with an internal hash function of HMAC-SHA-256.
Consider using a pepper to provide additional defense in depth (though alone, it provides no additional secure characteristics).
So, unless I'm completely misinterpreting these recommendation, it sounds like Argon2 is the current goto approach; and, that BCrypt is the best fallback approach. Which means that we're still doing pretty good at work.
Now, Lucee CFML 5.3.7.47 doesn't have any built-in password hashing functions (that I can find). But, considering the fact that Adobe ColdFusion 2021 recently add BCrypt and SCrypt functions, I'm assuming that Lucee CFML will have them in an upcoming release for compatibility reasons (if nothing else). Until then, we can always dip down into the Java layer.
While reading up on password hashing algorithms, I came across an open-source Java library called Password4j by David Bertoldi. The Password4j library supports Argon2, BCrypt, SCrypt, and PBKDF2. And, documents the good "default settings" for each algorithm as well. Password4j also uses a simple, fluent API which allows for "one line" invocations like this:
String hashedInput = Password
.hash( plainTextPassword )
.withBcrypt()
.getResult()
;
CAUTION: The default settings in Password4j do not necessarily match the OWASP recommendations above.
Of course, while it's very easy for ColdFusion to dip down into the Java layer, it is usually preferable to wrap such an interaction in a ColdFusion component so that the caller doesn't have to worry about interoperability (such as loading JAR files and type-casting ColdFusion data-types to Java data-types).
To this end, I have created a ColdFusion component, Password.cfc
, which uses Lucee CFML's ability to load JAR files on the fly. Then, I attempted to simplify the password hashing and verification by removing some of the flexibility that the Password4j library provides:
argon2HashGet( input [, ... ] )
argon2HashVerify( input, hashedInput )
bcryptHashGet( input [, ... ] )
bcryptHashVerify( input, hashedInput )
scryptHashGet( input [, ... ] )
scryptHashVerify( input, hashedInput )
For each of these, you can pass-in a series of password hashing characteristics; or, you can define the default arguments for each hashing algorithm:
withArgon2Defaults( ... )
withBCryptDefaults( ... )
withSCryptDefaults( ... )
And, here's what consuming this API looks like - the following ColdFusion code takes the same password and hashes it uses the three password hashing algorithms I've included:
<cfscript>
// Create our Password4j wrapper.
// --
// NOTE: One of the coolest features of Lucee CFML is the fact that we can load Java
// classes directly from a set of JAR files. I've downloaded the Password4j v1.5.3
// files from the Maven repository:
// - https://mvnrepository.com/artifact/com.password4j/password4j
password = new Password([
"./lib/password4j/commons-lang3-3.12.0.jar",
"./lib/password4j/password4j-1.5.3.jar",
"./lib/password4j/slf4j-api-1.7.30.jar",
"./lib/password4j/slf4j-nop-1.7.30.jar"
]);
myPassword = "Ca$hRulezEverythingAroundM3!";
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
timer
label = "Testing BCrypt with defaults"
type = "outline"
{
hashedPassword = password.bcryptHashGet( myPassword );
dump( myPassword );
dump( hashedPassword );
dump( hashedPassword.len() );
dump( password.bcryptHashVerify( myPassword, hashedPassword ) );
}
echo( "<hr />" );
timer
label = "Testing SCrypt with defaults"
type = "outline"
{
hashedPassword = password.scryptHashGet( myPassword );
dump( myPassword );
dump( hashedPassword );
dump( hashedPassword.len() );
dump( password.scryptHashVerify( myPassword, hashedPassword ) );
}
echo( "<hr />" );
timer
label = "Testing Argon2 with defaults"
type = "outline"
{
hashedPassword = password.argon2HashGet( myPassword );
dump( myPassword );
dump( hashedPassword );
dump( hashedPassword.len() );
dump( password.argon2HashVerify( myPassword, hashedPassword ) );
}
</cfscript>
When we run this ColdFusion code, we get the following output:
I wrapped each one of the hashes in a CFTimer
tag because I was curious to see how fast they would run. But, take the timing here with a grain of salt. The goal of password hashing isn't to finish fastest - it's to make a calculated trade-off between security, performance, and scalability. OWASP recommends that hashing the password should take long enough to slow-down an attacker; but, be fast enough so as to take less than a second to hash.
From the OWASP cheat sheet's section on "Work Factor" tuning:
The work factor is essentially the number of iterations of the hashing algorithm that are performed for each password (usually, it's actually 2^work iterations). The purpose of the work factor is to make calculating the hash more computationally expensive, which in turn reduces the speed and/or increases the cost for which an attacker can attempt to crack the password hash. The work factor is typically stored in the hash output.
When choosing a work factor, a balance needs to be struck between security and performance. Higher work factors will make the hashes more difficult for an attacker to crack but will also make the process of verifying a login attempt slower. If the work factor is too high, this may degrade the performance of the application and could also be used by an attacker to carry out a denial of service attack by making a large number of login attempts to exhaust the server's CPU.
There is no golden rule for the ideal work factor - it will depend on the performance of the server and the number of users on the application. Determining the optimal work factor will require experimentation on the specific server(s) used by the application. As a general rule, calculating a hash should take less than one second.
Once I had my Password.cfc
seemingly generating BCrypt, SCrypt, and Argon2 password hashes, I was curious to see if the inputs and outputs were compatible with the new Adobe ColdFusion 2021 hash functions. So, I set up an interoperability test in both directions. First, I generated some hashes in Adobe ColdFusion:
<cfscript>
input = "Kablamo$auce";
data = [
{
algorithm: "bcrypt",
input: input,
hashedInput: generateBcryptHash( input )
},
{
algorithm: "scrypt",
input: input,
hashedInput: generateScryptHash( input )
}
];
fileWrite( expandPath( "./interop.json" ), serializeJson( data ) );
</cfscript>
This just takes a single password, hashes it using the new BCrypt and SCrypt functions - with default values - and then writes them to a file. On the other side of the test, I then had Lucee CFML read that JSON file in and try to verify the password hashes:
<cfscript>
password = new Password([
"./lib/password4j/commons-lang3-3.12.0.jar",
"./lib/password4j/password4j-1.5.3.jar",
"./lib/password4j/slf4j-api-1.7.30.jar",
"./lib/password4j/slf4j-nop-1.7.30.jar"
]);
// Read-in the file generated by Adobe ColdFusion 2021 built-in functions.
data = deserializeJson( fileRead( "./interop.json" ) );
for ( test in data ) {
switch ( test.algorithm ) {
case "bcrypt":
input = test.input;
hashedInput = test.hashedInput;
dump( password.bcryptHashVerify( input, hashedInput ) );
break;
case "scrypt":
input = test.input;
// CAUTION: I have to prepend "$s0" to get Password4j to like the hash
// generated by the Adobe ColdFusion 2021 scrypt function.
hashedInput = ( "$s0" & test.hashedInput );
dump( password.scryptHashVerify( input, hashedInput ) );
break;
}
}
</cfscript>
What I found in both directions was that the Password4j seems to always include and expect an $s0
in the SCrypt hash. As such, I had to do a little manually manipulation (as you can see above). But, ultimately, when I run this Lucee CFML code, I get the following output:
True
True
As you can see, Lucee CFML was able to verify the BCrypt and SCrypt hashes (with a little bit of transformation).
Now, to test the interoperability in the other direction, I created a Lucee CFML page that would perform the same hashing:
<cfscript>
password = new Password([
"./lib/password4j/commons-lang3-3.12.0.jar",
"./lib/password4j/password4j-1.5.3.jar",
"./lib/password4j/slf4j-api-1.7.30.jar",
"./lib/password4j/slf4j-nop-1.7.30.jar"
]);
// Configure Password4j defaults to use the same settings that Adobe ColdFusion 2021
// generateBcryptHash() and generateScryptHash() functions will use if we don't pass-
// in any additional options.
password
.withBcryptDefaults(
version = "a",
costFactor = 10
)
.withScryptDefaults(
// CAUTION: The Adobe ColdFusion 2021 documentation says that the default
// work factor it "16,348", which is the WRONG output of (2^14).
workFactor = 16384,
resources = 8,
parallelisation = 1,
outputLength = 32,
saltLength = 8
)
;
input = "Kablamo$auce";
// When generating the Lucee CFML outputs, we'll just use the Password4j defaults.
data = [
{
algorithm: "bcrypt",
input: input,
hashedInput: password.bcryptHashGet( input )
},
{
algorithm: "scrypt",
input: input,
hashedInput: password.scryptHashGet( input )
}
];
fileWrite( expandPath( "./interop.json" ), serializeJson( data ) );
</cfscript>
Since I am not using any options on the Adobe ColdFusion 2021 side, I had to use settings on the Lucee CFML side that used the same "defaults". On the Adobe ColdFusion side, I then read this JSON file and tried to verify the password hashes:
<cfscript>
// Read-in the file generated by Lucee CFML (with Password4j).
data = deserializeJson( fileRead( expandPath( "./interop.json" ) ) );
for ( test in data ) {
switch ( test.algorithm ) {
case "bcrypt":
input = test.input;
hashedInput = test.hashedInput;
writeDump( verifyBcryptHash( input, hashedInput ) );
break;
case "scrypt":
input = test.input;
// CAUTION: I have to remove "$s0" to get Adobe ColdFusion 2021 to like
// the hash generated by the Password4j scrypt function.
hashedInput = test
.hashedInput
.reReplace( "^\$s0", "" )
;
writeDump( verifyScryptHash( input, hashedInput ) );
break;
}
}
</cfscript>
Again, I had to a little manipulation to get the SCrypt formats to line-up. But, when I ran this ColdFusion code, I got the following output:
Yes
Yes
Adobe ColdFusion 2021 doesn't have any Argon2 password hashing, so I couldn't compare that algorithm. But, Password4j and the existing Adobe ColdFusion 2021 password functions do seem to have pretty good interoperability.
At work, we're likely to stick with the current BCrypt implementation since it is a solid choice. But, doing this research has given me more confidence in how password hashing is supposed to work.
And finally, here's the code for my Password.cfc
ColdFusoin component (view the full code on GitHub):
/**
* I provide a Lucee CFML wrapper around the Password4j Java library.
*
* GitHub: https://github.com/Password4j/password4j
* Maven: https://mvnrepository.com/artifact/com.password4j/password4j
*/
component
output = false
hint = "I provide password hashing and verification functions (using Password4j)."
{
variables.defaults = {
// These properties are described here:
// - https://github.com/Password4j/password4j/wiki/Argon2
argon2: {
memory: 12,
iterations: 20,
parallelisation: 2,
ouputLength: 32,
type: "Argon2id",
version: 19
},
// These properties are described here:
// - https://github.com/Password4j/password4j/wiki/BCrypt
bcrypt: {
version: "B",
costFactor: 10
},
// These properties are described here:
// - https://github.com/Password4j/password4j/wiki/Scrypt
scrypt: {
workFactor: 32768,
resources: 8,
parallelisation: 1,
outputLength: 64,
// Added for Adobe ColdFusion 2021 interoperability.
saltLength: 8
}
};
/**
* I initialize the password component with the given Password4j JAR paths.
*/
public void function init( required array jarPaths ) {
variables.jarPaths = arguments.jarPaths;
}
// ---
// PUBLIC METHODS.
// ---
/**
* I generate an Argon2 hash of the given input using the given characteristics. All
* characteristics are optional; and, any not provided will use the defined defaults.
*/
public string function argon2HashGet(
required string input,
numeric memory = defaults.argon2.memory,
numeric iterations = defaults.argon2.iterations,
numeric parallelisation = defaults.argon2.parallelisation,
numeric ouputLength = defaults.argon2.ouputLength,
string type = defaults.argon2.type,
numeric version = defaults.argon2.version
) {
var enum = javaNew( "com.password4j.types.Argon2" );
switch ( type ) {
case "Argon2d":
case "Argon2i":
case "Argon2id":
var enumToken = type.listRest( "2" ).ucase();
var enumType = enum[ enumToken ];
break;
default:
throw(
type = "InvalidArgon2Type",
message = "Valid argon2 types are: Argon2d, Argon2i, Argon2id (recommended)",
detail = "Provided type: #type#"
);
break;
}
var hashingFunction = javaNew( "com.password4j.Argon2Function" )
.getInstance( memory, iterations, parallelisation, ouputLength, enumType, version )
;
var hashedInput = javaNew( "com.password4j.Password" )
.hash( input )
.with( hashingFunction )
.getResult()
;
return( hashedInput );
}
/**
* I verify the Argon2 hash of the given input against the expected hash.
*
* NOTE: All hash-algorithm characteristics will be pulled directly out of the
* expected hash. As such, they do not need to be provided as arguments.
*/
public boolean function argon2HashVerify(
required string input,
required string hashedInput
) {
var hashingFunction = javaNew( "com.password4j.Argon2Function" )
.getInstanceFromHash( hashedInput )
;
var isVerified = javaNew( "com.password4j.Password" )
.check( input, hashedInput )
.with( hashingFunction )
;
return( isVerified );
}
/**
* I generate a BCrypt hash of the given input using the given characteristics. All
* characteristics are optional; and, any not provided will use the defined defaults.
*/
public string function bcryptHashGet(
required string input,
string version = variables.defaults.bcrypt.version,
numeric costFactor = variables.defaults.bcrypt.costFactor
) {
var enum = javaNew( "com.password4j.types.BCrypt" );
switch ( version ) {
case "a":
case "b":
case "x":
case "y":
var enumToken = version.ucase();
var enumVersion = enum[ enumToken ];
break;
default:
throw(
type = "InvalidBcryptVersion",
message = "Valid bcrypt versions are: a, b (recommended), x, y",
detail = "Provided version: #version#"
);
break;
}
var hashingFunction = javaNew( "com.password4j.BCryptFunction" )
.getInstance( enumVersion, costFactor )
;
var hashedInput = javaNew( "com.password4j.Password" )
.hash( input )
.with( hashingFunction )
.getResult()
;
return( hashedInput );
}
/**
* I verify the BCrypt hash of the given input against the expected hash.
*
* NOTE: All hash-algorithm characteristics will be pulled directly out of the
* expected hash. As such, they do not need to be provided as arguments.
*/
public boolean function bcryptHashVerify(
required string input,
required string hashedInput
) {
var hashingFunction = javaNew( "com.password4j.BCryptFunction" )
.getInstanceFromHash( hashedInput )
;
var isVerified = javaNew( "com.password4j.Password" )
.check( input, hashedInput )
.with( hashingFunction )
;
return( isVerified );
}
/**
* I generate a SCrypt hash of the given input using the given characteristics. All
* characteristics are optional; and, any not provided will use the defined defaults.
*/
public string function scryptHashGet(
required string input,
numeric workFactor = defaults.scrypt.workFactor,
numeric resources = defaults.scrypt.resources,
numeric parallelisation = defaults.scrypt.parallelisation,
numeric outputLength = defaults.scrypt.outputLength,
numeric saltLength = defaults.scrypt.saltLength
) {
var hashingFunction = javaNew( "com.password4j.SCryptFunction" )
.getInstance( workFactor, resources, parallelisation, outputLength )
;
var hashedInput = javaNew( "com.password4j.Password" )
.hash( input )
.addRandomSalt( saltLength )
.with( hashingFunction )
.getResult()
;
return( hashedInput );
}
/**
* I verify the SCrypt hash of the given input against the expected hash.
*
* NOTE: All hash-algorithm characteristics will be pulled directly out of the
* expected hash. As such, they do not need to be provided as arguments.
*/
public boolean function scryptHashVerify(
required string input,
required string hashedInput
) {
var hashingFunction = javaNew( "com.password4j.SCryptFunction" )
.getInstanceFromHash( hashedInput )
;
var isVerified = javaNew( "com.password4j.Password" )
.check( input, hashedInput )
.with( hashingFunction )
;
return( isVerified );
}
/**
* I update the default arguments for the Argon2 hashing method.
*/
public any function withArgon2Defaults(
numeric memory = defaults.argon2.memory,
numeric iterations = defaults.argon2.iterations,
numeric parallelisation = defaults.argon2.parallelisation,
numeric ouputLength = defaults.argon2.ouputLength,
string type = defaults.argon2.type,
numeric version = defaults.argon2.version
) {
defaults.argon2.memory = memory;
defaults.argon2.iterations = iterations;
defaults.argon2.parallelisation = parallelisation;
defaults.argon2.ouputLength = ouputLength;
defaults.argon2.type = type;
defaults.argon2.version = version;
return( this );
}
/**
* I update the default arguments for the BCrypt hashing method.
*/
public any function withBCryptDefaults(
string version = defaults.bcrypt.version,
string costFactor = defaults.bcrypt.costFactor
) {
defaults.bcrypt.version = version;
defaults.bcrypt.costFactor = costFactor;
return( this );
}
/**
* I update the default arguments for the SCrypt hashing method.
*/
public any function withSCryptDefaults(
numeric workFactor = defaults.scrypt.workFactor,
numeric resources = defaults.scrypt.resources,
numeric parallelisation = defaults.scrypt.parallelisation,
numeric outputLength = defaults.scrypt.outputLength,
numeric saltLength = defaults.scrypt.saltLength
) {
defaults.scrypt.workFactor = workFactor;
defaults.scrypt.resources = resources;
defaults.scrypt.parallelisation = parallelisation;
defaults.scrypt.outputLength = outputLength;
defaults.scrypt.saltLength = saltLength;
return( this );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I create the given Java class using the Password4j JAR paths.
*/
private any function javaNew( required string className ) {
return( createObject( "java", className, jarPaths ) );
}
}
Want to use code from this post? Check out the license.
Reader Comments
Hey Ben,
Just wanted to reference that ACF also supports BCrypt and SCrypt since the 2021 version. We've added functions for it namely, GenerateBCryptHash, VerifyBCryptHash, GenerateSCryptHash and VerifySCryptHash. These functions support default values, but you can adjust them according to the performance and security factor that you are satisfied with.
Thanks,
Edwin
@Edwin,
Thank you -- when you have a moment, you should read my post. Half of it discusses the interoperability between Password4j and the Adobe ColdFusion 2021 functions. It seems to be pretty good, though I noticed the SCrypt hashing adds and expects the hash to being with
$s0
. Not sure if that is part of the "spec"; or, if it's just something the Password4j library decided to do.@All,
One of the APIs that Password4j exposes is the ability to update a hash. This is usually needed when migrating a system from an older, weaker hashing algorithm (like MD5) to a new, stronger hashing algorithm (like BCrypt). This sounds like a complicated thing; but, it's actually quite straightforward. I wanted to write up an exploration of hash migration:
www.bennadel.com/blog/4057-migrating-password-hashing-algorithms-in-lucee-cfml-5-3-7-47.htm
This later post builds on top of the current post, using my
Password.cfc
and the Password4j library. Though, the concept is universal and doesn't pertain to these libraries specifically.@All,
In this post, the various
.withXYZDefaults()
methods use a technique that I'm not sure I've used before. Which is to create a way to change the runtime behavior of Function argument defaults. As such, I wanted to carve out a moment of self-reflection on that approach:www.bennadel.com/blog/4060-changing-function-argument-defaults-at-runtime-in-lucee-cfml-5-3-7-47.htm
I think it probably makes the most sense as an instantiation time workflow, not necessarily a "runtime" workflow.
Really great article Ben! A little side note:
Thanks to Andrew Dixon we have Argon2 available in Lucee since the last quarter of the last year. See his blog entry here:
https://www.andrewdixon.co.uk/2020/09/19/using-argon2-in-lucee-cfml/
He has also a very good blog post about password hashing.
@Amdreas,
Very cool -- I will check it out. I also see that Argon2 is now a native part of Lucee CFML in the 5.3.8 release 💪
@Ben,
thank you for your observation about the
$s0
prepended by scrypt. The scrypt library followed a non official standard and it has been fixed with version 1.6.0 🔨Note that 1.6.0 brings some little function renaming.
Hope you enjoyed working with Password4j 🙂
@David,
Oh very cool! And yeah, Password4j is a great library! It makes working with the various algorithms super easy. Amazing job!