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";
?>