PBKDF2 Security-Focused API Documentation
Version: 1.0
Last Updated: 2025-07-05
**Security Classification: PUBLIC
Author: Phantom (phantom@metamui.id)
Overview
PBKDF2 (Password-Based Key Derivation Function 2) is a key derivation function designed to make brute-force attacks on passwords computationally expensive. It applies a pseudorandom function (typically HMAC) repeatedly to derive cryptographic keys from passwords. While still secure, Argon2 is preferred for new applications.
Security Level: Depends on iterations and hash
Recommended Hash: HMAC-SHA256 or HMAC-SHA512
Minimum Iterations: 100,000 (2023 standard)
Salt Size: ≥16 bytes (recommended 32)
Security Warnings ⚠️
- Prefer Argon2: PBKDF2 lacks memory hardness - use Argon2 for new systems
- Iteration Count: Must be high enough to slow attackers (100,000+ in 2023)
- Salt Required: NEVER derive keys without unique salts
- Not Memory Hard: Vulnerable to GPU/ASIC acceleration
- Fixed Time: Cannot adapt to faster hardware without changing iterations
API Functions
derive_key(password: str, salt: bytes, iterations: int, key_length: int, hash_algo: str = "sha256") -> bytes
Security Contract:
- Preconditions:
passwordis user input (any strength)saltmust be unique per password (≥16 bytes)iterations≥ 100,000 (2023 minimum)key_lengthtypically 32-64 byteshash_algomust be cryptographically secure
- Postconditions:
- Returns derived key of requested length
- Same inputs always produce same output
- Computational cost proportional to iterations
Attack Resistance: | Attack Type | Protected | Notes | |————-|———–|——-| | Brute Force | ⚠️ | Limited by iteration count | | Dictionary | ⚠️ | Slowed by iterations | | Rainbow Tables | ✅ | Salt prevents | | GPU/ASIC | ❌ | No memory hardness | | Side Channel | ⚠️ | Timing may leak password length | | Weak Passwords | ❌ | Cannot fix weak passwords |
Security Requirements:
- Use high iteration counts (adjust yearly)
- Always use unique salts per password
- Consider Argon2 for better security
- Store version info for upgrades
Secure Usage Example:
import hashlib
import hmac
import secrets
import struct
import json
from typing import Tuple, Optional
import time
class SecurePBKDF2:
"""Secure PBKDF2 implementation with best practices"""
# NIST SP 800-132 recommendations
MIN_ITERATIONS_2023 = 100_000
MIN_SALT_LENGTH = 16
RECOMMENDED_SALT_LENGTH = 32
@classmethod
def derive_password_key(
cls,
password: str,
salt: Optional[bytes] = None,
iterations: Optional[int] = None,
key_length: int = 32
) -> Tuple[bytes, dict]:
"""Derive key from password with secure defaults"""
# Generate salt if not provided
if salt is None:
salt = secrets.token_bytes(cls.RECOMMENDED_SALT_LENGTH)
elif len(salt) < cls.MIN_SALT_LENGTH:
raise ValueError(f"Salt must be at least {cls.MIN_SALT_LENGTH} bytes")
# Use secure iteration count
if iterations is None:
# Auto-calculate based on target time (100ms)
iterations = cls._calibrate_iterations()
elif iterations < cls.MIN_ITERATIONS_2023:
raise ValueError(f"Iterations must be at least {cls.MIN_ITERATIONS_2023}")
# Derive key using PBKDF2-HMAC-SHA256
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
iterations,
dklen=key_length
)
# Return key with metadata
metadata = {
'algorithm': 'PBKDF2-HMAC-SHA256',
'iterations': iterations,
'salt': salt.hex(),
'key_length': key_length,
'version': '1.0'
}
return key, metadata
@classmethod
def _calibrate_iterations(cls, target_time: float = 0.1) -> int:
"""Auto-calibrate iterations for target time"""
test_password = "calibration_test"
test_salt = b"calibration_salt"
iterations = cls.MIN_ITERATIONS_2023
# Measure time for minimum iterations
start = time.perf_counter()
hashlib.pbkdf2_hmac('sha256', test_password.encode(), test_salt, iterations)
elapsed = time.perf_counter() - start
# Scale to target time
if elapsed < target_time:
iterations = int(iterations * (target_time / elapsed))
# Ensure minimum is met
return max(iterations, cls.MIN_ITERATIONS_2023)
@classmethod
def verify_password(
cls,
password: str,
stored_hash: bytes,
metadata: dict
) -> bool:
"""Verify password against stored hash"""
# Extract parameters
salt = bytes.fromhex(metadata['salt'])
iterations = metadata['iterations']
key_length = metadata['key_length']
# Re-derive key
derived_key, _ = cls.derive_password_key(
password,
salt=salt,
iterations=iterations,
key_length=key_length
)
# Constant-time comparison
return hmac.compare_digest(derived_key, stored_hash)
@classmethod
def upgrade_if_needed(
cls,
password: str,
stored_hash: bytes,
metadata: dict
) -> Optional[Tuple[bytes, dict]]:
"""Upgrade parameters if outdated"""
current_iterations = metadata.get('iterations', 0)
# Check if upgrade needed
if current_iterations < cls.MIN_ITERATIONS_2023:
# Verify old password first
if cls.verify_password(password, stored_hash, metadata):
# Derive with new parameters
return cls.derive_password_key(password)
return None
# SECURE: Database storage format
class PasswordStorage:
def __init__(self):
self.pbkdf2 = SecurePBKDF2()
def hash_password(self, password: str) -> str:
"""Hash password for storage"""
key, metadata = self.pbkdf2.derive_password_key(password)
# Create storage format
storage_data = {
'hash': key.hex(),
'algorithm': metadata['algorithm'],
'parameters': {
'iterations': metadata['iterations'],
'salt': metadata['salt'],
'key_length': metadata['key_length']
},
'version': metadata['version'],
'created_at': time.time()
}
# Encode as single string
return json.dumps(storage_data)
def verify_password(self, password: str, stored: str) -> bool:
"""Verify password from storage"""
storage_data = json.loads(stored)
# Check algorithm
if storage_data['algorithm'] != 'PBKDF2-HMAC-SHA256':
raise ValueError(f"Unsupported algorithm: {storage_data['algorithm']}")
# Verify
stored_hash = bytes.fromhex(storage_data['hash'])
return self.pbkdf2.verify_password(
password,
stored_hash,
storage_data['parameters']
)
# SECURE: Key derivation for encryption
def derive_encryption_key(
password: str,
context: bytes,
key_size: int = 32
) -> Tuple[bytes, bytes, dict]:
"""Derive encryption key with authentication"""
# Generate salt
salt = secrets.token_bytes(32)
# High iteration count for encryption keys
iterations = 200_000
# Derive main key material (key + auth key)
key_material = hashlib.pbkdf2_hmac(
'sha512', # Use SHA-512 for more output
password.encode('utf-8'),
salt + context,
iterations,
dklen=key_size + 32 # Extra 32 bytes for auth
)
# Split into encryption and authentication keys
enc_key = key_material[:key_size]
auth_key = key_material[key_size:]
# Metadata for reconstruction
metadata = {
'salt': salt.hex(),
'context': context.hex(),
'iterations': iterations,
'key_size': key_size,
'hash_algo': 'sha512'
}
return enc_key, auth_key, metadata
Common Mistakes:
# INSECURE: No salt
key = pbkdf2(password, b"", 10000) # Rainbow tables!
# INSECURE: Low iterations
key = pbkdf2(password, salt, 1000) # Too fast!
# INSECURE: Static salt
GLOBAL_SALT = b"myapp_salt" # Same for all users!
key = pbkdf2(password, GLOBAL_SALT, 100000)
# INSECURE: Short salt
salt = os.urandom(8) # Too short!
# INSECURE: Not upgrading iterations
# Using 10,000 iterations from 2010 in 2023
pbkdf2_hmac(hash_name: str, password: bytes, salt: bytes, iterations: int, dklen: Optional[int] = None) -> bytes
Security Contract:
- Preconditions:
hash_namemust be secure hash functionpasswordcan be any byte sequencesaltshould be uniqueiterationsdetermines work factor
- Postconditions:
- Returns key of length
dklenor hash output size - Deterministic output
- Time proportional to iterations
- Returns key of length
Security Notes:
- This is the low-level function
- Prefer higher-level wrappers
- Ensure proper encoding of passwords
- Monitor iteration performance
Secure Usage Example:
use pbkdf2::{pbkdf2_hmac, pbkdf2_hmac_array};
use sha2::Sha256;
use rand::{RngCore, thread_rng};
use zeroize::Zeroize;
/// Secure PBKDF2 implementation
pub struct SecurePasswordDerivation {
min_iterations: u32,
target_ms: u32,
}
impl SecurePasswordDerivation {
pub fn new() -> Self {
Self {
min_iterations: 100_000, // 2023 minimum
target_ms: 100, // Target 100ms
}
}
pub fn derive_key(&self, password: &str) -> Result<DerivedKey, Error> {
// Generate secure salt
let mut salt = [0u8; 32];
thread_rng().fill_bytes(&mut salt);
// Calculate iterations for target time
let iterations = self.calibrate_iterations()?;
// Derive key
let mut key = [0u8; 32];
pbkdf2_hmac::<Sha256>(
password.as_bytes(),
&salt,
iterations,
&mut key
);
Ok(DerivedKey {
key,
salt,
iterations,
algorithm: "PBKDF2-HMAC-SHA256",
})
}
fn calibrate_iterations(&self) -> Result<u32, Error> {
use std::time::Instant;
let test_password = b"calibration";
let test_salt = b"calibration_salt";
let mut test_output = [0u8; 32];
// Time minimum iterations
let start = Instant::now();
pbkdf2_hmac::<Sha256>(
test_password,
test_salt,
self.min_iterations,
&mut test_output
);
let elapsed_ms = start.elapsed().as_millis() as u32;
// Scale to target time
let iterations = if elapsed_ms < self.target_ms {
(self.min_iterations as u64 * self.target_ms as u64 / elapsed_ms as u64) as u32
} else {
self.min_iterations
};
Ok(iterations.max(self.min_iterations))
}
}
/// Secure key with automatic cleanup
pub struct DerivedKey {
key: [u8; 32],
salt: [u8; 32],
iterations: u32,
algorithm: &'static str,
}
impl DerivedKey {
pub fn verify(&self, password: &str) -> bool {
let mut check_key = [0u8; 32];
pbkdf2_hmac::<Sha256>(
password.as_bytes(),
&self.salt,
self.iterations,
&mut check_key
);
let result = constant_time_eq(&self.key, &check_key);
check_key.zeroize();
result
}
pub fn export_metadata(&self) -> PasswordMetadata {
PasswordMetadata {
salt: self.salt.to_vec(),
iterations: self.iterations,
algorithm: self.algorithm.to_string(),
version: 1,
}
}
}
impl Drop for DerivedKey {
fn drop(&mut self) {
self.key.zeroize();
}
}
/// Multi-factor key derivation
pub fn derive_multi_factor_key(
password: &str,
pin: &str,
biometric_hash: &[u8; 32],
) -> Result<[u8; 32], Error> {
// Derive key from each factor
let mut password_key = [0u8; 32];
pbkdf2_hmac::<Sha256>(
password.as_bytes(),
b"password_factor",
150_000,
&mut password_key
);
let mut pin_key = [0u8; 32];
pbkdf2_hmac::<Sha256>(
pin.as_bytes(),
b"pin_factor",
50_000, // Lower for shorter input
&mut pin_key
);
// Combine factors
let mut combined_key = [0u8; 32];
for i in 0..32 {
combined_key[i] = password_key[i] ^ pin_key[i] ^ biometric_hash[i];
}
// Final key derivation
let mut final_key = [0u8; 32];
pbkdf2_hmac::<Sha256>(
&combined_key,
b"combined_factors",
10_000,
&mut final_key
);
// Clean up intermediate keys
password_key.zeroize();
pin_key.zeroize();
combined_key.zeroize();
Ok(final_key)
}
Security Best Practices
Parameter Selection
class PBKDF2Parameters:
"""Secure parameter selection for PBKDF2"""
@staticmethod
def get_recommended_iterations(year: int) -> int:
"""Get recommended iterations by year"""
# Based on Moore's law and security analysis
base_year = 2000
base_iterations = 1000
# Double every 2 years
years_passed = year - base_year
doublings = years_passed / 2
iterations = int(base_iterations * (2 ** doublings))
# Apply minimums
minimums = {
2020: 50_000,
2023: 100_000,
2025: 200_000,
}
for min_year, min_iter in sorted(minimums.items()):
if year >= min_year:
iterations = max(iterations, min_iter)
return iterations
@staticmethod
def estimate_crack_time(
iterations: int,
password_entropy_bits: int,
attacker_hash_rate: int = 10**12 # 1 TH/s
) -> float:
"""Estimate time to crack password"""
# Total operations needed
total_ops = (2 ** password_entropy_bits) * iterations
# Time in seconds
crack_time_seconds = total_ops / attacker_hash_rate
return crack_time_seconds
# Example usage
params = PBKDF2Parameters()
# For 2023
iterations_2023 = params.get_recommended_iterations(2023)
print(f"2023 iterations: {iterations_2023:,}")
# Estimate security
# 40-bit password (weak) with 100k iterations
crack_time = params.estimate_crack_time(100_000, 40)
print(f"40-bit password: {crack_time/3600:.1f} hours")
# 60-bit password (moderate) with 100k iterations
crack_time = params.estimate_crack_time(100_000, 60)
print(f"60-bit password: {crack_time/(365*24*3600):.1f} years")
Migration Strategy
/// Migrate from old to new password hashing
pub struct PasswordMigration {
old_iterations: u32,
new_iterations: u32,
}
impl PasswordMigration {
pub fn new(old: u32, new: u32) -> Self {
Self {
old_iterations: old,
new_iterations: new,
}
}
pub fn migrate_on_login(
&self,
password: &str,
stored_data: &StoredPassword,
) -> Result<Option<StoredPassword>, Error> {
// Verify with old parameters
let mut old_key = vec![0u8; stored_data.key_length];
pbkdf2_hmac::<Sha256>(
password.as_bytes(),
&stored_data.salt,
stored_data.iterations,
&mut old_key
);
if !constant_time_eq(&old_key, &stored_data.hash) {
old_key.zeroize();
return Ok(None); // Wrong password
}
old_key.zeroize();
// Check if migration needed
if stored_data.iterations >= self.new_iterations {
return Ok(None); // Already current
}
// Derive with new parameters
let mut new_salt = [0u8; 32];
thread_rng().fill_bytes(&mut new_salt);
let mut new_key = vec![0u8; stored_data.key_length];
pbkdf2_hmac::<Sha256>(
password.as_bytes(),
&new_salt,
self.new_iterations,
&mut new_key
);
Ok(Some(StoredPassword {
hash: new_key,
salt: new_salt.to_vec(),
iterations: self.new_iterations,
key_length: stored_data.key_length,
algorithm: "PBKDF2-HMAC-SHA256".to_string(),
version: stored_data.version + 1,
}))
}
}
Secure Storage Format
class PBKDF2Storage:
"""Secure storage format for PBKDF2 hashes"""
@staticmethod
def encode_for_storage(
derived_key: bytes,
salt: bytes,
iterations: int,
hash_algo: str = "sha256"
) -> str:
"""Encode PBKDF2 data for storage"""
# Use PHC string format
# $pbkdf2-sha256$iterations$salt$hash
import base64
salt_b64 = base64.b64encode(salt).decode('ascii').rstrip('=')
hash_b64 = base64.b64encode(derived_key).decode('ascii').rstrip('=')
return f"$pbkdf2-{hash_algo}${iterations}${salt_b64}${hash_b64}"
@staticmethod
def decode_from_storage(encoded: str) -> dict:
"""Decode PBKDF2 data from storage"""
import base64
parts = encoded.split('$')
if len(parts) != 5 or not parts[1].startswith('pbkdf2-'):
raise ValueError("Invalid PBKDF2 format")
algorithm = parts[1]
hash_algo = algorithm.split('-')[1]
iterations = int(parts[2])
# Restore base64 padding
salt_b64 = parts[3] + '=' * (4 - len(parts[3]) % 4)
hash_b64 = parts[4] + '=' * (4 - len(parts[4]) % 4)
salt = base64.b64decode(salt_b64)
derived_key = base64.b64decode(hash_b64)
return {
'algorithm': algorithm,
'hash_algo': hash_algo,
'iterations': iterations,
'salt': salt,
'derived_key': derived_key
}
Common Integration Patterns
Web Application Authentication
class WebAuthPBKDF2:
"""PBKDF2 for web authentication"""
def __init__(self):
self.min_iterations = 100_000
self.pepper = os.environ.get('AUTH_PEPPER', '').encode()
def hash_password_for_storage(self, password: str, email: str) -> str:
"""Hash password with email as additional context"""
# Add pepper and email for defense in depth
salted_password = password.encode() + self.pepper + email.lower().encode()
# Generate salt
salt = secrets.token_bytes(32)
# Derive key
key = hashlib.pbkdf2_hmac(
'sha256',
salted_password,
salt,
self.min_iterations,
dklen=32
)
# Store in database format
return PBKDF2Storage.encode_for_storage(
key, salt, self.min_iterations
)
Encryption Key Derivation
/// Derive encryption keys from password
pub fn derive_file_encryption_keys(
password: &str,
file_id: &[u8],
) -> Result<FileKeys, Error> {
// Master key derivation
let mut master_salt = [0u8; 32];
master_salt[..16].copy_from_slice(b"FileEnc-Master-1");
master_salt[16..].copy_from_slice(&file_id[..16]);
let mut master_key = [0u8; 64]; // 512 bits
pbkdf2_hmac::<Sha512>(
password.as_bytes(),
&master_salt,
200_000, // Higher for encryption
&mut master_key
);
// Derive subkeys
let mut enc_key = [0u8; 32];
pbkdf2_hmac::<Sha256>(
&master_key[..32],
b"encryption",
1000, // Fast, already from slow master
&mut enc_key
);
let mut mac_key = [0u8; 32];
pbkdf2_hmac::<Sha256>(
&master_key[32..],
b"authentication",
1000,
&mut mac_key
);
// Clear master key
master_key.zeroize();
Ok(FileKeys {
encryption_key: enc_key,
mac_key,
})
}
Performance Considerations
| Iterations | Time (i7 CPU) | Time (ARM) | Security Level |
|---|---|---|---|
| 10,000 | 10 ms | 20 ms | Weak (2010) |
| 50,000 | 50 ms | 100 ms | Moderate (2020) |
| 100,000 | 100 ms | 200 ms | Current (2023) |
| 200,000 | 200 ms | 400 ms | Strong (2025) |
| 1,000,000 | 1 sec | 2 sec | Very Strong |
Performance vs Security Trade-offs
- User login: 50-200ms acceptable
- Encryption keys: 200-1000ms acceptable
- Master keys: 1-5 seconds acceptable
- Key backup: 5-30 seconds acceptable
Security Auditing
Verification Checklist
- Iterations ≥ 100,000 (2023)
- Salt ≥ 16 bytes (prefer 32)
- Each password has unique salt
- Using HMAC-SHA256 or stronger
- Storing version/parameters
- Migration path exists
- Timing doesn’t leak password length
Common Vulnerabilities
# AUDIT: Look for these patterns
# ❌ Low iteration count
pbkdf2_hmac('sha256', password, salt, 1000) # 2005 called!
# ❌ No salt
pbkdf2_hmac('sha256', password, b'', 100000) # Rainbow tables!
# ❌ Shared salt
SALT = b'myapp' # Same for everyone!
# ❌ Weak hash function
pbkdf2_hmac('md5', password, salt, 100000) # MD5 is broken!
# ❌ Not storing parameters
# Just storing hash without iterations/salt info
# ❌ Password in source
DEFAULT_PASSWORD = "admin123"
Security Analysis
Threat Model: PBKDF2 Threat Model
The comprehensive threat analysis covers:
- Algorithm-specific attack vectors
- Implementation vulnerabilities
- Side-channel considerations
- Quantum resistance analysis (where applicable)
- Deployment recommendations
For complete security analysis and risk assessment, see the dedicated threat model documentation.
References
Support
Security Issues: security@metamui.id
Documentation Updates: phantom@metamui.id
Vulnerability Disclosure: See SECURITY.md
Document Version: 1.0
Review Cycle: Quarterly
Next Review: 2025-04-05
Classification: PUBLIC