/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ 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", "../../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] ['', '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@.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')"); } }