Skip to content

Chapter 4 of 715 min readBy Deepak GuptaUpdated

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 well, 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

In my experience, an enterprise passwordless migration takes well over a year to reach near-universal 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 are 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 250 ms, a single core testing 10 billion guesses needs about 79 years. Attackers parallelize across many GPUs, which is why memory-hard algorithms matter.

The purpose of a password hash is not to resist an unlimited adversary. It is to make cracking cost more than the credentials are worth.

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 None RFC 7914 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 approved Yes No No No

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 requirements, PBKDF2 is often the only approved option. The OWASP Password Storage Cheat Sheet recommends 600,000 iterations with HMAC-SHA-256.

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. OWASP sets a minimum cost factor of 10. I use 12 for new bcrypt hashes, which typically takes a few hundred milliseconds on current server 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, so 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. It uses Argon2i for the first pass (side-channel protection) and Argon2d for subsequent passes (GPU resistance). This is the variant RFC 9106 and OWASP recommend.

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

  • Memory (m): RAM required per hash. RFC 9106's second recommended option is 64 MiB with t=3 and p=4. OWASP's floor for interactive logins is lower, 19 MiB with t=2 and p=1.
  • Time (t): Number of passes over memory.
  • Parallelism (p): Number of lanes (threads).
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

Speed vs. Cost to Attackers

Hashing speed and security exist in tension. The table below compares the settings in this chapter qualitatively. It is not a benchmark: measure hash time on your own hardware before you choose parameters.

Algorithm Configuration Typical hash time (target) Relative cost to a GPU attacker
PBKDF2-SHA256 600,000 iterations A few hundred ms Low: parallelizes well on GPUs
bcrypt Cost factor 12 A few hundred ms Moderate
scrypt N=2^15, r=8, p=1 A few hundred ms High
Argon2id m=64 MiB, t=3, p=4 A few hundred ms High
Argon2id m=256 MiB, t=4, p=4 Close to a second Very high

Hash time is the user-facing latency: how long a legitimate login waits. As a rule of thumb, keep it under about half a second for interactive sign-in. The last column shows why memory-hard algorithms matter. Argon2id at 64 MiB forces attackers to provision 64 MiB of GPU memory per parallel guess, which cuts their throughput sharply.

Warning

Never benchmark password hashing on its speed. Fast hashing is a vulnerability, not a feature. If your password hash completes in a few milliseconds, your parameters are 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 NIST plans to deprecate by 2030 Begin migration planning now
ECDSA P-256 (signatures, including WebAuthn) Broken by Shor's algorithm NIST plans to deprecate by 2030 Begin migration planning now
ECDH (key exchange) Broken by Shor's algorithm NIST plans to deprecate by 2030 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 longer outputs and sensible work factors keep the margin comfortable. 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 three post-quantum standards (FIPS 203, 204 and 205). In March 2025 it selected HQC as a backup key-encapsulation algorithm. Its draft transition plan, NIST IR 8547, proposes deprecating today's quantum-vulnerable public-key algorithms by 2030 and disallowing them by 2035.

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

Post-quantum passkeys are not yet standardized for production use. My expectation is that specifications and platform support will arrive over the next few years. The transition will be gradual: your current passkey deployment will not become insecure overnight, but your architecture should be able 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 and 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.