Key Derivation

🔑 Argon2 Memory-Hard Key Derivation

Security Level Configurable
Performance ⭐⭐⭐ Tunable
Quantum Resistant ⚠️ Partial
Standardization RFC 9106, PHC Winner
Memory Usage 64 MB - 1 GB+
Output Size 16-64 bytes

📖 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:

  1. Memory Matrix: Divided into blocks of 1024 bytes
  2. Filling Phase: Blocks computed using previous blocks
  3. 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

  1. Offline Attacks: Primary concern for password hashing
  2. GPU Attacks: Memory requirements make GPU attacks expensive
  3. ASIC Resistance: Custom hardware provides limited advantage
  4. 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

  1. Use Argon2id: Best balance of security and performance
  2. Minimum Parameters: At least 64MB memory, 3 iterations
  3. Store Parameters: Save all parameters with hash for future compatibility
  4. Use Random Salt: At least 16 bytes, unique per password
  5. 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