$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'); }); }); });