DevLearningTools

MODULE 15 · LESSON 03

Password Hashing

Why a password is never stored as plain text, why a general-purpose function like hash() is the wrong tool for the job, and how Adobe ColdFusion's unified PasswordHashGenerate()/PasswordHashVerify() and Lucee's Argon2Hash()/BCryptHash()/SCryptHash() pairs actually differ.

New lessons are added one at a time as the course gets built out — a graded quiz for each lesson is still on the way.

A password should never be stored, logged, or compared as plain text. The question this lesson answers isn't "should I hash it", that part is non-negotiable, it's which function actually belongs in that job, because several that look like reasonable candidates (hash(), MD5, plain SHA-256) are the wrong tool entirely.

Learning Objectives

After completing this lesson, you'll be able to:

  • Explain why a fast, general-purpose hash function is unsafe for passwords specifically.
  • Use Adobe ColdFusion's unified PasswordHashGenerate()/PasswordHashVerify() API.
  • Use Lucee's equivalent Argon2Hash()/BCryptHash()/SCryptHash() function pairs.
  • Recognize the older generateBCryptHash()/verifyBCryptHash() path still common in existing code, and know it's now deprecated.
  • Know why generateSecretKey() belongs to a different problem (two-way encryption) than password hashing.

How Password Hashing Fits Together

Plaintext Password

from the signup or login form

PasswordHashGenerate() / Argon2Hash()

one-way, deliberately slow

Store Only the Hash

never the plaintext

Verify on Next Login

PasswordHashVerify() / Argon2Verify()

Why hash() Is the Wrong Tool

hash() is built for fast, general-purpose checksums, verifying a file wasn't corrupted, generating a cache key, that kind of thing. Fast is exactly the problem for a password: it means an attacker who steals the hashed values can try billions of guesses a second against them. hash() also has no built-in salt, so identical passwords produce identical hashes, making precomputed lookup tables (rainbow tables) effective against it.

NOTE

Adobe's own docs say hash()'s default algorithm is SHA-256 as of ColdFusion (2023 release) Update 8 and later. Older references (including cfdocs.org) describe MD5 as the default, which was true for earlier ColdFusion versions, it's worth checking which version a given codebase targets rather than assuming either default. Either way, the real point stands regardless of which algorithm hash() defaults to: don't use it for passwords.

The Modern Standard: PasswordHashGenerate() / PasswordHashVerify()

ColdFusion 2025 Update 8 introduced a single unified API for password hashing that picks the algorithm via a parameter instead of calling a different function per algorithm. Argon2 is the default, the algorithm that won the Password Hashing Competition and is OWASP's first-choice recommendation for new applications.

Tag Syntax / CFScript
// algorithm defaults to "Argon2" if omitted
hashedPassword = PasswordHashGenerate(form.password);

// explicit algorithm choice: "Argon2" (default), "BCrypt", or "SCrypt"
hashedPassword = PasswordHashGenerate(form.password, "Argon2");

// verification
isValid = PasswordHashVerify(form.password, storedHash);
NOTE

This same Update 8 release marks generateBCryptHash(), verifyBCryptHash(), generateSCryptHash(), and verifySCryptHash() as deprecated. They still work for backward compatibility, but new Adobe ColdFusion code should use PasswordHashGenerate()/PasswordHashVerify() instead.

Lucee's Equivalent: One Function Pair Per Algorithm

Lucee solved the same problem differently: instead of one dispatcher function with an algorithm parameter, it has a dedicated hash/verify pair for each algorithm, all under its Cryptography Extension. The recommendation is the same (Argon2 first), the shape of the API is just organized per-algorithm instead of unified.

CFScript
// Argon2 (recommended first choice), OWASP defaults: argon2id, 19 MB memory, 2 iterations
hashedPassword = Argon2Hash(form.password);
isValid = Argon2Verify(form.password, hashedPassword);

// BCrypt, cost factor passed as a plain integer (default 10, range 4-31)
hashedPassword = BCryptHash(form.password, 12);
isValid = BCryptVerify(form.password, hashedPassword);
NOTE

Lucee also deprecated its own older GenerateBCryptHash()/VerifyBCryptHash()/GenerateSCryptHash()/VerifySCryptHash() names in favor of this {Algorithm}Hash()/{Algorithm}Verify() pattern.

A Real Detail: The Legacy Path Still Common in Existing Code

Plenty of code written between ColdFusion 2021 (when BCrypt support first shipped) and ColdFusion 2025 Update 8 uses generateBCryptHash()/verifyBCryptHash() directly, since that was the recommended approach at the time. It's worth recognizing even though it's now deprecated.

Tag Syntax / CFScript
// options struct: version defaults to "$2a", rounds (work factor) defaults to 10
hashedPassword = generateBCryptHash(form.password, {"version": "$2b", "rounds": 12});
isValid = verifyBCryptHash(form.password, hashedPassword);
NOTE

BCrypt itself, regardless of which function calls it, silently truncates input past 72 bytes. An unusually long passphrase won't error, it'll just be hashed as if everything past that cutoff weren't there.

A Different Problem: generateSecretKey() Is Not for Passwords

generateSecretKey(algorithm, keysize) generates a random key for two-way block-cipher encryption (AES, Blowfish, DES, DESede), the kind you'd use with encrypt()/decrypt() to protect data you need to read back later. A password hash is deliberately one-way, there's no key to lose because there's nothing to decrypt. Mixing these two up, trying to "encrypt" a password with a secret key instead of hashing it, is a real and serious mistake, covered in more depth in the next lesson on encryption.

Common Beginner Mistakes

Using hash() (or raw MD5/SHA-256) to store a password

These are fast by design and have no built-in salt, which is exactly backwards from what a password hash needs. Use PasswordHashGenerate() (Adobe) or Argon2Hash()/BCryptHash() (Lucee) instead.

Assuming a longer BCrypt input is always more secure

BCrypt silently drops everything past 72 bytes. A 200-character passphrase gets hashed as if it were truncated, with no error or warning.

Writing new code against generateBCryptHash()/verifyBCryptHash() on current Adobe ColdFusion

They still work, but they're deprecated as of ColdFusion 2025 Update 8. New code should call PasswordHashGenerate()/PasswordHashVerify() instead.

Confusing generateSecretKey() with a password hashing function

It generates a key for two-way encryption (encrypt()/decrypt()), a completely different problem from one-way password hashing.

Best Practices

  • On current Adobe ColdFusion, use PasswordHashGenerate()/PasswordHashVerify() with the Argon2 default.
  • On Lucee, use Argon2Hash()/Argon2Verify() as the first choice.
  • Never use hash(), MD5, or raw SHA-256 for password storage, regardless of which default algorithm a given ColdFusion version ships with.
  • If migrating an older codebase off generateBCryptHash()/verifyBCryptHash(), there's no need to rehash every password at once, verify against the old hash on next login, then rehash with the new function and store that instead.

Interview Questions

Why is hash() unsafe for storing passwords, even with a strong algorithm like SHA-256?

hash() is fast by design and has no built-in salt. Speed lets an attacker try enormous numbers of guesses against a stolen hash, and the lack of salting makes precomputed lookup tables effective, regardless of which specific algorithm is used underneath.

What's the practical difference between Adobe's and Lucee's modern password hashing APIs?

Adobe uses one unified function, PasswordHashGenerate()/PasswordHashVerify(), with an algorithm parameter choosing between Argon2, BCrypt, and SCrypt. Lucee instead provides a dedicated function pair per algorithm, like Argon2Hash()/Argon2Verify() and BCryptHash()/BCryptVerify(), with no single dispatcher function.

What happens if you pass a 200-character password into generateBCryptHash() or BCryptHash()?

BCrypt silently truncates input past 72 bytes. The function doesn't error, it just hashes only the first 72 bytes, which can create a false sense of security for long passphrases.

Summary

In this lesson, you saw why a general-purpose function like hash() is unsafe for passwords, used Adobe's current unified PasswordHashGenerate()/PasswordHashVerify() API (Argon2 default), used Lucee's equivalent per-algorithm Argon2Hash()/BCryptHash()/SCryptHash() pairs, recognized the older deprecated-but-still-working generateBCryptHash()/verifyBCryptHash() path, and drew a clear line between password hashing and generateSecretKey()'s two-way encryption use case.

What's Next?

The next lesson covers encryption itself, protecting data you do need to read back later, using generateSecretKey() together with encrypt() and decrypt().