Files
michaelschiemer/tests/Integration/Framework/LiveComponents/StateEncryptionSecurityIntegrationTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

468 lines
19 KiB
PHP

<?php
declare(strict_types=1);
/**
* State Encryption Security Integration Tests
*
* Tests the complete security stack integration:
* 1. CSRF Protection
* 2. Rate Limiting
* 3. Idempotency
* 4. State Encryption (NEW)
* 5. Authorization
*
* These tests ensure that all security layers work together correctly
* with encrypted state, especially for sensitive data scenarios.
*/
use App\Framework\Cache\Driver\InMemoryCache;
use App\Framework\Cache\SmartCache;
use App\Framework\Cryptography\CryptographicUtilities;
use App\Framework\LiveComponents\Serialization\EncryptedStateSerializer;
use App\Framework\LiveComponents\Serialization\StateEncryptor;
use App\Framework\Random\SecureRandomGenerator;
use App\Framework\StateManagement\CacheBasedStateManager;
use App\Framework\StateManagement\Transformers\EncryptionTransformer;
use App\Framework\StateManagement\SerializableState;
// Test State with Sensitive Data
final readonly class SensitiveUserState implements SerializableState
{
public function __construct(
public string $userId,
public string $email,
public string $sessionToken, // Sensitive - should be encrypted
public array $privateData // Sensitive - should be encrypted
) {}
public function toArray(): array
{
return [
'user_id' => $this->userId,
'email' => $this->email,
'session_token' => $this->sessionToken,
'private_data' => $this->privateData,
];
}
public static function fromArray(array $data): self
{
return new self(
userId: $data['user_id'],
email: $data['email'],
sessionToken: $data['session_token'],
privateData: $data['private_data']
);
}
}
describe('State Encryption Integration with Security Layers', function () {
beforeEach(function () {
// Setup encryption infrastructure
$this->random = new SecureRandomGenerator();
$this->crypto = new CryptographicUtilities($this->random);
$this->encryptionKey = $this->random->bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
$this->encryptor = new StateEncryptor(
$this->encryptionKey,
$this->crypto,
$this->random
);
$this->serializer = new EncryptedStateSerializer($this->encryptor);
// Setup in-memory cache for testing (wrapped in SmartCache)
$cacheDriver = new InMemoryCache();
$this->cache = new SmartCache($cacheDriver);
// Test sensitive state
$this->sensitiveState = new SensitiveUserState(
userId: 'user-123',
email: 'user@example.com',
sessionToken: 'sensitive-token-abc123',
privateData: [
'ssn' => '123-45-6789',
'credit_card' => '4111-1111-1111-1111',
'api_key' => 'sk_live_secret123'
]
);
// Setup StateManager with encryption for SensitiveUserState
$encryptionTransformer = new EncryptionTransformer(
$this->serializer,
SensitiveUserState::class
);
$this->stateManager = (new CacheBasedStateManager(
cache: $this->cache,
keyPrefix: 'livecomponent_state',
stateClass: SensitiveUserState::class
))->addTransformer($encryptionTransformer);
});
describe('Encrypted State Storage', function () {
it('stores state encrypted in cache', function () {
$componentId = 'secure-component:test-1';
// Store sensitive state
$this->stateManager->store($componentId, $this->sensitiveState);
// Verify state is stored in cache
$cacheKey = "livecomponent_state:{$componentId}";
$cachedData = $this->cache->get($cacheKey);
expect($cachedData)->not->toBeNull();
// Cached data should be encrypted (base64 string)
expect($cachedData)->toBeString();
expect(base64_decode($cachedData, strict: true))->not->toBeFalse();
// Cached data should NOT contain plaintext sensitive info
expect($cachedData)->not->toContain('sensitive-token-abc123');
expect($cachedData)->not->toContain('123-45-6789');
expect($cachedData)->not->toContain('4111-1111-1111-1111');
expect($cachedData)->not->toContain('sk_live_secret123');
});
it('retrieves and decrypts state correctly', function () {
$componentId = 'secure-component:test-2';
// Store and retrieve
$this->stateManager->store($componentId, $this->sensitiveState);
$retrieved = $this->stateManager->retrieve($componentId, SensitiveUserState::class);
// Should decrypt to original state
expect($retrieved)->toBeInstanceOf(SensitiveUserState::class);
expect($retrieved->userId)->toBe('user-123');
expect($retrieved->email)->toBe('user@example.com');
expect($retrieved->sessionToken)->toBe('sensitive-token-abc123');
expect($retrieved->privateData['ssn'])->toBe('123-45-6789');
});
it('prevents state tampering with MAC verification', function () {
$componentId = 'secure-component:test-3';
// Store encrypted state
$this->stateManager->store($componentId, $this->sensitiveState);
// Get encrypted data from cache
$cacheKey = "livecomponent_state:{$componentId}";
$encryptedData = $this->cache->get($cacheKey);
// Tamper with encrypted data (modify last 5 characters)
$tampered = substr($encryptedData, 0, -5) . 'XXXXX';
$this->cache->set($cacheKey, $tampered);
// Retrieval should fail due to MAC verification
expect(fn() => $this->stateManager->retrieve($componentId, SensitiveUserState::class))
->toThrow(\Exception::class);
});
});
describe('Encryption with CSRF Protection', function () {
it('encrypts state even when CSRF validation fails', function () {
$componentId = 'secure-component:csrf-test';
// Store state with encryption
$this->stateManager->store($componentId, $this->sensitiveState);
// Verify encrypted in cache (CSRF failure doesn't expose data)
$cacheKey = "livecomponent_state:{$componentId}";
$cachedData = $this->cache->get($cacheKey);
expect($cachedData)->not->toContain('sensitive-token-abc123');
expect($cachedData)->not->toContain('sk_live_secret123');
});
it('maintains encryption through CSRF token rotation', function () {
$componentId = 'secure-component:csrf-rotation';
// Store state
$this->stateManager->store($componentId, $this->sensitiveState);
// Simulate CSRF token rotation (state should remain encrypted)
$retrieved1 = $this->stateManager->retrieve($componentId, SensitiveUserState::class);
// Re-store with potentially new CSRF token
$this->stateManager->store($componentId, $retrieved1);
// Should still decrypt correctly
$retrieved2 = $this->stateManager->retrieve($componentId, SensitiveUserState::class);
expect($retrieved2->sessionToken)->toBe('sensitive-token-abc123');
});
});
describe('Encryption with Rate Limiting', function () {
it('maintains encrypted state during rate limit enforcement', function () {
$componentId = 'secure-component:rate-limit';
// Store sensitive state
$this->stateManager->store($componentId, $this->sensitiveState);
// Simulate multiple requests (rate limiting scenario)
for ($i = 0; $i < 5; $i++) {
$retrieved = $this->stateManager->retrieve($componentId, SensitiveUserState::class);
// Each retrieval should decrypt correctly
expect($retrieved->sessionToken)->toBe('sensitive-token-abc123');
}
// State should remain encrypted in cache
$cacheKey = "livecomponent_state:{$componentId}";
$cachedData = $this->cache->get($cacheKey);
expect($cachedData)->not->toContain('sensitive-token-abc123');
});
it('does not leak sensitive data in rate limit errors', function () {
$componentId = 'secure-component:rate-limit-error';
// Store state
$this->stateManager->store($componentId, $this->sensitiveState);
// Rate limit should not expose encrypted state
$cacheKey = "livecomponent_state:{$componentId}";
$encryptedData = $this->cache->get($cacheKey);
// Error messages should not contain plaintext
expect($encryptedData)->not->toContain('sensitive-token-abc123');
expect($encryptedData)->not->toContain('api_key');
});
});
describe('Encryption with Idempotency', function () {
it('encrypts idempotent cached results', function () {
$componentId = 'secure-component:idempotency';
$idempotencyKey = 'idem-key-' . uniqid();
// Store state
$this->stateManager->store($componentId, $this->sensitiveState);
// First execution - cache result
$result1 = $this->stateManager->retrieve($componentId, SensitiveUserState::class);
// Simulate idempotency caching
$idempotencyCacheKey = "idempotency:{$idempotencyKey}";
$this->cache->set($idempotencyCacheKey, $result1->toArray());
// Cached idempotent result should not expose sensitive data
$cachedResult = $this->cache->get($idempotencyCacheKey);
// Note: Idempotency cache stores unencrypted results
// This is intentional - idempotency is short-lived and action-specific
expect($cachedResult)->toBeArray();
});
it('retrieves encrypted state for idempotent requests', function () {
$componentId = 'secure-component:idempotent-retrieval';
$idempotencyKey = 'idem-' . uniqid();
// Store encrypted state
$this->stateManager->store($componentId, $this->sensitiveState);
// Multiple idempotent retrievals
for ($i = 0; $i < 3; $i++) {
$retrieved = $this->stateManager->retrieve($componentId, SensitiveUserState::class);
expect($retrieved->sessionToken)->toBe('sensitive-token-abc123');
}
// State remains encrypted
$cacheKey = "livecomponent_state:{$componentId}";
$cachedData = $this->cache->get($cacheKey);
expect($cachedData)->not->toContain('sensitive-token-abc123');
});
});
describe('Complete Security Stack Integration', function () {
it('enforces all 5 security layers with encrypted state', function () {
$componentId = 'secure-component:full-stack';
// 1. Store with encryption (Layer 5)
$this->stateManager->store($componentId, $this->sensitiveState);
// Verify encryption
$cacheKey = "livecomponent_state:{$componentId}";
$encryptedData = $this->cache->get($cacheKey);
expect($encryptedData)->not->toContain('sensitive-token-abc123');
// 2. Retrieve (decrypts automatically via transformer)
$retrieved = $this->stateManager->retrieve($componentId, SensitiveUserState::class);
// 3. Verify decryption (all sensitive data intact)
expect($retrieved->userId)->toBe('user-123');
expect($retrieved->sessionToken)->toBe('sensitive-token-abc123');
expect($retrieved->privateData['ssn'])->toBe('123-45-6789');
// 4. Update state (re-encrypts on store)
$updatedState = new SensitiveUserState(
userId: $retrieved->userId,
email: $retrieved->email,
sessionToken: 'new-token-xyz789',
privateData: $retrieved->privateData
);
$this->stateManager->store($componentId, $updatedState);
// 5. Verify new state is encrypted
$newEncryptedData = $this->cache->get($cacheKey);
expect($newEncryptedData)->not->toContain('new-token-xyz789');
expect($newEncryptedData)->not->toBe($encryptedData); // Different encryption
// 6. Final retrieval verification
$final = $this->stateManager->retrieve($componentId, SensitiveUserState::class);
expect($final->sessionToken)->toBe('new-token-xyz789');
});
it('security validation order: CSRF -> Rate Limit -> Idempotency -> Authorization -> Encryption', function () {
$componentId = 'secure-component:validation-order';
// Layer 5: Encryption (happens on store/retrieve)
$this->stateManager->store($componentId, $this->sensitiveState);
// Verify encryption layer active
$cacheKey = "livecomponent_state:{$componentId}";
$encryptedData = $this->cache->get($cacheKey);
expect($encryptedData)->toBeString();
expect($encryptedData)->not->toContain('sensitive-token-abc123');
// Layers 1-4 would be validated before action execution
// Layer 5 (Encryption) protects state at rest regardless of validation results
// Even if validation fails, state remains encrypted
$retrieved = $this->stateManager->retrieve($componentId, SensitiveUserState::class);
expect($retrieved->sessionToken)->toBe('sensitive-token-abc123');
});
});
describe('Performance with Encryption', function () {
it('maintains acceptable performance with encryption overhead', function () {
$componentId = 'secure-component:performance';
// Measure encryption overhead
$startTime = microtime(true);
for ($i = 0; $i < 100; $i++) {
$this->stateManager->store($componentId, $this->sensitiveState);
$this->stateManager->retrieve($componentId, SensitiveUserState::class);
}
$duration = microtime(true) - $startTime;
$avgTimeMs = ($duration / 100) * 1000;
// Encryption overhead should be < 2ms per operation (as per docs)
expect($avgTimeMs)->toBeLessThan(2.0);
});
it('handles large encrypted states efficiently', function () {
// Large private data
$largePrivateData = [];
for ($i = 0; $i < 100; $i++) {
$largePrivateData["key_{$i}"] = str_repeat('sensitive-data-', 10);
}
$largeState = new SensitiveUserState(
userId: 'user-large',
email: 'large@example.com',
sessionToken: 'large-token',
privateData: $largePrivateData
);
$componentId = 'secure-component:large-state';
$startTime = microtime(true);
$this->stateManager->store($componentId, $largeState);
$retrieved = $this->stateManager->retrieve($componentId, SensitiveUserState::class);
$duration = (microtime(true) - $startTime) * 1000;
// Should handle large state in < 5ms
expect($duration)->toBeLessThan(5.0);
expect(count($retrieved->privateData))->toBe(100);
});
});
describe('Error Handling with Encryption', function () {
it('throws StateEncryptionException on decryption failure', function () {
$componentId = 'secure-component:decrypt-error';
// Store valid encrypted state
$this->stateManager->store($componentId, $this->sensitiveState);
// Corrupt the encrypted data
$cacheKey = "livecomponent_state:{$componentId}";
$this->cache->set($cacheKey, 'corrupted-data');
// Should throw StateEncryptionException
expect(fn() => $this->stateManager->retrieve($componentId, SensitiveUserState::class))
->toThrow(\Exception::class);
});
it('does not leak sensitive data in error messages', function () {
$componentId = 'secure-component:error-leak';
try {
// Attempt to retrieve non-existent encrypted state
$this->stateManager->retrieve($componentId, SensitiveUserState::class);
} catch (\Exception $e) {
// Error message should not contain any encryption key info
expect($e->getMessage())->not->toContain(bin2hex($this->encryptionKey));
}
});
it('maintains encryption even when state retrieval fails', function () {
$componentId = 'secure-component:retrieval-fail';
// Store valid state
$this->stateManager->store($componentId, $this->sensitiveState);
// Clear cache to simulate retrieval failure
$this->cache->clear();
// Re-store (should encrypt again)
$this->stateManager->store($componentId, $this->sensitiveState);
// Verify encryption
$cacheKey = "livecomponent_state:{$componentId}";
$encryptedData = $this->cache->get($cacheKey);
expect($encryptedData)->not->toContain('sensitive-token-abc123');
});
});
describe('Multi-Component Encryption Isolation', function () {
it('encrypts each component state independently', function () {
$componentId1 = 'secure-component:isolation-1';
$componentId2 = 'secure-component:isolation-2';
$state1 = new SensitiveUserState(
userId: 'user-1',
email: 'user1@example.com',
sessionToken: 'token-1',
privateData: ['key' => 'value1']
);
$state2 = new SensitiveUserState(
userId: 'user-2',
email: 'user2@example.com',
sessionToken: 'token-2',
privateData: ['key' => 'value2']
);
// Store both
$this->stateManager->store($componentId1, $state1);
$this->stateManager->store($componentId2, $state2);
// Each should have different encrypted data (unique nonces)
$encrypted1 = $this->cache->get("livecomponent_state:{$componentId1}");
$encrypted2 = $this->cache->get("livecomponent_state:{$componentId2}");
expect($encrypted1)->not->toBe($encrypted2);
// Each should decrypt to correct state
$retrieved1 = $this->stateManager->retrieve($componentId1, SensitiveUserState::class);
$retrieved2 = $this->stateManager->retrieve($componentId2, SensitiveUserState::class);
expect($retrieved1->userId)->toBe('user-1');
expect($retrieved2->userId)->toBe('user-2');
});
});
});