Python Implementation

Python Implementation

Python's ecosystem offers excellent cryptographic libraries with clean APIs for password hashing. The recommended approach uses the argon2-cffi library for Argon2 (the most modern algorithm) with bcrypt as a fallback for compatibility. Python's built-in secrets module provides cryptographically secure random generation for scenarios requiring custom salt handling.

import secrets
import hmac
import json
import base64
from datetime import datetime
from typing import Optional, Tuple, Dict
from argon2 import PasswordHasher, Type
from argon2.exceptions import VerifyMismatchError, InvalidHash
import bcrypt

class SecurePasswordManager:
    """Production-ready password hashing implementation for Python"""
    
    def __init__(self, algorithm='argon2', pepper: Optional[bytes] = None):
        self.algorithm = algorithm
        self.pepper = pepper
        
        # Configure Argon2 with secure defaults for 2024
        self.argon2 = PasswordHasher(
            time_cost=3,        # iterations
            memory_cost=65536,  # 64MB
            parallelism=4,      # threads
            hash_len=32,        # output length
            salt_len=16,        # salt length
            type=Type.ID        # Argon2id variant
        )
        
        # Version for migration support
        self.version = "v3"
        
    def hash_password(self, password: str) -> str:
        """Hash a password using the configured algorithm"""
        
        if not password:
            raise ValueError("Password cannot be empty")
            
        if len(password) > 1024:  # Prevent DoS
            raise ValueError("Password too long")
        
        # Apply pepper if configured
        if self.pepper:
            password = self._apply_pepper(password)
        
        try:
            if self.algorithm == 'argon2':
                hash_value = self.argon2.hash(password)
                return f"{self.version}$argon2${hash_value}"
                
            elif self.algorithm == 'bcrypt':
                # bcrypt has 72-byte limit, hash longer passwords first
                if len(password.encode('utf-8')) > 72:
                    password = base64.b64encode(
                        hmac.new(b'bcrypt-key', password.encode(), 'sha256').digest()
                    ).decode('ascii')
                
                salt = bcrypt.gensalt(rounds=13)  # 2^13 iterations
                hash_value = bcrypt.hashpw(password.encode('utf-8'), salt)
                return f"{self.version}$bcrypt${hash_value.decode('ascii')}"
                
            else:
                raise ValueError(f"Unknown algorithm: {self.algorithm}")
                
        except Exception as e:
            # Log error securely without exposing password
            self._log_error(f"Hashing failed: {type(e).__name__}")
            raise RuntimeError("Password hashing failed")
    
    def verify_password(self, password: str, stored_hash: str) -> Tuple[bool, Optional[str]]:
        """
        Verify password and return (is_valid, new_hash_if_needs_rehash)
        """
        
        if not password or not stored_hash:
            return False, None
        
        try:
            # Parse stored hash format
            parts = stored_hash.split('$', 2)
            if len(parts) != 3:
                # Legacy format - needs migration
                return self._verify_legacy(password, stored_hash)
            
            version, algorithm, hash_value = parts
            
            # Apply pepper if configured
            if self.pepper:
                password = self._apply_pepper(password)
            
            # Verify based on algorithm
            if algorithm == 'argon2':
                try:
                    self.argon2.verify(hash_value, password)
                    
                    # Check if rehashing needed
                    if self.argon2.check_needs_rehash(hash_value):
                        new_hash = self.hash_password(password)
                        return True, new_hash
                        
                    return True, None
                    
                except VerifyMismatchError:
                    return False, None
                    
            elif algorithm == 'bcrypt':
                # Handle bcrypt's 72-byte limit
                if len(password.encode('utf-8')) > 72:
                    password = base64.b64encode(
                        hmac.new(b'bcrypt-key', password.encode(), 'sha256').digest()
                    ).decode('ascii')
                
                if bcrypt.checkpw(password.encode('utf-8'), hash_value.encode('ascii')):
                    # Migrate bcrypt to argon2
                    if self.algorithm == 'argon2':
                        new_hash = self.hash_password(password)
                        return True, new_hash
                    return True, None
                    
                return False, None
                
            else:
                self._log_error(f"Unknown algorithm in stored hash: {algorithm}")
                return False, None
                
        except Exception as e:
            self._log_error(f"Verification error: {type(e).__name__}")
            return False, None
    
    def _apply_pepper(self, password: str) -> str:
        """Apply pepper using HMAC"""
        
        peppered = hmac.new(
            self.pepper,
            password.encode('utf-8'),
            'sha256'
        ).digest()
        
        return base64.b64encode(peppered).decode('ascii')
    
    def _verify_legacy(self, password: str, legacy_hash: str) -> Tuple[bool, Optional[str]]:
        """Handle legacy hash formats for migration"""
        
        # Example: Migrate from old bcrypt format
        try:
            if bcrypt.checkpw(password.encode('utf-8'), legacy_hash.encode('ascii')):
                # Generate new hash with current algorithm
                new_hash = self.hash_password(password)
                return True, new_hash
        except:
            pass
            
        return False, None
    
    def _log_error(self, message: str):
        """Secure error logging"""
        print(f"[SECURITY] {datetime.utcnow().isoformat()} - {message}")
    
    def generate_secure_password(self, length: int = 16) -> str:
        """Generate a cryptographically secure random password"""
        
        # Character set with good entropy and usability
        charset = (
            "abcdefghijklmnopqrstuvwxyz"
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
            "0123456789"
            "!@#$%^&*"
        )
        
        return ''.join(secrets.choice(charset) for _ in range(length))


# Example usage and tests
def demonstrate_python_implementation():
    """Demonstrate the password manager usage"""
    
    # Initialize password manager
    pm = SecurePasswordManager(algorithm='argon2')
    
    # Hash some passwords
    passwords = [
        "SimplePassword123",
        "VeryLongPasswordThatExceedsNormalLengthRequirements" * 3,
        "Short",
        "Unicode password: café été 你好"
    ]
    
    print("Python Password Hashing Demo\n")
    
    hashed_passwords = {}
    for pwd in passwords:
        try:
            hashed = pm.hash_password(pwd)
            hashed_passwords[pwd] = hashed
            print(f"Password: {pwd[:20]}...")
            print(f"Hash: {hashed[:60]}...")
            print()
        except ValueError as e:
            print(f"Failed to hash '{pwd[:20]}...': {e}\n")
    
    # Verify passwords
    print("Verification Tests:")
    for pwd, hashed in hashed_passwords.items():
        valid, new_hash = pm.verify_password(pwd, hashed)
        print(f"'{pwd[:20]}...' : {'Valid' if valid else 'Invalid'}")
        if new_hash:
            print(f"  → Needs rehashing")
    
    # Test wrong password
    if hashed_passwords:
        first_pwd = list(hashed_passwords.keys())[0]
        first_hash = hashed_passwords[first_pwd]
        valid, _ = pm.verify_password("wrong password", first_hash)
        print(f"\nWrong password: {'Valid' if valid else 'Invalid'}")
    
    # Generate secure password
    secure_pwd = pm.generate_secure_password(20)
    print(f"\nGenerated secure password: {secure_pwd}")

# Run demonstration
demonstrate_python_implementation()