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 ⚠️

  1. Prefer Argon2: PBKDF2 lacks memory hardness - use Argon2 for new systems
  2. Iteration Count: Must be high enough to slow attackers (100,000+ in 2023)
  3. Salt Required: NEVER derive keys without unique salts
  4. Not Memory Hard: Vulnerable to GPU/ASIC acceleration
  5. 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:

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:

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:

Security Notes:

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

Security Auditing

Verification Checklist

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:

For complete security analysis and risk assessment, see the dedicated threat model documentation.

References

  1. RFC 8018 - PKCS #5 v2.1 (PBKDF2)
  2. NIST SP 800-132
  3. OWASP Password Storage Cheat Sheet

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