Files
michaelschiemer/tests/Unit/Framework/Cryptography/CryptographicUtilitiesTest.php
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

268 lines
9.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Cryptography\CryptographicUtilities;
use App\Framework\Random\SecureRandomGenerator;
it('performs timing-safe string comparison', function () {
$utils = new CryptographicUtilities(new SecureRandomGenerator());
$string1 = 'secret-value';
$string2 = 'secret-value';
$string3 = 'different-value';
expect($utils->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);
});