Key Derivation Functions
Side-channel claim limit. Nothing on this page claims constant-time behaviour or side-channel resistance for any target: the side-channel harness covers nothing (finding A48) and no measurement exists. The Rust and C cores are designed for constant time and unmeasured; managed runtimes (JIT, GC, bounds checks) make no timing claim. Where this page names constant-time or side-channel techniques, in prose or in code, they are design intent or recommendations, not measured properties.
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
- Variants: Argon2i, Argon2d, Argon2id (recommended)
- Memory Cost: Configurable (64MB default)
- Time Cost: Configurable (3 iterations default)
- Parallelism: Configurable
- Output: Variable length
- Detailed Documentation
PBKDF2
Password-Based Key Derivation Function 2
- Hash Functions: SHA-256, SHA-512
- Iterations: Configurable (100,000+ recommended)
- Salt: Required
- Output: Variable length
- Standard: NIST SP 800-132
- Detailed Documentation
HKDF
HMAC-Based Key Derivation Function
- Hash Functions: SHA-256, SHA-512
- Modes: Extract-then-Expand
- Salt: Optional
- Info: Context binding
- Standard: RFC 5869
- Detailed Documentation
Algorithm Selection Guide
For Password Hashing
Recommended: Argon2id
- Resistant to GPU/ASIC attacks
- Memory-hard algorithm
- Constant-time behaviour and side-channel resistance: not claimed — unmeasured for every target (A48)
- Winner of Password Hashing Competition
For Legacy Systems
Use: PBKDF2
- Wide compatibility
- NIST approved
- Simple implementation
- Well-tested
For Key Expansion
Use: HKDF
- Derive multiple keys from one source
- Add context binding
- Cryptographically sound
- Fast operation
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)