scrypt Password Hashing in Lucee 7

edit Introduced: 7.0

scrypt Password Hashing in Lucee 7

Adobe ColdFusion 2021 has the functions GenerateSCryptHash() and VerifySCryptHash(). Lucee doesn't ship scrypt functions, because Lucee 7's Maven integration makes it easy to load a well-maintained Java library and use it directly from CFML. No extension is needed.

This recipe uses the scrypt implementation of Bouncy Castle.

Maven Dependency

First, find the latest Bouncy Castle version at mvnrepository.com. Currently version 1.86 is available:

implementation("org.bouncycastle:bcprov-jdk18on:1.86")

Simple Example

Create a component ScryptHasher.cfc that generates and verifies hashes:

import java.security.MessageDigest;
import java.security.SecureRandom;
import org.bouncycastle.crypto.generators.SCrypt;
component javasettings='{"maven":["org.bouncycastle:bcprov-jdk18on:1.86"]}' {
    // cost parameters, see "Choosing parameters" below
    variables.N = 131072; // CPU/memory cost, must be a power of 2 (2^17)
    variables.r = 8;      // block size
    variables.p = 1;      // parallelization
    variables.keyLength = 32; // bytes
    // SCrypt is loaded from the Maven dependency above, and only has static methods,
    // so it is called with the :: operator, no instance needed
    function generate(required string password) {
        // random 16 byte salt
        var salt = new SecureRandom().generateSeed(16);
        var hash = SCrypt::generate(
            charsetDecode(arguments.password, "utf-8"),
            salt, variables.N, variables.r, variables.p, variables.keyLength
        );
        // store the parameters and the salt together with the hash
        return "$scrypt$#variables.N#$#variables.r#$#variables.p#$#toBase64(salt)#$#toBase64(hash)#";
    }
    function verify(required string password, required string stored) {
        var parts = listToArray(arguments.stored, "$");
        if (arrayLen(parts) != 6 || parts[1] != "scrypt") return false;
        var salt     = toBinary(parts[5]);
        var expected = toBinary(parts[6]);
        var actual   = SCrypt::generate(
            charsetDecode(arguments.password, "utf-8"),
            salt, val(parts[2]), val(parts[3]), val(parts[4]), len(expected)
        );
        // constant-time comparison, avoids timing attacks
        return MessageDigest::isEqual(actual, expected);
    }
}

Usage:

scrypt = new ScryptHasher();
// when a user registers or changes their password
stored = scrypt.generate("my secret password");
dump(stored); // $scrypt$131072$8$1$<salt>$<hash>, store this string in your database
// when a user logs in
dump(scrypt.verify("my secret password", stored)); // true
dump(scrypt.verify("wrong password", stored));     // false

How it works

  • No createObject(): the import lines at the top of the file make the Java classes available by their short name. new SecureRandom() creates an instance, and SCrypt::generate() and MessageDigest::isEqual() call static methods. The classes of the Maven dependency are found because the component declares them in javasettings.
  • Random salt: every hash gets its own 16 byte salt from java.security.SecureRandom, so equal passwords produce different hashes.
  • Parameters are stored with the hash: the string contains N, r, p, the salt and the hash. verify() reads the parameters from the stored value, so you can raise the cost later and old hashes still verify. You can compare them with the current parameters on login and re-hash the password if they are weaker.
  • Constant-time comparison: MessageDigest.isEqual() takes the same time no matter where the two values differ. Don't compare the hashes with == or compare().
  • Tunable: the four scrypt parameters (N, r, p and the key length) are plain variables at the top of the component. Turn them into arguments of generate() if you need different values per call.

Choosing parameters

The memory needed is about 128 * N * r bytes. N=131072, r=8, p=1 (2^17) needs roughly 128 MB and takes a few hundred milliseconds, which is the OWASP Password Storage Cheat Sheet recommendation for scrypt. Measure it on your own hardware and make sure your JVM has enough heap for your number of concurrent logins. If memory is tight, OWASP lists equivalent combinations with a lower N and a higher p, for example N=32768 (2^15), r=8, p=3.

That's all you need to start using scrypt in Lucee 7! Maven handles downloading the library automatically.

See also