PHP Implementation

PHP Implementation

PHP's password hashing API, introduced in PHP 5.5, provides a simple yet secure interface for password management. The password_hash() and password_verify() functions handle salt generation and algorithm selection automatically. For applications requiring more control or newer algorithms like Argon2, PHP 7.2+ offers native support with proper configuration options.

<?php

class SecurePasswordManager {
    
    private const VERSION = 'v3';
    private const MAX_PASSWORD_LENGTH = 1024;
    
    private string $algorithm;
    private ?string $pepper;
    private array $options;
    
    public function __construct(string $algorithm = 'argon2id', ?string $pepper = null) {
        $this->algorithm = $algorithm;
        $this->pepper = $pepper;
        
        // Configure algorithm options
        switch ($algorithm) {
            case 'argon2id':
            case 'argon2i':
                $this->options = [
                    'memory_cost' => 65536,  // 64MB
                    'time_cost' => 3,        // iterations
                    'threads' => 4           // parallelism
                ];
                break;
                
            case 'bcrypt':
                $this->options = [
                    'cost' => 13  // 2^13 iterations
                ];
                break;
                
            default:
                throw new InvalidArgumentException("Unknown algorithm: $algorithm");
        }
    }
    
    public function hashPassword(string $password): string {
        if (empty($password)) {
            throw new InvalidArgumentException('Password cannot be empty');
        }
        
        if (strlen($password) > self::MAX_PASSWORD_LENGTH) {
            throw new InvalidArgumentException('Password too long');
        }
        
        // Apply pepper if configured
        $processedPassword = $password;
        if ($this->pepper !== null) {
            $processedPassword = $this->applyPepper($password);
        }
        
        // Select PHP algorithm constant
        $algo = $this->getAlgorithmConstant();
        
        // Handle bcrypt's 72-byte limit
        if ($this->algorithm === 'bcrypt' && strlen($processedPassword) > 72) {
            $processedPassword = base64_encode(
                hash('sha256', $processedPassword, true)
            );
        }
        
        // Hash password
        $hash = password_hash($processedPassword, $algo, $this->options);
        
        if ($hash === false) {
            $this->logError('Password hashing failed');
            throw new RuntimeException('Password hashing failed');
        }
        
        // Store with version and algorithm prefix
        return sprintf('%s$%s$%s', self::VERSION, $this->algorithm, $hash);
    }
    
    public function verifyPassword(string $password, string $storedHash): array {
        if (empty($password) || empty($storedHash)) {
            return ['valid' => false, 'needs_rehash' => false];
        }
        
        try {
            // Parse stored hash
            $parts = explode('$', $storedHash, 3);
            
            if (count($parts) !== 3) {
                // Legacy format
                return $this->verifyLegacy($password, $storedHash);
            }
            
            [$version, $algorithm, $hash] = $parts;
            
            // Apply pepper if configured
            $processedPassword = $password;
            if ($this->pepper !== null) {
                $processedPassword = $this->applyPepper($password);
            }
            
            // Handle bcrypt's limit for verification
            if ($algorithm === 'bcrypt' && strlen($processedPassword) > 72) {
                $processedPassword = base64_encode(
                    hash('sha256', $processedPassword, true)
                );
            }
            
            // Verify password
            $valid = password_verify($processedPassword, $hash);
            
            if (!$valid) {
                return ['valid' => false, 'needs_rehash' => false];
            }
            
            // Check if rehashing needed
            $needsRehash = false;
            
            // Check if algorithm changed
            if ($algorithm !== $this->algorithm) {
                $needsRehash = true;
            } else {
                // Check if options changed
                $algo = $this->getAlgorithmConstant();
                $needsRehash = password_needs_rehash($hash, $algo, $this->options);
            }
            
            return ['valid' => true, 'needs_rehash' => $needsRehash];
            
        } catch (Exception $e) {
            $this->logError('Verification failed: ' . $e->getMessage());
            return ['valid' => false, 'needs_rehash' => false];
        }
    }
    
    private function applyPepper(string $password): string {
        return base64_encode(
            hash_hmac('sha256', $password, $this->pepper, true)
        );
    }
    
    private function getAlgorithmConstant(): int {
        switch ($this->algorithm) {
            case 'argon2id':
                return PASSWORD_ARGON2ID;
            case 'argon2i':
                return PASSWORD_ARGON2I;
            case 'bcrypt':
                return PASSWORD_BCRYPT;
            default:
                throw new RuntimeException('Invalid algorithm');
        }
    }
    
    private function verifyLegacy(string $password, string $legacyHash): array {
        // Example: Handle old PHP crypt() format
        if (password_verify($password, $legacyHash)) {
            return ['valid' => true, 'needs_rehash' => true];
        }
        
        // Example: Handle old MD5 format (DO NOT USE IN NEW CODE)
        if (strlen($legacyHash) === 32 && md5($password) === $legacyHash) {
            return ['valid' => true, 'needs_rehash' => true];
        }
        
        return ['valid' => false, 'needs_rehash' => false];
    }
    
    private function logError(string $message): void {
        error_log('[SECURITY] ' . date('c') . ' - ' . $message);
    }
    
    public function generateSecurePassword(int $length = 16): string {
        if ($length < 8) {
            throw new InvalidArgumentException('Password length must be at least 8');
        }
        
        $charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
        $charsetLength = strlen($charset);
        $password = '';
        
        for ($i = 0; $i < $length; $i++) {
            $randomIndex = random_int(0, $charsetLength - 1);
            $password .= $charset[$randomIndex];
        }
        
        return $password;
    }
}

// Demonstration
echo "PHP Password Hashing Demo\n\n";

$pm = new SecurePasswordManager('argon2id');

$passwords = [
    'SimplePassword123',
    str_repeat('VeryLongPassword', 10),
    'Unicode: café été 你好',
    'Special chars: !@#$%^&*()'
];

$hashedPasswords = [];

// Hash passwords
foreach ($passwords as $password) {
    try {
        $hash = $pm->hashPassword($password);
        $hashedPasswords[$password] = $hash;
        
        echo "Password: " . substr($password, 0, 20) . "...\n";
        echo "Hash: " . substr($hash, 0, 60) . "...\n\n";
        
    } catch (Exception $e) {
        echo "Failed to hash password: " . $e->getMessage() . "\n\n";
    }
}

// Verify passwords
echo "Verification Tests:\n";

foreach ($hashedPasswords as $password => $hash) {
    $result = $pm->verifyPassword($password, $hash);
    echo "Password: " . substr($password, 0, 20) . "...\n";
    echo "Valid: " . ($result['valid'] ? 'Yes' : 'No') . ", ";
    echo "Needs rehash: " . ($result['needs_rehash'] ? 'Yes' : 'No') . "\n";
}

// Test wrong password
if (!empty($hashedPasswords)) {
    $firstHash = reset($hashedPasswords);
    $wrongResult = $pm->verifyPassword('wrong password', $firstHash);
    echo "\nWrong password - Valid: " . ($wrongResult['valid'] ? 'Yes' : 'No') . "\n";
}

// Generate secure password
$securePassword = $pm->generateSecurePassword(20);
echo "\nGenerated secure password: $securePassword\n";

?>