From d3197f44e73c228354d2b318eb674dee5b33a906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Thu, 15 Jan 2026 15:24:45 +0100 Subject: [PATCH 1/5] data protection: - use `Ecotone\DataProtection\Attribute\UsingSensitiveData` to define messages with sensitive data - use `Ecotone\DataProtection\Attribute\Sensitive` to mark properties with sensitive data - define encryption keys with `Ecotone\DataProtection\Configuration\DataProtectionConfiguration` - sensitive data will be encrypted right before its sended to queue and decrypted right after message is being retrieved from queue - data protection require JMSModule to be enabled --- composer.json | 7 ++ packages/DataProtection/.gitattributes | 7 ++ packages/DataProtection/.github/FUNDING.yml | 12 +++ .../.github/ISSUE_TEMPLATE/bug_report.md | 10 ++ packages/DataProtection/.gitignore | 9 ++ packages/DataProtection/LICENSE | 21 ++++ packages/DataProtection/LICENSE-ENTERPRISE | 3 + packages/DataProtection/README.md | 63 +++++++++++ packages/DataProtection/composer.json | 80 ++++++++++++++ packages/DataProtection/phpstan.neon | 4 + packages/DataProtection/phpunit.xml.dist | 20 ++++ .../src/Attribute/Sensitive.php | 9 ++ .../src/Attribute/UsingSensitiveData.php | 19 ++++ .../DataProtectionConfiguration.php | 37 +++++++ .../Configuration/DataProtectionModule.php | 88 +++++++++++++++ .../src/Obfuscator/MessageObfuscator.php | 44 ++++++++ .../src/Obfuscator/Obfuscator.php | 63 +++++++++++ .../src/OutboundDecryptionChannelBuilder.php | 35 ++++++ .../OutboundDecryptionChannelInterceptor.php | 37 +++++++ .../src/OutboundEncryptionChannelBuilder.php | 35 ++++++ .../OutboundEncryptionChannelInterceptor.php | 37 +++++++ .../FullyObfuscatedMessage.php | 18 ++++ .../PartiallyObfuscatedMessage.php | 21 ++++ .../TestCommandHandler.php | 22 ++++ .../tests/Fixture/ObfuscatedMessage.php | 15 +++ .../tests/Fixture/TestClass.php | 12 +++ .../DataProtection/tests/Fixture/TestEnum.php | 8 ++ .../ObfuscateAnnotatedMessagesTest.php | 74 +++++++++++++ .../tests/Unit/MessageObfuscatorTest.php | 101 ++++++++++++++++++ .../tests/Unit/ObfuscatorTest.php | 52 +++++++++ ...utboundSerializationChannelInterceptor.php | 10 +- .../src/Messaging/Config/ModuleClassList.php | 5 + .../Messaging/Config/ModulePackageList.php | 3 + .../AutoCollectionConversionService.php | 1 - .../DeserializingConverter.php | 6 -- packages/local_packages.json | 4 + 36 files changed, 980 insertions(+), 12 deletions(-) create mode 100644 packages/DataProtection/.gitattributes create mode 100644 packages/DataProtection/.github/FUNDING.yml create mode 100644 packages/DataProtection/.github/ISSUE_TEMPLATE/bug_report.md create mode 100644 packages/DataProtection/.gitignore create mode 100644 packages/DataProtection/LICENSE create mode 100644 packages/DataProtection/LICENSE-ENTERPRISE create mode 100644 packages/DataProtection/README.md create mode 100644 packages/DataProtection/composer.json create mode 100644 packages/DataProtection/phpstan.neon create mode 100644 packages/DataProtection/phpunit.xml.dist create mode 100644 packages/DataProtection/src/Attribute/Sensitive.php create mode 100644 packages/DataProtection/src/Attribute/UsingSensitiveData.php create mode 100644 packages/DataProtection/src/Configuration/DataProtectionConfiguration.php create mode 100644 packages/DataProtection/src/Configuration/DataProtectionModule.php create mode 100644 packages/DataProtection/src/Obfuscator/MessageObfuscator.php create mode 100644 packages/DataProtection/src/Obfuscator/Obfuscator.php create mode 100644 packages/DataProtection/src/OutboundDecryptionChannelBuilder.php create mode 100644 packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php create mode 100644 packages/DataProtection/src/OutboundEncryptionChannelBuilder.php create mode 100644 packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestCommandHandler.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscatedMessage.php create mode 100644 packages/DataProtection/tests/Fixture/TestClass.php create mode 100644 packages/DataProtection/tests/Fixture/TestEnum.php create mode 100644 packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php create mode 100644 packages/DataProtection/tests/Unit/MessageObfuscatorTest.php create mode 100644 packages/DataProtection/tests/Unit/ObfuscatorTest.php diff --git a/composer.json b/composer.json index 0d8c2ca14..5ba197ea2 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,9 @@ ], "Ecotone\\Amqp\\": "packages/Amqp/src", "Ecotone\\AnnotationFinder\\": "packages/Ecotone/src/AnnotationFinder/", + "Ecotone\\DataProtection\\": [ + "packages/DataProtection/src" + ], "Ecotone\\Dbal\\": [ "packages/Ecotone/src/Dbal/", "packages/Dbal/src" @@ -75,6 +78,9 @@ "Test\\Ecotone\\Amqp\\": [ "packages/Amqp/tests" ], + "Test\\Ecotone\\DataProtection\\": [ + "packages/DataProtection/tests" + ], "Test\\Ecotone\\Dbal\\": [ "packages/Dbal/tests" ], @@ -113,6 +119,7 @@ "php": "^8.2", "doctrine/dbal": "^3.9|^4.0", "doctrine/persistence": "^2.5|^3.4", + "defuse/php-encryption": "^2.4", "enqueue/amqp-lib": "^0.10.25", "enqueue/redis": "^0.10.9", "enqueue/sqs": "^0.10.15", diff --git a/packages/DataProtection/.gitattributes b/packages/DataProtection/.gitattributes new file mode 100644 index 000000000..5699823c5 --- /dev/null +++ b/packages/DataProtection/.gitattributes @@ -0,0 +1,7 @@ +tests/ export-ignore +.coveralls.yml export-ignore +.gitattributes export-ignore +.gitignore export-ignore +behat.yaml export-ignore +phpstan.neon export-ignore +phpunit.xml export-ignore \ No newline at end of file diff --git a/packages/DataProtection/.github/FUNDING.yml b/packages/DataProtection/.github/FUNDING.yml new file mode 100644 index 000000000..c7eaae65e --- /dev/null +++ b/packages/DataProtection/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [dgafka] +patreon: # Replace with a single Open Collective username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/packages/DataProtection/.github/ISSUE_TEMPLATE/bug_report.md b/packages/DataProtection/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..2fc86f2cf --- /dev/null +++ b/packages/DataProtection/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,10 @@ +--- +name: This is Read-Only repository +about: Report at ecotoneframework/ecotone-dev +title: '' +labels: '' +assignees: '' + +--- + +Report issue at [ecotone-dev](ecotoneframework/ecotone-dev) \ No newline at end of file diff --git a/packages/DataProtection/.gitignore b/packages/DataProtection/.gitignore new file mode 100644 index 000000000..18c159d80 --- /dev/null +++ b/packages/DataProtection/.gitignore @@ -0,0 +1,9 @@ +.idea/ +vendor/ +bin/ +tests/coverage +!tests/coverage/.gitkeep +file +.phpunit.result.cache +composer.lock +phpunit.xml diff --git a/packages/DataProtection/LICENSE b/packages/DataProtection/LICENSE new file mode 100644 index 000000000..82205508a --- /dev/null +++ b/packages/DataProtection/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2025 Dariusz Gafka + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +**Scope of the License** + +Apache-2.0 Licence applies to non Enterprise Functionalities of the Ecotone Framework. +Functionalities of the Ecotone Framework referred to as Enterprise functionalities, are not covered under the Apache-2.0 license. These functionalities are provided under a separate Enterprise License. +For details on the Enterprise License, please refer to the [LICENSE-ENTERPRISE](./LICENSE-ENTERPRISE) file. \ No newline at end of file diff --git a/packages/DataProtection/LICENSE-ENTERPRISE b/packages/DataProtection/LICENSE-ENTERPRISE new file mode 100644 index 000000000..fad1a5a8d --- /dev/null +++ b/packages/DataProtection/LICENSE-ENTERPRISE @@ -0,0 +1,3 @@ +Copyright (c) 2025 Dariusz Gafka + +Licence is available at [ecotone.tech/documents/ecotone_enterprise_licence.pdf](https://ecotone.tech/documents/ecotone_enterprise_licence.pdf) \ No newline at end of file diff --git a/packages/DataProtection/README.md b/packages/DataProtection/README.md new file mode 100644 index 000000000..a46452a91 --- /dev/null +++ b/packages/DataProtection/README.md @@ -0,0 +1,63 @@ +# This is Read Only Repository +To contribute make use of [Ecotone-Dev repository](https://github.com/ecotoneframework/ecotone-dev). + +

+ +

+ +![Github Actions](https://github.com/ecotoneFramework/ecotone-dev/actions/workflows/split-testing.yml/badge.svg) +[![Latest Stable Version](https://poser.pugx.org/ecotone/ecotone/v/stable)](https://packagist.org/packages/ecotone/ecotone) +[![License](https://poser.pugx.org/ecotone/ecotone/license)](https://packagist.org/packages/ecotone/ecotone) +[![Total Downloads](https://img.shields.io/packagist/dt/ecotone/ecotone)](https://packagist.org/packages/ecotone/ecotone) +[![PHP Version Require](https://img.shields.io/packagist/dependency-v/ecotone/ecotone/php.svg)](https://packagist.org/packages/ecotone/ecotone) + +The roots of Object Oriented Programming (OOP) were mainly about communication using Messages and logic encapsulation. +`Ecotone` aims to return to the origins of OOP, by providing tools which allows us to fully move the focus from Objects to Flows, from Data storage to Application Design, from Technicalities to Business logic. +Ecotone does that by making Messages first class-citizen in our Applications. + +Thanks to being Message-Driven at the foundation level, Ecotone provides architecture which is resilient and scalable by default, making it possible for Developers to focus on business problems instead of technical concerns. +Together with declarative configuration and higher level building blocks, it makes the system design explicit, easy to follow and change no matter of Developers experience. + +Visit main page [ecotone.tech](https://ecotone.tech) to learn more. + +> Ecotone can be used with [Symfony](https://docs.ecotone.tech/modules/symfony-ddd-cqrs-event-sourcing) and [Laravel](https://docs.ecotone.tech/modules/laravel-ddd-cqrs-event-sourcing) frameworks, or any other framework using [Ecotone Lite](https://docs.ecotone.tech/install-php-service-bus#install-ecotone-lite-no-framework). +> +## Getting started + +The quickstart [page](https://docs.ecotone.tech/quick-start) of the +[reference guide](https://docs.ecotone.tech) provides a starting point for using Ecotone. +Read more on the [Ecotone's Blog](https://blog.ecotone.tech). + +## AI-Friendly Documentation + +Ecotone provides AI-optimized documentation for use with AI assistants and code editors: + +- **MCP Server**: `https://docs.ecotone.tech/~gitbook/mcp` - [Install in VSCode](vscode:mcp/install?%7B%22name%22%3A%22Ecotone%22%2C%22url%22%3A%22https%3A%2F%2Fdocs.ecotone.tech%2F~gitbook%2Fmcp%22%7D) +- **LLMs.txt**: [ecotone.tech/llms.txt](https://ecotone.tech/llms.txt) +- **Context7**: Available via [@upstash/context7-mcp](https://github.com/upstash/context7) + +Learn more: [AI Integration Guide](https://docs.ecotone.tech/other/ai-integration) + +## Feature requests and issue reporting + +Use [issue tracking system](https://github.com/ecotoneframework/ecotone-dev/issues) for new feature request and bugs. +Please verify that it's not already reported by someone else. + +## Contact + +If you want to talk or ask questions about Ecotone + +- [**Twitter**](https://twitter.com/EcotonePHP) +- **support@simplycodedsoftware.com** +- [**Community Channel**](https://discord.gg/GwM2BSuXeg) + +## Support Ecotone + +If you want to help building and improving Ecotone consider becoming a sponsor: + +- [Sponsor Ecotone](https://github.com/sponsors/dgafka) +- [Contribute to Ecotone](https://github.com/ecotoneframework/ecotone-dev). + +## Tags + +PHP, DDD, CQRS, Event Sourcing, Symfony, Laravel, Service Bus, Event Driven Architecture, SOA, Events, Commands diff --git a/packages/DataProtection/composer.json b/packages/DataProtection/composer.json new file mode 100644 index 000000000..afc90acb1 --- /dev/null +++ b/packages/DataProtection/composer.json @@ -0,0 +1,80 @@ +{ + "name": "ecotone/data-protection", + "license": [ + "Apache-2.0", + "proprietary" + ], + "homepage": "https://docs.ecotone.tech/", + "forum": "https://discord.gg/GwM2BSuXeg", + "type": "library", + "minimum-stability": "dev", + "prefer-stable": true, + "authors": [ + { + "name": "Dariusz Gafka", + "email": "support@simplycodedsoftware.com" + } + ], + "keywords": ["ecotone", "Encryption", "OpenSSL", "Data Protection", "Data Obfuscation"], + "description": "Extends Ecotone with Data Protection features allowing to obfuscate messages with sensitive data.", + "autoload": { + "psr-4": { + "Ecotone\\DataProtection\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Test\\Ecotone\\DataProtection\\": [ + "tests" + ] + } + }, + "require": { + "ext-openssl": "*", + "ecotone/ecotone": "~1.293.0", + "defuse/php-encryption": "^2.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.5|^10.5|^11.0", + "phpstan/phpstan": "^1.8", + "psr/container": "^2.0", + "wikimedia/composer-merge-plugin": "^2.1" + }, + "scripts": { + "tests:phpstan": "vendor/bin/phpstan", + "tests:phpunit": "vendor/bin/phpunit", + "tests:ci": [ + "@tests:phpstan", + "@tests:phpunit" + ] + }, + "extra": { + "branch-alias": { + "dev-main": "1.62-dev" + }, + "ecotone": { + "repository": "DataProtection" + }, + "merge-plugin": { + "include": [ + "../local_packages.json" + ] + }, + "license-info": { + "Apache-2.0": { + "name": "Apache License 2.0", + "url": "https://github.com/ecotoneframework/ecotone-dev/blob/main/LICENSE", + "description": "Allows to use non Enterprise features of Ecotone. For more information please write to support@simplycodedsoftware.com" + }, + "proprietary": { + "name": "Enterprise License", + "description": "Allows to use Enterprise features of Ecotone. For more information please write to support@simplycodedsoftware.com" + } + } + }, + "config": { + "allow-plugins": { + "wikimedia/composer-merge-plugin": true + } + } +} diff --git a/packages/DataProtection/phpstan.neon b/packages/DataProtection/phpstan.neon new file mode 100644 index 000000000..672e0fa1f --- /dev/null +++ b/packages/DataProtection/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 1 + paths: + - src \ No newline at end of file diff --git a/packages/DataProtection/phpunit.xml.dist b/packages/DataProtection/phpunit.xml.dist new file mode 100644 index 000000000..fc3ebe4f7 --- /dev/null +++ b/packages/DataProtection/phpunit.xml.dist @@ -0,0 +1,20 @@ + + + + + ./src + + + + + + + + tests + + + diff --git a/packages/DataProtection/src/Attribute/Sensitive.php b/packages/DataProtection/src/Attribute/Sensitive.php new file mode 100644 index 000000000..dfc6bb81d --- /dev/null +++ b/packages/DataProtection/src/Attribute/Sensitive.php @@ -0,0 +1,9 @@ +encryptionKeyName; + } +} diff --git a/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php b/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php new file mode 100644 index 000000000..3dfbecc9e --- /dev/null +++ b/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php @@ -0,0 +1,37 @@ + $key], defaultKey: $key); + } + + public function withKey(string $name, Key $key, bool $asDefault = false): self + { + Assert::keyNotExists($this->keys, $name, sprintf('Encryption key name `%s` already exists', $name)); + + $config = clone $this; + $config->keys[$name] = $key; + + if ($asDefault) { + $config->defaultKey = $key; + } + + return $config; + } + + public function key(?string $name): Key + { + return $this->keys[$name] ?? $this->defaultKey; + } +} diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php new file mode 100644 index 000000000..da041823f --- /dev/null +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -0,0 +1,88 @@ +findAnnotatedClasses(UsingSensitiveData::class); + + foreach ($messagesUsingSensitiveData as $messageUsingSensitiveData) { + /** @var UsingSensitiveData $attribute */ + $usingSensitiveDataAttribute = $annotationRegistrationService->getAttributeForClass($messageUsingSensitiveData, UsingSensitiveData::class); + + $reflectionClass = new \ReflectionClass($messageUsingSensitiveData); + $sensitiveProperties = array_filter($reflectionClass->getProperties(), fn(\ReflectionProperty $property) => $property->getAttributes(Sensitive::class) !== []); + $scalarProperties = array_filter($reflectionClass->getProperties(), fn(\ReflectionProperty $property) => Type::create($property->getType()->getName())->isScalar()); + + $obfuscators[$messageUsingSensitiveData] = [ + 'sensitive' => array_map(fn(\ReflectionProperty $property) => $property->getName(), $sensitiveProperties), + 'scalar' => array_map(fn(\ReflectionProperty $property) => $property->getName(), $scalarProperties), + 'encryptionKey' => $usingSensitiveDataAttribute->encryptionKeyName(), + ]; + } + + return new self($obfuscators); + } + + public function prepare(Configuration $messagingConfiguration, array $extensionObjects, ModuleReferenceSearchService $moduleReferenceSearchService, InterfaceToCallRegistry $interfaceToCallRegistry): void + { + Assert::isTrue(ExtensionObjectResolver::contains(DataProtectionConfiguration::class, $extensionObjects), sprintf('%s was not found.', DataProtectionConfiguration::class)); + + $dataProtectionConfiguration = ExtensionObjectResolver::resolveUnique(DataProtectionConfiguration::class, $extensionObjects, new stdClass()); + + $obfuscators = array_map(static fn (array $config) => new Obfuscator($config['sensitive'], $config['scalar'], $dataProtectionConfiguration->key($config['encryptionKey'])), $this->obfuscators); + $messagingConfiguration->registerServiceDefinition(id: MessageObfuscator::class, definition: new Definition(MessageObfuscator::class, [$obfuscators])); + + $pollableMessageChannels = ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects); + + foreach ($pollableMessageChannels as $pollableMessageChannel) { + $messagingConfiguration->registerChannelInterceptor( + new OutboundEncryptionChannelBuilder($pollableMessageChannel->getMessageChannelName()) + ); + $messagingConfiguration->registerChannelInterceptor( + new OutboundDecryptionChannelBuilder($pollableMessageChannel->getMessageChannelName()) + ); + } + } + + public function canHandle($extensionObject): bool + { + return $extensionObject instanceof DataProtectionConfiguration || ($extensionObject instanceof MessageChannelWithSerializationBuilder && $extensionObject->isPollable()); + } + + public function getModulePackageName(): string + { + return ModulePackageList::DATA_PROTECTION_PACKAGE; + } +} diff --git a/packages/DataProtection/src/Obfuscator/MessageObfuscator.php b/packages/DataProtection/src/Obfuscator/MessageObfuscator.php new file mode 100644 index 000000000..5df5eca81 --- /dev/null +++ b/packages/DataProtection/src/Obfuscator/MessageObfuscator.php @@ -0,0 +1,44 @@ + $obfuscators + */ + public function __construct(private array $obfuscators) + { + } + + public function encrypt(Message $message): string + { + if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { + return $message->getPayload(); + } + + $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); + if (! array_key_exists($type, $this->obfuscators)) { + return $message->getPayload(); + } + + return $this->obfuscators[$type]->encrypt($message->getPayload()); + } + + public function decrypt(Message $message): string + { + if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { + return $message->getPayload(); + } + + $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); + if (! array_key_exists($type, $this->obfuscators)) { + return $message->getPayload(); + } + + return $this->obfuscators[$type]->decrypt($message->getPayload()); + } +} diff --git a/packages/DataProtection/src/Obfuscator/Obfuscator.php b/packages/DataProtection/src/Obfuscator/Obfuscator.php new file mode 100644 index 000000000..4c83c7846 --- /dev/null +++ b/packages/DataProtection/src/Obfuscator/Obfuscator.php @@ -0,0 +1,63 @@ +obfuscateAll = $sensitive === []; + } + + public function encrypt(string $json): string + { + $payload = json_decode($json, true); + $sensitiveParameters = $this->resolveSensitiveParameters($payload); + + foreach ($sensitiveParameters as $key) { + $value = in_array($key, $this->scalar) ? $payload[$key] : json_encode($payload[$key]); + + $payload[$key] = base64_encode(Crypto::encrypt($value, $this->encryptionKey)); + } + + return json_encode($payload); + } + + public function decrypt(string $json): string + { + $payload = json_decode($json, true); + $sensitiveParameters = $this->resolveSensitiveParameters($payload); + + foreach ($sensitiveParameters as $key) { + $value = Crypto::decrypt(base64_decode($payload[$key]), $this->encryptionKey); + + $payload[$key] = in_array($key, $this->scalar) ? $value : json_decode($value, true); + } + + return json_encode($payload); + } + + private function resolveSensitiveParameters(array $payload): array + { + if ($this->obfuscateAll) { + return array_keys($payload); + } + + return array_filter($this->sensitive, static fn (string $key) => array_key_exists($key, $payload)); + } + + public function getDefinition(): Definition + { + return Definition::createFor(self::class, [$this->sensitive, $this->scalar, $this->encryptionKey]); + } +} diff --git a/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php new file mode 100644 index 000000000..ee6212a1a --- /dev/null +++ b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php @@ -0,0 +1,35 @@ +relatedChannel; + } + + public function getPrecedence(): int + { + return PrecedenceChannelInterceptor::MESSAGE_SERIALIZATION + 1; + } + + public function compile(MessagingContainerBuilder $builder): Definition + { + return new Definition(OutboundDecryptionChannelInterceptor::class, [ + Reference::to(MessageObfuscator::class), + ]); + } +} diff --git a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php new file mode 100644 index 000000000..a6960eb7a --- /dev/null +++ b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php @@ -0,0 +1,37 @@ +canHandle($message)) { + return $message; + } + + $payload = $this->messageObfuscator->decrypt($message); + + $preparedMessage = MessageBuilder::withPayload($payload) + ->setMultipleHeaders($message->getHeaders()->headers()) + ; + + return $preparedMessage->build(); + } + + private function canHandle(Message $message): bool + { + return $message->getHeaders()->containsKey('contentType') && MediaType::parseMediaType($message->getHeaders()->get('contentType'))->isCompatibleWith(MediaType::createApplicationJson()); + } +} diff --git a/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php new file mode 100644 index 000000000..b5cbc3e3f --- /dev/null +++ b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php @@ -0,0 +1,35 @@ +relatedChannel; + } + + public function getPrecedence(): int + { + return PrecedenceChannelInterceptor::MESSAGE_SERIALIZATION - 1; + } + + public function compile(MessagingContainerBuilder $builder): Definition + { + return new Definition(OutboundEncryptionChannelInterceptor::class, [ + Reference::to(MessageObfuscator::class), + ]); + } +} diff --git a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php new file mode 100644 index 000000000..c6d06cf2c --- /dev/null +++ b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php @@ -0,0 +1,37 @@ +canHandle($message)) { + return $message; + } + + $payload = $this->messageObfuscator->encrypt($message); + + $preparedMessage = MessageBuilder::withPayload($payload) + ->setMultipleHeaders($message->getHeaders()->headers()) + ; + + return $preparedMessage->build(); + } + + private function canHandle(Message $message): bool + { + return $message->getHeaders()->containsKey('contentType') && MediaType::parseMediaType($message->getHeaders()->get('contentType'))->isCompatibleWith(MediaType::createApplicationJson()); + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php new file mode 100644 index 000000000..56a8c20dc --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php @@ -0,0 +1,18 @@ +key = Key::createNewRandomKey(); + } + + public function test_fully_obfuscated_message(): void + { + $ecotone = $this->bootstrapEcotone(); + + $ecotone->sendCommand( + new FullyObfuscatedMessage( + class: new TestClass('value', TestEnum::FIRST), + enum: TestEnum::FIRST, + argument: 'value', + ) + ); + + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + } + + public function test_message_with_obfuscated_enum(): void + { + $ecotone = $this->bootstrapEcotone(); + + $ecotone->sendCommand( + new PartiallyObfuscatedMessage( + class: new TestClass('value', TestEnum::FIRST), + enum: TestEnum::FIRST, + argument: 'value', + ) + ); + + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + } + + public function bootstrapEcotone(): FlowTestSupport + { + return EcotoneLite::bootstrapFlowTesting( + containerOrAvailableServices: [ + new TestCommandHandler(), + ], + configuration: ServiceConfiguration::createWithDefaults() + ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::AMQP_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) + ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages']) + ->withExtensionObjects([ + DataProtectionConfiguration::create('default', $this->key), + SimpleMessageChannelBuilder::createQueueChannel('test'), + ]) + ); + } +} diff --git a/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php b/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php new file mode 100644 index 000000000..17c7642d7 --- /dev/null +++ b/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php @@ -0,0 +1,101 @@ +message = MessageBuilder::withPayload(json_encode([ + 'foo' => 'value', + 'bar' => 'value', + ], JSON_THROW_ON_ERROR)) + ->setHeader(MessageHeaders::TYPE_ID, ObfuscatedMessage::class) + ->build() + ; + } + + public function test_obfuscate_message_fully(): void + { + $obfuscator = new Obfuscator([], ['foo', 'bar'], Key::createNewRandomKey()); + $messageObfuscator = new MessageObfuscator([ObfuscatedMessage::class => $obfuscator]); + + $encryptedPayload = $messageObfuscator->encrypt($this->message); + + $encryptedMessage = MessageBuilder::fromMessage($this->message) + ->setPayload($encryptedPayload) + ->build() + ; + + $decryptedPayload = $messageObfuscator->decrypt($encryptedMessage); + + $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); + + self::assertNotEquals('value', $payload['foo']); + self::assertNotEquals('value', $payload['bar']); + self::assertNotEquals($this->message->getPayload(), $encryptedPayload); + self::assertEquals($this->message->getPayload(), $decryptedPayload); + } + + public function test_obfuscate_message_partially(): void + { + $obfuscator = new Obfuscator(['foo', 'non-existing-argument'], ['foo', 'bar'], Key::createNewRandomKey()); + $messageObfuscator = new MessageObfuscator([ObfuscatedMessage::class => $obfuscator]); + + $encryptedPayload = $messageObfuscator->encrypt($this->message); + + $encryptedMessage = MessageBuilder::fromMessage($this->message) + ->setPayload($encryptedPayload) + ->build() + ; + + $decryptedPayload = $messageObfuscator->decrypt($encryptedMessage); + + $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); + + self::assertNotEquals('value', $payload['foo']); + self::assertEquals('value', $payload['bar']); + self::assertNotEquals($this->message->getPayload(), $encryptedPayload); + self::assertEquals($this->message->getPayload(), $decryptedPayload); + self::assertArrayNotHasKey('non-existing-argument', $payload); + self::assertArrayNotHasKey('non-existing-argument', json_decode($decryptedPayload, true, 512, JSON_THROW_ON_ERROR)); + } + + public function test_dont_obfuscate_unsupported_message(): void + { + $obfuscator = new Obfuscator(['foo', 'bar'], ['foo', 'bar'], Key::createNewRandomKey()); + $messageObfuscator = new MessageObfuscator([\stdClass::class => $obfuscator]); + + $encryptedPayload = $messageObfuscator->encrypt($this->message); + + $encryptedMessage = MessageBuilder::fromMessage($this->message) + ->setPayload($encryptedPayload) + ->build() + ; + + $decryptedPayload = $messageObfuscator->decrypt($encryptedMessage); + + $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); + + self::assertEquals('value', $payload['foo']); + self::assertEquals('value', $payload['bar']); + self::assertEquals($this->message->getPayload(), $encryptedPayload); + self::assertEquals($this->message->getPayload(), $decryptedPayload); + self::assertArrayNotHasKey('non-existing-argument', $payload); + self::assertArrayNotHasKey('non-existing-argument', json_decode($decryptedPayload, true, 512, JSON_THROW_ON_ERROR)); + } +} diff --git a/packages/DataProtection/tests/Unit/ObfuscatorTest.php b/packages/DataProtection/tests/Unit/ObfuscatorTest.php new file mode 100644 index 000000000..3007a60d8 --- /dev/null +++ b/packages/DataProtection/tests/Unit/ObfuscatorTest.php @@ -0,0 +1,52 @@ +payload = json_encode([ + 'foo' => 'value', + 'bar' => 'value', + ], JSON_THROW_ON_ERROR); + } + + public function test_obfuscate_payload_fully(): void + { + $obfuscator = new Obfuscator([], ['foo', 'bar'], Key::createNewRandomKey()); + + $encryptedPayload = $obfuscator->encrypt($this->payload); + $decryptedPayload = $obfuscator->decrypt($encryptedPayload); + + $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); + + self::assertNotEquals('value', $payload['foo']); + self::assertNotEquals('value', $payload['bar']); + self::assertNotEquals($this->payload, $encryptedPayload); + self::assertEquals($this->payload, $decryptedPayload); + } + + public function test_obfuscate_payload_partially(): void + { + $obfuscator = new Obfuscator(['foo', 'non-existing-argument'], ['foo', 'bar'], Key::createNewRandomKey()); + + $encryptedPayload = $obfuscator->encrypt($this->payload); + $decryptedPayload = $obfuscator->decrypt($encryptedPayload); + + $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); + + self::assertNotEquals('value', $payload['foo']); + self::assertEquals('value', $payload['bar']); + self::assertNotEquals($this->payload, $encryptedPayload); + self::assertEquals($this->payload, $decryptedPayload); + self::assertArrayNotHasKey('non-existing-argument', $payload); + self::assertArrayNotHasKey('non-existing-argument', json_decode($decryptedPayload, true, 512, JSON_THROW_ON_ERROR)); + } +} diff --git a/packages/Ecotone/src/Messaging/Channel/PollableChannel/Serialization/OutboundSerializationChannelInterceptor.php b/packages/Ecotone/src/Messaging/Channel/PollableChannel/Serialization/OutboundSerializationChannelInterceptor.php index 51ee82584..c863f084c 100644 --- a/packages/Ecotone/src/Messaging/Channel/PollableChannel/Serialization/OutboundSerializationChannelInterceptor.php +++ b/packages/Ecotone/src/Messaging/Channel/PollableChannel/Serialization/OutboundSerializationChannelInterceptor.php @@ -16,7 +16,7 @@ /** * licence Apache-2.0 */ -final class OutboundSerializationChannelInterceptor extends AbstractChannelInterceptor implements ChannelInterceptor +final class OutboundSerializationChannelInterceptor extends AbstractChannelInterceptor { public function __construct( private OutboundMessageConverter $outboundMessageConverter, @@ -27,13 +27,13 @@ public function __construct( /** * @inheritDoc */ - public function preSend(Message $messageToConvert, MessageChannel $messageChannel): ?Message + public function preSend(Message $message, MessageChannel $messageChannel): ?Message { - if ($messageToConvert instanceof ErrorMessage) { - return $messageToConvert; + if ($message instanceof ErrorMessage) { + return $message; } - $outboundMessage = $this->outboundMessageConverter->prepare($messageToConvert, $this->conversionService); + $outboundMessage = $this->outboundMessageConverter->prepare($message, $this->conversionService); $preparedMessage = MessageBuilder::withPayload($outboundMessage->getPayload()) ->setMultipleHeaders($outboundMessage->getHeaders()); diff --git a/packages/Ecotone/src/Messaging/Config/ModuleClassList.php b/packages/Ecotone/src/Messaging/Config/ModuleClassList.php index 846295781..58c55f1fd 100644 --- a/packages/Ecotone/src/Messaging/Config/ModuleClassList.php +++ b/packages/Ecotone/src/Messaging/Config/ModuleClassList.php @@ -8,6 +8,7 @@ use Ecotone\Amqp\Configuration\RabbitConsumerModule; use Ecotone\Amqp\Publisher\AmqpMessagePublisherModule; use Ecotone\Amqp\Transaction\AmqpTransactionModule; +use Ecotone\DataProtection\Configuration\DataProtectionModule; use Ecotone\Dbal\Configuration\DbalConnectionModule; use Ecotone\Dbal\Configuration\DbalPublisherModule; use Ecotone\Dbal\Database\DatabaseSetupModule; @@ -204,4 +205,8 @@ class ModuleClassList public const KAFKA_MODULES = [ KafkaModule::class, ]; + + const DATA_PROTECTION_MODULES = [ + DataProtectionModule::class, + ]; } diff --git a/packages/Ecotone/src/Messaging/Config/ModulePackageList.php b/packages/Ecotone/src/Messaging/Config/ModulePackageList.php index 51adc667b..32b8e2e1d 100644 --- a/packages/Ecotone/src/Messaging/Config/ModulePackageList.php +++ b/packages/Ecotone/src/Messaging/Config/ModulePackageList.php @@ -15,6 +15,7 @@ final class ModulePackageList */ public const ASYNCHRONOUS_PACKAGE = 'asynchronous'; public const AMQP_PACKAGE = 'amqp'; + public const DATA_PROTECTION_PACKAGE = 'dataProtection'; public const DBAL_PACKAGE = 'dbal'; public const REDIS_PACKAGE = 'redis'; public const SQS_PACKAGE = 'sqs'; @@ -42,6 +43,7 @@ public static function getModuleClassesForPackage(string $packageName): array ModulePackageList::TEST_PACKAGE => ModuleClassList::TEST_MODULES, ModulePackageList::LARAVEL_PACKAGE => ModuleClassList::LARAVEL_MODULES, ModulePackageList::SYMFONY_PACKAGE => ModuleClassList::SYMFONY_MODULES, + ModulePackageList::DATA_PROTECTION_PACKAGE => ModuleClassList::DATA_PROTECTION_MODULES, default => throw ConfigurationException::create(sprintf('Given unknown package name %s. Available packages name are: %s', $packageName, implode(',', self::allPackages()))) }; } @@ -64,6 +66,7 @@ public static function allPackages(): array self::TRACING_PACKAGE, self::LARAVEL_PACKAGE, self::SYMFONY_PACKAGE, + self::DATA_PROTECTION_PACKAGE, ]; } diff --git a/packages/Ecotone/src/Messaging/Conversion/AutoCollectionConversionService.php b/packages/Ecotone/src/Messaging/Conversion/AutoCollectionConversionService.php index 46b4f4964..0e38f7404 100644 --- a/packages/Ecotone/src/Messaging/Conversion/AutoCollectionConversionService.php +++ b/packages/Ecotone/src/Messaging/Conversion/AutoCollectionConversionService.php @@ -63,7 +63,6 @@ public function convert($source, Type $sourcePHPType, MediaType $sourceMediaType public function canConvert(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool { if ($this->getConverter($sourceType, $sourceMediaType, $targetType, $targetMediaType)) { - ; return true; } if ($sourceType->isIterable() && $sourceType instanceof Type\GenericType diff --git a/packages/Ecotone/src/Messaging/Conversion/SerializedToObject/DeserializingConverter.php b/packages/Ecotone/src/Messaging/Conversion/SerializedToObject/DeserializingConverter.php index b0ed7d5d9..4a7e60674 100644 --- a/packages/Ecotone/src/Messaging/Conversion/SerializedToObject/DeserializingConverter.php +++ b/packages/Ecotone/src/Messaging/Conversion/SerializedToObject/DeserializingConverter.php @@ -20,9 +20,6 @@ */ class DeserializingConverter implements Converter { - /** - * @inheritDoc - */ public function convert($source, Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType, ?ConversionService $conversionService = null) { $phpVar = unserialize(stripslashes($source)); @@ -36,9 +33,6 @@ public function convert($source, Type $sourceType, MediaType $sourceMediaType, T return $phpVar; } - /** - * @inheritDoc - */ public function matches(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool { return $sourceMediaType->isCompatibleWithParsed(MediaType::APPLICATION_X_PHP_SERIALIZED) diff --git a/packages/local_packages.json b/packages/local_packages.json index b97dbf8d1..bf61019b7 100644 --- a/packages/local_packages.json +++ b/packages/local_packages.json @@ -12,6 +12,10 @@ "type": "path", "url": "Dbal" }, + { + "type": "path", + "url": "DataProtection" + }, { "type": "path", "url": "Enqueue" From ff23dab3646b835e0ea6c68d2c615d722e49fa73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Mon, 19 Jan 2026 18:03:58 +0100 Subject: [PATCH 2/5] tests for Event and Command Handler --- .../src/AmqpBackedMessageChannelBuilder.php | 9 +- .../src/Attribute/UsingSensitiveData.php | 1 - .../DataProtectionConfiguration.php | 21 ++- .../Configuration/DataProtectionModule.php | 43 +++-- .../OutboundDecryptionChannelInterceptor.php | 5 +- .../OutboundEncryptionChannelInterceptor.php | 5 +- .../tests/Fixture/MessageReceiver.php | 18 ++ .../MessageWithSecondaryKeyEncryption.php | 14 ++ .../TestCommandHandler.php | 22 --- .../tests/Fixture/TestCommandHandler.php | 39 +++++ .../tests/Fixture/TestEventHandler.php | 40 +++++ .../ObfuscateAnnotatedMessagesTest.php | 157 ++++++++++++++++-- .../PollableChannelInterceptorAdapter.php | 2 +- 13 files changed, 313 insertions(+), 63 deletions(-) create mode 100644 packages/DataProtection/tests/Fixture/MessageReceiver.php create mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/MessageWithSecondaryKeyEncryption.php delete mode 100644 packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/TestCommandHandler.php create mode 100644 packages/DataProtection/tests/Fixture/TestCommandHandler.php create mode 100644 packages/DataProtection/tests/Fixture/TestEventHandler.php diff --git a/packages/Amqp/src/AmqpBackedMessageChannelBuilder.php b/packages/Amqp/src/AmqpBackedMessageChannelBuilder.php index 28d44602d..dabe3600d 100644 --- a/packages/Amqp/src/AmqpBackedMessageChannelBuilder.php +++ b/packages/Amqp/src/AmqpBackedMessageChannelBuilder.php @@ -48,25 +48,20 @@ public static function create( ); } - private function getAmqpOutboundChannelAdapter(): AmqpOutboundChannelAdapterBuilder - { - return $this->outboundChannelAdapter; - } - /** * @deprecated use withPublisherConfirms * @TODO Ecotone 2.0 remove */ public function withPublisherAcknowledgments(bool $enabled): self { - $this->getAmqpOutboundChannelAdapter()->withPublisherConfirms($enabled); + $this->outboundChannelAdapter->withPublisherConfirms($enabled); return $this; } public function withPublisherConfirms(bool $enabled): self { - $this->getAmqpOutboundChannelAdapter()->withPublisherConfirms($enabled); + $this->outboundChannelAdapter->withPublisherConfirms($enabled); return $this; } diff --git a/packages/DataProtection/src/Attribute/UsingSensitiveData.php b/packages/DataProtection/src/Attribute/UsingSensitiveData.php index 4edd60cf0..ade3b3841 100644 --- a/packages/DataProtection/src/Attribute/UsingSensitiveData.php +++ b/packages/DataProtection/src/Attribute/UsingSensitiveData.php @@ -3,7 +3,6 @@ namespace Ecotone\DataProtection\Attribute; use Attribute; -use Ecotone\Messaging\Support\Assert; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] class UsingSensitiveData diff --git a/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php b/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php index 3dfbecc9e..8aed2dd99 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php +++ b/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php @@ -7,13 +7,16 @@ class DataProtectionConfiguration { - private function __construct(private array $keys, private Key $defaultKey) + /** + * @param array $keys + */ + private function __construct(private array $keys, private string $defaultKey) { } public static function create(string $name, Key $key): self { - return new self(keys: [$name => $key], defaultKey: $key); + return new self(keys: [$name => $key], defaultKey: $name); } public function withKey(string $name, Key $key, bool $asDefault = false): self @@ -24,7 +27,7 @@ public function withKey(string $name, Key $key, bool $asDefault = false): self $config->keys[$name] = $key; if ($asDefault) { - $config->defaultKey = $key; + $config->defaultKey = $name; } return $config; @@ -32,6 +35,16 @@ public function withKey(string $name, Key $key, bool $asDefault = false): self public function key(?string $name): Key { - return $this->keys[$name] ?? $this->defaultKey; + return $this->keys[$name] ?? $this->keys[$this->defaultKey]; + } + + public function keyName(?string $name): string + { + return array_key_exists($name, $this->keys) ? $name : $this->defaultKey; + } + + public function keys(): array + { + return $this->keys; } } diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index da041823f..0ff1de775 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -11,6 +11,7 @@ use Ecotone\DataProtection\Obfuscator\Obfuscator; use Ecotone\DataProtection\OutboundDecryptionChannelBuilder; use Ecotone\DataProtection\OutboundEncryptionChannelBuilder; +use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Messaging\Attribute\ModuleAnnotation; use Ecotone\Messaging\Channel\MessageChannelWithSerializationBuilder; use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\ExtensionObjectResolver; @@ -19,6 +20,7 @@ use Ecotone\Messaging\Config\Container\Definition; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ModuleReferenceSearchService; +use Ecotone\Messaging\Handler\ClassPropertyDefinition; use Ecotone\Messaging\Handler\InterfaceToCallRegistry; use Ecotone\Messaging\Handler\Type; use Ecotone\Messaging\Support\Assert; @@ -40,14 +42,10 @@ public static function create(AnnotationFinder $annotationRegistrationService, I foreach ($messagesUsingSensitiveData as $messageUsingSensitiveData) { /** @var UsingSensitiveData $attribute */ $usingSensitiveDataAttribute = $annotationRegistrationService->getAttributeForClass($messageUsingSensitiveData, UsingSensitiveData::class); - - $reflectionClass = new \ReflectionClass($messageUsingSensitiveData); - $sensitiveProperties = array_filter($reflectionClass->getProperties(), fn(\ReflectionProperty $property) => $property->getAttributes(Sensitive::class) !== []); - $scalarProperties = array_filter($reflectionClass->getProperties(), fn(\ReflectionProperty $property) => Type::create($property->getType()->getName())->isScalar()); + $classDefinition = $interfaceToCallRegistry->getClassDefinitionFor(Type::create($messageUsingSensitiveData)); $obfuscators[$messageUsingSensitiveData] = [ - 'sensitive' => array_map(fn(\ReflectionProperty $property) => $property->getName(), $sensitiveProperties), - 'scalar' => array_map(fn(\ReflectionProperty $property) => $property->getName(), $scalarProperties), + 'properties' => $classDefinition->getProperties(), 'encryptionKey' => $usingSensitiveDataAttribute->encryptionKeyName(), ]; } @@ -58,15 +56,34 @@ public static function create(AnnotationFinder $annotationRegistrationService, I public function prepare(Configuration $messagingConfiguration, array $extensionObjects, ModuleReferenceSearchService $moduleReferenceSearchService, InterfaceToCallRegistry $interfaceToCallRegistry): void { Assert::isTrue(ExtensionObjectResolver::contains(DataProtectionConfiguration::class, $extensionObjects), sprintf('%s was not found.', DataProtectionConfiguration::class)); + Assert::isTrue(ExtensionObjectResolver::contains(JMSConverterConfiguration::class, $extensionObjects), sprintf('%s package require %s package to be enabled. Did you forget to define %s?', ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE, JMSConverterConfiguration::class)); $dataProtectionConfiguration = ExtensionObjectResolver::resolveUnique(DataProtectionConfiguration::class, $extensionObjects, new stdClass()); + $jMSConverterConfiguration = ExtensionObjectResolver::resolveUnique(JMSConverterConfiguration::class, $extensionObjects, JMSConverterConfiguration::createWithDefaults()); - $obfuscators = array_map(static fn (array $config) => new Obfuscator($config['sensitive'], $config['scalar'], $dataProtectionConfiguration->key($config['encryptionKey'])), $this->obfuscators); - $messagingConfiguration->registerServiceDefinition(id: MessageObfuscator::class, definition: new Definition(MessageObfuscator::class, [$obfuscators])); - $pollableMessageChannels = ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects); + $isScalarProperty = static function (ClassPropertyDefinition $property) use ($jMSConverterConfiguration): bool { + $type = $property->getType(); + + return $type->isScalar() || ($type->isEnum() && $jMSConverterConfiguration->isEnumSupportEnabled()); + }; + + $obfuscators = array_map(function (array $config) use ($dataProtectionConfiguration, $isScalarProperty): Obfuscator { + $sensitiveProperties = array_map( + static fn (ClassPropertyDefinition $property): string => $property->getName(), + array_filter($config['properties'], static fn (ClassPropertyDefinition $property) => $property->hasAnnotation(Type::create(Sensitive::class))) + ); + $scalarProperties = array_map( + static fn (ClassPropertyDefinition $property): string => $property->getName(), + array_filter($config['properties'], static fn (ClassPropertyDefinition $property) => $isScalarProperty($property)) + ); + + return new Obfuscator($sensitiveProperties, $scalarProperties, $dataProtectionConfiguration->key($config['encryptionKey'])); + }, $this->obfuscators); + + $messagingConfiguration->registerServiceDefinition(id: MessageObfuscator::class, definition: new Definition(MessageObfuscator::class, [$obfuscators])); - foreach ($pollableMessageChannels as $pollableMessageChannel) { + foreach (ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects) as $pollableMessageChannel) { $messagingConfiguration->registerChannelInterceptor( new OutboundEncryptionChannelBuilder($pollableMessageChannel->getMessageChannelName()) ); @@ -78,7 +95,11 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO public function canHandle($extensionObject): bool { - return $extensionObject instanceof DataProtectionConfiguration || ($extensionObject instanceof MessageChannelWithSerializationBuilder && $extensionObject->isPollable()); + return + $extensionObject instanceof DataProtectionConfiguration + || $extensionObject instanceof JMSConverterConfiguration + || ($extensionObject instanceof MessageChannelWithSerializationBuilder && $extensionObject->isPollable()) + ; } public function getModulePackageName(): string diff --git a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php index a6960eb7a..15a0594a2 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php @@ -7,6 +7,7 @@ use Ecotone\Messaging\Conversion\MediaType; use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageChannel; +use Ecotone\Messaging\MessageHeaders; use Ecotone\Messaging\Support\MessageBuilder; class OutboundDecryptionChannelInterceptor extends AbstractChannelInterceptor @@ -32,6 +33,8 @@ public function postReceive(Message $message, MessageChannel $messageChannel): ? private function canHandle(Message $message): bool { - return $message->getHeaders()->containsKey('contentType') && MediaType::parseMediaType($message->getHeaders()->get('contentType'))->isCompatibleWith(MediaType::createApplicationJson()); + return $message->getHeaders()->containsKey(MessageHeaders::CONTENT_TYPE) + && MediaType::parseMediaType($message->getHeaders()->get(MessageHeaders::CONTENT_TYPE))->isCompatibleWith(MediaType::createApplicationJson()) + ; } } diff --git a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php index c6d06cf2c..70b917f40 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php @@ -7,6 +7,7 @@ use Ecotone\Messaging\Conversion\MediaType; use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageChannel; +use Ecotone\Messaging\MessageHeaders; use Ecotone\Messaging\Support\MessageBuilder; class OutboundEncryptionChannelInterceptor extends AbstractChannelInterceptor @@ -32,6 +33,8 @@ public function preSend(Message $message, MessageChannel $messageChannel): ?Mess private function canHandle(Message $message): bool { - return $message->getHeaders()->containsKey('contentType') && MediaType::parseMediaType($message->getHeaders()->get('contentType'))->isCompatibleWith(MediaType::createApplicationJson()); + return $message->getHeaders()->containsKey(MessageHeaders::CONTENT_TYPE) + && MediaType::parseMediaType($message->getHeaders()->get('contentType'))->isCompatibleWith(MediaType::createApplicationJson()) + ; } } diff --git a/packages/DataProtection/tests/Fixture/MessageReceiver.php b/packages/DataProtection/tests/Fixture/MessageReceiver.php new file mode 100644 index 000000000..65f2da33d --- /dev/null +++ b/packages/DataProtection/tests/Fixture/MessageReceiver.php @@ -0,0 +1,18 @@ +receivedMessage = $message; + } + + public function receivedMessage(): object + { + return $this->receivedMessage; + } +} diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/MessageWithSecondaryKeyEncryption.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/MessageWithSecondaryKeyEncryption.php new file mode 100644 index 000000000..075f6c04f --- /dev/null +++ b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/MessageWithSecondaryKeyEncryption.php @@ -0,0 +1,14 @@ +withReceivedMessage($message); + } + + #[CommandHandler(endpointId: 'test.PartiallyObfuscatedMessage')] + public function handlePartiallyObfuscatedMessage( + #[Payload] PartiallyObfuscatedMessage $message, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceivedMessage($message); + } + + #[CommandHandler(endpointId: 'test.MessageWithSecondaryKeyEncryption')] + public function handleMessageWithSecondaryKeyEncryption( + #[Payload] MessageWithSecondaryKeyEncryption $message, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceivedMessage($message); + } +} diff --git a/packages/DataProtection/tests/Fixture/TestEventHandler.php b/packages/DataProtection/tests/Fixture/TestEventHandler.php new file mode 100644 index 000000000..4d3440948 --- /dev/null +++ b/packages/DataProtection/tests/Fixture/TestEventHandler.php @@ -0,0 +1,40 @@ +withReceivedMessage($message); + } + + #[EventHandler(endpointId: 'test.PartiallyObfuscatedMessage')] + public function handlePartiallyObfuscatedMessage( + #[Payload] PartiallyObfuscatedMessage $message, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceivedMessage($message); + } + + #[EventHandler(endpointId: 'test.MessageWithSecondaryKeyEncryption')] + public function handleMessageWithSecondaryKeyEncryption( + #[Payload] MessageWithSecondaryKeyEncryption $message, + #[Reference] MessageReceiver $messageReceiver, + ): void { + $messageReceiver->withReceivedMessage($message); + } +} diff --git a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php index 3ea3c00a5..ebc3b1737 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php @@ -2,72 +2,199 @@ namespace Test\Ecotone\DataProtection\Integration; +use Defuse\Crypto\Crypto; use Defuse\Crypto\Key; use Ecotone\DataProtection\Configuration\DataProtectionConfiguration; +use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Lite\EcotoneLite; use Ecotone\Lite\Test\FlowTestSupport; +use Ecotone\Messaging\Channel\QueueChannel; use Ecotone\Messaging\Channel\SimpleMessageChannelBuilder; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\Endpoint\ExecutionPollingMetadata; +use Ecotone\Messaging\MessageChannel; +use Enqueue\AmqpExt\AmqpConnectionFactory as AmqpExtConnection; +use Enqueue\AmqpLib\AmqpConnectionFactory as AmqpLibConnection; +use Interop\Amqp\AmqpConnectionFactory; use PHPUnit\Framework\TestCase; +use Test\Ecotone\Amqp\AmqpMessagingTestCase; +use Test\Ecotone\DataProtection\Fixture\MessageReceiver; use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\FullyObfuscatedMessage; +use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\MessageWithSecondaryKeyEncryption; use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\PartiallyObfuscatedMessage; -use Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages\TestCommandHandler; use Test\Ecotone\DataProtection\Fixture\TestClass; +use Test\Ecotone\DataProtection\Fixture\TestCommandHandler; +use Test\Ecotone\DataProtection\Fixture\TestEventHandler; use Test\Ecotone\DataProtection\Fixture\TestEnum; class ObfuscateAnnotatedMessagesTest extends TestCase { - private Key $key; + private Key $primaryKey; + private Key $secondaryKey; protected function setUp(): void { - $this->key = Key::createNewRandomKey(); + $this->primaryKey = Key::createNewRandomKey(); + $this->secondaryKey = Key::createNewRandomKey(); } - public function test_fully_obfuscated_message(): void + public function test_fully_obfuscated_command_handler_message(): void { - $ecotone = $this->bootstrapEcotone(); + $ecotone = $this->bootstrapEcotoneWithCommandHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); $ecotone->sendCommand( - new FullyObfuscatedMessage( + $messageSent = new FullyObfuscatedMessage( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', ) ); + $messagePayload = json_decode($channel->receive()->getPayload(), true); + + self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); + self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); + self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->primaryKey)); + + $ecotone->sendCommand($messageSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); } - public function test_message_with_obfuscated_enum(): void + public function test_partially_obfuscated_command_handler_message(): void { - $ecotone = $this->bootstrapEcotone(); + $ecotone = $this->bootstrapEcotoneWithCommandHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); $ecotone->sendCommand( - new PartiallyObfuscatedMessage( + $messageSent = new PartiallyObfuscatedMessage( + class: new TestClass('value', TestEnum::FIRST), + enum: TestEnum::FIRST, + argument: 'value', + ) + ); + + $messagePayload = json_decode($channel->receive()->getPayload(), true); + + self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); + self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); + self::assertEquals('value', $messagePayload['argument']); + + $ecotone->sendCommand($messageSent); + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + } + + public function test_obfuscate_command_handler_message_with_non_default_key(): void + { + $ecotone = $this->bootstrapEcotoneWithCommandHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone->sendCommand($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')); + + $messagePayload = json_decode($channel->receive()->getPayload(), true); + + self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->secondaryKey)); + + $ecotone->sendCommand($messageSent); + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + } + + public function test_fully_obfuscated_event_handler_message(): void + { + $ecotone = $this->bootstrapEcotoneWithEventHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone->publishEvent( + $messageSent = new FullyObfuscatedMessage( + class: new TestClass('value', TestEnum::FIRST), + enum: TestEnum::FIRST, + argument: 'value', + ) + ); + + $messagePayload = json_decode($channel->receive()->getPayload(), true); + + self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); + self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); + self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->primaryKey)); + + $ecotone->publishEvent($messageSent); + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + } + + public function test_partially_obfuscated_event_handler_message(): void + { + $ecotone = $this->bootstrapEcotoneWithEventHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone->publishEvent( + $messageSent = new PartiallyObfuscatedMessage( class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', ) ); + $messagePayload = json_decode($channel->receive()->getPayload(), true); + + self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); + self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); + self::assertEquals('value', $messagePayload['argument']); + + $ecotone->publishEvent($messageSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + } + + public function test_obfuscate_event_handler_message_with_non_default_key(): void + { + $ecotone = $this->bootstrapEcotoneWithEventHandler($channel = QueueChannel::create('test'), $messageReceiver = new MessageReceiver()); + + $ecotone->publishEvent($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')); + + $messagePayload = json_decode($channel->receive()->getPayload(), true); + + self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->secondaryKey)); + + $ecotone->publishEvent($messageSent); + $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + + self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + } + + private function bootstrapEcotoneWithCommandHandler(MessageChannel $messageChannel, MessageReceiver $messageReceiver): FlowTestSupport + { + return $this->bootstrapEcotone([TestCommandHandler::class], [new TestCommandHandler()], $messageChannel, $messageReceiver); + } + + private function bootstrapEcotoneWithEventHandler(MessageChannel $messageChannel, MessageReceiver $messageReceiver): FlowTestSupport + { + return $this->bootstrapEcotone([TestEventHandler::class], [new TestEventHandler()], $messageChannel, $messageReceiver); } - public function bootstrapEcotone(): FlowTestSupport + private function bootstrapEcotone(array $classesToResolve, array $container, MessageChannel $messageChannel, MessageReceiver $receivedMessage): FlowTestSupport { return EcotoneLite::bootstrapFlowTesting( - containerOrAvailableServices: [ - new TestCommandHandler(), - ], + classesToResolve: $classesToResolve, + containerOrAvailableServices: array_merge([ + $receivedMessage, + AmqpConnectionFactory::class => AmqpMessagingTestCase::getRabbitConnectionFactory(), + AmqpExtConnection::class => AmqpMessagingTestCase::getRabbitConnectionFactory(), + AmqpLibConnection::class => AmqpMessagingTestCase::getRabbitConnectionFactory(), + ], $container), configuration: ServiceConfiguration::createWithDefaults() ->withSkippedModulePackageNames(ModulePackageList::allPackagesExcept([ModulePackageList::AMQP_PACKAGE, ModulePackageList::ASYNCHRONOUS_PACKAGE, ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE])) ->withNamespaces(['Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages']) ->withExtensionObjects([ - DataProtectionConfiguration::create('default', $this->key), - SimpleMessageChannelBuilder::createQueueChannel('test'), + DataProtectionConfiguration::create('primary', $this->primaryKey) + ->withKey('secondary', $this->secondaryKey), + SimpleMessageChannelBuilder::create('test', $messageChannel), + JMSConverterConfiguration::createWithDefaults()->withDefaultEnumSupport(true), ]) ); } diff --git a/packages/Ecotone/src/Messaging/Channel/PollableChannelInterceptorAdapter.php b/packages/Ecotone/src/Messaging/Channel/PollableChannelInterceptorAdapter.php index 0220c4fbf..a987e5e74 100644 --- a/packages/Ecotone/src/Messaging/Channel/PollableChannelInterceptorAdapter.php +++ b/packages/Ecotone/src/Messaging/Channel/PollableChannelInterceptorAdapter.php @@ -89,7 +89,7 @@ private function receiveFor(?PollingMetadata $pollingMetadata): ?Message } foreach ($this->sortedChannelInterceptors as $channelInterceptor) { - $channelInterceptor->postReceive($message, $this->messageChannel); + $message = $channelInterceptor->postReceive($message, $this->messageChannel); } return $message; From 2d3f784a555c4869ba76c04c543d19dc3361fad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Mon, 19 Jan 2026 18:08:26 +0100 Subject: [PATCH 3/5] add Enterprise license annotation --- packages/DataProtection/src/Attribute/Sensitive.php | 3 +++ packages/DataProtection/src/Attribute/UsingSensitiveData.php | 3 +++ .../src/Configuration/DataProtectionConfiguration.php | 3 +++ .../DataProtection/src/Configuration/DataProtectionModule.php | 3 +++ packages/DataProtection/src/Obfuscator/MessageObfuscator.php | 3 +++ packages/DataProtection/src/Obfuscator/Obfuscator.php | 3 +++ .../DataProtection/src/OutboundDecryptionChannelBuilder.php | 3 +++ .../src/OutboundDecryptionChannelInterceptor.php | 3 +++ .../DataProtection/src/OutboundEncryptionChannelBuilder.php | 3 +++ .../src/OutboundEncryptionChannelInterceptor.php | 3 +++ 10 files changed, 30 insertions(+) diff --git a/packages/DataProtection/src/Attribute/Sensitive.php b/packages/DataProtection/src/Attribute/Sensitive.php index dfc6bb81d..6b1a07d38 100644 --- a/packages/DataProtection/src/Attribute/Sensitive.php +++ b/packages/DataProtection/src/Attribute/Sensitive.php @@ -1,5 +1,8 @@ Date: Mon, 19 Jan 2026 22:56:22 +0100 Subject: [PATCH 4/5] - obfuscate full payload when message use sensitive data - allow to define sensitive headers --- .../src/Attribute/Sensitive.php | 12 -- .../src/Attribute/WithSensitiveHeader.php | 16 +++ .../src/Attribute/WithSensitiveHeaders.php | 18 +++ .../DataProtectionConfiguration.php | 5 - .../Configuration/DataProtectionModule.php | 51 +++++---- .../src/Obfuscator/MessageObfuscator.php | 73 +++++++++--- .../src/Obfuscator/Obfuscator.php | 66 ----------- .../src/OutboundDecryptionChannelBuilder.php | 10 +- .../OutboundDecryptionChannelInterceptor.php | 9 +- .../src/OutboundEncryptionChannelBuilder.php | 4 +- .../OutboundEncryptionChannelInterceptor.php | 9 +- .../tests/Fixture/MessageReceiver.php | 11 +- .../FullyObfuscatedMessage.php | 4 + .../PartiallyObfuscatedMessage.php | 6 +- .../tests/Fixture/TestCommandHandler.php | 10 +- .../tests/Fixture/TestEventHandler.php | 10 +- .../ObfuscateAnnotatedMessagesTest.php | 106 +++++++++++++----- .../tests/Unit/MessageObfuscatorTest.php | 75 +++---------- .../tests/Unit/ObfuscatorTest.php | 52 --------- .../src/Messaging/Handler/ClassDefinition.php | 23 +++- 20 files changed, 266 insertions(+), 304 deletions(-) delete mode 100644 packages/DataProtection/src/Attribute/Sensitive.php create mode 100644 packages/DataProtection/src/Attribute/WithSensitiveHeader.php create mode 100644 packages/DataProtection/src/Attribute/WithSensitiveHeaders.php delete mode 100644 packages/DataProtection/src/Obfuscator/Obfuscator.php delete mode 100644 packages/DataProtection/tests/Unit/ObfuscatorTest.php diff --git a/packages/DataProtection/src/Attribute/Sensitive.php b/packages/DataProtection/src/Attribute/Sensitive.php deleted file mode 100644 index 6b1a07d38..000000000 --- a/packages/DataProtection/src/Attribute/Sensitive.php +++ /dev/null @@ -1,12 +0,0 @@ -headers, 'Header names should be all strings.'); + } +} diff --git a/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php b/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php index 20b28b993..4b0c8286c 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php +++ b/packages/DataProtection/src/Configuration/DataProtectionConfiguration.php @@ -36,11 +36,6 @@ public function withKey(string $name, Key $key, bool $asDefault = false): self return $config; } - public function key(?string $name): Key - { - return $this->keys[$name] ?? $this->keys[$this->defaultKey]; - } - public function keyName(?string $name): string { return array_key_exists($name, $this->keys) ? $name : $this->defaultKey; diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index 13caeb1dd..dc37801e4 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -7,9 +7,11 @@ namespace Ecotone\DataProtection\Configuration; +use Defuse\Crypto\Key; use Ecotone\AnnotationFinder\AnnotationFinder; -use Ecotone\DataProtection\Attribute\Sensitive; use Ecotone\DataProtection\Attribute\UsingSensitiveData; +use Ecotone\DataProtection\Attribute\WithSensitiveHeader; +use Ecotone\DataProtection\Attribute\WithSensitiveHeaders; use Ecotone\DataProtection\Obfuscator\MessageObfuscator; use Ecotone\DataProtection\Obfuscator\Obfuscator; use Ecotone\DataProtection\OutboundDecryptionChannelBuilder; @@ -21,9 +23,9 @@ use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\NoExternalConfigurationModule; use Ecotone\Messaging\Config\Configuration; use Ecotone\Messaging\Config\Container\Definition; +use Ecotone\Messaging\Config\Container\Reference; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ModuleReferenceSearchService; -use Ecotone\Messaging\Handler\ClassPropertyDefinition; use Ecotone\Messaging\Handler\InterfaceToCallRegistry; use Ecotone\Messaging\Handler\Type; use Ecotone\Messaging\Support\Assert; @@ -43,13 +45,17 @@ public static function create(AnnotationFinder $annotationRegistrationService, I $messagesUsingSensitiveData = $annotationRegistrationService->findAnnotatedClasses(UsingSensitiveData::class); foreach ($messagesUsingSensitiveData as $messageUsingSensitiveData) { - /** @var UsingSensitiveData $attribute */ - $usingSensitiveDataAttribute = $annotationRegistrationService->getAttributeForClass($messageUsingSensitiveData, UsingSensitiveData::class); $classDefinition = $interfaceToCallRegistry->getClassDefinitionFor(Type::create($messageUsingSensitiveData)); + $usingSensitiveDataAttribute = $classDefinition->getSingleClassAnnotation(Type::create(UsingSensitiveData::class)); + + $sensitiveHeaders = $classDefinition->findSingleClassAnnotation(Type::create(WithSensitiveHeaders::class))?->headers ?? []; + foreach ($classDefinition->getClassAnnotations(Type::create(WithSensitiveHeader::class)) as $sensitiveHeader) { + $sensitiveHeaders[] = $sensitiveHeader->header; + } $obfuscators[$messageUsingSensitiveData] = [ - 'properties' => $classDefinition->getProperties(), 'encryptionKey' => $usingSensitiveDataAttribute->encryptionKeyName(), + 'sensitiveHeaders' => $sensitiveHeaders, ]; } @@ -62,29 +68,26 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO Assert::isTrue(ExtensionObjectResolver::contains(JMSConverterConfiguration::class, $extensionObjects), sprintf('%s package require %s package to be enabled. Did you forget to define %s?', ModulePackageList::DATA_PROTECTION_PACKAGE, ModulePackageList::JMS_CONVERTER_PACKAGE, JMSConverterConfiguration::class)); $dataProtectionConfiguration = ExtensionObjectResolver::resolveUnique(DataProtectionConfiguration::class, $extensionObjects, new stdClass()); - $jMSConverterConfiguration = ExtensionObjectResolver::resolveUnique(JMSConverterConfiguration::class, $extensionObjects, JMSConverterConfiguration::createWithDefaults()); - - - $isScalarProperty = static function (ClassPropertyDefinition $property) use ($jMSConverterConfiguration): bool { - $type = $property->getType(); - - return $type->isScalar() || ($type->isEnum() && $jMSConverterConfiguration->isEnumSupportEnabled()); - }; - $obfuscators = array_map(function (array $config) use ($dataProtectionConfiguration, $isScalarProperty): Obfuscator { - $sensitiveProperties = array_map( - static fn (ClassPropertyDefinition $property): string => $property->getName(), - array_filter($config['properties'], static fn (ClassPropertyDefinition $property) => $property->hasAnnotation(Type::create(Sensitive::class))) - ); - $scalarProperties = array_map( - static fn (ClassPropertyDefinition $property): string => $property->getName(), - array_filter($config['properties'], static fn (ClassPropertyDefinition $property) => $isScalarProperty($property)) + foreach ($dataProtectionConfiguration->keys() as $encryptionKeyName => $key) { + $messagingConfiguration->registerServiceDefinition( + id: sprintf('ecotone.encryption.key.%s', $encryptionKeyName), + definition: new Definition( + Key::class, + [$key->saveToAsciiSafeString()], + 'loadFromAsciiSafeString' + ) ); + } - return new Obfuscator($sensitiveProperties, $scalarProperties, $dataProtectionConfiguration->key($config['encryptionKey'])); - }, $this->obfuscators); + $messageObfuscatorDefinition = new Definition(MessageObfuscator::class); + + foreach ($this->obfuscators as $messageClass => $config) { + $messageObfuscatorDefinition->addMethodCall('withKey', [$messageClass, Reference::to(sprintf('ecotone.encryption.key.%s', $dataProtectionConfiguration->keyName($config['encryptionKey'])))]); + $messageObfuscatorDefinition->addMethodCall('withSensitiveHeaders', [$messageClass, $config['sensitiveHeaders']]); + } - $messagingConfiguration->registerServiceDefinition(id: MessageObfuscator::class, definition: new Definition(MessageObfuscator::class, [$obfuscators])); + $messagingConfiguration->registerServiceDefinition(id: MessageObfuscator::class, definition: $messageObfuscatorDefinition); foreach (ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects) as $pollableMessageChannel) { $messagingConfiguration->registerChannelInterceptor( diff --git a/packages/DataProtection/src/Obfuscator/MessageObfuscator.php b/packages/DataProtection/src/Obfuscator/MessageObfuscator.php index e9ffa4b51..3e4a3a13e 100644 --- a/packages/DataProtection/src/Obfuscator/MessageObfuscator.php +++ b/packages/DataProtection/src/Obfuscator/MessageObfuscator.php @@ -5,43 +5,88 @@ */ namespace Ecotone\DataProtection\Obfuscator; +use Defuse\Crypto\Crypto; +use Defuse\Crypto\Key; use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageHeaders; +use Ecotone\Messaging\Support\Assert; +use Ecotone\Messaging\Support\MessageBuilder; class MessageObfuscator { /** - * @param array $obfuscators + * @var array */ - public function __construct(private array $obfuscators) - { - } + private array $encryptionKeys = []; + + /** + * @var array> + */ + private array $sensitiveHeaders = []; - public function encrypt(Message $message): string + public function encrypt(Message $message): Message { if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { - return $message->getPayload(); + return $message; } $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); - if (! array_key_exists($type, $this->obfuscators)) { - return $message->getPayload(); + if (! array_key_exists($type, $this->encryptionKeys)) { + return $message; } - return $this->obfuscators[$type]->encrypt($message->getPayload()); + $key = $this->encryptionKeys[$type]; + $encryptedPayload = base64_encode(Crypto::encrypt($message->getPayload(), $key)); + $headers = $message->getHeaders()->headers(); + foreach ($this->sensitiveHeaders[$type] as $sensitiveHeader) { + if (array_key_exists($sensitiveHeader, $headers)) { + $headers[$sensitiveHeader] = base64_encode(Crypto::encrypt($headers[$sensitiveHeader], $key)); + } + } + + $preparedMessage = MessageBuilder::withPayload($encryptedPayload) + ->setMultipleHeaders($headers) + ; + + return $preparedMessage->build(); } - public function decrypt(Message $message): string + public function decrypt(Message $message): Message { if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { - return $message->getPayload(); + return $message; } $type = $message->getHeaders()->get(MessageHeaders::TYPE_ID); - if (! array_key_exists($type, $this->obfuscators)) { - return $message->getPayload(); + if (! array_key_exists($type, $this->encryptionKeys)) { + return $message; + } + + $key = $this->encryptionKeys[$type]; + $decryptedPayload = Crypto::decrypt(base64_decode($message->getPayload()), $key); + $headers = $message->getHeaders()->headers(); + foreach ($this->sensitiveHeaders[$type] as $sensitiveHeader) { + if (array_key_exists($sensitiveHeader, $headers)) { + $headers[$sensitiveHeader] = Crypto::decrypt(base64_decode($headers[$sensitiveHeader]), $key); + } } - return $this->obfuscators[$type]->decrypt($message->getPayload()); + $preparedMessage = MessageBuilder::withPayload($decryptedPayload) + ->setMultipleHeaders($headers) + ; + + return $preparedMessage->build(); + } + + public function withKey(string $messageClass, Key $key): void + { + $this->encryptionKeys[$messageClass] = $key; + } + + public function withSensitiveHeaders(string $messageClass, array $headers): void + { + Assert::allStrings($headers, sprintf('Headers for message class %s should be array of strings', $messageClass)); + + $this->sensitiveHeaders[$messageClass] = $headers; } } diff --git a/packages/DataProtection/src/Obfuscator/Obfuscator.php b/packages/DataProtection/src/Obfuscator/Obfuscator.php deleted file mode 100644 index 98d41121a..000000000 --- a/packages/DataProtection/src/Obfuscator/Obfuscator.php +++ /dev/null @@ -1,66 +0,0 @@ -obfuscateAll = $sensitive === []; - } - - public function encrypt(string $json): string - { - $payload = json_decode($json, true); - $sensitiveParameters = $this->resolveSensitiveParameters($payload); - - foreach ($sensitiveParameters as $key) { - $value = in_array($key, $this->scalar) ? $payload[$key] : json_encode($payload[$key]); - - $payload[$key] = base64_encode(Crypto::encrypt($value, $this->encryptionKey)); - } - - return json_encode($payload); - } - - public function decrypt(string $json): string - { - $payload = json_decode($json, true); - $sensitiveParameters = $this->resolveSensitiveParameters($payload); - - foreach ($sensitiveParameters as $key) { - $value = Crypto::decrypt(base64_decode($payload[$key]), $this->encryptionKey); - - $payload[$key] = in_array($key, $this->scalar) ? $value : json_decode($value, true); - } - - return json_encode($payload); - } - - private function resolveSensitiveParameters(array $payload): array - { - if ($this->obfuscateAll) { - return array_keys($payload); - } - - return array_filter($this->sensitive, static fn (string $key) => array_key_exists($key, $payload)); - } - - public function getDefinition(): Definition - { - return Definition::createFor(self::class, [$this->sensitive, $this->scalar, $this->encryptionKey]); - } -} diff --git a/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php index af0f1a806..643fadb14 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelBuilder.php @@ -3,6 +3,7 @@ /** * licence Enterprise */ + namespace Ecotone\DataProtection; use Ecotone\DataProtection\Obfuscator\MessageObfuscator; @@ -14,9 +15,8 @@ class OutboundDecryptionChannelBuilder implements ChannelInterceptorBuilder { - public function __construct( - private string $relatedChannel - ) { + public function __construct(private string $relatedChannel) + { } public function relatedChannelName(): string @@ -31,8 +31,6 @@ public function getPrecedence(): int public function compile(MessagingContainerBuilder $builder): Definition { - return new Definition(OutboundDecryptionChannelInterceptor::class, [ - Reference::to(MessageObfuscator::class), - ]); + return new Definition(OutboundDecryptionChannelInterceptor::class, [Reference::to(MessageObfuscator::class)]); } } diff --git a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php index 28a566405..a638cb6bf 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php @@ -11,7 +11,6 @@ use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageChannel; use Ecotone\Messaging\MessageHeaders; -use Ecotone\Messaging\Support\MessageBuilder; class OutboundDecryptionChannelInterceptor extends AbstractChannelInterceptor { @@ -25,13 +24,7 @@ public function postReceive(Message $message, MessageChannel $messageChannel): ? return $message; } - $payload = $this->messageObfuscator->decrypt($message); - - $preparedMessage = MessageBuilder::withPayload($payload) - ->setMultipleHeaders($message->getHeaders()->headers()) - ; - - return $preparedMessage->build(); + return $this->messageObfuscator->decrypt($message); } private function canHandle(Message $message): bool diff --git a/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php index f3378c064..ce24c42ec 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelBuilder.php @@ -31,8 +31,6 @@ public function getPrecedence(): int public function compile(MessagingContainerBuilder $builder): Definition { - return new Definition(OutboundEncryptionChannelInterceptor::class, [ - Reference::to(MessageObfuscator::class), - ]); + return new Definition(OutboundEncryptionChannelInterceptor::class, [Reference::to(MessageObfuscator::class)]); } } diff --git a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php index 4604c073f..2ba652023 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php @@ -11,7 +11,6 @@ use Ecotone\Messaging\Message; use Ecotone\Messaging\MessageChannel; use Ecotone\Messaging\MessageHeaders; -use Ecotone\Messaging\Support\MessageBuilder; class OutboundEncryptionChannelInterceptor extends AbstractChannelInterceptor { @@ -25,13 +24,7 @@ public function preSend(Message $message, MessageChannel $messageChannel): ?Mess return $message; } - $payload = $this->messageObfuscator->encrypt($message); - - $preparedMessage = MessageBuilder::withPayload($payload) - ->setMultipleHeaders($message->getHeaders()->headers()) - ; - - return $preparedMessage->build(); + return $this->messageObfuscator->encrypt($message); } private function canHandle(Message $message): bool diff --git a/packages/DataProtection/tests/Fixture/MessageReceiver.php b/packages/DataProtection/tests/Fixture/MessageReceiver.php index 65f2da33d..6530b67bc 100644 --- a/packages/DataProtection/tests/Fixture/MessageReceiver.php +++ b/packages/DataProtection/tests/Fixture/MessageReceiver.php @@ -5,14 +5,21 @@ class MessageReceiver { private ?object $receivedMessage = null; + private array $receivedHeaders = []; - public function withReceivedMessage(object $message): void + public function withReceived(object $message, array $headers): void { $this->receivedMessage = $message; + $this->receivedHeaders = $headers; } - public function receivedMessage(): object + public function receivedMessage(): ?object { return $this->receivedMessage; } + + public function receivedHeaders(): array + { + return $this->receivedHeaders; + } } diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php index 56a8c20dc..26fa7722d 100644 --- a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php +++ b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/FullyObfuscatedMessage.php @@ -3,10 +3,14 @@ namespace Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages; use Ecotone\DataProtection\Attribute\UsingSensitiveData; +use Ecotone\DataProtection\Attribute\WithSensitiveHeader; +use Ecotone\DataProtection\Attribute\WithSensitiveHeaders; use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; #[UsingSensitiveData] +#[WithSensitiveHeaders(['foo', 'bar'])] +#[WithSensitiveHeader('fos')] class FullyObfuscatedMessage { public function __construct( diff --git a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php index eef1b87aa..fc1f7c2b8 100644 --- a/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php +++ b/packages/DataProtection/tests/Fixture/ObfuscateAnnotatedMessages/PartiallyObfuscatedMessage.php @@ -2,18 +2,18 @@ namespace Test\Ecotone\DataProtection\Fixture\ObfuscateAnnotatedMessages; -use Ecotone\DataProtection\Attribute\Sensitive; use Ecotone\DataProtection\Attribute\UsingSensitiveData; +use Ecotone\DataProtection\Attribute\WithSensitiveHeader; use Test\Ecotone\DataProtection\Fixture\TestClass; use Test\Ecotone\DataProtection\Fixture\TestEnum; #[UsingSensitiveData] +#[WithSensitiveHeader('foo')] +#[WithSensitiveHeader('bar')] class PartiallyObfuscatedMessage { public function __construct( - #[Sensitive] public TestClass $class, - #[Sensitive] public TestEnum $enum, public string $argument ) { diff --git a/packages/DataProtection/tests/Fixture/TestCommandHandler.php b/packages/DataProtection/tests/Fixture/TestCommandHandler.php index 5156dbaa5..2d8a87dfe 100644 --- a/packages/DataProtection/tests/Fixture/TestCommandHandler.php +++ b/packages/DataProtection/tests/Fixture/TestCommandHandler.php @@ -3,6 +3,7 @@ namespace Test\Ecotone\DataProtection\Fixture; use Ecotone\Messaging\Attribute\Asynchronous; +use Ecotone\Messaging\Attribute\Parameter\Headers; use Ecotone\Messaging\Attribute\Parameter\Payload; use Ecotone\Messaging\Attribute\Parameter\Reference; use Ecotone\Modelling\Attribute\CommandHandler; @@ -16,24 +17,27 @@ class TestCommandHandler #[CommandHandler(endpointId: 'test.FullyObfuscatedMessage')] public function handleFullyObfuscatedMessage( #[Payload] FullyObfuscatedMessage $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceivedMessage($message); + $messageReceiver->withReceived($message, $headers); } #[CommandHandler(endpointId: 'test.PartiallyObfuscatedMessage')] public function handlePartiallyObfuscatedMessage( #[Payload] PartiallyObfuscatedMessage $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceivedMessage($message); + $messageReceiver->withReceived($message, $headers); } #[CommandHandler(endpointId: 'test.MessageWithSecondaryKeyEncryption')] public function handleMessageWithSecondaryKeyEncryption( #[Payload] MessageWithSecondaryKeyEncryption $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceivedMessage($message); + $messageReceiver->withReceived($message, $headers); } } diff --git a/packages/DataProtection/tests/Fixture/TestEventHandler.php b/packages/DataProtection/tests/Fixture/TestEventHandler.php index 4d3440948..a2ab8c9f9 100644 --- a/packages/DataProtection/tests/Fixture/TestEventHandler.php +++ b/packages/DataProtection/tests/Fixture/TestEventHandler.php @@ -3,6 +3,7 @@ namespace Test\Ecotone\DataProtection\Fixture; use Ecotone\Messaging\Attribute\Asynchronous; +use Ecotone\Messaging\Attribute\Parameter\Headers; use Ecotone\Messaging\Attribute\Parameter\Payload; use Ecotone\Messaging\Attribute\Parameter\Reference; use Ecotone\Modelling\Attribute\CommandHandler; @@ -17,24 +18,27 @@ class TestEventHandler #[EventHandler(endpointId: 'test.FullyObfuscatedMessage')] public function handleFullyObfuscatedMessage( #[Payload] FullyObfuscatedMessage $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceivedMessage($message); + $messageReceiver->withReceived($message, $headers); } #[EventHandler(endpointId: 'test.PartiallyObfuscatedMessage')] public function handlePartiallyObfuscatedMessage( #[Payload] PartiallyObfuscatedMessage $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceivedMessage($message); + $messageReceiver->withReceived($message, $headers); } #[EventHandler(endpointId: 'test.MessageWithSecondaryKeyEncryption')] public function handleMessageWithSecondaryKeyEncryption( #[Payload] MessageWithSecondaryKeyEncryption $message, + #[Headers] array $headers, #[Reference] MessageReceiver $messageReceiver, ): void { - $messageReceiver->withReceivedMessage($message); + $messageReceiver->withReceived($message, $headers); } } diff --git a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php index ebc3b1737..b26bcac76 100644 --- a/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php +++ b/packages/DataProtection/tests/Integration/ObfuscateAnnotatedMessagesTest.php @@ -48,19 +48,31 @@ public function test_fully_obfuscated_command_handler_message(): void class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', - ) + ), + metadata: $metadataSent = [ + 'foo' => 'secret-value', + 'bar' => 'even-more-secret-value', + 'baz' => 'non-sensitive-value', + ] ); - $messagePayload = json_decode($channel->receive()->getPayload(), true); + $channelMessage = $channel->receive(); + $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); + $messageHeaders = $channelMessage->getHeaders(); - self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); - self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); - self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->primaryKey)); + self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); + self::assertEquals('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); + self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); + self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - $ecotone->sendCommand($messageSent); + $ecotone->sendCommand($messageSent, metadata: $metadataSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('secret-value', $receivedHeaders['foo']); + self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); + self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); } public function test_partially_obfuscated_command_handler_message(): void @@ -72,19 +84,31 @@ public function test_partially_obfuscated_command_handler_message(): void class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', - ) + ), + metadata: $metadataSent = [ + 'foo' => 'secret-value', + 'bar' => 'even-more-secret-value', + 'baz' => 'non-sensitive-value', + ] ); - $messagePayload = json_decode($channel->receive()->getPayload(), true); + $channelMessage = $channel->receive(); + $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); + $messageHeaders = $channelMessage->getHeaders(); - self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); - self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); - self::assertEquals('value', $messagePayload['argument']); + self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); + self::assertEquals('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); + self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); + self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - $ecotone->sendCommand($messageSent); + $ecotone->sendCommand($messageSent, metadata: $metadataSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('secret-value', $receivedHeaders['foo']); + self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); + self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); } public function test_obfuscate_command_handler_message_with_non_default_key(): void @@ -93,9 +117,10 @@ public function test_obfuscate_command_handler_message_with_non_default_key(): v $ecotone->sendCommand($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')); - $messagePayload = json_decode($channel->receive()->getPayload(), true); + $channelMessage = $channel->receive(); + $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); - self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->secondaryKey)); + self::assertEquals('{"argument":"value"}', $messagePayload); $ecotone->sendCommand($messageSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); @@ -112,19 +137,31 @@ public function test_fully_obfuscated_event_handler_message(): void class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', - ) + ), + metadata: $metadataSent = [ + 'foo' => 'secret-value', + 'bar' => 'even-more-secret-value', + 'baz' => 'non-sensitive-value', + ] ); - $messagePayload = json_decode($channel->receive()->getPayload(), true); + $channelMessage = $channel->receive(); + $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); + $messageHeaders = $channelMessage->getHeaders(); - self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); - self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); - self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->primaryKey)); + self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); + self::assertEquals('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); + self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); + self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - $ecotone->publishEvent($messageSent); + $ecotone->publishEvent($messageSent, metadata: $metadataSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('secret-value', $receivedHeaders['foo']); + self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); + self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); } public function test_partially_obfuscated_event_handler_message(): void @@ -136,19 +173,31 @@ public function test_partially_obfuscated_event_handler_message(): void class: new TestClass('value', TestEnum::FIRST), enum: TestEnum::FIRST, argument: 'value', - ) + ), + metadata: $metadataSent = [ + 'foo' => 'secret-value', + 'bar' => 'even-more-secret-value', + 'baz' => 'non-sensitive-value', + ] ); - $messagePayload = json_decode($channel->receive()->getPayload(), true); + $channelMessage = $channel->receive(); + $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); + $messageHeaders = $channelMessage->getHeaders(); - self::assertEquals('{"argument":"value","enum":"first"}', Crypto::decrypt(base64_decode($messagePayload['class']), $this->primaryKey)); - self::assertEquals(TestEnum::FIRST->value, Crypto::decrypt(base64_decode($messagePayload['enum']), $this->primaryKey)); - self::assertEquals('value', $messagePayload['argument']); + self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); + self::assertEquals('secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('foo')), $this->primaryKey)); + self::assertEquals('even-more-secret-value', Crypto::decrypt(base64_decode($messageHeaders->get('bar')), $this->primaryKey)); + self::assertEquals('non-sensitive-value', $messageHeaders->get('baz')); - $ecotone->publishEvent($messageSent); + $ecotone->publishEvent($messageSent, metadata: $metadataSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); + $receivedHeaders = $messageReceiver->receivedHeaders(); self::assertEquals($messageSent, $messageReceiver->receivedMessage()); + self::assertEquals('secret-value', $receivedHeaders['foo']); + self::assertEquals('even-more-secret-value', $receivedHeaders['bar']); + self::assertEquals('non-sensitive-value', $receivedHeaders['baz']); } public function test_obfuscate_event_handler_message_with_non_default_key(): void @@ -157,9 +206,10 @@ public function test_obfuscate_event_handler_message_with_non_default_key(): voi $ecotone->publishEvent($messageSent = new MessageWithSecondaryKeyEncryption(argument: 'value')); - $messagePayload = json_decode($channel->receive()->getPayload(), true); + $channelMessage = $channel->receive(); + $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); - self::assertEquals('value', Crypto::decrypt(base64_decode($messagePayload['argument']), $this->secondaryKey)); + self::assertEquals('{"argument":"value"}', $messagePayload); $ecotone->publishEvent($messageSent); $ecotone->run('test', ExecutionPollingMetadata::createWithTestingSetup()); diff --git a/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php b/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php index 17c7642d7..49066d74a 100644 --- a/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php +++ b/packages/DataProtection/tests/Unit/MessageObfuscatorTest.php @@ -17,6 +17,7 @@ class MessageObfuscatorTest extends TestCase { private Message $message; + private Message $messageWithoutTypeId; protected function setUp(): void { @@ -25,77 +26,27 @@ protected function setUp(): void 'bar' => 'value', ], JSON_THROW_ON_ERROR)) ->setHeader(MessageHeaders::TYPE_ID, ObfuscatedMessage::class) + ->setHeader('foo', 'bar') ->build() ; - } - - public function test_obfuscate_message_fully(): void - { - $obfuscator = new Obfuscator([], ['foo', 'bar'], Key::createNewRandomKey()); - $messageObfuscator = new MessageObfuscator([ObfuscatedMessage::class => $obfuscator]); - - $encryptedPayload = $messageObfuscator->encrypt($this->message); - - $encryptedMessage = MessageBuilder::fromMessage($this->message) - ->setPayload($encryptedPayload) - ->build() - ; - - $decryptedPayload = $messageObfuscator->decrypt($encryptedMessage); - - $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); - - self::assertNotEquals('value', $payload['foo']); - self::assertNotEquals('value', $payload['bar']); - self::assertNotEquals($this->message->getPayload(), $encryptedPayload); - self::assertEquals($this->message->getPayload(), $decryptedPayload); - } - - public function test_obfuscate_message_partially(): void - { - $obfuscator = new Obfuscator(['foo', 'non-existing-argument'], ['foo', 'bar'], Key::createNewRandomKey()); - $messageObfuscator = new MessageObfuscator([ObfuscatedMessage::class => $obfuscator]); - $encryptedPayload = $messageObfuscator->encrypt($this->message); - - $encryptedMessage = MessageBuilder::fromMessage($this->message) - ->setPayload($encryptedPayload) + $this->messageWithoutTypeId = MessageBuilder::withPayload(json_encode([ + 'foo' => 'value', + 'bar' => 'value', + ], JSON_THROW_ON_ERROR)) + ->setHeader('foo', 'bar') ->build() ; - - $decryptedPayload = $messageObfuscator->decrypt($encryptedMessage); - - $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); - - self::assertNotEquals('value', $payload['foo']); - self::assertEquals('value', $payload['bar']); - self::assertNotEquals($this->message->getPayload(), $encryptedPayload); - self::assertEquals($this->message->getPayload(), $decryptedPayload); - self::assertArrayNotHasKey('non-existing-argument', $payload); - self::assertArrayNotHasKey('non-existing-argument', json_decode($decryptedPayload, true, 512, JSON_THROW_ON_ERROR)); } - public function test_dont_obfuscate_unsupported_message(): void + public function test_obfuscate_only_supported_message(): void { - $obfuscator = new Obfuscator(['foo', 'bar'], ['foo', 'bar'], Key::createNewRandomKey()); - $messageObfuscator = new MessageObfuscator([\stdClass::class => $obfuscator]); - - $encryptedPayload = $messageObfuscator->encrypt($this->message); - - $encryptedMessage = MessageBuilder::fromMessage($this->message) - ->setPayload($encryptedPayload) - ->build() - ; - - $decryptedPayload = $messageObfuscator->decrypt($encryptedMessage); + $messageObfuscator = new MessageObfuscator(); - $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); + self::assertSame($this->message, $messageObfuscator->encrypt($this->message)); + self::assertSame($this->messageWithoutTypeId, $messageObfuscator->encrypt($this->messageWithoutTypeId)); - self::assertEquals('value', $payload['foo']); - self::assertEquals('value', $payload['bar']); - self::assertEquals($this->message->getPayload(), $encryptedPayload); - self::assertEquals($this->message->getPayload(), $decryptedPayload); - self::assertArrayNotHasKey('non-existing-argument', $payload); - self::assertArrayNotHasKey('non-existing-argument', json_decode($decryptedPayload, true, 512, JSON_THROW_ON_ERROR)); + self::assertSame($this->message, $messageObfuscator->decrypt($this->message)); + self::assertSame($this->messageWithoutTypeId, $messageObfuscator->decrypt($this->messageWithoutTypeId)); } } diff --git a/packages/DataProtection/tests/Unit/ObfuscatorTest.php b/packages/DataProtection/tests/Unit/ObfuscatorTest.php deleted file mode 100644 index 3007a60d8..000000000 --- a/packages/DataProtection/tests/Unit/ObfuscatorTest.php +++ /dev/null @@ -1,52 +0,0 @@ -payload = json_encode([ - 'foo' => 'value', - 'bar' => 'value', - ], JSON_THROW_ON_ERROR); - } - - public function test_obfuscate_payload_fully(): void - { - $obfuscator = new Obfuscator([], ['foo', 'bar'], Key::createNewRandomKey()); - - $encryptedPayload = $obfuscator->encrypt($this->payload); - $decryptedPayload = $obfuscator->decrypt($encryptedPayload); - - $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); - - self::assertNotEquals('value', $payload['foo']); - self::assertNotEquals('value', $payload['bar']); - self::assertNotEquals($this->payload, $encryptedPayload); - self::assertEquals($this->payload, $decryptedPayload); - } - - public function test_obfuscate_payload_partially(): void - { - $obfuscator = new Obfuscator(['foo', 'non-existing-argument'], ['foo', 'bar'], Key::createNewRandomKey()); - - $encryptedPayload = $obfuscator->encrypt($this->payload); - $decryptedPayload = $obfuscator->decrypt($encryptedPayload); - - $payload = json_decode($encryptedPayload, true, 512, JSON_THROW_ON_ERROR); - - self::assertNotEquals('value', $payload['foo']); - self::assertEquals('value', $payload['bar']); - self::assertNotEquals($this->payload, $encryptedPayload); - self::assertEquals($this->payload, $decryptedPayload); - self::assertArrayNotHasKey('non-existing-argument', $payload); - self::assertArrayNotHasKey('non-existing-argument', json_decode($decryptedPayload, true, 512, JSON_THROW_ON_ERROR)); - } -} diff --git a/packages/Ecotone/src/Messaging/Handler/ClassDefinition.php b/packages/Ecotone/src/Messaging/Handler/ClassDefinition.php index 927a9e413..9c75584a6 100644 --- a/packages/Ecotone/src/Messaging/Handler/ClassDefinition.php +++ b/packages/Ecotone/src/Messaging/Handler/ClassDefinition.php @@ -151,13 +151,12 @@ public function getPropertiesWithAnnotation(Type $annotationClass): array /** * @return object[] */ - public function getClassAnnotations(): array + public function getClassAnnotations(?ObjectType $annotationType = null): array { - return $this->classAnnotations; - } + if ($annotationType === null) { + return $this->classAnnotations; + } - public function getSingleClassAnnotation(ObjectType $annotationType): object - { $foundAnnotations = []; foreach ($this->classAnnotations as $classAnnotation) { if ($annotationType->accepts($classAnnotation)) { @@ -165,6 +164,13 @@ public function getSingleClassAnnotation(ObjectType $annotationType): object } } + return $foundAnnotations; + } + + public function getSingleClassAnnotation(ObjectType $annotationType): object + { + $foundAnnotations = $this->getClassAnnotations($annotationType); + if (count($foundAnnotations) < 1) { throw InvalidArgumentException::create("Attribute {$annotationType} was not found for {$this}"); } @@ -175,6 +181,13 @@ public function getSingleClassAnnotation(ObjectType $annotationType): object return $foundAnnotations[0]; } + public function findSingleClassAnnotation(ObjectType $annotationType): ?object + { + $foundAnnotations = $this->getClassAnnotations($annotationType); + + return $foundAnnotations[0] ?? null; + } + public function isAnnotation(): bool { return $this->isAnnotation; From 7460dcac305d0196ce95f7460a8b5b759bbbadcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Zaj=C4=85c?= Date: Mon, 19 Jan 2026 22:57:42 +0100 Subject: [PATCH 5/5] bump ecotone version --- packages/DataProtection/composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/DataProtection/composer.json b/packages/DataProtection/composer.json index afc90acb1..5e56ed9fc 100644 --- a/packages/DataProtection/composer.json +++ b/packages/DataProtection/composer.json @@ -31,7 +31,8 @@ }, "require": { "ext-openssl": "*", - "ecotone/ecotone": "~1.293.0", + "ecotone/ecotone": "~1.295.0", + "ecotone/jms-converter": "~1.295.0", "defuse/php-encryption": "^2.4" }, "require-dev": {