Key Derivation

πŸ”‘ PBKDF2 Password-Based Key Derivation

Security Level Iteration-dependent
Performance ⭐⭐⭐ Good
Quantum Resistant ⚠️ Partial
Standardization NIST SP 800-132, RFC 8018
Memory Usage Low (vulnerable to GPU attacks)
Recommended Use Legacy systems only

πŸ“– 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

Password
Any length
Input password or passphrase
Salt
β‰₯ 16 bytes
Random salt (128+ bits recommended)
Iterations
100,000 - 1,000,000
Number of PRF applications
Key Length
16-64 bytes
Desired output key length
PRF
HMAC-SHA256/512
Pseudorandom function
Security
Iteration-dependent
Depends on iteration count

πŸ“ˆ 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:

  1. Iteration Count: Higher is more secure but slower
  2. Salt Length: Prevents rainbow table attacks
  3. PRF Security: Usually HMAC-SHA256 or HMAC-SHA512
  4. 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

  1. 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
    
  2. 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
    
  3. 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

  1. Use High Iteration Counts: Follow OWASP 2024 recommendations (600K+ for SHA-256)
  2. Generate Random Salts: Use cryptographically secure random for each password
  3. Store All Parameters: Include algorithm, PRF, iterations, and salt with hash
  4. Plan for Migration: Design systems to upgrade to Argon2 over time
  5. 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