DEV Community

ZèD
ZèD

Posted on Edited on Originally published at imzihad21.github.io

BCrypt vs Argon2: Password Hashing in .NET – A Practical Deep Dive

BCrypt vs Argon2 password hashing in .NET: a practical comparison

Password storage security depends on computationally expensive, intentionally slow cryptographic functions. Fast cryptographic hashes verify data integrity at high throughput, but using them for passwords exposes systems to rapid offline credential recovery attacks.

Choosing between BCrypt and Argon2 in .NET requires balancing CPU and memory hardness, hardware-assisted cracking resistance, interactive login latency, and progressive credential migration strategies.

The problem and production context

General-purpose hashing algorithms compromise user credentials when database storage layers are breached.

  • Failure scenario: An application stores user credentials using SHA-256 or SHA-512. Following an unauthorized database extraction, an attacker deploys consumer GPU or ASIC clusters capable of calculating billions of SHA evaluations per second. Weak and moderately complex passwords crack within minutes via offline dictionary and rainbow table lookups.
  • Why default approaches fall short: General-purpose cryptographic hash functions prioritize execution throughput and low resource utilization. When applied to passwords, this efficiency works entirely in favor of the attacker. Defending against offline attacks requires introducing artificial delays and hardware resource constraints that degrade GPU parallelism.
  • Production impact: Compromised databases result in account takeover across systems, credential stuffing cascades, and compliance violations. Conversely, misconfigured password hashers with excessively high cost parameters can trigger denial-of-service conditions during concurrent login spikes.

Key production considerations when selecting an algorithm include:

  • Password hashing algorithms must be intentionally slow and resource-intensive.
  • Attackers run parallelized brute-force attacks on specialized GPU and ASIC hardware.
  • The hashing algorithm you choose determines how effectively your system withstands compromised database dumps.
  • Safe default configurations reduce the risk of implementation flaws in authentication systems.

Mental model and core concepts

Modern password hashing functions prevent mass parallel cracking by exhausting specific hardware resource classes.

1. The failure of fast hashes for credential storage

Cryptographic hash functions such as SHA-256 and SHA-512 are engineered for throughput. Modern GPUs feature thousands of small execution cores capable of parallelizing billions of independent SHA computations per second. Dedicated password hashers counter this hardware advantage by requiring tunable iterations and substantial operational memory.

2. BCrypt mechanics and CPU-bound stretching

BCrypt derives from the Blowfish cipher and uses an expensive key setup phase to enforce computational cost.

  • Operates primarily as a CPU-bound key derivation function.
  • Tuned via a single logarithmic work factor parameter (cost 10 to 14).
  • Maintains a fixed, minimal memory footprint under 4 KB.

3. Argon2 and memory-hard computational resistance

Argon2, winner of the Password Hashing Competition, introduces memory hardness in addition to iteration counts.

  • The Argon2id variant is recommended for general password storage because it resists both side-channel cache-timing attacks and GPU-assisted cracking.
  • Tunable across three distinct axes: memory size (m), iteration count (t), and parallelism threads (p).
  • Allocating tens or hundreds of megabytes per computation prevents GPUs from running thousands of concurrent hashing threads, as GPU video memory quickly exhausts.

4. Memory footprint divergence between BCrypt and Argon2

The primary architectural difference lies in memory consumption:

  • BCrypt enforces high CPU overhead while keeping memory usage under 4 KB.
  • Argon2 enforces both CPU overhead and substantial memory allocation (often tens or hundreds of megabytes per hash).

Because GPUs possess thousands of lightweight processing cores with limited memory per core, memory-hard algorithms like Argon2 scale poorly for attackers. An attacker cannot easily run thousands of concurrent Argon2 computations if each thread requires 64 MB of RAM.

5. Production parameter tuning

Benchmark hashing performance on hardware that matches the production environment. A standard target latency for interactive user logins ranges between 400 ms and 800 ms per operation, adjusted according to peak authentication throughput and SLA constraints. Setting parameters too low weakens protection, while setting them too high exposes authentication endpoints to denial-of-service vulnerabilities under sudden traffic spikes.

6. Transparent rehash-on-login migration

Upgrading an existing authentication system to a stronger algorithm does not require resetting user passwords. When a user logs in successfully using the legacy hash format (such as BCrypt or PBKDF2), the server verifies the plaintext password against the old hash, immediately rehashes the password using the new algorithm (such as Argon2id), and updates the database record.

Production implementation

The following implementation provides decoupled BCrypt and Argon2 hashing implementations with an abstract authentication contract in .NET.

using System;
using BCrypt.Net;
using Isopoh.Cryptography.Argon2;

public interface IPasswordHasher
{
    string Hash(string password);
    bool Verify(string password, string hash);
}

public sealed class BcryptPasswordHasher : IPasswordHasher
{
    private const int WorkFactor = 13;

    public string Hash(string password)
    {
        if (string.IsNullOrWhiteSpace(password))
        {
            throw new ArgumentException("Password cannot be empty", nameof(password));
        }
        return BCrypt.Net.BCrypt.HashPassword(password, WorkFactor);
    }

    public bool Verify(string password, string hash)
    {
        if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(hash))
        {
            return false;
        }
        return BCrypt.Net.BCrypt.Verify(password, hash);
    }
}

public sealed class Argon2PasswordHasher : IPasswordHasher
{
    public string Hash(string password)
    {
        if (string.IsNullOrWhiteSpace(password))
        {
            throw new ArgumentException("Password cannot be empty", nameof(password));
        }
        return Argon2.Hash(password);
    }

    public bool Verify(string password, string hash)
    {
        if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(hash))
        {
            return false;
        }
        return Argon2.Verify(hash, password);
    }
}

public sealed class PasswordService
{
    private readonly IPasswordHasher _hasher;

    public PasswordService(IPasswordHasher hasher)
    {
        _hasher = hasher ?? throw new ArgumentNullException(nameof(hasher));
    }

    public string CreateHash(string password)
    {
        return _hasher.Hash(password);
    }

    public bool Validate(string password, string storedHash)
    {
        return _hasher.Verify(password, storedHash);
    }
}
Enter fullscreen mode Exit fullscreen mode

Architectural trade-offs and edge cases

Choosing between BCrypt and Argon2 involves balancing memory allocations against resistance to specialized cracking hardware.

  • Latency versus consistency: Higher iteration counts and memory parameters exponentially increase attacker resistance, but directly increase CPU thread holding times and HTTP request latencies. Systems handling thousands of concurrent logins per second must budget thread pool utilization to avoid thread starvation.
  • Failure recovery: Authentication endpoints must handle thread pool saturation. If memory pressure spikes due to concurrent Argon2 evaluations, authentication services must reject requests or apply backpressure before triggering process out-of-memory aborts.
  • Scale limitations: Argon2 requires substantial dedicated memory (e.g., 64 MB per hash). Under high concurrent login volume (e.g., thousands of simultaneous requests), total memory consumption can exceed available RAM on lightweight container instances, making BCrypt with an elevated work factor more suitable in memory-constrained environments.

Common anti-patterns and gotchas

  • Direct use of general-purpose digests: Storing credentials using SHA-256, SHA-512, or MD5 without key stretching allows GPU arrays to crack passwords via precomputed tables and parallel dictionaries.
  • Stale default work factors: Retaining library default work factors configured for decade-old hardware reduces computational barriers against modern hardware.
  • Forced credential invalidation during migrations: Forcing all users to reset passwords during algorithm upgrades degrades user experience. Instead, employ transparent rehash-on-login verification pipelines.
  • Unconstrained Argon2 memory settings: Configuring memory allocations without evaluating peak login concurrency can exhaust container memory and trigger operating system out-of-memory kills.
  • Isolated password hashing without perimeter defenses: Relying entirely on password hashing while neglecting complementary protections such as rate limiting, account lockouts, and multi-factor authentication leaves endpoints vulnerable to credential stuffing.

Implementation checklist

  1. Measure execution times for both BCrypt and Argon2 on staging hardware that mirrors production specs.
  2. Balance login latency against expected throughput to select appropriate cost factors (target 400 ms to 800 ms).
  3. Validate non-empty password inputs prior to executing key stretching functions.
  4. Abstract hashing logic behind an IPasswordHasher interface to decouple algorithms from domain handlers.
  5. Build a rehash-on-login workflow to upgrade legacy hashes gradually without user password resets.
  6. Protect authentication endpoints with rate limiting, account lockouts, and multi-factor authentication to defend against credential stuffing.

Top comments (0)