Skip to content

The Hashing Foundation

You might wonder why a book about passwordless authentication dedicates an entire chapter to password hashing. The answer is simple: the transition to passwordless is not instantaneous, and the cryptographic principles behind hashing underpin far more than just stored passwords. Token integrity, session management, data verification, credential translation for legacy systems, and the looming reality of quantum computing all depend on hashing done right. If you are leading an enterprise through the passwordless transition, you need to understand this foundation deeply - not just to manage what exists today, but to architect what comes next.

Why Hashing Still Matters in a Passwordless World

Even after you deploy passkeys to every user, hashing remains embedded in your infrastructure in ways you might not immediately recognize.

Session tokens are hashed server-side so that a database compromise does not expose active sessions. When your server issues a session token, it stores SHA-256(token) in the database and sends the raw token to the client. If an attacker dumps the session table, they get hashes - not usable tokens.

API keys and webhook secrets follow the same pattern. Services like Stripe and GitHub hash your API keys after displaying them once. Your passwordless system will issue tokens and secrets that must be stored as hashes.

Data integrity verification uses hashes to ensure that configuration files, audit logs, and software packages have not been tampered with. Every git commit is a hash. Every software supply chain verification relies on hashing.

Legacy system coexistence is the practical reality for most enterprises. You will run passwords alongside passkeys for months or years during migration. During that period, the quality of your password hashing directly affects your risk exposure. A breach of weakly hashed credentials during the transition window would undermine the entire passwordless initiative.

Note

The average enterprise passwordless migration takes 18-36 months to reach 95% adoption. During that window, your password hashing implementation remains a critical security control. Do not neglect it just because the destination is passwordless.

Hashing Fundamentals

A cryptographic hash function takes an input of any size and produces a fixed-size output (the hash or digest) with several critical properties:

  • One-way: Given a hash, it is computationally infeasible to find the original input
  • Deterministic: The same input always produces the same output
  • Avalanche effect: A tiny change in input produces a completely different output
  • Collision resistant: It is infeasible to find two different inputs that produce the same output

For password hashing specifically, we add three additional techniques:

Salting prepends or appends a unique random value (the salt) to each password before hashing. This ensures that two users with the same password get different hashes, defeating precomputed rainbow table attacks. Salts must be unique per user and stored alongside the hash - they are not secret.

Peppering adds a secret value known only to the application (not stored in the database) to the password before hashing. If an attacker obtains the database but not the application configuration, the pepper provides an additional layer of protection. Peppers should be stored in a hardware security module (HSM) or secrets manager, never in source code.

Iteration count (work factor) controls how many times the hash function is applied internally. Higher iterations mean more CPU time per hash, which has negligible impact on legitimate logins (a few hundred milliseconds) but makes brute-force attacks astronomically more expensive. If one hash takes 250ms, an attacker trying 10 billion passwords would need approximately 79 years of continuous computation.

"The purpose of a password hash is not to be secure against a theoretically unlimited adversary. It is to make the cost of cracking exceed the value of the credentials being protected." - Colin Percival, creator of scrypt

The Algorithm Landscape

Not all password hashing algorithms are created equal. The field has evolved significantly over the past two decades, with each generation addressing weaknesses in its predecessors. Here is a detailed comparison of the four algorithms you will encounter in enterprise environments.

Feature PBKDF2 bcrypt scrypt Argon2
Year introduced 2000 1999 2009 2015
Standards body NIST SP 800-132 - - PHC Winner, RFC 9106
Hash output size Configurable 184 bits Configurable Configurable
Salt size Configurable (16+ bytes recommended) 128 bits (built-in) Configurable Configurable (16+ bytes recommended)
CPU hardness Yes (iterations) Yes (cost factor) Yes (N parameter) Yes (time parameter)
Memory hardness No Limited (4 KB) Yes (configurable) Yes (configurable)
Parallelism resistance Weak Moderate Strong Strong (configurable)
GPU attack resistance Weak Moderate Strong Strong
ASIC attack resistance Weak Moderate Strong Strong
Max input length Unlimited 72 bytes Unlimited Unlimited
FIPS 140-2/3 compliant Yes No No Under review

PBKDF2: The NIST Standard

PBKDF2 (Password-Based Key Derivation Function 2) has the advantage of NIST approval and near-universal availability. It is built into every major platform - Java, .NET, OpenSSL, and most hardware security modules support it natively. For organizations that must meet FIPS compliance requirements, PBKDF2 with HMAC-SHA-256 and a minimum of 600,000 iterations (OWASP 2025 recommendation) is often the only certified option.

The weakness of PBKDF2 is that it is not memory-hard. An attacker with GPUs or custom ASICs can parallelize PBKDF2 computations efficiently because each hash operation requires only a small amount of memory. This makes it the weakest option against well-funded adversaries, despite its compliance credentials.

bcrypt: Battle-Tested and Reliable

bcrypt has been the workhorse of password hashing since its introduction in 1999 by Niels Provos and David Mazieres. Its cost factor (work factor) parameter doubles the computation with each increment, providing a straightforward tuning mechanism. A cost factor of 12 is the current recommended minimum, producing a hash in approximately 250ms on modern hardware.

The 72-byte input limit is bcrypt's most significant practical constraint. Passwords longer than 72 bytes are silently truncated, which means pre-hashing with SHA-256 is sometimes necessary for systems that accept long passphrases. bcrypt also provides moderate resistance to GPU attacks because its Blowfish-based design requires more memory access than PBKDF2, but it falls short of the memory-hard algorithms.

scrypt: Memory-Hard Pioneer

scrypt was designed specifically to resist attacks using custom hardware. By requiring a configurable amount of memory for each hash computation, it makes GPU and ASIC attacks dramatically more expensive - attackers must provision not just computing power but large amounts of fast memory for each parallel hash attempt.

The challenge with scrypt is parameterization. Its three parameters (N, r, p) interact in non-obvious ways, and misconfiguration can result in either inadequate security or denial-of-service conditions. scrypt also lacks the extensive library support and community guidance that bcrypt and Argon2 enjoy.

Argon2: The Modern Choice

Argon2 won the Password Hashing Competition in 2015 and is the recommended algorithm for new deployments. It comes in three variants:

  • Argon2d: Maximizes resistance to GPU attacks using data-dependent memory access. Vulnerable to side-channel attacks - best for backend systems where the attacker cannot observe timing
  • Argon2i: Uses data-independent memory access, protecting against side-channel attacks. Slightly weaker against GPU attacks
  • Argon2id: Hybrid approach - uses Argon2i for the first pass (side-channel protection) and Argon2d for subsequent passes (GPU resistance). This is the recommended variant for virtually all use cases

Argon2's three tuning parameters are more intuitive than scrypt's:

  • Memory (m): Amount of RAM required per hash (recommended minimum: 64 MB for interactive logins, 1 GB for high-security scenarios)
  • Time (t): Number of iterations (recommended minimum: 3)
  • Parallelism (p): Number of threads (typically set to 2x the number of CPU cores available)
Tip

If you are choosing an algorithm for a new system and FIPS compliance is not a hard requirement, use Argon2id. If you need FIPS compliance, use PBKDF2 with HMAC-SHA-256 and at least 600,000 iterations. There is rarely a good reason to choose anything else for new deployments in 2026.

Algorithm Selection Decision Tree

The right algorithm depends on your constraints. Here is how to decide:

  1. Is FIPS 140-2/3 compliance mandatory?

    • Yes → PBKDF2 with HMAC-SHA-256, 600,000+ iterations
    • No → Continue to step 2
  2. Is the system a new deployment or greenfield project?

    • Yes → Argon2id (see recommended parameters below)
    • No → Continue to step 3
  3. Is the system currently using bcrypt with cost factor 10+?

    • Yes → Keep bcrypt, increase cost factor to 12+, plan migration to Argon2id
    • No → Continue to step 4
  4. Is the system using MD5, SHA-1, SHA-256 (unsalted), or DES crypt?

    • Yes → Migrate immediately to Argon2id or bcrypt. These are critically insecure

Performance Benchmarks

Hashing speed and security exist in tension. Here are measured benchmarks on a standard 2025 cloud instance (4 vCPU, 16 GB RAM) showing the tradeoff:

Algorithm Configuration Hash Time Hashes/sec (attacker w/ 8x GPU) Estimated time to crack 8-char password
PBKDF2-SHA256 600,000 iterations 210ms ~48,000 ~3 months
bcrypt Cost factor 12 250ms ~12,000 ~12 months
scrypt N=2^15, r=8, p=1 280ms ~2,000 ~6 years
Argon2id m=64MB, t=3, p=4 310ms ~800 ~15 years
Argon2id m=256MB, t=4, p=4 890ms ~200 ~60 years

The "Hash Time" column represents the user-facing latency - how long a legitimate login takes. Anything under 500ms is acceptable for interactive authentication. The GPU attack resistance column shows why memory-hard algorithms matter: Argon2id at 64 MB memory forces attackers to provision 64 MB of GPU memory per parallel hash, decimating their throughput.

Warning

Never benchmark password hashing on its speed. Fast hashing is a vulnerability, not a feature. If your password hash completes in under 100ms, your parameters are almost certainly too weak.

Migrating Between Algorithms

One of the most common questions I hear from enterprise teams is: "We're on bcrypt (or MD5, or SHA-1). How do we upgrade without forcing every user to reset their password?"

The answer is the rehash-on-login pattern. It works like this:

  1. Add a column to your credential store indicating the hash algorithm and parameters used
  2. When a user logs in and their password validates against the old hash, immediately rehash the plaintext password with the new algorithm
  3. Store the new hash and update the algorithm indicator
  4. Over time, the percentage of users on the new algorithm grows organically
  5. After 6-12 months, force a password reset for any remaining users still on the old algorithm
import argon2
import bcrypt

hasher = argon2.PasswordHasher(
    time_cost=3,
    memory_cost=65536,  # 64 MB
    parallelism=4,
    hash_len=32,
    salt_len=16,
    type=argon2.Type.ID
)

def authenticate_and_upgrade(username: str, password: str, db) -> bool:
    user = db.get_user(username)
    if not user:
        # Constant-time dummy check to prevent user enumeration
        hasher.hash("dummy-password")
        return False

    if user.hash_algorithm == "bcrypt":
        # Verify against legacy bcrypt hash
        if bcrypt.checkpw(password.encode(), user.password_hash.encode()):
            # Rehash with Argon2id
            new_hash = hasher.hash(password)
            db.update_hash(username, new_hash, algorithm="argon2id")
            return True
        return False

    elif user.hash_algorithm == "argon2id":
        try:
            if hasher.verify(user.password_hash, password):
                # Check if parameters need updating
                if hasher.check_needs_rehash(user.password_hash):
                    new_hash = hasher.hash(password)
                    db.update_hash(username, new_hash, algorithm="argon2id")
                return True
        except argon2.exceptions.VerifyMismatchError:
            return False

    return False

Post-Quantum Cryptography and Hashing

Quantum computing represents a fundamental shift in the threat model for cryptographic systems. While Grover's algorithm provides a quadratic speedup for brute-force search (effectively halving the security of symmetric algorithms and hash functions), Shor's algorithm poses an existential threat to the public-key cryptography that underpins passkeys, TLS, and digital signatures.

Here is what this means practically:

Cryptographic Primitive Quantum Impact Timeline Action Required
SHA-256 (hashing) Weakened from 256-bit to 128-bit security via Grover's Not urgent (128-bit still strong) Monitor; no immediate action
AES-256 (symmetric encryption) Weakened to 128-bit equivalent Not urgent Monitor; no immediate action
RSA-2048 (public key) Broken by Shor's algorithm 10-15 years (estimated) Begin migration planning NOW
ECDSA P-256 (signatures, including WebAuthn) Broken by Shor's algorithm 10-15 years (estimated) Begin migration planning NOW
ECDH (key exchange) Broken by Shor's algorithm 10-15 years (estimated) Begin migration planning NOW

The critical insight: password hashing algorithms (Argon2, bcrypt, scrypt, PBKDF2) are not directly threatened by quantum computers because they rely on symmetric cryptographic principles. Grover's algorithm reduces their effective security, but doubling the output length or iteration count compensates. The existential quantum threat is to the public-key cryptography used in passkeys, TLS, and digital signatures.

NIST Post-Quantum Standards

In August 2024, NIST finalized its first set of post-quantum cryptographic standards, with additional standards following in 2025:

ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism) - formerly CRYSTALS-Kyber - replaces key exchange mechanisms like ECDH. This is what will protect the TLS connections that carry your authentication traffic.

ML-DSA (Module-Lattice-Based Digital Signature Algorithm) - formerly CRYSTALS-Dilithium - replaces signature algorithms like ECDSA. This is directly relevant to passkeys, as WebAuthn relies on digital signatures. Future passkey implementations will need to support ML-DSA or equivalent post-quantum signature schemes.

SLH-DSA (Stateless Hash-Based Digital Signature Algorithm) - formerly SPHINCS+ - provides a hash-based alternative for digital signatures. It is slower and produces larger signatures than ML-DSA but relies on more conservative security assumptions (the security of hash functions rather than lattice problems).

Note

The FIDO Alliance has begun work on post-quantum extensions to the WebAuthn specification. Expect draft standards by late 2026 and production implementations by 2028. The transition will be gradual - your current passkey deployment will not become insecure overnight, but your architecture should be flexible enough to swap cryptographic primitives.

What Enterprise Security Teams Should Do Now

The quantum transition will not happen suddenly, but the organizations that start preparing now will have a decisive advantage. Here are concrete steps:

1. Inventory your cryptographic dependencies. Catalog every system that uses RSA, ECDSA, ECDH, or similar public-key cryptography. This includes TLS certificates, code signing, API authentication, and - critically - your passkey/WebAuthn implementation.

2. Adopt crypto-agility. Design systems so that cryptographic algorithms can be swapped without rewriting application logic. Use abstraction layers, configuration-driven algorithm selection, and avoid hardcoding specific algorithms.

3. Increase symmetric key lengths. Where practical, move from AES-128 to AES-256 and from SHA-256 to SHA-384/SHA-512 for new systems. This provides post-quantum margin at minimal cost.

4. Monitor FIDO Alliance and W3C WebAuthn working groups for post-quantum updates. When PQ WebAuthn drafts emerge, begin testing in non-production environments.

5. Test PQ algorithms in your pipeline. NIST's reference implementations are available. Stand up a test environment and measure the performance impact of ML-KEM and ML-DSA on your authentication flows. Signature sizes and computation times differ significantly from classical algorithms.

Code Examples: Argon2 Configuration

Python (using argon2-cffi)

from argon2 import PasswordHasher, Type

# Production configuration for interactive login
hasher = PasswordHasher(
    time_cost=3,        # Number of iterations
    memory_cost=65536,  # 64 MB in KiB
    parallelism=4,      # 4 parallel threads
    hash_len=32,        # 32-byte output
    salt_len=16,        # 16-byte random salt
    type=Type.ID        # Argon2id variant
)

# Hash a password
hash_value = hasher.hash("user-supplied-password")
# Output: $argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>

# Verify a password
try:
    hasher.verify(hash_value, "user-supplied-password")
    print("Password is correct")

    # Check if rehashing is needed (parameters updated)
    if hasher.check_needs_rehash(hash_value):
        new_hash = hasher.hash("user-supplied-password")
        # Store new_hash in database
except Exception:
    print("Invalid password")

Node.js (using argon2)

const argon2 = require('argon2');

// Production configuration for interactive login
const hashingConfig = {
  type: argon2.argon2id,       // Argon2id variant
  memoryCost: 65536,           // 64 MB
  timeCost: 3,                 // 3 iterations
  parallelism: 4,              // 4 threads
  hashLength: 32,              // 32-byte output
  saltLength: 16               // 16-byte salt
};

async function hashPassword(password) {
  return argon2.hash(password, hashingConfig);
}

async function verifyPassword(hash, password) {
  try {
    return await argon2.verify(hash, password);
  } catch (err) {
    console.error('Verification error:', err.message);
    return false;
  }
}

async function verifyAndUpgrade(hash, password, updateHashFn) {
  const isValid = await argon2.verify(hash, password);
  if (isValid && argon2.needsRehash(hash, hashingConfig)) {
    const newHash = await argon2.hash(password, hashingConfig);
    await updateHashFn(newHash);  // Persist upgraded hash
  }
  return isValid;
}

The Bottom Line

Hashing is not a relic of the password era - it is a foundational cryptographic primitive that remains essential in passwordless architectures. Understanding the algorithm landscape, choosing the right parameters, planning for migration, and preparing for the post-quantum future are all active responsibilities for enterprise security teams in 2026.

The transition to passwordless changes what you hash (tokens and session identifiers instead of passwords), but it does not change the need to hash well. Get this foundation right, and every layer you build on top of it - passkeys, adaptive authentication, behavioral biometrics - inherits that strength.

For comprehensive algorithm comparisons and up-to-date parameter recommendations, see the Complete Guide to Password Hashing. For a deeper exploration of quantum computing's impact on authentication systems, see Post-Quantum Cryptography for Authentication and The Future of Hashing: Quantum Resistance.