validateConfiguration(); } /** * Hash a plain text password */ public function hash( #[SensitiveParameter] string $plainPassword, ?string $algorithm = null, ?string $securityLevel = null ): HashedPassword { $this->validatePassword($plainPassword); $algorithm = $algorithm ?? $this->defaultAlgorithm; $securityLevel = $securityLevel ?? $this->defaultSecurityLevel; $derivedKey = $this->kdf->hashPassword( $plainPassword, $algorithm, $this->getParametersForLevel($algorithm, $securityLevel) ); return HashedPassword::fromDerivedKey($derivedKey); } /** * Verify a password against a hash */ public function verify( #[SensitiveParameter] string $plainPassword, HashedPassword $hashedPassword ): bool { if (empty($plainPassword)) { return false; } try { return $this->kdf->verify($plainPassword, $hashedPassword->getDerivedKey()); } catch (\Exception) { // Log exception for debugging but don't expose details return false; } } /** * Check if a password hash needs to be rehashed */ public function needsRehash( HashedPassword $hashedPassword, ?string $algorithm = null, ?string $securityLevel = null ): bool { $algorithm = $algorithm ?? $this->defaultAlgorithm; $securityLevel = $securityLevel ?? $this->defaultSecurityLevel; $currentParameters = $this->getParametersForLevel($algorithm, $securityLevel); return $hashedPassword->needsRehash($algorithm, $currentParameters); } /** * Rehash a password if needed (requires plain password) */ public function rehashIfNeeded( #[SensitiveParameter] string $plainPassword, HashedPassword $currentHash, ?string $algorithm = null, ?string $securityLevel = null ): ?HashedPassword { if (! $this->needsRehash($currentHash, $algorithm, $securityLevel)) { return null; } return $this->hash($plainPassword, $algorithm, $securityLevel); } /** * Validate password strength */ public function validatePasswordStrength( #[SensitiveParameter] string $plainPassword ): PasswordValidationResult { $errors = []; $warnings = []; $score = 100; $length = mb_strlen($plainPassword); // Length validation if ($length < self::MIN_PASSWORD_LENGTH) { $errors[] = sprintf('Password must be at least %d characters long', self::MIN_PASSWORD_LENGTH); $score -= 50; } elseif ($length < 12) { $warnings[] = 'Consider using a longer password (12+ characters recommended)'; $score -= 10; } // Complexity checks $hasUppercase = preg_match('/[A-Z]/', $plainPassword); $hasLowercase = preg_match('/[a-z]/', $plainPassword); $hasNumbers = preg_match('/[0-9]/', $plainPassword); $hasSpecialChars = preg_match('/[^A-Za-z0-9]/', $plainPassword); $complexityCount = (int)$hasUppercase + (int)$hasLowercase + (int)$hasNumbers + (int)$hasSpecialChars; if ($complexityCount < 2) { $errors[] = 'Password must contain at least 2 different character types'; $score -= 30; } elseif ($complexityCount < 3) { $warnings[] = 'Consider using more character types for better security'; $score -= 10; } // Common patterns if ($this->containsCommonPatterns($plainPassword)) { $warnings[] = 'Password contains common patterns'; $score -= 20; } // Sequential characters if ($this->hasSequentialCharacters($plainPassword)) { $warnings[] = 'Avoid sequential characters (e.g., "123", "abc")'; $score -= 15; } // Repeated characters if ($this->hasExcessiveRepeatedCharacters($plainPassword)) { $warnings[] = 'Avoid excessive character repetition'; $score -= 10; } $score = max(0, $score); return new PasswordValidationResult( isValid: empty($errors), errors: $errors, warnings: $warnings, strengthScore: $score, strength: $this->calculateStrengthFromScore($score) ); } /** * Generate a secure random password */ public function generateSecurePassword( int $length = 16, bool $includeUppercase = true, bool $includeLowercase = true, bool $includeNumbers = true, bool $includeSpecialChars = true, string $excludeChars = '' ): string { if ($length < self::MIN_PASSWORD_LENGTH) { throw new InvalidArgumentException( sprintf('Password length must be at least %d', self::MIN_PASSWORD_LENGTH) ); } if ($length > self::MAX_PASSWORD_LENGTH) { throw new InvalidArgumentException( sprintf('Password length cannot exceed %d', self::MAX_PASSWORD_LENGTH) ); } $charset = ''; if ($includeUppercase) { $charset .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; } if ($includeLowercase) { $charset .= 'abcdefghijklmnopqrstuvwxyz'; } if ($includeNumbers) { $charset .= '0123456789'; } if ($includeSpecialChars) { $charset .= '!@#$%^&*()-_=+[]{}|;:,.<>?/~`'; } if (empty($charset)) { throw new InvalidArgumentException('At least one character type must be included'); } // Remove excluded characters if (! empty($excludeChars)) { $charset = str_replace(str_split($excludeChars), '', $charset); } $password = ''; $charsetLength = strlen($charset); for ($i = 0; $i < $length; $i++) { $randomIndex = random_int(0, $charsetLength - 1); $password .= $charset[$randomIndex]; } return $password; } /** * Get parameters for security level * @return array */ private function getParametersForLevel(string $algorithm, string $level): array { try { $params = $this->kdf->getRecommendedParameters($algorithm, $level); return match ($algorithm) { 'argon2id' => [ 'memory_cost' => $params['memory_cost'] ?? 65536, 'time_cost' => $params['time_cost'] ?? 4, 'threads' => $params['threads'] ?? 3, 'key_length' => $params['key_length'] ?? 32, ], 'pbkdf2-sha256', 'pbkdf2-sha512' => [ 'iterations' => $params['iterations'] ?? 100000, 'key_length' => $params['key_length'] ?? 32, ], 'scrypt' => [ 'cost_parameter' => $params['cost_parameter'] ?? 16384, 'block_size' => $params['block_size'] ?? 8, 'parallelization' => $params['parallelization'] ?? 1, 'key_length' => $params['key_length'] ?? 32, ], default => [] }; } catch (\Exception) { // Fallback to standard parameters return $this->getDefaultParameters($algorithm); } } /** * Get default parameters for algorithm * @return array */ private function getDefaultParameters(string $algorithm): array { return match ($algorithm) { 'argon2id' => [ 'memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3, 'key_length' => 32, ], 'pbkdf2-sha256', 'pbkdf2-sha512' => [ 'iterations' => 100000, 'key_length' => 32, ], 'scrypt' => [ 'cost_parameter' => 16384, 'block_size' => 8, 'parallelization' => 1, 'key_length' => 32, ], default => [] }; } /** * Validate password */ private function validatePassword(#[SensitiveParameter] string $password): void { if (empty($password)) { throw new InvalidArgumentException('Password cannot be empty'); } $length = mb_strlen($password); if ($length < self::MIN_PASSWORD_LENGTH) { throw new InvalidArgumentException( sprintf('Password must be at least %d characters long', self::MIN_PASSWORD_LENGTH) ); } if ($length > self::MAX_PASSWORD_LENGTH) { throw new InvalidArgumentException( sprintf('Password cannot exceed %d characters', self::MAX_PASSWORD_LENGTH) ); } } /** * Validate configuration */ private function validateConfiguration(): void { $supportedAlgorithms = ['argon2id', 'pbkdf2-sha256', 'pbkdf2-sha512', 'scrypt']; if (! in_array($this->defaultAlgorithm, $supportedAlgorithms, true)) { throw new InvalidArgumentException( sprintf('Unsupported algorithm: %s', $this->defaultAlgorithm) ); } $supportedLevels = [self::LEVEL_LOW, self::LEVEL_STANDARD, self::LEVEL_HIGH]; if (! in_array($this->defaultSecurityLevel, $supportedLevels, true)) { throw new InvalidArgumentException( sprintf('Unsupported security level: %s', $this->defaultSecurityLevel) ); } } /** * Check for common patterns */ private function containsCommonPatterns(#[SensitiveParameter] string $password): bool { $commonPatterns = [ 'password', '123456', 'qwerty', 'admin', 'letmein', 'welcome', 'monkey', 'dragon', 'master', 'abc123', ]; $lowerPassword = strtolower($password); foreach ($commonPatterns as $pattern) { if (str_contains($lowerPassword, $pattern)) { return true; } } return false; } /** * Check for sequential characters */ private function hasSequentialCharacters(#[SensitiveParameter] string $password): bool { $sequences = [ '012', '123', '234', '345', '456', '567', '678', '789', 'abc', 'bcd', 'cde', 'def', 'efg', 'fgh', 'ghi', 'hij', 'ijk', 'jkl', 'klm', 'lmn', 'mno', 'nop', 'opq', 'pqr', 'qrs', 'rst', 'stu', 'tuv', 'uvw', 'vwx', 'wxy', 'xyz', ]; $lowerPassword = strtolower($password); foreach ($sequences as $sequence) { if (str_contains($lowerPassword, $sequence)) { return true; } } return false; } /** * Check for excessive repeated characters */ private function hasExcessiveRepeatedCharacters(#[SensitiveParameter] string $password): bool { // Check for 3+ repeated characters return preg_match('/(.)\1{2,}/', $password) === 1; } /** * Calculate strength from score */ private function calculateStrengthFromScore(int $score): PasswordStrength { return match (true) { $score >= 90 => PasswordStrength::VERY_STRONG, $score >= 70 => PasswordStrength::STRONG, $score >= 50 => PasswordStrength::MODERATE, $score >= 30 => PasswordStrength::WEAK, default => PasswordStrength::WEAK }; } /** * Create hasher with custom configuration */ public static function create( KeyDerivationFunction $kdf, string $algorithm = 'argon2id', string $securityLevel = self::LEVEL_STANDARD ): self { return new self($kdf, $algorithm, $securityLevel); } }