Argon2 / bcrypt

Backend Fundamentals · Backend Security · 9 min read

1. What problem does this solve?

  • Problem: Storing user passwords in plain text or weakly hashed form exposes them to attackers if the database is compromised.
  • Why it matters: Passwords are often reused across sites. A single leak can lead to credential stuffing attacks, account takeover, and downstream data breaches.
  • Consequence of not using:
    • Attackers can brute‑force or pre‑compute rainbow tables quickly.
    • Even if the hash is stored, a fast hash (e.g., MD5) can be cracked in seconds.
  • Real‑world appearance:
    • Authentication services (OAuth providers, SaaS platforms).
    • Any system that stores user credentials (web apps, mobile backends, microservices).
    • Password reset flows, multi‑factor authentication setups, and audit logs.

2. Mental model

Analogy
Think of a password as a secret recipe.

  • Plain text: You write the recipe on a sticky note and leave it on the kitchen counter. Anyone can read it.
  • Fast hash (e.g., SHA‑256): You put the recipe in a small lockbox that opens in a few seconds. Still easy for a thief.
  • bcrypt/Argon2: You put the recipe in a heavy, tamper‑evident vault that requires a slow, computationally expensive key‑derivation process to open. Even if the vault is stolen, the thief must spend a lot of time and resources to get the recipe.

Engineering model

  • Key‑Derivation Function (KDF): Takes a password + salt → produces a hash.
  • Cost parameters:
    • bcrypt: cost (log₂ of the number of iterations).
    • Argon2: time, memory, parallelism.
  • Salt: Random per‑user value to defeat pre‑computed tables.
  • Pepper (optional): A server‑side secret added to every password before hashing, stored outside the database.

3. Deep explanation

Internals

FeaturebcryptArgon2
AlgorithmBlowfish‑based, iterative key‑expansionMemory‑hard, uses a variant of the BLAKE2 hash
Output184‑bit hash (22‑char base64)256‑bit hash (base64 or hex)
Salt128‑bit (16 bytes)128‑bit (16 bytes)
ParallelismSingle‑threaded (but can be run in parallel across users)Configurable parallelism (threads)
Memory usageFixed small (≈128 kB)Tunable (e.g., 64 MiB)

Important terminology

  • Cost factor: Determines how many iterations or how much memory is used.
  • Work factor: Same as cost factor but often used in Argon2 context.
  • Salt: Random data appended to the password before hashing.
  • Pepper: Server‑side secret added to the password before hashing.
  • Hash: The derived value stored in the database.
  • Time‑memory trade‑off: Argon2 allows explicit tuning of CPU vs. memory consumption.

Trade‑offs

AspectbcryptArgon2
SpeedFast on modern CPUs; can be tuned.Slower by design; can be tuned for memory hardness.
Memory usageLowHigh (configurable).
SecurityStrong against GPU cracking; still vulnerable to side‑channel attacks if not implemented carefully.Memory‑hardness makes GPU/ASIC attacks expensive.
MaturityWidely deployed; many libraries.Newer but standardized (RFC 9106).
Backwards compatibilityLegacy systems often use bcrypt.Argon2 may require migration.

Failure modes

  • Weak cost factor: Allows attackers to brute‑force quickly.
  • Reusing salts: Breaks uniqueness; leads to identical hashes for identical passwords.
  • Storing pepper in code: If the code is leaked, pepper is compromised.
  • Not verifying timing: Timing attacks can leak hash comparison results.

Limitations

  • bcrypt: Limited to 2⁶⁴ iterations; cannot use memory hardness.
  • Argon2: Requires careful tuning; too high memory can exhaust server resources.
  • Both: Still vulnerable if the password itself is weak (short, common).

Where it fits in a backend architecture

┌───────────────────────┐
│  Client (Web/Mobile)  │
└─────────────┬─────────┘
              │
              ▼
┌───────────────────────┐
│  API Gateway / Auth   │
│  Service (Node.js)    │
└───────┬───────────────┘
        │
        ▼
┌───────────────────────┐
│  User Service         │
│  - Hashes passwords   │
│  - Stores hash+salt   │
└───────┬───────────────┘
        │
        ▼
┌───────────────────────┐
│  PostgreSQL / MongoDB │
└───────────────────────┘
  • Hashing happens before persisting to the database.
  • Verification happens on login: retrieve hash+salt, recompute, compare.
  • Password policy enforcement: Hashing is only part of security; enforce length, complexity, or use password‑strength libraries.
  • Rate limiting: Protect login endpoints to mitigate brute‑force attempts.
  • Account lockout / MFA: Complement hashing with additional layers.
  • Secrets management: Store pepper in Vault, AWS KMS, or environment variables.

4. How it works

Step‑by‑step flow

  1. User signs up with password P.
  2. Generate a random salt S (16 bytes).
  3. Concatenate P + S (and optional pepper K).
  4. Run KDF (bcrypt or Argon2) with chosen cost parameters → hash H.
  5. Store S, H, and cost parameters in the database.
  6. Login: Retrieve S, H, cost.
  7. Re‑hash input password with same S and cost → H'.
  8. Compare H' to stored H using constant‑time comparison.
  9. Grant access if equal; otherwise reject.

Mermaid diagram

sequenceDiagram
    participant U as User
    participant A as Auth Service
    participant DB as Database

    U->>A: POST /signup {password: P}
    A->>A: Generate salt S
    A->>A: Compute hash H = KDF(P+S)
    A->>DB: INSERT (user_id, H, S, cost)
    DB-->>A: Success

    U->>A: POST /login {password: P}
    A->>DB: SELECT H, S, cost WHERE user_id=U
    DB-->>A: H, S, cost
    A->>A: Compute H' = KDF(P+S)
    A->>A: ConstantTimeCompare(H, H')
    alt success
        A->>U: 200 OK
    else
        A->>U: 401 Unauthorized
    end

5. Practical implementation

TypeScript/Node.js example (bcrypt)

// src/auth/hash.ts
import { randomBytes } from 'crypto';
import { hash, compare } from 'bcryptjs'; // lightweight, pure JS

const BCRYPT_COST = 12; // 2^12 iterations (~4096 rounds)

// Hash a password
export async function hashPassword(plain: string): Promise<string> {
  // bcryptjs automatically generates a salt when cost is provided
  return await hash(plain, BCRYPT_COST);
}

// Verify a password
export async function verifyPassword(
  plain: string,
  hashed: string
): Promise<boolean> {
  return await compare(plain, hashed);
}

TypeScript/Node.js example (Argon2)

// src/auth/argon2.ts
import argon2 from 'argon2';

const ARGON2_OPTIONS: argon2.Options = {
  type: argon2.argon2id, // recommended
  memoryCost: 64 * 1024, // 64 MiB
  timeCost: 3,           // 3 iterations
  parallelism: 2,        // 2 threads
};

export async function hashPassword(plain: string): Promise<string> {
  return await argon2.hash(plain, ARGON2_OPTIONS);
}

export async function verifyPassword(
  plain: string,
  hashed: string
): Promise<boolean> {
  return await argon2.verify(hashed, plain);
}

Using the hash in a FastAPI endpoint (Python)

# auth.py
import bcrypt
from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.post("/signup")
async def signup(username: str, password: str):
    hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
    # store username, hashed in DB
    return {"status": "ok"}

@app.post("/login")
async def login(username: str, password: str):
    # fetch hashed from DB
    if not bcrypt.checkpw(password.encode(), stored_hash):
        raise HTTPException(status_code=401, detail="Invalid credentials")
    return {"status": "ok"}

Note: In production, use a dedicated auth service, store salts explicitly if needed, and never log raw passwords.

6. Production considerations

ConcernbcryptArgon2
ScalabilityCPU‑bound; can be parallelized across users.Memory‑bound; may limit concurrent users per instance.
ReliabilityMature libraries; stable.Newer but stable; watch for library bugs.
SecurityResistant to GPU cracking; still vulnerable to side‑channel.Memory hardness makes GPU/ASIC attacks expensive.
ObservabilityLog hash generation times; monitor for slow operations.Monitor memory usage per hash; alert on spikes.
PerformanceFast; cost factor 12 ≈ 100 ms on modern CPU.Slower; cost factor 3, memory 64 MiB ≈ 200–300 ms.
Error handlingCatch bcrypt errors; fallback to retry.Catch argon2 errors; ensure proper exception mapping.
Resource usageCPU heavy; keep cost factor reasonable.Memory heavy; avoid exceeding container limits.
Operational concernsUpgrade libraries regularly; keep cost factor in sync.Tune memory/time/parallelism per deployment; monitor for OOM.
Failure recoveryRe‑hash on upgrade; migration scripts.Migration from bcrypt to Argon2: store both hashes until all users migrate.
DeploymentSimple; no external dependencies.Requires enough RAM; may need larger Docker images.

7. Common mistakes

MistakeWhy it’s problematic
Using a fixed salt for all usersEnables rainbow table attacks; identical passwords produce identical hashes.
Setting cost factor too lowMakes brute‑force feasible.
Storing plain passwords in logs or error messagesImmediate credential leakage.
Using bcrypt on a GPU‑optimized environment without monitoringAttackers can massively parallelize cracking.
Ignoring constant‑time comparisonTiming attacks can reveal hash equality.
Forgetting to upgrade cost factors over timeSecurity degrades as hardware improves.
Using argon2id incorrectly (e.g., wrong options)May produce insecure hashes or crash.
Storing pepper in the same repo as codeIf repo is compromised, pepper is lost.

8. Senior engineer perspective

Architecture decisions

  • Dedicated Auth Service: Isolate hashing logic to avoid accidental leaks.
  • Hash Migration Strategy: Store both bcrypt and Argon2 hashes during transition; re‑hash on next successful login.
  • Secrets Management: Store pepper in Vault or KMS; rotate periodically.
  • Cost Parameter Policy: Define a policy (e.g., cost 12 for bcrypt, memory 128 MiB for Argon2) and enforce via CI checks.
  • Resource Allocation: For Argon2, allocate enough RAM per container; consider using a separate pool of workers.

Trade‑offs

  • Performance vs. Security: Higher memory/time reduces attack surface but increases latency and resource cost.
  • Operational Complexity: Argon2 requires monitoring memory usage; bcrypt is simpler.
  • Compatibility: Legacy systems may still rely on bcrypt; migrating to Argon2 may require user re‑registration or staged rollout.

Bottlenecks

  • CPU: bcrypt can saturate CPU on high login traffic.
  • Memory: Argon2 can exhaust RAM on large clusters if not tuned.
  • Database: Storing salts and cost parameters increases row size; consider compression.

Failure scenarios

  • OOM during Argon2: Crash the worker; implement graceful shutdown and retry.
  • Hash collision: Extremely unlikely; still log and alert if detected.
  • Pepper compromise: Trigger immediate rotation and re‑hash all passwords.

Monitoring

  • Hashing latency: Histogram per endpoint.
  • Memory usage: Prometheus metrics for Argon2 workers.
  • Failed login attempts: Rate‑limit alerts.
  • Cost factor drift: Alert if cost factor falls below policy threshold.

Cost/performance

  • CPU cost: bcrypt 12 ≈ 100 ms; Argon2 3×64 MiB ≈ 250 ms.
  • Memory cost: Argon2 64 MiB per hash; consider batching or worker pools.
  • Infrastructure: May need larger instances or more workers for Argon2.

When NOT to use

  • Very low‑traffic sites: bcrypt may be sufficient and simpler.
  • Resource‑constrained environments: Argon2’s memory usage may be prohibitive.
  • Legacy systems: If migration is impossible, stick with bcrypt but keep cost high.

9. Interview questions

1BeginnerWhat is the purpose of a salt in password hashing?
Answer
: Prevents identical passwords from producing identical hashes; defeats rainbow tables.
2BeginnerWhy is bcrypt considered slow, and why is that good?
Answer
: It intentionally performs many iterations to make brute‑force attacks computationally expensive.
3SeniorExplain how Argon2’s memory hardness protects against GPU cracking.
Answer
: GPUs excel at parallel CPU operations but have limited memory bandwidth; Argon2 forces each hash to use a large memory block, making GPU parallelism ineffective.
4SeniorDesign a migration strategy to move from bcrypt to Argon2 in a production system without forcing users to reset passwords.
Answer
: Store both hashes; on login, verify with bcrypt, then re‑hash with Argon2 and update DB; gradually phase out bcrypt after all users have logged in.
5Scenario/DebuggingA login endpoint is suddenly slower by 5×. You suspect hashing is the culprit. How would you investigate and resolve this?
Answer
:
  • Profile request latency; isolate hashing time.
  • Check cost parameters (maybe bumped).
  • Verify memory usage spikes (Argon2).
  • Consider scaling workers or adjusting cost.
  • Add caching for repeated logins? (not advisable).
  • Update monitoring and alert thresholds.

10. Hands‑on task

Task: Implement a password migration endpoint in a Node.js/Express app that:

  1. Accepts a username and password.
  2. Retrieves the stored bcrypt hash.
  3. Verifies the password.
  4. Re‑hashes the password with Argon2 (using argon2id).
  5. Stores the new Argon2 hash and marks the user as migrated.

Goal: Demonstrate understanding of both KDFs, migration logic, and secure storage.

11. Quick revision

  • Passwords must never be stored in plain text.
  • Salt: unique per user; prevents rainbow tables.
  • Pepper: server‑side secret; adds an extra layer.
  • bcrypt: CPU‑bound, cost factor 2ⁿ iterations.
  • Argon2: memory‑hard, configurable time/memory/parallelism.
  • Use constant‑time comparison to avoid timing attacks.
  • Migration: store both hashes until all users re‑hash.
  • Monitor latency and resource usage; adjust cost parameters over time.
  • Keep cost factors in sync with hardware improvements.
  • Never log raw passwords or hash errors.
  • Use secrets management for pepper.
  • Rate limit login attempts to mitigate brute‑force.
  • Unit test hashing and verification functions.
  • Upgrade libraries regularly; watch for deprecations.

12. References