Auth1BlogArgon2id vs bcrypt Migration
SecurityMigration· 13 min read

Why We Replaced bcrypt with Argon2id in Production

bcrypt was designed in 1999. Modern attackers use GPUs, FPGAs, and ASICs. Argon2id is memory-hard, GPU-resistant, and the OWASP recommended default. Here is why we switched and the zero-downtime migration pattern we used.

Why Argon2id Wins

Argon2 won the Password Hashing Competition in 2015. Argon2id combines side-channel resistance with GPU cracking resistance. With m=19,456 KiB, each hash requires 19 MB of memory. An attacker on a GPU with 80 GB VRAM can only run ~4,000 parallel computations vs. 20 million with bcrypt (4 KB per hash).

OWASP's 2025 Password Storage Cheat Sheet recommends Argon2id with m=19456, t=2, p=1 as the first choice. The practical impact: 5,000x higher cost per password guess compared to bcrypt cost=10.


The Problem with bcryptjs in Node.js

bcryptjs is a pure JavaScript implementation that runs on the main thread. A single hash at cost factor 10 takes 80-120ms, during which the event loop is completely blocked. The async version still runs on the main thread via setTimeout.


Our Solution: Argon2id via Rust napi-rs

JavaScriptpassword.js
const { hashPassword, verifyPassword } = require('auth-shield');

const hash = hashPassword('user-password-here');
// => $argon2id$v=19$m=19456,t=2,p=1$...

const valid = verifyPassword('user-password-here', hash);
// => true

Because this is compiled Rust running as a synchronous napi-rs function, it executes on a V8 worker thread — not the event loop. Zero event loop blocking, and no C++ build toolchain required.


Zero-Downtime Migration Pattern

The detect-and-rehash pattern: every login, detect the hash algorithm from the prefix, verify with the appropriate algorithm, and if bcrypt, re-hash with Argon2id and update the database.

JavaScriptsmart-verify.js
async function verifyPasswordSmart(password, storedHash) {
  const isBcrypt = storedHash.startsWith('$2a$') || storedHash.startsWith('$2b$');
  const isArgon2 = storedHash.startsWith('$argon2');

  if (isArgon2) {
    return { valid: verifyPassword(password, storedHash), rehashed: false };
  }
  if (isBcrypt) {
    const valid = await bcrypt.compare(password, storedHash);
    if (!valid) return { valid: false, rehashed: false };
    return { valid: true, rehashed: true, newHash: hashPassword(password) };
  }
}

In production, 94% of active users were migrated within 30 days. The remaining 6% were dormant accounts. For those, you can leave them on bcrypt indefinitely (still secure) or force a password reset.


Benchmark: Rust Argon2id vs JS bcryptjs

ImplementationHash TimeVerify TimeEvent Loop Blocked
Argon2id (auth-shield, Rust)22.3ms21.8ms0ms
bcryptjs (pure JS, cost=10)94.7ms93.2ms~94ms
bcrypt native (C++, cost=10)78.1ms77.4ms0ms

4.2x faster than bcryptjs with a fundamentally more secure algorithm. 3.5x faster than native bcrypt C++ binding.


Parameter Selection

Contextm (KiB)tpHash Time
Web login (default)19,45621~22ms
High-security (banking)65,53631~85ms
Mobile/embedded4,09631~8ms

Modern Password Hashing, Zero Effort

auth-shield gives you Argon2id with OWASP-recommended parameters. One function call. No event loop blocking.

Start Free →Read the Docs
Free tier · 1,000 verifications/month · No credit card required