generateSalt(32); expect(strlen($salt))->toBe(32); }); it('throws exception for too short salt', function () { $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); expect(fn () => $kdf->generateSalt(8)) ->toThrow(InvalidArgumentException::class, 'Salt length must be at least 16 bytes'); }); it('derives key using PBKDF2', function () { $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); $password = 'test-password'; $salt = $kdf->generateSalt(); $derivedKey = $kdf->pbkdf2($password, $salt, 10000, 32); expect($derivedKey)->toBeInstanceOf(DerivedKey::class); expect($derivedKey->getAlgorithm())->toBe('pbkdf2-sha256'); expect($derivedKey->getKeyLength())->toBe(32); expect($derivedKey->getIterations())->toBe(10000); expect(strlen($derivedKey->getKey()))->toBe(32); }); it('derives key using Argon2ID if available', function () { if (! function_exists('sodium_crypto_pwhash') || ! defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID')) { $this->markTestSkipped('Sodium extension or Argon2ID constant not available'); } $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); $password = 'test-password'; $salt = str_repeat("\x00", 32); // Argon2ID needs exactly 32 bytes $derivedKey = $kdf->argon2id($password, $salt, 65536, 4, 3, 32); expect($derivedKey)->toBeInstanceOf(DerivedKey::class); expect($derivedKey->getAlgorithm())->toBe('argon2id'); expect($derivedKey->getKeyLength())->toBe(32); expect(strlen($derivedKey->getKey()))->toBe(32); }); it('derives key using scrypt if available', function () { if (! function_exists('scrypt')) { $this->markTestSkipped('scrypt function not available'); } $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); $password = 'test-password'; $salt = $kdf->generateSalt(); $derivedKey = $kdf->scrypt($password, $salt, 16384, 8, 1, 32); expect($derivedKey)->toBeInstanceOf(DerivedKey::class); expect($derivedKey->getAlgorithm())->toBe('scrypt'); expect($derivedKey->getKeyLength())->toBe(32); expect(strlen($derivedKey->getKey()))->toBe(32); }); it('verifies password correctly', function () { $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); $password = 'correct-password'; $wrongPassword = 'wrong-password'; $salt = $kdf->generateSalt(); $derivedKey = $kdf->pbkdf2($password, $salt, 10000, 32); expect($kdf->verify($password, $derivedKey))->toBeTrue(); expect($kdf->verify($wrongPassword, $derivedKey))->toBeFalse(); }); it('hashes password with automatic salt generation', function () { $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); $password = 'test-password'; $derivedKey = $kdf->hashPassword($password, 'pbkdf2-sha256'); expect($derivedKey)->toBeInstanceOf(DerivedKey::class); expect($derivedKey->getAlgorithm())->toBe('pbkdf2-sha256'); expect(strlen($derivedKey->getSalt()))->toBe(32); expect($kdf->verify($password, $derivedKey))->toBeTrue(); }); it('throws exception for empty password', function () { $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); $salt = $kdf->generateSalt(); expect(fn () => $kdf->pbkdf2('', $salt)) ->toThrow(InvalidArgumentException::class, 'Password cannot be empty'); }); it('throws exception for short salt', function () { $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); expect(fn () => $kdf->pbkdf2('password', 'short')) ->toThrow(InvalidArgumentException::class, 'Salt must be at least 16 bytes'); }); it('throws exception for too few iterations', function () { $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); $salt = $kdf->generateSalt(); expect(fn () => $kdf->pbkdf2('password', $salt, 5000)) ->toThrow(InvalidArgumentException::class, 'Iterations must be at least 10,000'); }); it('supports different hash algorithms', function () { $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); $password = 'test-password'; $salt = $kdf->generateSalt(); $sha256Key = $kdf->pbkdf2($password, $salt, 10000, 32, 'sha256'); $sha512Key = $kdf->pbkdf2($password, $salt, 10000, 32, 'sha512'); expect($sha256Key->getAlgorithm())->toBe('pbkdf2-sha256'); expect($sha512Key->getAlgorithm())->toBe('pbkdf2-sha512'); expect($sha256Key->getKey())->not->toBe($sha512Key->getKey()); }); it('provides recommended parameters', function () { $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); $params = $kdf->getRecommendedParameters('pbkdf2-sha256', 'standard'); expect($params)->toHaveKey('iterations'); expect($params)->toHaveKey('key_length'); expect($params['iterations'])->toBe(100000); expect($params['key_length'])->toBe(32); }); it('throws exception for unsupported algorithm in recommendations', function () { $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); expect(fn () => $kdf->getRecommendedParameters('unsupported', 'standard')) ->toThrow(InvalidArgumentException::class); }); it('creates instance using factory method', function () { $randomGen = new SecureRandomGenerator(); $kdf = KeyDerivationFunction::create($randomGen); expect($kdf)->toBeInstanceOf(KeyDerivationFunction::class); }); it('generates different keys for same password with different salts', function () { $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); $password = 'same-password'; $salt1 = $kdf->generateSalt(); $salt2 = $kdf->generateSalt(); $key1 = $kdf->pbkdf2($password, $salt1); $key2 = $kdf->pbkdf2($password, $salt2); expect($key1->getKey())->not->toBe($key2->getKey()); }); it('generates consistent keys for same inputs', function () { $kdf = new KeyDerivationFunction(new SecureRandomGenerator()); $password = 'test-password'; $salt = str_repeat('a', 16); $key1 = $kdf->pbkdf2($password, $salt, 10000, 32); $key2 = $kdf->pbkdf2($password, $salt, 10000, 32); expect($key1->getKey())->toBe($key2->getKey()); });