I have to admit. This is not the most useful post I ever wrote, but today I discovered something funny but interesting when I tried to fix some small bug. I was working with the bcrypt module. If you don’t know what this module is doing: it is a very secure way for hashing passwords, and since checking the validity of your password is relatively slow it is quite useful to prevent password cracking. Before diving into bugfixing let’s see what bcrypt is doing. It is a coldbox module and only has a few relevant functions:
hashPassword(password, [ workfactor], [salt])
which generates a password hash based on a salt and a workfactor. If you don’t supply a workFactor or salt, the coldbox module will generate a salt with a default workfactor of 12. The higher the workfactor, the longer it takes to check for a valid password. (on my system a workfactor means it takes 200 milliseconds to check for a valid password.checkPassword( candidate, bCryptHash)
will check a password candidate agains a (stored) bcryptHash.generateSalt( workFactor )
will generate a salt, based on a workFactor. Increasing a workfactor by 1 will mean it takes double the time to check your bCryptHash. This way you can prevent password attacks, because generating and checking is relatively slow.
Bcrypt has a default workFactor, but you can also specify one in hashPassword. And there is our bug. I tested this with some simple code and it seemed a different workFactor didn’t lead to different checkPassword times. Let’s see.
var crypt=getInstance("bcrypt@BCrypt");
var myHash12 =crypt.hashPassword("simple",12);
var start=gettickCount();
writedump(myHash12);
crypt.checkPassword("simple", myHash12);
writedump("Time (workFactor 12): #getTickCount()-start#");
var myHash14 =crypt.hashPassword("simple",14);
var start=gettickCount();
writedump(myHash14);
crypt.checkPassword("simple", myHash14);
writedump("Time (workFactor 14): #getTickCount()-start#");
So two times the same password hash generated, and then the checkPassword timed. Let’s look at the result:
As you can see the workFactor has increased from 12 to 14, but the password checking takes the same amount of time, although it should be 4 (=22) times slower. From the generated hash you can also see the second generation uses an incorrect workfactor (the $12 instead of $14 at position 4 in the string. See my previous post for details on the bcrypt format. So what’s this hashPassword function doing in our module code?
string function hashPassword(
required string password,
workFactor=variables.settings.workFactor,
salt =generateSalt()
){
return variables.bcrypt.hashpw( arguments.password, arguments.salt );
}
From the code you will notice this function is calling a function on variables.bcrypt which is some Java object. There’s no reference to the workFactor at all, which seems strange at first, because this workFactor determines how easy a hash can be generated and compared to a password candidate. But let’s look a bit further at our arguments in the hashPassword function. There is a password
string for encryption and a workFactor
which get’s a default from variables.settings.workFactor
(which is a reference to module settings). The last argument is salt
, which gets it’s default from the generateSalt
function. It has no parameters here, but that’s a bit deceiving because the function looks like this
string function generateSalt( workFactor=variables.settings.workFactor ){
return variables.bcrypt.genSalt( javaCast( "int", arguments.workFactor ) );
}
So the generateSalt function has an optional workFactor parameter, and this also gets a default from the moduleSettings. Since the workfactor can be easily determined from the generated salt, the java object doesn’t need an extra workfactor argument, but you have to supply a correct salt.
So to be able to use the workFactor in our function and generate a correct salt, we have to specify a workFactor to our salt. Our salt argument has a default supplied by a function, so we change the code:
string function hashPassword(
required string password,
workFactor = variables.settings.workFactor,
salt = generateSalt( arguments.workFactor )
){
return variables.bcrypt.hashpw( arguments.password, arguments.salt );
}
So there is the crazy part. Inside our function declaration where we define our arguments, we use the arguments scope itself (arguments.workFactor)
to supply a default value for the salt argument ( salt = generateSalt( arguments.workFactor )
). I didn’t realize that was possible, but tested it on Lucee and ACF. I could also write some conditional code to change the salt inside the function, but this is actually enough. Our output now:
As you can see now: the time has increased 4-fold, and the hashed password has indeed the correct workfactor in it (the $14 at position 4).
Leave a Reply