bcrypt, scrypt, and Argon2: Choosing the Right Password Hashing Algorithm

Table of Contents

  1. Introduction
  2. Understanding Password Hashing Requirements
  3. Algorithm Deep Dive
    • bcrypt
    • scrypt
    • Argon2
  4. Comparative Analysis
  5. Implementation Considerations
  6. Real-world Usage Scenarios
  7. Performance Benchmarks
  8. Security Considerations
  9. Making the Right Choice
  10. Future Considerations

Introduction

Password hashing is a critical component of modern authentication systems. Unlike general-purpose hash functions like SHA-256, password hashing algorithms are specifically designed to be computationally intensive and memory-hard, making them resistant to various forms of attacks. This article provides a detailed technical analysis of three leading password hashing algorithms: bcrypt, scrypt, and Argon2.

Understanding Password Hashing Requirements

Before diving into specific algorithms, let's establish the key requirements for password hashing:

  1. Slow Computation: Unlike general-purpose hash functions, password hashing must be deliberately slow to prevent high-speed brute-force attacks.
  2. Salt Integration: The algorithm must incorporate cryptographic salts to prevent rainbow table attacks and ensure unique hashes for identical passwords.
  3. Memory Hardness: Resistance to hardware-based attacks (GPU, ASIC) through memory-intensive operations.
  4. Tunability: Ability to adjust computational and memory costs as hardware capabilities evolve.

Algorithm Deep Dive

bcrypt

bcrypt, derived from the Blowfish cipher, was introduced in 1999 by Niels Provos and David Mazières.

Technical Specifications:

bcrypt(password, salt, cost) = 
  EksBlowfishSetup(cost, salt, password) // Initialize Blowfish state
  state = "OrpheanBeholderScryDoubt"     // Initial state
  for (i = 0; i < 64; i++)
    state = ExpandKey(state, 0, salt)     // Multiple rounds of Blowfish
    state = ExpandKey(state, 0, password)
  return state

Key Features:

  • Cost factor (work factor) ranges from 4 to 31
  • Fixed output size of 24 bytes
  • Integrated salt handling (128-bit)
  • Built-in version identification in output format

Advantages:

  • Battle-tested (20+ years in production)
  • Simple to implement correctly
  • Built-in salt handling
  • Adjustable work factor

Limitations:

  • Fixed memory usage (~4KB)
  • No memory hardness
  • Maximum password length of 72 bytes

scrypt

scrypt, designed by Colin Percival in 2009, introduced memory hardness to password hashing.

Technical Parameters:

scrypt(password, salt, N, r, p) where:
N = CPU/Memory cost parameter
r = Block size parameter
p = Parallelization parameter

Core Functions:

def scrypt(password, salt, N, r, p):
    # Initialize blocks
    B = PBKDF2(password, salt)
    
    # Memory-hard mixing function
    V = ROMix(B, N)
    
    # Final derivation
    return PBKDF2(password, V)

def ROMix(B, N):
    V = [0] * N
    X = B
    
    # Memory-filling loop
    for i in range(N):
        V[i] = X
        X = BlockMix(X)
    
    # Memory-dependent mixing
    for i in range(N):
        j = Integrify(X) % N
        X = BlockMix(X ⊕ V[j])
    
    return X

Key Features:

  • Configurable memory usage
  • Time-memory trade-off resistance
  • Parallel processing support
  • Sequential memory-hard operations

Argon2

Argon2, the winner of the Password Hashing Competition (2015), comes in three variants:

  • Argon2d: Data-dependent memory access (fastest)
  • Argon2i: Data-independent memory access (side-channel resistant)
  • Argon2id: Hybrid approach (recommended default)

Core Parameters:

Argon2(P, S, m, t, p, v) where:
P = Password
S = Salt
m = Memory size
t = Number of iterations
p = Degree of parallelism
v = Version number

Implementation Example:

def argon2_hash(password, salt):
    # Parameters for modern systems (2023)
    memory_cost = 65536      # 64 MB
    time_cost = 3           # 3 iterations
    parallelism = 4         # 4 parallel threads
    
    return Argon2id.hash(
        password=password,
        salt=salt,
        memory_cost=memory_cost,
        time_cost=time_cost,
        parallelism=parallelism
    )

Comparative Analysis

Feature bcrypt scrypt Argon2
Year 1999 2009 2015
Memory Usage Fixed (~4KB) Configurable Configurable
Parallel Processing No Yes Yes
Side-Channel Resistance Partial No Yes (Argon2i/id)
GPU Resistance Moderate High Very High
Memory Hardness No Yes Yes
Implementation Complexity Low Medium Medium

Implementation Considerations

Parameter Selection Guidelines

Argon2:

# Modern (2023) recommended parameters
memory_cost = 65536  # 64 MB
time_cost = 3        # iterations
parallelism = 4      # threads

# Example implementation
def hash_password_argon2(password):
    salt = os.urandom(16)
    return argon2.hash_password(
        password.encode(),
        salt=salt,
        time_cost=time_cost,
        memory_cost=memory_cost,
        parallelism=parallelism,
        type=argon2.Type.ID
    )

scrypt:

# Modern (2023) recommended parameters
N = 2**15    # CPU/Memory cost
r = 8        # Block size
p = 1        # Parallelization

# Example implementation
def hash_password_scrypt(password):
    salt = os.urandom(16)
    return scrypt.hash(
        password,
        salt=salt,
        N=N,
        r=r,
        p=p,
        buflen=32
    )

bcrypt:

# Modern (2023) recommended parameters
work_factor = 12  # Targeting ~250ms hash time on modern hardware

# Example implementation
def hash_password_bcrypt(password):
    salt = os.urandom(16)
    return bcrypt.hashpw(password.encode(), salt, rounds=work_factor)

Real-world Usage Scenarios

1. High-Security Systems

Recommended: Argon2id

# High-security parameters
memory_cost = 131072  # 128 MB
time_cost = 4
parallelism = 4

2. Web Applications

Recommended: bcrypt or Argon2id

# Web-friendly parameters
bcrypt_work_factor = 12  # ~250ms
# or
argon2_memory = 32768    # 32 MB
argon2_time = 2

3. Resource-Constrained Systems

Recommended: bcrypt

# Resource-conscious parameters
work_factor = 10  # ~50ms

Performance Benchmarks

Performance measurements on a modern system (Intel i7, 16GB RAM):

bcrypt (work_factor=12):
- Hash time: ~250ms
- Memory usage: ~4KB

scrypt (N=2^15, r=8, p=1):
- Hash time: ~200ms
- Memory usage: ~32MB

Argon2id (m=65536, t=3, p=4):
- Hash time: ~150ms
- Memory usage: ~64MB

Security Considerations

  1. Attack Resistance:
    • bcrypt: Resistant to GPU attacks through Blowfish's heavy key setup
    • scrypt: Resistant to ASIC attacks through memory hardness
    • Argon2: Comprehensive resistance including side-channel attacks

Common Vulnerabilities:

# DON'T: Using static salts
static_salt = b"fixed_salt"  # Vulnerable

# DO: Generate unique salts
unique_salt = os.urandom(16)  # Secure

Making the Right Choice

Decision Framework:

  1. Choose bcrypt if:
    • You need a battle-tested solution
    • Implementation simplicity is crucial
    • Memory usage must be minimal
  2. Choose scrypt if:
    • Memory-hardness is a primary concern
    • You need flexible resource utilization
    • You have control over hardware resources
  3. Choose Argon2 if:
    • You want the most modern algorithm
    • Side-channel resistance is required
    • You need maximum flexibility in parameters

Future Considerations

  1. Quantum Computing Impact:
    • Current password hashing algorithms remain quantum-resistant
    • Focus on increasing parameter costs with hardware improvements
  2. Emerging Standards:
    • NIST recommendations increasingly favor Argon2
    • Industry trend toward memory-hard functions
  3. Hardware Evolution:
    • Regular parameter adjustments needed
    • Monitor hash timing to maintain security margins

Conclusion

For most modern applications, Argon2id represents the best choice due to its flexibility, security features, and ongoing development. However, bcrypt remains a solid, battle-tested alternative, especially when simplicity and limited resources are primary concerns. The key is to choose parameters appropriate for your specific use case and regularly review and update them as hardware capabilities evolve.

Remember: The security of any password hashing implementation depends not just on the algorithm choice, but on proper parameter selection, salt generation, and overall system design.