Authentication
Password Security
Bcrypt
Argon2
Hashing
+1 more

Password Security: Hashing, Salting & Bcrypt vs Argon2 Guide

SCR Team
February 15, 2026
14 min read
Share

Password Security: Hashing & Salting — Bcrypt vs Argon2 (2025)

Passwords are still the #1 authentication method. LinkedIn (6M passwords leaked), Yahoo (3B accounts), Equifax (147M identities) — inadequate hashing played a major role in each disaster.

This guide teaches proper password storage that stops attackers cold.

Why Password Hashing Matters

Plaintext passwords stolen? Instant full access to every user account.
Fast hashes (MD5, SHA1)? Modern GPUs crack billions per second using rainbow tables.
Unsalted hashes? Attacker uses pre-computed tables and bypasses your hash entirely.
Weak hashing algorithm? All your stored passwords are effectively encrypted with a weak cipher.


Algorithm Comparison (2025)

AlgorithmSpeedMemoryGPU-ResistantRecommendedBest For
Plaintext⚡⚡⚡ InstantNone❌ NoNever
MD5/SHA1⚡⚡ FastNone❌ NoNever
SHA256⚡ FastNone❌ NoAvoid
PBKDF2🚷 SlowLow⚠️ Medium✅ FIPS systemsLegacy compliance
bcrypt🚷 SlowLow✅ GoodYesMost applications
scrypt🚷🚷 SlowerHigh✅✅ ExcellentYesHigh-security apps
Argon2id🚷🚷 SlowerHigh✅✅ Excellent✅✅ BestNew projects (NIST)

Bcrypt: The Battle-Tested Choice

Bcrypt was designed in 1999 and is still safe TODAY because it intentionally uses exponential slowdown (cost factor). The slower hashing is a feature, not a bug.

How Bcrypt Works

  1. Generate a random salt from 2^cost rounds of permutation
  2. Run password + salt through Blowfish cipher cost times
  3. Store the salt + cost + hash together

Example:

  • Input password: "hunter2"
  • Cost: 12 (2^12 = 4,096 iterations)
  • Result: $2b$12$aWjd2hgh3vWs4Ejz8jK47DpO3U5/s8Zi5hj5H4M7HpI...
  • Time to hash: ~250ms (intentionally slow)

Node.js Bcrypt Implementation

Install:

npm install bcryptjs

Hash password:

const bcrypt = require('bcryptjs');

async function registerUser(email, password) {
  const salt = await bcrypt.genSalt(12);  // Cost factor 12
  const hashedPassword = await bcrypt.hash(password, salt);
  
  // Store hashedPassword in database
  await User.create({ email, passwordHash: hashedPassword });
}

Verify password:

async function loginUser(email, passwordAttempt) {
  const user = await User.findOne({ email });
  const isValid = await bcrypt.compare(passwordAttempt, user.passwordHash);
  
  if (isValid) {
    // Authentication successful
    return createSession(user);
  } else {
    // Wrong password
    return 'Invalid credentials';
  }
}

Cost factor timing:

  • Cost 10: 10ms (old hardware)
  • Cost 12: 250ms (standard, 2024)
  • Cost 14: 1 second (future-proofing)
  • Cost 16: 2 seconds (admin accounts only)

Argon2: The Modern Standard

Argon2 won the Password Hashing Competition (2015) and is now recommended by NIST. It's memory-hard, making GPU/ASIC attacks infeasible.

Node.js Argon2 Implementation

Install:

npm install argon2

Hash:

const argon2 = require('argon2');

async function registerUserArgon2(email, password) {
  const hash = await argon2.hash(password, {
    type: argon2.argon2id,  // Recommended
    memoryCost: 65536,      // 64 MB
    timeCost: 3,            // 3 iterations
    parallelism: 4          // 4 parallel threads
  });
  
  await User.create({ email, passwordHash: hash });
}

Verify:

async function loginUserArgon2(email, passwordAttempt) {
  const user = await User.findOne({ email });
  const isValid = await argon2.verify(user.passwordHash, passwordAttempt);
  
  if (isValid) {
    return createSession(user);
  } else {
    return 'Invalid credentials';
  }
}

Python Argon2

pip install argon2-cffi
from argon2 import PasswordHasher

hasher = PasswordHasher()
hash = hasher.hash('hunter2')
hasher.verify(hash, 'hunter2')  # True

Beyond Hashing: Defense in Depth

Salts & Peppers:

  • Salt: Random per-password, stored with hash (bcrypt/argon2 handle this)
  • Pepper: Server secret appended before hashing (stored in env, NOT in code)

Password Rotation:

  • Re-hash on next login when you upgrade algorithms
  • Support graceful migration from old to new algorithms

Additional Protections:

  • Block common passwords (rockyou.txt, have-i-been-pwned lists)
  • Enforce minimum length (12+ characters)
  • Rate limit login attempts (exponential backoff)
  • Monitor for credential stuffing (impossible logins from new IPs)
  • Require MFA (passwords alone insufficient)

Migration Plan: Upgrading Hash Algorithms

If you're using MD5/SHA1:

// On login, check if password matches old hash
if (md5(password) === user.oldMd5Hash) {
  // Authentication passed! Re-hash with bcrypt
  user.bcryptHash = await bcrypt.hash(password, 12);
  user.oldMd5Hash = null;
  await user.save();
}

Password Security Checklist

  • Use Argon2id (new projects) or bcrypt (existing)
  • Cost/time parameters tuned to 250-500ms per hash
  • Never use MD5, SHA1, or unsalted hashes
  • Block common passwords (NIST list)
  • Enforce minimum 12-character passwords
  • Rate limit login endpoints (exponential backoff)
  • Require MFA for sensitive operations
  • Monitor for credential stuffing
  • Support password rotation/upgrade on next login

Resources

Next Step: Audit your password storage now. If using MD5/SHA1, implement bcrypt upgrade on next login.

Advertisement