Skip to content

PBKDF2 (Password-Based Key Derivation Function 2)

Introduction

PBKDF2 (Password-Based Key Derivation Function 2) is a key derivation function that is part of RSA Laboratories' Public-Key Cryptography Standards (PKCS) series, specifically PKCS #5 v2.0. Released in 2000, it remains widely used due to its FIPS certification and extensive validation in the cryptographic community.

How PBKDF2 Works

PBKDF2 uses a technique called key stretching, applying a pseudorandom function (like HMAC-SHA256) repeatedly to derive a key from a password.

Core Components

  1. Password: The initial secret (user password)
  2. Salt: Random data to ensure unique outputs
  3. Iterations: Number of times the hash function is applied
  4. Length: Desired length of the final key
  5. PRF: Underlying pseudorandom function (typically HMAC-SHA1 or HMAC-SHA256)

Mathematical Expression

DK = PBKDF2(PRF, Password, Salt, c, dkLen)

Where:
- DK is the derived key
- PRF is the pseudorandom function
- c is the iteration count
- dkLen is the desired length of the derived key

Implementation Examples

Python Implementation

python
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import os

def hash_password(password: str) -> tuple[bytes, bytes]:
    salt = os.urandom(16)
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=480000,
    )
    hash = kdf.derive(password.encode())
    return hash, salt

def verify_password(password: str, hash: bytes, salt: bytes) -> bool:
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=480000,
    )
    try:
        kdf.verify(password.encode(), hash)
        return True
    except Exception:
        return False

Node.js Implementation

javascript
const crypto = require('crypto');
const util = require('util');

const pbkdf2Promise = util.promisify(crypto.pbkdf2);

async function hashPassword(password) {
    const salt = crypto.randomBytes(16);
    const iterations = 480000;
    const keylen = 32;
    
    const hash = await pbkdf2Promise(
        password,
        salt,
        iterations,
        keylen,
        'sha256'
    );
    
    return {
        hash: hash.toString('hex'),
        salt: salt.toString('hex')
    };
}

Security Considerations

Strengths

  1. NIST Approved

    • Validated under FIPS 140-2
    • Widely accepted in government applications
    • Well-studied cryptographic properties
  2. Configurable Parameters

    • Adjustable iteration count
    • Flexible key length
    • Choice of underlying PRF
  3. Simple Implementation

    • Available in most cryptographic libraries
    • Less prone to implementation errors
    • Clear specification

Limitations

  1. Computational Scaling

    • Linear scaling with iteration count
    • No inherent memory hardness
    • Vulnerable to GPU/ASIC acceleration
  2. Performance Trade-offs

    • High iteration counts needed for security
    • Can impact application responsiveness
    • Resource-intensive for high-volume authentication
  3. Modern Alternatives

    • Newer algorithms offer better memory hardness
    • More resistant to hardware acceleration
    • Better performance characteristics

Best Practices

Parameter Selection

  1. Iteration Count

    python
    # Recommended minimum iterations as of 2024
    iterations = 480000  # Adjust based on hardware capabilities
  2. Salt Length

    python
    # Minimum 16 bytes of salt
    salt_length = 16  # 128 bits
  3. Key Length

    python
    # For general password hashing
    key_length = 32  # 256 bits
  4. PRF Selection

    python
    # Prefer SHA-256 over SHA-1
    algorithm = hashes.SHA256()

Implementation Guidelines

  1. Always Use Cryptographic Random Salt
python
# Good
salt = os.urandom(16)

# Bad - Don't use predictable salts
salt = "1234567890abcdef"
  1. Handle Errors Gracefully
python
try:
    result = verify_password(password, stored_hash, stored_salt)
except Exception as e:
    # Log error securely
    return False
  1. Use Constant-Time Comparison
python
# Most crypto libraries handle this internally
# If implementing manually:
def constant_time_compare(a: bytes, b: bytes) -> bool:
    return hmac.compare_digest(a, b)

Performance Considerations

Benchmarking

Create a benchmark function to determine optimal iterations:

python
def benchmark_pbkdf2():
    password = b"test_password"
    salt = os.urandom(16)
    iterations = 1000
    target_time = 0.2  # 200ms
    
    while True:
        start = time.time()
        PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=iterations,
        ).derive(password)
        duration = time.time() - start
        
        if duration >= target_time:
            break
            
        iterations *= 2
    
    return iterations

Hardware Considerations

  • CPU Speed: Higher iterations on faster CPUs
  • Memory Usage: Relatively low (~1MB)
  • Parallelization: Possible but limited benefit

Migration Strategies

Upgrading from Legacy Systems

  1. Gradual Migration
python
def verify_and_upgrade(password, stored_hash):
    if is_legacy_hash(stored_hash):
        if verify_legacy_password(password, stored_hash):
            new_hash = create_pbkdf2_hash(password)
            update_stored_hash(new_hash)
            return True
    return verify_pbkdf2_password(password, stored_hash)
  1. Parameter Updates
python
def needs_rehash(stored_hash, current_params):
    hash_params = extract_params(stored_hash)
    return (
        hash_params.iterations < current_params.iterations or
        hash_params.algorithm != current_params.algorithm
    )

Additional Security Measures

  1. Pepper Integration
python
def hash_with_pepper(password: str, pepper: bytes) -> tuple[bytes, bytes]:
    peppered_password = password.encode() + pepper
    return hash_password(peppered_password)
  1. Rate Limiting
python
from functools import lru_cache
from time import time

@lru_cache(maxsize=1000)
def get_attempt_count(user_id: str) -> tuple[int, float]:
    return (0, time())

def check_rate_limit(user_id: str, max_attempts: int = 3) -> bool:
    attempts, first_attempt = get_attempt_count(user_id)
    if time() - first_attempt > 3600:  # 1 hour window
        return True
    return attempts < max_attempts

Monitoring and Logging

Key Metrics to Monitor

  1. Hash Timing

    • Average hash generation time
    • Verification time distribution
    • Resource utilization
  2. Error Rates

    • Failed verifications
    • Invalid parameters
    • System errors
  3. Security Events

    • Brute force attempts
    • Parameter tampering
    • Resource exhaustion attacks

Future Considerations

While PBKDF2 remains secure when properly configured, consider:

  1. Migration Planning

    • Evaluate newer algorithms (Argon2, scrypt)
    • Plan for quantum resistance
    • Monitor NIST recommendations
  2. Parameter Evolution

    • Regular iteration count updates
    • PRF upgrades (SHA-3 family)
    • Memory hardness requirements
  3. Compliance Requirements

    • FIPS 140-3 updates
    • Industry-specific standards
    • Regional regulations

Remember: PBKDF2 is still secure but requires careful parameter selection and regular updates to remain effective against modern attack vectors.

Released under the MIT License.