security
bcrypt: the safe, boring choice
bcrypt: the safe, boring choice
A password hashing function built to be slow, and to stay slow as hardware improves, via a tunable cost factor. I use the native bcrypt package rather than the pure-JS bcryptjs — faster, but it needs native compilation, which feeds into the Docker base image choice.
Fast hashes like MD5 or plain SHA-256 are wrong for passwords precisely because they're fast — a stolen hash dump lets an attacker try billions of guesses per second on a GPU. bcrypt's cost factor makes each attempt deliberately expensive, turning brute-forcing a strong password from hours into centuries, without meaningfully slowing the one legitimate login per user.
const hash = await bcrypt.hash(plainPassword, 12); // 12 = cost factor
Cost is exponential — each +1 roughly doubles the time. 10–12 is the common modern default. Higher costs more CPU per login, register or reset, which matters at scale since every one of those pays it. Worth re-benchmarking against current hardware every so often; what was slow enough a few years ago may be crackable faster than intended now.
Salting is automatic — bcrypt embeds a random salt directly in the output string, no separate column, no risk of forgetting. Part of why it's stayed the sane default despite its age.
Argon2id is the more modern choice — memory-hard, expensive to parallelise on GPUs and ASICs, not just CPU-slow. bcrypt has a max effective password length around 72 bytes, silently ignoring anything past that, and isn't memory-hard. For a greenfield project today, Argon2id is the stronger pick. bcrypt remains fine — unbroken, widely audited — it's just the safe, boring option rather than the strongest available.
One thing that isn't optional: never compare hashes with ===.
const isValid = await bcrypt.compare(plainPassword, storedHash);
A naive comparison leaks timing information that can theoretically be used to guess the hash byte by byte.