How to Store User Passwords Safely: Hash, Don't Encrypt

· 5 min read

If you're looking for a way to encrypt passwords, the safer answer is not to. Encryption can be reversed: whoever has the key can recover every password. Password hashing is one-way. You store a hash, and at login you hash what the user typed and compare the two. Nobody, including you, can get the original back.

Why a normal hash isn't enough

SHA-256 and MD5 are built to be fast, so an attacker with a leaked database and a GPU can try billions of guesses per second. Password hashes such as Argon2id, scrypt and bcrypt are deliberately slow and memory-hungry, which makes each guess expensive. Each hash also includes a random salt, so two users with the same password get different hashes.

Argon2id in Node.js

Install the argon2 package with npm install argon2, then:

passwords.mjs
import argon2 from "argon2";

// OWASP minimum for Argon2id: 19 MiB memory, 2 iterations, 1 degree of parallelism.
const options = { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1 };

export async function hashPassword(password) {
  return argon2.hash(password, options); // the salt is generated and stored in the hash string
}

export async function checkPassword(storedHash, password) {
  return argon2.verify(storedHash, password);
}

The stored string holds the algorithm, the settings and the salt, so one column is enough. argon2.needsRehash(storedHash, options) tells you when a hash uses older settings; rehash it the next time that user logs in.

Choosing the settings

The settings above are OWASP's minimum for Argon2id. If your servers can afford it, raise the memory first. Measure on your production hardware and keep logins well under a second. If you can't use Argon2id, use scrypt (built into node:crypto) or bcrypt with a cost factor of at least 10.

The rest of a safe login

  • Rate-limit login attempts per account and per IP address.
  • Check new passwords against lists of breached passwords instead of forcing symbols and digits, as NIST SP 800-63B recommends.
  • Return the same error for an unknown email and a wrong password, so attackers can't find out which accounts exist.
  • Offer two-factor authentication or passkeys, at least for accounts that matter.

Where AMARACUS fits

AMARACUS doesn't hash passwords, and you shouldn't send passwords to any third-party API. Hash them in your own server as shown above. Use AMARACUS for the other sensitive data in your users table, such as phone numbers, addresses or stored tokens; see encrypting database fields.

Frequently asked questions

Should passwords be encrypted or hashed?

Hashed, with a slow password hash such as Argon2id. Encrypted passwords can all be recovered by anyone who gets the key.

Is bcrypt still safe to use?

Yes, with a cost factor of 10 or more. Bcrypt only uses the first 72 bytes of a password, so Argon2id is the better choice for new systems.

Do I need a pepper?

It's optional. A pepper is a secret added to every password before hashing and kept outside the database, so a database leak alone isn't enough to start guessing. If you use one, keep it in a secret manager.

My app stores MD5 or SHA-256 password hashes. How do I fix it?

Hash every existing hash with Argon2id now, so no weak hashes stay in the database. Then switch each user to a plain Argon2id hash of their password the next time they log in.