Why I Switched from bcrypt to Argon2
When building Aegis2FA, I had to choose a password hashing algorithm. Here's why I went with Argon2id.
When I started building Aegis2FA, I needed to pick a password hashing algorithm. Most tutorials just say "use bcrypt" and move on. I wanted to understand why.
The Short Answer
Use Argon2id. It won the Password Hashing Competition in 2015 and it's designed to be hard to crack even with expensive hardware.
Why Not bcrypt?
bcrypt has been around since 1999 and it works. But it has limitations:
- Max 72 characters (anything longer gets silently truncated)
- Fixed memory usage (can't tune it)
- GPUs can crack it faster than Argon2
The GPU thing matters. An attacker with a farm of graphics cards can run thousands of bcrypt hashes in parallel. Argon2 uses a lot of memory per hash, which limits parallelism.
What I Used
import argon2 from 'argon2';
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3,
parallelism: 4,
});64 MB per hash sounds like a lot, but on a server it's fine. The point is that an attacker needs 64 MB per attempt, which adds up fast when you're trying millions of passwords.
One Gotcha
Don't use the library defaults without thinking. The defaults might be too weak (low memory) or too strong (slow login). Test with your actual hardware and pick values that give you ~100ms hash time.
Also: don't hash synchronously in Node.js. It blocks the event loop. Always use async.
Migration
If you have existing bcrypt hashes, you can migrate users gradually. Check the algorithm when they log in, and if it's bcrypt, rehash with Argon2:
if (storedHash.algorithm === 'bcrypt') {
const valid = await bcrypt.compare(password, storedHash.hash);
if (valid) {
const newHash = await argon2.hash(password);
await updateUserHash(userId, newHash, 'argon2id');
}
}That's it. Argon2id for new projects, gradual migration for existing ones.