Files
desk-moloni/modules/desk_moloni/tests/security/EncryptionSecurityTest.php
Emanuel Almeida 8c4f68576f chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow
- Standardized file signatures to Descomplicar® format
- Updated development configuration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 01:27:37 +01:00

402 lines
15 KiB
PHP

/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
declare(strict_types=1);
namespace DeskMoloni\Tests\Security;
use PHPUnit\Framework\TestCase;
/**
* Security Test: Encryption and Data Protection
*
* This test MUST FAIL initially as part of TDD methodology.
* Tests encryption implementation and security vulnerabilities.
*
* @group security
* @group encryption
*/
class EncryptionSecurityTest extends TestCase
{
private array $testConfig;
private \DeskMoloni\Encryption $encryption;
protected function setUp(): void
{
global $testConfig;
$this->testConfig = $testConfig;
// This will fail initially until Encryption class is implemented
$this->encryption = new \DeskMoloni\Encryption($testConfig['encryption']);
}
/**
* Test OAuth token encryption and decryption
* This test will initially fail until encryption implementation exists
*/
public function testOAuthTokenEncryption(): void
{
$originalToken = 'oauth_access_token_example_12345';
// Test encryption
$encrypted = $this->encryption->encrypt($originalToken);
$this->assertIsString($encrypted);
$this->assertNotEquals($originalToken, $encrypted);
$this->assertGreaterThan(strlen($originalToken), strlen($encrypted));
// Test decryption
$decrypted = $this->encryption->decrypt($encrypted);
$this->assertEquals($originalToken, $decrypted);
}
/**
* Test encryption key rotation security
*/
public function testEncryptionKeyRotation(): void
{
$sensitiveData = 'client_secret_sensitive_data';
// Encrypt with current key
$encrypted1 = $this->encryption->encrypt($sensitiveData);
// Simulate key rotation
$newKey = bin2hex(random_bytes(32));
$this->encryption->rotateKey($newKey);
// Should still be able to decrypt old data
$decrypted1 = $this->encryption->decrypt($encrypted1);
$this->assertEquals($sensitiveData, $decrypted1);
// New encryptions should use new key
$encrypted2 = $this->encryption->encrypt($sensitiveData);
$this->assertNotEquals($encrypted1, $encrypted2);
// Both should decrypt to same value
$decrypted2 = $this->encryption->decrypt($encrypted2);
$this->assertEquals($sensitiveData, $decrypted2);
}
/**
* Test encryption algorithm security (AES-256-GCM)
*/
public function testEncryptionAlgorithmSecurity(): void
{
$testData = 'sensitive_api_credentials';
// Test multiple encryptions produce different results (due to IV)
$encrypted1 = $this->encryption->encrypt($testData);
$encrypted2 = $this->encryption->encrypt($testData);
$this->assertNotEquals($encrypted1, $encrypted2, 'Same plaintext should produce different ciphertext due to random IV');
// Both should decrypt to same value
$this->assertEquals($testData, $this->encryption->decrypt($encrypted1));
$this->assertEquals($testData, $this->encryption->decrypt($encrypted2));
// Test encryption metadata
$metadata = $this->encryption->getEncryptionMetadata($encrypted1);
$this->assertIsArray($metadata);
$this->assertArrayHasKey('algorithm', $metadata);
$this->assertArrayHasKey('iv_length', $metadata);
$this->assertArrayHasKey('tag_length', $metadata);
$this->assertEquals('AES-256-GCM', $metadata['algorithm']);
$this->assertEquals(12, $metadata['iv_length']); // GCM standard IV length
$this->assertEquals(16, $metadata['tag_length']); // GCM tag length
}
/**
* Test encryption key strength requirements
*/
public function testEncryptionKeyStrength(): void
{
// Test weak key rejection
$weakKeys = [
'weak',
'12345678',
str_repeat('a', 16),
'password123'
];
foreach ($weakKeys as $weakKey) {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Encryption key does not meet security requirements');
new \DeskMoloni\Encryption(['key' => $weakKey, 'cipher' => 'AES-256-GCM']);
}
// Test strong key acceptance
$strongKey = bin2hex(random_bytes(32)); // 256-bit key
$strongEncryption = new \DeskMoloni\Encryption(['key' => $strongKey, 'cipher' => 'AES-256-GCM']);
$this->assertInstanceOf(\DeskMoloni\Encryption::class, $strongEncryption);
}
/**
* Test encryption tampering detection
*/
public function testEncryptionTamperingDetection(): void
{
$originalData = 'tamper_test_data';
$encrypted = $this->encryption->encrypt($originalData);
// Test various tampering scenarios
$tamperedVersions = [
substr($encrypted, 0, -1), // Remove last character
$encrypted . 'x', // Add character
substr($encrypted, 1), // Remove first character
str_replace('A', 'B', $encrypted, 1) // Change one character
];
foreach ($tamperedVersions as $tampered) {
$this->expectException(\DeskMoloni\Exceptions\EncryptionException::class);
$this->expectExceptionMessage('Data integrity verification failed');
$this->encryption->decrypt($tampered);
}
}
/**
* Test secure configuration storage
*/
public function testSecureConfigurationStorage(): void
{
$configManager = new \DeskMoloni\ConfigManager($this->encryption);
// Test storing sensitive configuration
$sensitiveConfig = [
'moloni_client_secret' => 'very_secret_value',
'oauth_refresh_token' => 'refresh_token_12345',
'webhook_secret' => 'webhook_secret_key'
];
foreach ($sensitiveConfig as $key => $value) {
$configManager->set($key, $value, true); // true = encrypt
}
// Verify data is encrypted in storage
$pdo = new \PDO(
"mysql:host={$this->testConfig['database']['hostname']};dbname={$this->testConfig['database']['database']}",
$this->testConfig['database']['username'],
$this->testConfig['database']['password'],
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
);
foreach ($sensitiveConfig as $key => $originalValue) {
$stmt = $pdo->prepare("SELECT setting_value, encrypted FROM tbl_desk_moloni_config WHERE setting_key = ?");
$stmt->execute([$key]);
$stored = $stmt->fetch();
$this->assertNotFalse($stored, "Config {$key} should be stored");
$this->assertEquals(1, $stored['encrypted'], "Config {$key} should be marked as encrypted");
$this->assertNotEquals($originalValue, $stored['setting_value'], "Config {$key} should not be stored in plaintext");
// Verify retrieval works
$retrieved = $configManager->get($key);
$this->assertEquals($originalValue, $retrieved, "Config {$key} should decrypt correctly");
}
}
/**
* Test password/token validation and security
*/
public function testPasswordTokenValidation(): void
{
$validator = new \DeskMoloni\SecurityValidator();
// Test OAuth token validation
$validTokens = [
'valid_oauth_token_123456789',
'Bearer_token_abcdef123456',
str_repeat('a', 64) // Long alphanumeric token
];
$invalidTokens = [
'', // Empty
'short', // Too short
'token with spaces', // Contains spaces
'token<script>', // Contains dangerous characters
str_repeat('a', 1000) // Too long
];
foreach ($validTokens as $token) {
$this->assertTrue($validator->isValidOAuthToken($token), "Token should be valid: {$token}");
}
foreach ($invalidTokens as $token) {
$this->assertFalse($validator->isValidOAuthToken($token), "Token should be invalid: {$token}");
}
}
/**
* Test SQL injection prevention in encrypted data
*/
public function testSqlInjectionPrevention(): void
{
$maliciousInputs = [
"'; DROP TABLE tbl_desk_moloni_config; --",
"1' OR '1'='1",
"'; UPDATE tbl_desk_moloni_config SET setting_value='hacked'; --",
"<script>alert('xss')</script>",
"../../etc/passwd"
];
$configManager = new \DeskMoloni\ConfigManager($this->encryption);
foreach ($maliciousInputs as $maliciousInput) {
// Should be able to store and retrieve malicious input safely
$configManager->set('test_malicious', $maliciousInput, true);
$retrieved = $configManager->get('test_malicious');
$this->assertEquals($maliciousInput, $retrieved, 'Malicious input should be safely stored and retrieved');
// Verify no SQL injection occurred
$pdo = new \PDO(
"mysql:host={$this->testConfig['database']['hostname']};dbname={$this->testConfig['database']['database']}",
$this->testConfig['database']['username'],
$this->testConfig['database']['password'],
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
);
$stmt = $pdo->query("SELECT COUNT(*) as count FROM tbl_desk_moloni_config");
$count = $stmt->fetch();
$this->assertGreaterThan(0, $count['count'], 'Table should not be dropped or corrupted');
}
}
/**
* Test timing attack resistance
*/
public function testTimingAttackResistance(): void
{
$correctPassword = 'correct_password_123';
$incorrectPasswords = [
'wrong_password_123',
'correct_password_124', // Very similar
'x', // Very different length
str_repeat('x', strlen($correctPassword)) // Same length, different content
];
$validator = new \DeskMoloni\SecurityValidator();
// Measure timing for correct password
$startTime = microtime(true);
$validator->verifyPassword($correctPassword, password_hash($correctPassword, PASSWORD_ARGON2ID));
$correctTime = microtime(true) - $startTime;
// Measure timing for incorrect passwords
$incorrectTimes = [];
foreach ($incorrectPasswords as $incorrectPassword) {
$startTime = microtime(true);
$validator->verifyPassword($incorrectPassword, password_hash($correctPassword, PASSWORD_ARGON2ID));
$incorrectTimes[] = microtime(true) - $startTime;
}
// Calculate timing variance
$avgIncorrectTime = array_sum($incorrectTimes) / count($incorrectTimes);
$timingDifference = abs($correctTime - $avgIncorrectTime);
$maxAllowedDifference = 0.01; // 10ms tolerance
$this->assertLessThan(
$maxAllowedDifference,
$timingDifference,
'Password verification should be resistant to timing attacks'
);
}
/**
* Test secure random generation
*/
public function testSecureRandomGeneration(): void
{
$randomGenerator = new \DeskMoloni\SecureRandom();
// Test token generation
$tokens = [];
for ($i = 0; $i < 100; $i++) {
$token = $randomGenerator->generateToken(32);
$this->assertEquals(32, strlen($token));
$this->assertNotContains($token, $tokens, 'Generated tokens should be unique');
$this->assertMatchesRegularExpression('/^[a-zA-Z0-9]+$/', $token, 'Token should be alphanumeric');
$tokens[] = $token;
}
// Test cryptographic randomness quality
$randomBytes = $randomGenerator->generateBytes(1000);
$this->assertEquals(1000, strlen($randomBytes));
// Basic entropy test - should not have long runs of same byte
$maxRun = 0;
$currentRun = 1;
$prevByte = ord($randomBytes[0]);
for ($i = 1; $i < strlen($randomBytes); $i++) {
$currentByte = ord($randomBytes[$i]);
if ($currentByte === $prevByte) {
$currentRun++;
$maxRun = max($maxRun, $currentRun);
} else {
$currentRun = 1;
}
$prevByte = $currentByte;
}
$this->assertLessThan(10, $maxRun, 'Random data should not have long runs of identical bytes');
}
/**
* Test data sanitization and validation
*/
public function testDataSanitizationAndValidation(): void
{
$sanitizer = new \DeskMoloni\DataSanitizer();
$testCases = [
// [input, expected_output, description]
['<script>alert("xss")</script>', 'alert("xss")', 'Should remove script tags'],
['SELECT * FROM users', 'SELECT * FROM users', 'Should allow safe SQL in strings'],
['user@example.com', 'user@example.com', 'Should preserve valid email'],
['user@<script>evil</script>.com', 'user@evil.com', 'Should sanitize email with XSS'],
['+351910000000', '+351910000000', 'Should preserve valid phone'],
['javascript:alert(1)', 'alert(1)', 'Should remove javascript protocol'],
["'; DROP TABLE users; --", "'; DROP TABLE users; --", 'Should escape SQL injection attempts']
];
foreach ($testCases as [$input, $expectedOutput, $description]) {
$sanitized = $sanitizer->sanitizeString($input);
$this->assertEquals($expectedOutput, $sanitized, $description);
}
// Test specific field validation
$this->assertTrue($sanitizer->isValidEmail('test@example.com'));
$this->assertFalse($sanitizer->isValidEmail('invalid-email'));
$this->assertTrue($sanitizer->isValidPhone('+351910000000'));
$this->assertFalse($sanitizer->isValidPhone('not-a-phone'));
$this->assertTrue($sanitizer->isValidVAT('123456789'));
$this->assertFalse($sanitizer->isValidVAT('invalid-vat'));
}
protected function tearDown(): void
{
// Clean up test configuration
$pdo = new \PDO(
"mysql:host={$this->testConfig['database']['hostname']};dbname={$this->testConfig['database']['database']}",
$this->testConfig['database']['username'],
$this->testConfig['database']['password'],
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
);
$pdo->exec("DELETE FROM tbl_desk_moloni_config WHERE setting_key LIKE 'test_%' OR setting_key IN ('moloni_client_secret', 'oauth_refresh_token', 'webhook_secret')");
}
}