timingSafeEquals($string1, $string2))->toBeTrue(); expect($utils->timingSafeEquals($string1, $string3))->toBeFalse(); }); it('performs timing-safe array comparison', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $array1 = ['key1' => 'value1', 'key2' => 'value2']; $array2 = ['key2' => 'value2', 'key1' => 'value1']; // Different order $array3 = ['key1' => 'value1', 'key2' => 'different']; expect($utils->timingSafeArrayEquals($array1, $array2))->toBeTrue(); expect($utils->timingSafeArrayEquals($array1, $array3))->toBeFalse(); }); it('generates cryptographically secure nonce', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $nonce1 = $utils->generateNonce(32); $nonce2 = $utils->generateNonce(32); expect(strlen($nonce1))->toBe(32); expect(strlen($nonce2))->toBe(32); expect($nonce1)->not->toBe($nonce2); // Should be different }); it('generates initialization vector', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $iv1 = $utils->generateIv(16); $iv2 = $utils->generateIv(16); expect(strlen($iv1))->toBe(16); expect(strlen($iv2))->toBe(16); expect($iv1)->not->toBe($iv2); // Should be different }); it('validates entropy correctly', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); // Use known high-entropy data (256 unique bytes) $highEntropyData = ''; for ($i = 0; $i < 256; $i++) { $highEntropyData .= chr($i); } $lowEntropyData = str_repeat('a', 32); // Low entropy $emptyData = ''; expect($utils->validateEntropy($highEntropyData, 6.0))->toBeTrue(); // Lower threshold for realistic testing expect($utils->validateEntropy($lowEntropyData))->toBeFalse(); expect($utils->validateEntropy($emptyData))->toBeFalse(); }); it('calculates Shannon entropy', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $randomData = $utils->generateNonce(32); $uniformData = str_repeat('a', 32); $randomEntropy = $utils->calculateShannonEntropy($randomData); $uniformEntropy = $utils->calculateShannonEntropy($uniformData); expect($randomEntropy)->toBeGreaterThan($uniformEntropy); expect($uniformEntropy)->toBe(0.0); // Uniform data has zero entropy }); it('performs constant-time modular exponentiation if GMP available', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); if (! extension_loaded('gmp')) { expect(fn () => $utils->constantTimeModPow('5', '3', '13')) ->toThrow(InvalidArgumentException::class, 'GMP extension required'); } else { $result = $utils->constantTimeModPow('5', '3', '13'); // 5^3 mod 13 = 125 mod 13 = 8 expect($result)->toBe('8'); } }); it('generates cryptographically secure UUID v4', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $uuid1 = $utils->generateUuid4(); $uuid2 = $utils->generateUuid4(); // Check UUID format $pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i'; expect($uuid1)->toMatch($pattern); expect($uuid2)->toMatch($pattern); expect($uuid1)->not->toBe($uuid2); // Should be unique }); it('stretches keys correctly', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $key = 'password'; $salt = 'salt1234'; $stretched1 = $utils->stretchKey($key, $salt, 10000, 32); $stretched2 = $utils->stretchKey($key, $salt, 10000, 32); // Same parameters $stretched3 = $utils->stretchKey($key, 'differentsalt1234', 10000, 32); // Different salt expect(strlen($stretched1))->toBe(32); expect($stretched1)->toBe($stretched2); // Same inputs = same output expect($stretched1)->not->toBe($stretched3); // Different salt = different output }); it('XORs strings correctly', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $str1 = "\x01\x02\x03\x04"; $str2 = "\xFF\xFE\xFD\xFC"; $result = $utils->xorStrings($str1, $str2); expect($result)->toBe("\xFE\xFC\xFE\xF8"); // XOR result expect(strlen($result))->toBe(4); }); it('generates and removes PKCS#7 padding', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $data = 'Hello World!'; // 12 bytes $blockSize = 16; $padding = $utils->generatePadding($blockSize, strlen($data)); $paddedData = $data . $padding; expect(strlen($paddedData) % $blockSize)->toBe(0); // Should be block-aligned $unpaddedData = $utils->removePadding($paddedData); expect($unpaddedData)->toBe($data); }); it('generates bit string correctly', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $bitString = $utils->generateBitString(16); // 2 bytes = 16 bits expect(strlen($bitString))->toBe(16); expect($bitString)->toMatch('/^[01]+$/'); // Only 0s and 1s }); it('converts bit string to bytes', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $bitString = '0110100001101001'; // "hi" in binary $bytes = $utils->bitStringToBytes($bitString); expect($bytes)->toBe('hi'); }); it('validates key strength', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); // Use known high-entropy data (64 different bytes) $strongKey = ''; for ($i = 0; $i < 64; $i++) { $strongKey .= chr($i); } $weakKey = str_repeat('a', 32); // No entropy $shortKey = 'short'; // Too short expect($utils->validateKeyStrength($strongKey, 128))->toBeTrue(); expect($utils->validateKeyStrength($weakKey, 128))->toBeFalse(); expect($utils->validateKeyStrength($shortKey, 128))->toBeFalse(); }); it('generates deterministic UUID v5', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $namespace = str_repeat("\x00", 16); // Null namespace $name1 = 'test-name'; $name2 = 'test-name'; // Same name $name3 = 'different-name'; $uuid1 = $utils->generateUuid5($namespace, $name1); $uuid2 = $utils->generateUuid5($namespace, $name2); $uuid3 = $utils->generateUuid5($namespace, $name3); // Check UUID v5 format (version 5) $pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i'; expect($uuid1)->toMatch($pattern); expect($uuid1)->toBe($uuid2); // Same inputs = same UUID expect($uuid1)->not->toBe($uuid3); // Different name = different UUID }); it('performs constant-time array search', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $haystack = ['value1', 'value2', 'secret-value', 'value3']; expect($utils->constantTimeArraySearch($haystack, 'secret-value'))->toBeTrue(); expect($utils->constantTimeArraySearch($haystack, 'not-found'))->toBeFalse(); }); it('securely wipes memory', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); $sensitiveData = 'very-secret-password'; $utils->secureWipe($sensitiveData); expect($sensitiveData)->toBe(''); // Should be empty after wipe }); it('throws exception for too short nonce', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); expect(fn () => $utils->generateNonce(4)) ->toThrow(InvalidArgumentException::class, 'Nonce length must be at least 8 bytes'); }); it('throws exception for too short IV', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); expect(fn () => $utils->generateIv(4)) ->toThrow(InvalidArgumentException::class, 'IV length must be at least 8 bytes'); }); it('throws exception for too few key stretching iterations', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); expect(fn () => $utils->stretchKey('key', 'salt', 500)) ->toThrow(InvalidArgumentException::class, 'Iterations must be at least 1000'); }); it('throws exception for invalid bit string length', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); expect(fn () => $utils->generateBitString(12)) // Not divisible by 8 ->toThrow(InvalidArgumentException::class, 'Bit length must be divisible by 8'); }); it('throws exception for invalid bit string characters', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); expect(fn () => $utils->bitStringToBytes('01012345')) // Contains invalid characters ->toThrow(InvalidArgumentException::class, 'Bit string contains invalid characters'); }); it('throws exception for invalid UUID v5 namespace', function () { $utils = new CryptographicUtilities(new SecureRandomGenerator()); expect(fn () => $utils->generateUuid5('short', 'name')) // Namespace too short ->toThrow(InvalidArgumentException::class, 'Namespace must be 16 bytes'); }); it('creates instance using factory method', function () { $randomGen = new SecureRandomGenerator(); $utils = CryptographicUtilities::create($randomGen); expect($utils)->toBeInstanceOf(CryptographicUtilities::class); });