π PBKDF2 Password-Based Key Derivation
π Quick Navigation
π Overview
PBKDF2 (Password-Based Key Derivation Function 2) is a key derivation function that applies a pseudorandom function many times to derive keys from passwords. While widely supported and required by many standards, newer alternatives like Argon2 offer better security against modern attack vectors.
β¨ Key Features
Universal Support
Available in virtually all cryptographic libraries
Standards Compliant
NIST SP 800-132, RFC 8018, FIPS approved
Configurable Iterations
Adjustable computational cost via iteration count
Deterministic
Same inputs always produce identical output
Multiple PRF Options
Supports various hash functions (SHA-1, SHA-256, SHA-512)
Legacy Compatibility
Essential for maintaining older systems
β οΈ Limitations
Not Memory-Hard
Vulnerable to GPU and ASIC attacks
Linear Time Cost
Only iteration count affects security
No Parallelism Control
Cannot limit parallel attack efficiency
Outdated for New Apps
Use Argon2 for modern applications
π― Common Use Cases
ποΈ Legacy System Scenarios
- FIPS Compliance: Required by federal standards
- Banking Systems: Legacy financial applications
- Enterprise Migration: Transitioning from older systems
- Standards Requirement: When specifications mandate PBKDF2
π Security Applications
- TLS/SSL: Key derivation in some cipher suites
- VPN Protocols: Legacy VPN implementations
- Database Encryption: Key derivation for data protection
- Certificate Storage: PKCS#12 and similar formats
π§ Algorithm Parameters
π PBKDF2 Parameters
π Recommended Iterations (2024)
HMAC-SHA256
600,000 - 1,000,000 iterations (OWASP 2024)
HMAC-SHA512
210,000 - 350,000 iterations (faster PRF)
Target Time
100ms - 500ms on server hardware
FIPS Minimum
100,000 iterations for compliance
π» Usage Examples
Basic Key Derivation
from metamui_crypto import PBKDF2
import os
# Derive key from password
password = "user_password"
salt = os.urandom(16) # 128-bit salt
# Using HMAC-SHA256 (default)
derived_key = PBKDF2.derive(
password=password.encode(),
salt=salt,
iterations=100000,
key_length=32 # 256-bit key
)
print(f"Derived key: {derived_key.hex()}")
# Using HMAC-SHA512
derived_key_512 = PBKDF2.derive(
password=password.encode(),
salt=salt,
iterations=100000,
key_length=64, # 512-bit key
prf='hmac-sha512'
)
Password Storage
from metamui_crypto import PBKDF2
import os
import json
import base64
import hmac
class PBKDF2PasswordStorage:
def __init__(self):
# OWASP 2023 recommendations
self.iterations = 600000 # SHA-256
self.salt_length = 16
self.key_length = 32
self.prf = 'hmac-sha256'
def hash_password(self, password):
"""Hash password for storage"""
salt = os.urandom(self.salt_length)
derived_key = PBKDF2.derive(
password=password.encode(),
salt=salt,
iterations=self.iterations,
key_length=self.key_length,
prf=self.prf
)
# Store all parameters for future verification
return {
'algorithm': 'pbkdf2',
'prf': self.prf,
'iterations': self.iterations,
'salt': base64.b64encode(salt).decode(),
'hash': base64.b64encode(derived_key).decode()
}
def verify_password(self, password, stored_data):
"""Verify password against stored hash"""
salt = base64.b64decode(stored_data['salt'])
expected_hash = base64.b64decode(stored_data['hash'])
# Derive key with same parameters
computed_hash = PBKDF2.derive(
password=password.encode(),
salt=salt,
iterations=stored_data['iterations'],
key_length=len(expected_hash),
prf=stored_data.get('prf', 'hmac-sha256')
)
# Constant-time comparison
return hmac.compare_digest(computed_hash, expected_hash)
def needs_rehash(self, stored_data):
"""Check if parameters need updating"""
return stored_data.get('iterations', 0) < self.iterations
Encryption Key Derivation
from metamui_crypto import PBKDF2, ChaCha20Poly1305, HMAC
import struct
def derive_encryption_keys(password, salt, info=b""):
"""Derive multiple keys from password"""
# Derive 96 bytes of key material
key_material = PBKDF2.derive(
password=password.encode(),
salt=salt,
iterations=100000,
key_length=96, # 768 bits
prf='hmac-sha512'
)
# Split into different keys
encryption_key = key_material[0:32] # 256-bit
mac_key = key_material[32:64] # 256-bit
iv_key = key_material[64:96] # 256-bit
return encryption_key, mac_key, iv_key
def encrypt_with_password(plaintext, password):
"""Password-based encryption"""
# Generate salt
salt = os.urandom(16)
# Derive keys
enc_key, mac_key, iv_key = derive_encryption_keys(password, salt)
# Generate IV from iv_key (deterministic but unique per salt)
iv_hasher = HMAC(iv_key, 'sha256')
iv_hasher.update(b"initialization vector")
iv = iv_hasher.finalize()[:12] # 96-bit nonce for ChaCha20
# Encrypt
cipher = ChaCha20Poly1305(enc_key)
ciphertext, tag = cipher.encrypt(plaintext, iv)
# MAC over everything
mac_hasher = HMAC(mac_key, 'sha256')
mac_hasher.update(salt)
mac_hasher.update(iv)
mac_hasher.update(ciphertext)
mac_hasher.update(tag)
mac = mac_hasher.finalize()
# Return encrypted package
return {
'salt': salt,
'ciphertext': ciphertext,
'tag': tag,
'mac': mac
}
Different PRF Options
from metamui_crypto import PBKDF2
password = b"test_password"
salt = os.urandom(16)
iterations = 100000
# Different hash functions
prfs = ['hmac-sha1', 'hmac-sha256', 'hmac-sha512']
for prf in prfs:
key = PBKDF2.derive(
password=password,
salt=salt,
iterations=iterations,
key_length=32,
prf=prf
)
print(f"{prf}: {key.hex()[:32]}...")
# Custom PRF (if supported)
def custom_prf(key, data):
"""Custom PRF implementation"""
from metamui_crypto import Blake2b
return Blake2b.new(key=key).update(data).finalize()
# Some implementations support custom PRFs
key_custom = PBKDF2.derive(
password=password,
salt=salt,
iterations=iterations,
key_length=32,
prf=custom_prf # If supported
)
Iteration Count Selection
import time
def benchmark_pbkdf2(target_time=0.1):
"""Find iteration count for target time"""
password = b"benchmark_password"
salt = os.urandom(16)
# Start with low iteration count
iterations = 1000
while True:
start = time.time()
PBKDF2.derive(
password=password,
salt=salt,
iterations=iterations,
key_length=32
)
duration = time.time() - start
if duration >= target_time:
break
# Estimate needed iterations
iterations = int(iterations * (target_time / duration) * 1.1)
return iterations
# Find iterations for 100ms delay
recommended_iterations = benchmark_pbkdf2(target_time=0.1)
print(f"Recommended iterations for 100ms: {recommended_iterations}")
# OWASP recommendations by year
owasp_iterations = {
2023: {'sha256': 600000, 'sha512': 210000},
2024: {'sha256': 800000, 'sha512': 280000},
2025: {'sha256': 1000000, 'sha512': 350000}
}
Implementation Details
PBKDF2 Algorithm
def pbkdf2_simple(password, salt, iterations, dk_len, prf=hmac_sha256):
"""Simplified PBKDF2 implementation"""
# Calculate number of blocks needed
hlen = prf.digest_size
blocks_needed = (dk_len + hlen - 1) // hlen
derived_key = b''
for block_num in range(1, blocks_needed + 1):
# F(Password, Salt, c, i)
block = _pbkdf2_f(password, salt, iterations, block_num, prf)
derived_key += block
return derived_key[:dk_len]
def _pbkdf2_f(password, salt, iterations, block_num, prf):
"""The F function in PBKDF2"""
# U_1 = PRF(Password, Salt || INT_32_BE(i))
u = prf(password, salt + struct.pack('>I', block_num))
result = u
# U_2 = PRF(Password, U_1)
# ...
# U_c = PRF(Password, U_{c-1})
for _ in range(iterations - 1):
u = prf(password, u)
# F(Password, Salt, c, i) = U_1 ^ U_2 ^ ... ^ U_c
result = bytes(a ^ b for a, b in zip(result, u))
return result
Security Analysis
The security of PBKDF2 depends on:
- Iteration Count: Higher is more secure but slower
- Salt Length: Prevents rainbow table attacks
- PRF Security: Usually HMAC-SHA256 or HMAC-SHA512
- Key Length: Output length doesnβt affect security much
Performance Characteristics
def analyze_pbkdf2_performance():
"""Analyze PBKDF2 performance characteristics"""
password = b"test"
salt = os.urandom(16)
results = []
# Test different iteration counts
for iterations in [1000, 10000, 100000, 1000000]:
start = time.time()
PBKDF2.derive(
password=password,
salt=salt,
iterations=iterations,
key_length=32
)
duration = time.time() - start
results.append({
'iterations': iterations,
'time': duration,
'iterations_per_second': iterations / duration
})
# Performance is linear with iteration count
for r in results:
print(f"{r['iterations']:>8} iterations: {r['time']:>6.3f}s "
f"({r['iterations_per_second']:>8.0f} iter/s)")
π Security Considerations
β οΈ Major Security Limitations
- Not Memory-Hard: Vulnerable to GPU and ASIC attacks
- No Parallelism Defense: Cannot limit parallel attack efficiency
- Linear Cost Model: Only iteration count provides security
- Regular Updates Required: Iteration counts must increase over time
- Superseded Technology: Use Argon2 for new applications
Attack Vectors
- GPU/ASIC Attacks: PBKDF2 is not memory-hard
# GPU can compute many PBKDF2 instances in parallel # Each instance only needs ~100 bytes of memory # Modern GPUs can test millions of passwords per second - Iteration Count Must Increase: Mooreβs Law requires regular updates
# Iteration counts become insufficient over time # 2010: 10,000 iterations # 2015: 50,000 iterations # 2020: 100,000 iterations # 2025: 1,000,000 iterations - Side-Channel Attacks: Timing can leak information
# Use constant-time comparison import hmac def safe_compare(a, b): return hmac.compare_digest(a, b)
Best Practices for PBKDF2
class SecurePBKDF2:
"""Secure PBKDF2 usage patterns"""
@staticmethod
def derive_with_context(password, context, salt=None):
"""Derive key with application context"""
if salt is None:
salt = os.urandom(32) # 256-bit salt
# Include context in derivation
context_data = f"PBKDF2-v1:{context}:".encode()
return PBKDF2.derive(
password=password,
salt=context_data + salt,
iterations=600000, # 2023 recommendation
key_length=32,
prf='hmac-sha256'
)
@staticmethod
def derive_multiple_keys(password, salt, purposes):
"""Derive multiple keys with domain separation"""
keys = {}
for i, purpose in enumerate(purposes):
# Domain separation using counter
domain_salt = salt + struct.pack('>I', i)
keys[purpose] = PBKDF2.derive(
password=password,
salt=domain_salt,
iterations=600000,
key_length=32
)
return keys
Common Use Cases
1. Legacy System Compatibility
class LegacyPasswordMigration:
"""Migrate from old PBKDF2 to Argon2"""
def __init__(self):
self.old_iterations = 10000 # Legacy system
self.new_iterations = 600000 # Current standard
def verify_and_upgrade(self, password, stored_hash):
"""Verify old hash and upgrade if needed"""
# Verify with old parameters
is_valid = self._verify_pbkdf2(
password,
stored_hash['salt'],
stored_hash['hash'],
stored_hash.get('iterations', self.old_iterations)
)
if is_valid:
# Check if upgrade needed
if stored_hash.get('iterations', 0) < self.new_iterations:
# Upgrade to new iteration count
new_hash = PBKDF2.derive(
password=password.encode(),
salt=base64.b64decode(stored_hash['salt']),
iterations=self.new_iterations,
key_length=32
)
return {
'valid': True,
'needs_update': True,
'new_hash': base64.b64encode(new_hash).decode(),
'iterations': self.new_iterations
}
# Consider migration to Argon2
if stored_hash.get('algorithm') == 'pbkdf2':
return {
'valid': True,
'suggest_argon2': True
}
return {'valid': is_valid}
2. FIPS Compliance
class FIPSCompliantDerivation:
"""FIPS 140-2 compliant key derivation"""
def __init__(self):
# FIPS approved parameters
self.min_password_length = 8
self.min_salt_length = 16
self.min_iterations = 100000
self.approved_prfs = ['hmac-sha256', 'hmac-sha512']
def derive_key(self, password, salt, key_length=32):
"""Derive key with FIPS compliance checks"""
# Validate inputs
if len(password) < self.min_password_length:
raise ValueError(f"Password must be at least {self.min_password_length} characters")
if len(salt) < self.min_salt_length:
raise ValueError(f"Salt must be at least {self.min_salt_length} bytes")
# Use FIPS-approved parameters
return PBKDF2.derive(
password=password.encode(),
salt=salt,
iterations=max(self.min_iterations, 600000),
key_length=key_length,
prf='hmac-sha256' # FIPS approved
)
3. Key Stretching for Weak Passwords
def stretch_weak_password(weak_password, extra_entropy=None):
"""Strengthen weak passwords with key stretching"""
# Combine with additional entropy if available
if extra_entropy:
password_data = weak_password.encode() + extra_entropy
else:
password_data = weak_password.encode()
# Use high iteration count for weak passwords
iterations = 1000000 # 1 million iterations
# Use longer salt
salt = os.urandom(32) # 256-bit salt
# Derive longer key
stretched_key = PBKDF2.derive(
password=password_data,
salt=salt,
iterations=iterations,
key_length=64, # 512 bits
prf='hmac-sha512'
)
# Return first 32 bytes for use
return stretched_key[:32], salt
4. Deterministic Key Generation
class DeterministicKeyGenerator:
"""Generate deterministic keys from passwords"""
def __init__(self, master_password, application_id):
self.master_password = master_password
self.app_salt = application_id.encode()
def generate_key(self, purpose, index=0):
"""Generate deterministic key for purpose"""
# Create deterministic salt
salt = self.app_salt + purpose.encode() + struct.pack('>I', index)
# Fixed parameters for determinism
return PBKDF2.derive(
password=self.master_password.encode(),
salt=salt,
iterations=100000,
key_length=32,
prf='hmac-sha256'
)
def generate_keypair_seed(self, account_index):
"""Generate deterministic seed for keypair"""
seed = self.generate_key(f"keypair-seed", account_index)
return seed
# Usage
keygen = DeterministicKeyGenerator("master_password", "MyApp-v1")
encryption_key = keygen.generate_key("encryption", 0)
signing_seed = keygen.generate_keypair_seed(0)
π Migration to Modern Alternatives
To Argon2
def migrate_pbkdf2_to_argon2(password, pbkdf2_hash_data):
"""Migrate from PBKDF2 to Argon2"""
# First verify PBKDF2 hash
salt = base64.b64decode(pbkdf2_hash_data['salt'])
expected = base64.b64decode(pbkdf2_hash_data['hash'])
computed = PBKDF2.derive(
password=password.encode(),
salt=salt,
iterations=pbkdf2_hash_data['iterations'],
key_length=len(expected)
)
if not hmac.compare_digest(computed, expected):
return None # Wrong password
# Create new Argon2 hash
from metamui_crypto import Argon2
new_salt = os.urandom(16)
argon2_hash = Argon2.hash(
password=password.encode(),
salt=new_salt,
time_cost=3,
memory_cost=65536,
parallelism=4,
hash_length=32
)
return {
'algorithm': 'argon2id',
'hash': base64.b64encode(argon2_hash).decode(),
'salt': base64.b64encode(new_salt).decode(),
'time_cost': 3,
'memory_cost': 65536,
'parallelism': 4,
'migrated_from': 'pbkdf2'
}
Gradual Migration Strategy
class PasswordMigrationManager:
"""Manage gradual migration from PBKDF2"""
def __init__(self):
self.supported_algorithms = ['pbkdf2', 'argon2id']
def verify_password(self, password, stored_data):
"""Verify password with any supported algorithm"""
algorithm = stored_data.get('algorithm', 'pbkdf2')
if algorithm == 'pbkdf2':
return self._verify_pbkdf2(password, stored_data)
elif algorithm == 'argon2id':
return self._verify_argon2(password, stored_data)
else:
raise ValueError(f"Unsupported algorithm: {algorithm}")
def hash_password(self, password, force_algorithm=None):
"""Hash password with best available algorithm"""
if force_algorithm == 'pbkdf2':
# For compatibility
return self._hash_pbkdf2(password)
else:
# Use Argon2 for new passwords
return self._hash_argon2(password)
β Best Practices
- Use High Iteration Counts: Follow OWASP 2024 recommendations (600K+ for SHA-256)
- Generate Random Salts: Use cryptographically secure random for each password
- Store All Parameters: Include algorithm, PRF, iterations, and salt with hash
- Plan for Migration: Design systems to upgrade to Argon2 over time
- Regular Reviews: Update iteration counts annually based on hardware advances
Secure Implementation Pattern
class SecurePBKDF2Implementation:
"""Production-ready PBKDF2 implementation"""
CURRENT_ITERATIONS = {
'hmac-sha256': 800000, # 2024 recommendation
'hmac-sha512': 300000 # Faster PRF, fewer iterations needed
}
def hash_password(self, password, salt=None, prf='hmac-sha256'):
if salt is None:
salt = os.urandom(32) # 256-bit salt
iterations = self.CURRENT_ITERATIONS[prf]
hash_value = PBKDF2.derive(
password=password.encode(),
salt=salt,
iterations=iterations,
key_length=32,
prf=prf
)
return {
'algorithm': 'pbkdf2',
'prf': prf,
'iterations': iterations,
'salt': base64.b64encode(salt).decode(),
'hash': base64.b64encode(hash_value).decode(),
'version': '2024.1'
}
β οΈ Common Pitfalls
1. Low Iteration Count
# BAD: Too few iterations
key = PBKDF2.derive(password, salt, iterations=1000) # Insecure!
# GOOD: Current recommendations
key = PBKDF2.derive(password, salt, iterations=600000) # 2023 standard
2. Small or Fixed Salt
# BAD: Fixed salt
SALT = b"myapp" # Never do this!
# BAD: Small salt
salt = os.urandom(8) # Too small
# GOOD: Random 128-bit or larger salt
salt = os.urandom(16) # 128 bits minimum
3. Not Storing Parameters
# BAD: Only storing the hash
stored = hash_value.hex()
# GOOD: Store all parameters
stored = {
'algorithm': 'pbkdf2',
'prf': 'hmac-sha256',
'iterations': 600000,
'salt': base64.b64encode(salt).decode(),
'hash': base64.b64encode(hash_value).decode()
}
Test Vectors
# RFC 6070 test vectors
test_vectors = [
{
'password': b'password',
'salt': b'salt',
'iterations': 1,
'key_length': 20,
'prf': 'hmac-sha1',
'expected': '0c60c80f961f0e71f3a9b524af6012062fe037a6'
},
{
'password': b'password',
'salt': b'salt',
'iterations': 4096,
'key_length': 20,
'prf': 'hmac-sha1',
'expected': '4b007901b765489abead49d926f721d065a429c1'
},
{
'password': b'passwordPASSWORDpassword',
'salt': b'saltSALTsaltSALTsaltSALTsaltSALTsalt',
'iterations': 4096,
'key_length': 25,
'prf': 'hmac-sha256',
'expected': '348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c'
}
]
# Verify implementation
for vector in test_vectors:
result = PBKDF2.derive(
password=vector['password'],
salt=vector['salt'],
iterations=vector['iterations'],
key_length=vector['key_length'],
prf=vector['prf']
)
assert result.hex() == vector['expected'], f"Test failed for {vector['iterations']} iterations"
print(f"β PBKDF2 test passed: {vector['iterations']} iterations")
Resources
- RFC 8018 - PKCS #5 v2.1 (PBKDF2)
- NIST SP 800-132 - Password-Based Key Derivation
- OWASP Password Storage - Current recommendations
- Security Analysis - Security properties
- Migration Guide - Why PBKDF2 is outdated