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
- 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
- Side-channel resistant
- 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)