256) { throw new InvalidArgumentException('Salt length cannot exceed 256 bytes'); } return $this->randomGenerator->bytes($length); } /** * Derive key using PBKDF2 */ public function pbkdf2( string $password, string $salt, int $iterations = 100000, int $keyLength = 32, string $algorithm = 'sha256' ): DerivedKey { if (empty($password)) { throw new InvalidArgumentException('Password cannot be empty'); } if (strlen($salt) < 16) { throw new InvalidArgumentException('Salt must be at least 16 bytes'); } if ($iterations < 10000) { throw new InvalidArgumentException('Iterations must be at least 10,000 for security'); } if ($keyLength < 16 || $keyLength > 256) { throw new InvalidArgumentException('Key length must be between 16 and 256 bytes'); } $supportedAlgorithms = ['sha256', 'sha512', 'sha384', 'sha224']; if (! in_array($algorithm, $supportedAlgorithms, true)) { throw new InvalidArgumentException('Unsupported hash algorithm'); } $derivedKey = hash_pbkdf2($algorithm, $password, $salt, $iterations, $keyLength, true); return new DerivedKey( key: $derivedKey, salt: $salt, algorithm: "pbkdf2-{$algorithm}", iterations: $iterations, keyLength: $keyLength ); } /** * Derive key using Argon2ID (recommended) */ public function argon2id( string $password, string $salt, int $memoryCost = 65536, // 64 MB int $timeCost = 4, int $threads = 3, int $keyLength = 32 ): DerivedKey { if (empty($password)) { throw new InvalidArgumentException('Password cannot be empty'); } if (strlen($salt) < 16) { throw new InvalidArgumentException('Salt must be at least 16 bytes'); } if ($memoryCost < 1024) { throw new InvalidArgumentException('Memory cost must be at least 1024 KB'); } if ($timeCost < 1) { throw new InvalidArgumentException('Time cost must be at least 1'); } if ($threads < 1 || $threads > 24) { throw new InvalidArgumentException('Threads must be between 1 and 24'); } if ($keyLength < 16 || $keyLength > 256) { throw new InvalidArgumentException('Key length must be between 16 and 256 bytes'); } if (! function_exists('sodium_crypto_pwhash')) { throw new InvalidArgumentException('Sodium extension required for Argon2ID'); } if (! defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID')) { throw new InvalidArgumentException('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID constant not available'); } $derivedKey = sodium_crypto_pwhash( $keyLength, $password, $salt, $timeCost, $memoryCost, SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID ); return new DerivedKey( key: $derivedKey, salt: $salt, algorithm: 'argon2id', iterations: $timeCost, keyLength: $keyLength, memoryCost: $memoryCost, threads: $threads ); } /** * Derive key using scrypt */ public function scrypt( string $password, string $salt, int $costParameter = 16384, // N int $blockSize = 8, // r int $parallelization = 1, // p int $keyLength = 32 ): DerivedKey { if (empty($password)) { throw new InvalidArgumentException('Password cannot be empty'); } if (strlen($salt) < 16) { throw new InvalidArgumentException('Salt must be at least 16 bytes'); } if ($costParameter < 1024) { throw new InvalidArgumentException('Cost parameter must be at least 1024'); } if ($blockSize < 1) { throw new InvalidArgumentException('Block size must be at least 1'); } if ($parallelization < 1) { throw new InvalidArgumentException('Parallelization must be at least 1'); } if ($keyLength < 16 || $keyLength > 256) { throw new InvalidArgumentException('Key length must be between 16 and 256 bytes'); } if (! function_exists('scrypt')) { throw new InvalidArgumentException('scrypt function not available'); } $derivedKey = scrypt( $password, $salt, $costParameter, $blockSize, $parallelization, $keyLength ); return new DerivedKey( key: $derivedKey, salt: $salt, algorithm: 'scrypt', iterations: $costParameter, keyLength: $keyLength, blockSize: $blockSize, parallelization: $parallelization ); } /** * Verify password against derived key */ public function verify(string $password, DerivedKey $derivedKey): bool { try { $verificationKey = match ($derivedKey->getAlgorithm()) { 'pbkdf2-sha256' => $this->pbkdf2( $password, $derivedKey->getSalt(), $derivedKey->getIterations(), $derivedKey->getKeyLength(), 'sha256' ), 'pbkdf2-sha512' => $this->pbkdf2( $password, $derivedKey->getSalt(), $derivedKey->getIterations(), $derivedKey->getKeyLength(), 'sha512' ), 'argon2id' => $this->argon2id( $password, $derivedKey->getSalt(), $derivedKey->getMemoryCost() ?? 65536, $derivedKey->getIterations(), $derivedKey->getThreads() ?? 3, $derivedKey->getKeyLength() ), 'scrypt' => $this->scrypt( $password, $derivedKey->getSalt(), $derivedKey->getIterations(), $derivedKey->getBlockSize() ?? 8, $derivedKey->getParallelization() ?? 1, $derivedKey->getKeyLength() ), default => throw new InvalidArgumentException('Unsupported algorithm: ' . $derivedKey->getAlgorithm()) }; return hash_equals($derivedKey->getKey(), $verificationKey->getKey()); } catch (\Exception) { return false; } } /** * Hash password with automatic salt generation (recommended for new passwords) */ public function hashPassword( string $password, string $algorithm = 'argon2id', array $options = [] ): DerivedKey { $salt = $this->generateSalt(32); return match ($algorithm) { 'pbkdf2-sha256' => $this->pbkdf2( $password, $salt, $options['iterations'] ?? 100000, $options['key_length'] ?? 32, 'sha256' ), 'pbkdf2-sha512' => $this->pbkdf2( $password, $salt, $options['iterations'] ?? 100000, $options['key_length'] ?? 32, 'sha512' ), 'argon2id' => $this->argon2id( $password, $salt, $options['memory_cost'] ?? 65536, $options['time_cost'] ?? 4, $options['threads'] ?? 3, $options['key_length'] ?? 32 ), 'scrypt' => $this->scrypt( $password, $salt, $options['cost_parameter'] ?? 16384, $options['block_size'] ?? 8, $options['parallelization'] ?? 1, $options['key_length'] ?? 32 ), default => throw new InvalidArgumentException("Unsupported algorithm: {$algorithm}") }; } /** * Get recommended parameters for different security levels * @return array */ public function getRecommendedParameters(string $algorithm, string $securityLevel = 'standard'): array { return match ([$algorithm, $securityLevel]) { ['pbkdf2-sha256', 'low'] => ['iterations' => 50000, 'key_length' => 32], ['pbkdf2-sha256', 'standard'] => ['iterations' => 100000, 'key_length' => 32], ['pbkdf2-sha256', 'high'] => ['iterations' => 200000, 'key_length' => 32], ['argon2id', 'low'] => ['memory_cost' => 32768, 'time_cost' => 3, 'threads' => 2, 'key_length' => 32], ['argon2id', 'standard'] => ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3, 'key_length' => 32], ['argon2id', 'high'] => ['memory_cost' => 131072, 'time_cost' => 6, 'threads' => 4, 'key_length' => 32], ['scrypt', 'low'] => ['cost_parameter' => 8192, 'block_size' => 8, 'parallelization' => 1, 'key_length' => 32], ['scrypt', 'standard'] => ['cost_parameter' => 16384, 'block_size' => 8, 'parallelization' => 1, 'key_length' => 32], ['scrypt', 'high'] => ['cost_parameter' => 32768, 'block_size' => 8, 'parallelization' => 2, 'key_length' => 32], default => throw new InvalidArgumentException("Unsupported algorithm or security level: {$algorithm}, {$securityLevel}") }; } /** * Factory method */ public static function create(RandomGenerator $randomGenerator): self { return new self($randomGenerator); } }