Key Derivation Functions

Key derivation functions (KDFs) transform passwords or other input material into cryptographic keys. MetaMUI implements modern KDFs for password hashing and key expansion.

Available Algorithms

Argon2

Memory-Hard Password Hashing

PBKDF2

Password-Based Key Derivation Function 2

HKDF

HMAC-Based Key Derivation Function

Algorithm Selection Guide

For Password Hashing

Recommended: Argon2id

For Legacy Systems

Use: PBKDF2

For Key Expansion

Use: HKDF

Usage Examples

Password Hashing with Argon2

from metamui_crypto import Argon2
import os

# Hash password for storage
password = b"user_password"
salt = os.urandom(16)  # Random salt

# Hash with recommended parameters
hash = Argon2.hash(
    password=password,
    salt=salt,
    time_cost=3,        # iterations
    memory_cost=65536,  # 64MB
    parallelism=4,      # threads
    hash_length=32      # output bytes
)

# Store salt + hash
stored = salt + hash

# Verify password
def verify_password(password, stored):
    salt = stored[:16]
    hash = stored[16:]
    
    computed = Argon2.hash(
        password=password,
        salt=salt,
        time_cost=3,
        memory_cost=65536,
        parallelism=4,
        hash_length=32
    )
    
    return computed == hash

Key Derivation with PBKDF2

from metamui_crypto import PBKDF2
import os

# Derive encryption key from password
password = b"user_password"
salt = os.urandom(32)

# PBKDF2 with SHA-256
key = PBKDF2.derive(
    password=password,
    salt=salt,
    iterations=100000,
    key_length=32,
    hash_function='sha256'
)

# Use derived key for encryption
from metamui_crypto import ChaCha20Poly1305
cipher = ChaCha20Poly1305(key)

Key Expansion with HKDF

from metamui_crypto import HKDF
import os

# Master key material
master_key = os.urandom(32)
salt = os.urandom(32)

# Derive multiple keys for different purposes
encryption_key = HKDF.derive(
    key_material=master_key,
    salt=salt,
    info=b"encryption",
    length=32,
    hash_function='sha256'
)

mac_key = HKDF.derive(
    key_material=master_key,
    salt=salt,
    info=b"mac",
    length=32,
    hash_function='sha256'
)

# Keys are cryptographically independent
assert encryption_key != mac_key

Secure Password Storage

from metamui_crypto import Argon2
import os
import json

class PasswordManager:
    def __init__(self):
        # Argon2 parameters
        self.time_cost = 3
        self.memory_cost = 65536
        self.parallelism = 4
        self.hash_length = 32
        self.salt_length = 16
    
    def hash_password(self, password: str) -> dict:
        """Hash password for storage"""
        salt = os.urandom(self.salt_length)
        
        hash = Argon2.hash(
            password=password.encode('utf-8'),
            salt=salt,
            time_cost=self.time_cost,
            memory_cost=self.memory_cost,
            parallelism=self.parallelism,
            hash_length=self.hash_length
        )
        
        # Return parameters with hash for future verification
        return {
            'hash': (salt + hash).hex(),
            'algorithm': 'argon2id',
            'time_cost': self.time_cost,
            'memory_cost': self.memory_cost,
            'parallelism': self.parallelism
        }
    
    def verify_password(self, password: str, stored: dict) -> bool:
        """Verify password against stored hash"""
        stored_bytes = bytes.fromhex(stored['hash'])
        salt = stored_bytes[:self.salt_length]
        expected_hash = stored_bytes[self.salt_length:]
        
        computed_hash = Argon2.hash(
            password=password.encode('utf-8'),
            salt=salt,
            time_cost=stored['time_cost'],
            memory_cost=stored['memory_cost'],
            parallelism=stored['parallelism'],
            hash_length=self.hash_length
        )
        
        # Constant-time comparison
        return computed_hash == expected_hash

Hierarchical Key Derivation

from metamui_crypto import HKDF
import os

class KeyHierarchy:
    def __init__(self, master_secret: bytes):
        self.master = master_secret
        
    def derive_user_key(self, user_id: str) -> bytes:
        """Derive key for specific user"""
        return HKDF.derive(
            key_material=self.master,
            salt=b"users",
            info=user_id.encode('utf-8'),
            length=32
        )
    
    def derive_session_key(self, user_key: bytes, session_id: str) -> bytes:
        """Derive key for specific session"""
        return HKDF.derive(
            key_material=user_key,
            salt=b"sessions",
            info=session_id.encode('utf-8'),
            length=32
        )

# Usage
hierarchy = KeyHierarchy(os.urandom(32))
user_key = hierarchy.derive_user_key("user123")
session_key = hierarchy.derive_session_key(user_key, "session456")

Security Parameters

Argon2 Recommendations

| Security Level | Time Cost | Memory Cost | Parallelism | |—————-|———–|————-|————-| | Interactive | 1 | 64 MB | 4 | | Moderate | 3 | 256 MB | 4 | | Sensitive | 5 | 1 GB | 8 |

PBKDF2 Iterations

| Year | Minimum Iterations | Recommended | |——|——————-|————-| | 2023 | 100,000 | 600,000 | | 2024 | 150,000 | 800,000 | | 2025 | 200,000 | 1,000,000 |

Performance Considerations

Argon2 Benchmarks

| Parameters | Time | Memory Usage | |————|——|————–| | t=1, m=64MB | 50ms | 64 MB | | t=3, m=256MB | 300ms | 256 MB | | t=5, m=1GB | 2s | 1 GB |

PBKDF2 Performance

| Iterations | SHA-256 | SHA-512 | |————|———|———| | 100,000 | 100ms | 150ms | | 500,000 | 500ms | 750ms | | 1,000,000 | 1s | 1.5s |

Best Practices

1. Use Appropriate Parameters

# Good: Sufficient security parameters
hash = Argon2.hash(
    password,
    salt=os.urandom(16),
    time_cost=3,
    memory_cost=65536,  # 64MB
    parallelism=4
)

# Bad: Weak parameters
# hash = Argon2.hash(
#     password,
#     salt=salt,
#     time_cost=1,      # Too fast
#     memory_cost=1024, # Too little memory
#     parallelism=1
# )

2. Always Use Salt

# Good: Random salt for each password
salt = os.urandom(16)
hash = Argon2.hash(password, salt=salt)

# Bad: No salt or fixed salt
# hash = Argon2.hash(password, salt=b"fixed")  # Vulnerable to rainbow tables

3. Store Parameters

# Good: Store all parameters for future compatibility
password_record = {
    'hash': hash.hex(),
    'salt': salt.hex(),
    'algorithm': 'argon2id',
    'version': 19,
    'time_cost': 3,
    'memory_cost': 65536,
    'parallelism': 4
}

# Allows parameter upgrades later
if password_record['time_cost'] < current_minimum:
    # Re-hash with stronger parameters on next login
    pass

4. Implement Rate Limiting

from time import time
from collections import defaultdict

class RateLimiter:
    def __init__(self, max_attempts=5, window=300):
        self.attempts = defaultdict(list)
        self.max_attempts = max_attempts
        self.window = window
    
    def check_rate_limit(self, identifier):
        now = time()
        # Clean old attempts
        self.attempts[identifier] = [
            t for t in self.attempts[identifier] 
            if now - t < self.window
        ]
        
        if len(self.attempts[identifier]) >= self.max_attempts:
            return False
            
        self.attempts[identifier].append(now)
        return True

Migration Strategies

From MD5/SHA1 Password Hashing

# Legacy system with weak hashing
# old_hash = hashlib.md5(password + salt).hexdigest()

# Migration approach
def migrate_password(password, old_hash, salt):
    # Verify against old system
    if hashlib.md5(password + salt).hexdigest() != old_hash:
        return None
    
    # Re-hash with Argon2
    new_salt = os.urandom(16)
    new_hash = Argon2.hash(
        password,
        salt=new_salt,
        time_cost=3,
        memory_cost=65536
    )
    
    return {
        'hash': new_hash,
        'salt': new_salt,
        'algorithm': 'argon2id',
        'migrated': True
    }

Upgrading PBKDF2 Parameters

def upgrade_pbkdf2_iterations(password, stored_hash, old_iterations):
    # Verify with old parameters
    computed = PBKDF2.derive(
        password=password,
        salt=stored_hash['salt'],
        iterations=old_iterations,
        key_length=32
    )
    
    if computed != stored_hash['hash']:
        return None
    
    # Re-derive with more iterations
    new_iterations = 1000000  # Current recommendation
    new_hash = PBKDF2.derive(
        password=password,
        salt=stored_hash['salt'],
        iterations=new_iterations,
        key_length=32
    )
    
    return {
        'hash': new_hash,
        'salt': stored_hash['salt'],
        'iterations': new_iterations
    }

Common Pitfalls

1. Using KDFs for Non-Password Data

# Bad: Using Argon2 for non-password data
# key = Argon2.hash(random_data)  # Wasteful

# Good: Use HKDF for key derivation
key = HKDF.derive(random_data, info=b"encryption")

2. Insufficient Entropy

# Bad: Weak password without policy
# password = "123456"

# Good: Enforce password requirements
import secrets
import string

def generate_strong_password(length=16):
    alphabet = string.ascii_letters + string.digits + string.punctuation
    return ''.join(secrets.choice(alphabet) for _ in range(length))

3. Timing Attacks

# Bad: Early return on mismatch
# if computed_hash != stored_hash:
#     return False  # Timing leak

# Good: Constant-time comparison
import hmac
return hmac.compare_digest(computed_hash, stored_hash)

Resources