🔑 Argon2 Memory-Hard Key Derivation
📋 Quick Navigation
📖 Overview
Argon2 is a memory-hard key derivation function designed for password hashing and key derivation. It won the Password Hashing Competition in 2015 and is the recommended choice for new applications requiring password storage. Argon2 is specifically designed to resist attacks using GPUs, ASICs, and other specialized hardware by requiring significant amounts of memory.
✨ Key Features
Memory-Hard
Requires significant memory to compute, resisting hardware attacks
GPU/ASIC Resistant
Expensive to parallelize on specialized hardware
Configurable
Adjustable time, memory, and parallelism parameters
Side-Channel Resistant
Argon2i variant resistant to side-channel attacks
Competition Winner
Winner of Password Hashing Competition (2015)
Standardized
Specified in RFC 9106 and widely adopted
Multiple Variants
Argon2i, Argon2d, and Argon2id for different use cases
Future-Proof
Parameters can be increased as hardware improves
🎯 Algorithm Variants
🔐 Argon2 Variants
- Argon2i: Optimized for password hashing, resistant to side-channel attacks
- Argon2d: Faster, data-dependent, suitable for cryptocurrency applications
- Argon2id: Hybrid approach (recommended), combines benefits of both variants
💻 Common Applications
- Password Storage: Secure user password hashing
- Key Derivation: Derive encryption keys from passwords
- Cryptocurrency: Proof-of-work mining algorithms
- Authentication: Challenge-response protocols
Algorithm Parameters
| Parameter | Description | Typical Range |
|---|---|---|
| Password | Input password | Any length |
| Salt | Random salt | ≥ 16 bytes (recommended) |
| Time Cost (t) | Number of iterations | 1-10 |
| Memory Cost (m) | Memory usage in KB | 64 MB - 1 GB |
| Parallelism (p) | Degree of parallelism | 1-8 |
| Tag Length | Output length | 16-64 bytes |
| Version | Algorithm version | 0x13 (current) |
Usage Examples
Basic Password Hashing
from metamui_crypto import Argon2
import os
# Hash a password with recommended settings
password = "user_password"
salt = os.urandom(16) # 128-bit salt
# Using default parameters (Argon2id)
hash_value = Argon2.hash(
password=password.encode(),
salt=salt,
time_cost=3, # 3 iterations
memory_cost=65536, # 64 MB
parallelism=4, # 4 parallel threads
hash_length=32 # 256-bit output
)
print(f"Argon2 hash: {hash_value.hex()}")
Password Storage and Verification
from metamui_crypto import Argon2
import os
import json
import base64
class PasswordManager:
def __init__(self):
# Recommended parameters for 2024
self.time_cost = 3
self.memory_cost = 65536 # 64 MB
self.parallelism = 4
self.salt_length = 16
self.hash_length = 32
def hash_password(self, password):
"""Hash password for storage"""
salt = os.urandom(self.salt_length)
hash_value = Argon2.hash(
password=password.encode(),
salt=salt,
time_cost=self.time_cost,
memory_cost=self.memory_cost,
parallelism=self.parallelism,
hash_length=self.hash_length,
variant='argon2id' # Recommended variant
)
# Store all parameters with hash for future verification
return {
'hash': base64.b64encode(hash_value).decode(),
'salt': base64.b64encode(salt).decode(),
'time_cost': self.time_cost,
'memory_cost': self.memory_cost,
'parallelism': self.parallelism,
'variant': 'argon2id',
'version': 19 # Argon2 version 1.3
}
def verify_password(self, password, stored_data):
"""Verify password against stored hash"""
# Decode stored values
stored_hash = base64.b64decode(stored_data['hash'])
salt = base64.b64decode(stored_data['salt'])
# Recompute hash with same parameters
computed_hash = Argon2.hash(
password=password.encode(),
salt=salt,
time_cost=stored_data['time_cost'],
memory_cost=stored_data['memory_cost'],
parallelism=stored_data['parallelism'],
hash_length=len(stored_hash),
variant=stored_data['variant']
)
# Constant-time comparison
import hmac
return hmac.compare_digest(computed_hash, stored_hash)
def needs_rehash(self, stored_data):
"""Check if password needs rehashing with updated parameters"""
return (
stored_data.get('time_cost', 0) < self.time_cost or
stored_data.get('memory_cost', 0) < self.memory_cost or
stored_data.get('variant') != 'argon2id'
)
Different Argon2 Variants
from metamui_crypto import Argon2
password = b"my_password"
salt = os.urandom(16)
# Argon2i - Best for password hashing (side-channel resistant)
hash_i = Argon2.hash(
password=password,
salt=salt,
variant='argon2i',
time_cost=3,
memory_cost=65536,
parallelism=4
)
# Argon2d - Faster but vulnerable to side-channel attacks
hash_d = Argon2.hash(
password=password,
salt=salt,
variant='argon2d',
time_cost=3,
memory_cost=65536,
parallelism=4
)
# Argon2id - Recommended: hybrid approach
hash_id = Argon2.hash(
password=password,
salt=salt,
variant='argon2id',
time_cost=3,
memory_cost=65536,
parallelism=4
)
print(f"Argon2i: {hash_i.hex()}")
print(f"Argon2d: {hash_d.hex()}")
print(f"Argon2id: {hash_id.hex()}")
Key Derivation
from metamui_crypto import Argon2, ChaCha20Poly1305
def derive_encryption_key(password, salt, key_length=32):
"""Derive encryption key from password"""
return Argon2.hash(
password=password.encode(),
salt=salt,
time_cost=3,
memory_cost=65536,
parallelism=4,
hash_length=key_length,
variant='argon2id'
)
# Password-based encryption
password = "strong_password"
salt = os.urandom(16)
# Derive 256-bit encryption key
encryption_key = derive_encryption_key(password, salt)
# Use for encryption
cipher = ChaCha20Poly1305(encryption_key)
plaintext = b"Secret data"
nonce = os.urandom(12)
ciphertext, tag = cipher.encrypt(plaintext, nonce)
# Store salt with encrypted data
encrypted_data = {
'salt': salt.hex(),
'nonce': nonce.hex(),
'ciphertext': ciphertext.hex(),
'tag': tag.hex()
}
Parameter Tuning
import time
import psutil
def tune_argon2_params(target_time=0.5, max_memory_mb=128):
"""Find optimal Argon2 parameters for system"""
password = b"test_password"
salt = os.urandom(16)
# Start with minimal parameters
time_cost = 1
memory_cost = 8192 # 8 MB
parallelism = psutil.cpu_count()
results = []
# Test different memory costs
while memory_cost <= max_memory_mb * 1024:
start = time.time()
try:
Argon2.hash(
password=password,
salt=salt,
time_cost=time_cost,
memory_cost=memory_cost,
parallelism=parallelism,
hash_length=32
)
duration = time.time() - start
results.append({
'time_cost': time_cost,
'memory_cost': memory_cost,
'parallelism': parallelism,
'duration': duration
})
# Adjust parameters
if duration < target_time * 0.5:
memory_cost *= 2
elif duration < target_time * 0.8:
time_cost += 1
else:
break
except MemoryError:
break
# Find best parameters closest to target time
best = min(results, key=lambda x: abs(x['duration'] - target_time))
return best
# Find optimal parameters for 500ms target
optimal = tune_argon2_params(target_time=0.5, max_memory_mb=128)
print(f"Optimal parameters: {optimal}")
Implementation Details
Memory-Hard Function Design
Argon2 fills a large memory matrix and accesses it in a pattern that depends on the input, making it expensive to compute with limited memory:
- Memory Matrix: Divided into blocks of 1024 bytes
- Filling Phase: Blocks computed using previous blocks
- Finalization: Final blocks processed to produce output
Algorithm Phases
# Simplified Argon2 structure
def argon2_simplified(password, salt, t, m, p):
# 1. Initial hashing
H0 = blake2b(p || t || m || len(password) || password ||
len(salt) || salt || len(K) || K || len(X) || X)
# 2. Memory allocation
memory = allocate_matrix(m, p)
# 3. Initialize first blocks
for lane in range(p):
memory[lane][0] = hash(H0 || 0 || lane)
memory[lane][1] = hash(H0 || 1 || lane)
# 4. Fill memory matrix (t passes)
for pass in range(t):
for segment in range(4):
for lane in range(p):
fill_segment(memory, pass, lane, segment)
# 5. Finalization
final_block = xor_all_final_blocks(memory)
return hash(final_block)
Compression Function
# Blake2b-based compression function
def compress(X, Y):
R = X ^ Y
# Apply Blake2b round function
for round in range(12):
# Column step
G(R, 0, 4, 8, 12)
G(R, 1, 5, 9, 13)
G(R, 2, 6, 10, 14)
G(R, 3, 7, 11, 15)
# Diagonal step
G(R, 0, 5, 10, 15)
G(R, 1, 6, 11, 12)
G(R, 2, 7, 8, 13)
G(R, 3, 4, 9, 14)
return R ^ X ^ Y
Security Considerations
Parameter Selection Guidelines
| Security Level | Time Cost | Memory Cost | Parallelism |
|---|---|---|---|
| Interactive Login | 1 | 64 MB | 4 |
| Standard Web App | 3 | 64 MB | 4 |
| Sensitive Data | 4 | 256 MB | 4 |
| High Security | 5 | 1 GB | 8 |
Threat Model
- Offline Attacks: Primary concern for password hashing
- GPU Attacks: Memory requirements make GPU attacks expensive
- ASIC Resistance: Custom hardware provides limited advantage
- Side-Channel: Use Argon2i for environments with side-channel risks
Common Attack Scenarios
# Attack cost estimation
def estimate_attack_cost(memory_mb, hash_rate):
"""Estimate cost of password cracking attack"""
# Assumptions:
# - GPU with 8GB memory
# - $0.10 per GPU-hour
gpu_memory_gb = 8
max_parallel = (gpu_memory_gb * 1024) // memory_mb
# Reduced parallelism due to memory bandwidth
effective_parallel = max_parallel * 0.3
# Hashes per hour
hashes_per_hour = effective_parallel * hash_rate * 3600
# Cost per million hashes
cost_per_million = (0.10 / hashes_per_hour) * 1_000_000
return {
'max_parallel': max_parallel,
'effective_parallel': int(effective_parallel),
'hashes_per_hour': int(hashes_per_hour),
'cost_per_million_hashes': f"${cost_per_million:.4f}"
}
# Example: 64MB memory cost
attack_cost = estimate_attack_cost(memory_mb=64, hash_rate=10)
print(f"Attack cost analysis: {attack_cost}")
Performance Optimization
Multi-threading
from concurrent.futures import ThreadPoolExecutor
import multiprocessing
def parallel_argon2(passwords, salt, params):
"""Hash multiple passwords in parallel"""
def hash_single(password):
return Argon2.hash(
password=password.encode(),
salt=salt,
**params
)
# Use thread pool for I/O bound operations
with ThreadPoolExecutor(max_workers=multiprocessing.cpu_count()) as executor:
results = list(executor.map(hash_single, passwords))
return results
# Batch password hashing
passwords = ["password1", "password2", "password3", "password4"]
salt = os.urandom(16)
params = {
'time_cost': 3,
'memory_cost': 65536,
'parallelism': 1, # Set to 1 for parallel batch processing
'hash_length': 32
}
hashes = parallel_argon2(passwords, salt, params)
Memory Management
import gc
class ArgonMemoryManager:
"""Manage memory for Argon2 operations"""
def __init__(self, max_memory_mb=256):
self.max_memory_mb = max_memory_mb
self.active_operations = 0
def hash_with_memory_limit(self, password, salt, memory_cost_mb):
"""Hash with memory management"""
if memory_cost_mb > self.max_memory_mb:
raise ValueError(f"Memory cost {memory_cost_mb}MB exceeds limit")
self.active_operations += 1
try:
result = Argon2.hash(
password=password,
salt=salt,
time_cost=3,
memory_cost=memory_cost_mb * 1024,
parallelism=4,
hash_length=32
)
# Force garbage collection for large operations
if memory_cost_mb > 128:
gc.collect()
return result
finally:
self.active_operations -= 1
Migration Strategies
From PBKDF2
class PasswordMigration:
"""Migrate from PBKDF2 to Argon2"""
def __init__(self):
self.pbkdf2_iterations = 100000
self.argon2_params = {
'time_cost': 3,
'memory_cost': 65536,
'parallelism': 4,
'hash_length': 32
}
def verify_and_upgrade(self, password, stored_hash_data):
"""Verify old hash and upgrade to Argon2"""
if stored_hash_data['algorithm'] == 'pbkdf2':
# Verify PBKDF2 hash
from metamui_crypto import PBKDF2
computed = PBKDF2.derive(
password=password.encode(),
salt=base64.b64decode(stored_hash_data['salt']),
iterations=stored_hash_data['iterations'],
key_length=32
)
if hmac.compare_digest(computed, base64.b64decode(stored_hash_data['hash'])):
# Upgrade to Argon2
new_salt = os.urandom(16)
new_hash = Argon2.hash(
password=password.encode(),
salt=new_salt,
**self.argon2_params
)
return {
'algorithm': 'argon2id',
'hash': base64.b64encode(new_hash).decode(),
'salt': base64.b64encode(new_salt).decode(),
**self.argon2_params,
'upgraded': True
}
return None
From bcrypt
def migrate_from_bcrypt(password, bcrypt_hash):
"""Migrate from bcrypt to Argon2"""
import bcrypt
# Verify bcrypt hash
if bcrypt.checkpw(password.encode(), bcrypt_hash.encode()):
# Create new Argon2 hash
salt = os.urandom(16)
argon2_hash = Argon2.hash(
password=password.encode(),
salt=salt,
time_cost=3,
memory_cost=65536,
parallelism=4,
hash_length=32
)
return {
'algorithm': 'argon2id',
'hash': argon2_hash.hex(),
'salt': salt.hex(),
'migrated_from': 'bcrypt'
}
return None
Best Practices
- Use Argon2id: Best balance of security and performance
- Minimum Parameters: At least 64MB memory, 3 iterations
- Store Parameters: Save all parameters with hash for future compatibility
- Use Random Salt: At least 16 bytes, unique per password
- Regular Updates: Review and update parameters annually
Parameter Update Strategy
class PasswordParameterManager:
"""Manage Argon2 parameter updates"""
# Recommended minimums by year
PARAM_SCHEDULE = {
2023: {'time_cost': 3, 'memory_cost': 65536},
2024: {'time_cost': 3, 'memory_cost': 131072},
2025: {'time_cost': 4, 'memory_cost': 131072},
2026: {'time_cost': 4, 'memory_cost': 262144}
}
@classmethod
def get_current_params(cls):
"""Get recommended parameters for current year"""
import datetime
year = datetime.datetime.now().year
# Use latest known parameters
for y in sorted(cls.PARAM_SCHEDULE.keys(), reverse=True):
if year >= y:
return cls.PARAM_SCHEDULE[y]
# Fallback to oldest known
return cls.PARAM_SCHEDULE[min(cls.PARAM_SCHEDULE.keys())]
@classmethod
def needs_upgrade(cls, stored_params):
"""Check if parameters need upgrading"""
current = cls.get_current_params()
return (
stored_params.get('time_cost', 0) < current['time_cost'] or
stored_params.get('memory_cost', 0) < current['memory_cost']
)
Common Pitfalls
1. Insufficient Memory Cost
# BAD: Too little memory
hash = Argon2.hash(password, salt, memory_cost=1024) # Only 1MB!
# GOOD: Adequate memory cost
hash = Argon2.hash(password, salt, memory_cost=65536) # 64MB
2. Reusing Salts
# BAD: Fixed salt
SALT = b"my_app_salt"
hash = Argon2.hash(password, SALT)
# GOOD: Random salt per password
salt = os.urandom(16)
hash = Argon2.hash(password, salt)
3. Not Storing Parameters
# BAD: Only storing hash
stored = {'hash': hash.hex()}
# GOOD: Store all parameters
stored = {
'hash': hash.hex(),
'salt': salt.hex(),
'time_cost': 3,
'memory_cost': 65536,
'parallelism': 4,
'variant': 'argon2id',
'version': 19
}
Test Vectors
# Official Argon2 test vectors
test_vectors = [
{
'password': b'password',
'salt': b'somesalt',
'time_cost': 2,
'memory_cost': 65536,
'parallelism': 1,
'hash_length': 32,
'variant': 'argon2i',
'expected': 'c1628832147d9720c5bd1cfd61367078729f6dfb6f8fea9ff98158e0d7816ed0'
},
{
'password': b'password',
'salt': b'somesalt',
'time_cost': 2,
'memory_cost': 65536,
'parallelism': 1,
'hash_length': 32,
'variant': 'argon2id',
'expected': '09316115d5cf24ed5a15a31a3ba326e5cf32edc24702987c02b6566f61913cf7'
}
]
# Verify implementation
for vector in test_vectors:
result = Argon2.hash(
password=vector['password'],
salt=vector['salt'],
time_cost=vector['time_cost'],
memory_cost=vector['memory_cost'],
parallelism=vector['parallelism'],
hash_length=vector['hash_length'],
variant=vector['variant']
)
assert result.hex() == vector['expected'], f"Test failed for {vector['variant']}"
print(f"✓ {vector['variant']} test passed")
Resources
- Argon2 RFC 9106 - Official specification
- Password Hashing Competition - Competition results
- Argon2 Paper - Original paper
- Security Analysis - Security properties
- OWASP Guidelines - Password storage best practices