diff --git a/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php b/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php index 14a1ff1fb..a3290e312 100644 --- a/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php +++ b/packages/DataProtection/src/Configuration/ChannelProtectionConfiguration.php @@ -5,13 +5,13 @@ /** * licence Enterprise */ -class ChannelProtectionConfiguration +readonly class ChannelProtectionConfiguration { private function __construct( - private string $channelName, - private ?string $encryptionKey, - private bool $isPayloadSensitive, - private array $sensitiveHeaders, + public string $channelName, + public ?string $encryptionKey, + public bool $isPayloadSensitive, + public array $sensitiveHeaders, ) { } @@ -20,29 +20,18 @@ public static function create(string $channelName, ?string $encryptionKey = null return new self($channelName, $encryptionKey, $isPayloadSensitive, $sensitiveHeaders); } - public function channelName(): string + public function withEncryptionKey(string $encryptionKey): self { - return $this->channelName; - } - - public function messageEncryptionConfig(): MessageEncryptionConfig - { - return new MessageEncryptionConfig($this->encryptionKey, $this->isPayloadSensitive, $this->sensitiveHeaders); + return self::create($this->channelName, $encryptionKey, $this->isPayloadSensitive, $this->sensitiveHeaders); } public function withSensitivePayload(bool $isPayloadSensitive): self { - $config = clone $this; - $config->isPayloadSensitive = $isPayloadSensitive; - - return $config; + return self::create($this->channelName, $this->encryptionKey, $isPayloadSensitive, $this->sensitiveHeaders); } public function withSensitiveHeader(string $sensitiveHeader): self { - $config = clone $this; - $config->sensitiveHeaders[] = $sensitiveHeader; - - return $config; + return self::create($this->channelName, $this->encryptionKey, $this->isPayloadSensitive, array_merge($this->sensitiveHeaders, [$sensitiveHeader])); } } diff --git a/packages/DataProtection/src/Configuration/DataProtectionModule.php b/packages/DataProtection/src/Configuration/DataProtectionModule.php index dfff37927..6e98cc01e 100644 --- a/packages/DataProtection/src/Configuration/DataProtectionModule.php +++ b/packages/DataProtection/src/Configuration/DataProtectionModule.php @@ -11,11 +11,13 @@ use Ecotone\AnnotationFinder\AnnotationFinder; use Ecotone\DataProtection\Attribute\Sensitive; use Ecotone\DataProtection\Attribute\WithEncryptionKey; -use Ecotone\DataProtection\Attribute\WithSensitiveHeader; use Ecotone\DataProtection\Encryption\Key; -use Ecotone\DataProtection\MessageEncryption\MessageEncryptor; use Ecotone\DataProtection\OutboundDecryptionChannelBuilder; use Ecotone\DataProtection\OutboundEncryptionChannelBuilder; +use Ecotone\DataProtection\Protector\ChannelProtector; +use Ecotone\DataProtection\Protector\DataDecryptor; +use Ecotone\DataProtection\Protector\DataEncryptor; +use Ecotone\DataProtection\Protector\DataProtectorConfig; use Ecotone\JMSConverter\JMSConverterConfiguration; use Ecotone\Messaging\Attribute\ModuleAnnotation; use Ecotone\Messaging\Attribute\Parameter\Header; @@ -28,6 +30,7 @@ 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,19 +46,19 @@ final class DataProtectionModule extends NoExternalConfigurationModule final public const KEY_SERVICE_ID_FORMAT = 'ecotone.encryption.key.%s'; /** - * @param array $encryptionConfigs + * @param array $dataProtectorConfigs */ - public function __construct(private array $encryptionConfigs) + public function __construct(private array $dataProtectorConfigs) { } public static function create(AnnotationFinder $annotationRegistrationService, InterfaceToCallRegistry $interfaceToCallRegistry): static { - $encryptionConfigs = self::resolveEncryptionConfigsFromAnnotatedClasses($annotationRegistrationService->findAnnotatedClasses(Sensitive::class), $interfaceToCallRegistry); - $encryptionConfigs = self::resolveEncryptionConfigsFromAnnotatedMethods($annotationRegistrationService->findAnnotatedMethods(CommandHandler::class), $encryptionConfigs, $interfaceToCallRegistry); - $encryptionConfigs = self::resolveEncryptionConfigsFromAnnotatedMethods($annotationRegistrationService->findAnnotatedMethods(EventHandler::class), $encryptionConfigs, $interfaceToCallRegistry); + $dataProtectorConfigs = self::resolveProtectorConfigsFromAnnotatedClasses($annotationRegistrationService->findAnnotatedClasses(Sensitive::class), $interfaceToCallRegistry); + $dataProtectorConfigs = self::resolveProtectorConfigsFromAnnotatedMethods($annotationRegistrationService->findAnnotatedMethods(CommandHandler::class), $dataProtectorConfigs, $interfaceToCallRegistry); + $dataProtectorConfigs = self::resolveProtectorConfigsFromAnnotatedMethods($annotationRegistrationService->findAnnotatedMethods(EventHandler::class), $dataProtectorConfigs, $interfaceToCallRegistry); - return new self($encryptionConfigs); + return new self($dataProtectorConfigs); } public function prepare(Configuration $messagingConfiguration, array $extensionObjects, ModuleReferenceSearchService $moduleReferenceSearchService, InterfaceToCallRegistry $interfaceToCallRegistry): void @@ -82,39 +85,48 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO ); } - $channelEncryptorReferences = $messageEncryptorReferences = []; + $channelProtectorReferences = $messageEncryptorReferences = []; foreach ($channelProtectionConfigurations as $channelProtectionConfiguration) { Assert::isTrue($messagingConfiguration->isPollableChannel($channelProtectionConfiguration->channelName()), sprintf('`%s` channel must be pollable channel to use Data Protection.', $channelProtectionConfiguration->channelName())); - $encryptionConfig = $channelProtectionConfiguration->messageEncryptionConfig(); $messagingConfiguration->registerServiceDefinition( - id: $id = sprintf(self::ENCRYPTOR_SERVICE_ID_FORMAT, $channelProtectionConfiguration->channelName()), + id: $id = sprintf(self::ENCRYPTOR_SERVICE_ID_FORMAT, $channelProtectionConfiguration->channelName), definition: new Definition( - MessageEncryptor::class, + ChannelProtector::class, [ - Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $encryptionConfig->encryptionKeyName($dataProtectionConfiguration))), - $encryptionConfig->isPayloadSensitive, - $encryptionConfig->sensitiveHeaders, + Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $dataProtectionConfiguration->keyName($channelProtectionConfiguration->encryptionKey))), + $channelProtectionConfiguration->isPayloadSensitive, + $channelProtectionConfiguration->sensitiveHeaders, ], ) ); - $channelEncryptorReferences[$channelProtectionConfiguration->channelName()] = Reference::to($id); + $channelProtectorReferences[$channelProtectionConfiguration->channelName()] = Reference::to($id); } - foreach ($this->encryptionConfigs as $messageClass => $encryptionConfig) { - $messagingConfiguration->registerServiceDefinition( - id: $id = sprintf(self::ENCRYPTOR_SERVICE_ID_FORMAT, $messageClass), - definition: new Definition( - MessageEncryptor::class, + foreach ($this->dataProtectorConfigs as $protectorConfig) { + $messagingConfiguration->registerDataProtector( + new Definition( + DataEncryptor::class, [ - Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $encryptionConfig->encryptionKeyName($dataProtectionConfiguration))), - $encryptionConfig->isPayloadSensitive, - $encryptionConfig->sensitiveHeaders, + $protectorConfig->supportedType, + Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $protectorConfig->encryptionKeyName($dataProtectionConfiguration))), + $protectorConfig->sensitiveProperties, + $protectorConfig->scalarProperties, + ], + ) + ); + $messagingConfiguration->registerDataProtector( + new Definition( + DataDecryptor::class, + [ + $protectorConfig->supportedType, + Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $protectorConfig->encryptionKeyName($dataProtectionConfiguration))), + $protectorConfig->sensitiveProperties, + $protectorConfig->scalarProperties, ], ) ); - $messageEncryptorReferences[$messageClass] = Reference::to($id); } foreach (ExtensionObjectResolver::resolve(MessageChannelWithSerializationBuilder::class, $extensionObjects) as $pollableMessageChannel) { @@ -125,14 +137,14 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO $messagingConfiguration->registerChannelInterceptor( new OutboundEncryptionChannelBuilder( relatedChannel: $pollableMessageChannel->getMessageChannelName(), - channelEncryptorReference: $channelEncryptorReferences[$pollableMessageChannel->getMessageChannelName()] ?? null, + channelEncryptorReference: $channelProtectorReferences[$pollableMessageChannel->getMessageChannelName()] ?? null, messageEncryptorReferences: $messageEncryptorReferences, ) ); $messagingConfiguration->registerChannelInterceptor( new OutboundDecryptionChannelBuilder( relatedChannel: $pollableMessageChannel->getMessageChannelName(), - channelEncryptionReference: $channelEncryptorReferences[$pollableMessageChannel->getMessageChannelName()] ?? null, + channelEncryptionReference: $channelProtectorReferences[$pollableMessageChannel->getMessageChannelName()] ?? null, messageEncryptionReferences: $messageEncryptorReferences, ) ); @@ -154,21 +166,27 @@ public function getModulePackageName(): string return ModulePackageList::DATA_PROTECTION_PACKAGE; } - private static function resolveEncryptionConfigsFromAnnotatedClasses(array $sensitiveMessages, InterfaceToCallRegistry $interfaceToCallRegistry): array + private static function resolveProtectorConfigsFromAnnotatedClasses(array $sensitiveMessages, InterfaceToCallRegistry $interfaceToCallRegistry): array { - $encryptionConfigs = []; + $dataEncryptorConfigs = []; foreach ($sensitiveMessages as $message) { - $classDefinition = $interfaceToCallRegistry->getClassDefinitionFor(Type::create($message)); + $classDefinition = $interfaceToCallRegistry->getClassDefinitionFor($messageType = Type::create($message)); $encryptionKey = $classDefinition->findSingleClassAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey(); - $sensitiveHeaders = array_map(static fn (WithSensitiveHeader $annotation) => $annotation->header, $classDefinition->getClassAnnotations(Type::create(WithSensitiveHeader::class)) ?? []); - $encryptionConfigs[$message] = new MessageEncryptionConfig(encryptionKey: $encryptionKey, isPayloadSensitive: true, sensitiveHeaders: $sensitiveHeaders); + $sensitiveProperties = $classDefinition->getPropertiesWithAnnotation(Type::create(Sensitive::class)); + if ($sensitiveProperties === []) { + $sensitiveProperties = array_map(static fn (ClassPropertyDefinition $property): string => $property->getName(), $classDefinition->getProperties()); + } + + $scalarProperties = array_values(array_filter($sensitiveProperties, static fn (string $propertyName): bool => $classDefinition->getProperty($propertyName)->getType()->isScalar())); + + $dataEncryptorConfigs[$message] = new DataProtectorConfig(supportedType: $messageType, encryptionKey: $encryptionKey, sensitiveProperties: $sensitiveProperties, scalarProperties: $scalarProperties); } - return $encryptionConfigs; + return $dataEncryptorConfigs; } - private static function resolveEncryptionConfigsFromAnnotatedMethods(array $annotatedMethods, array $encryptionConfigs, InterfaceToCallRegistry $interfaceToCallRegistry): array + private static function resolveProtectorConfigsFromAnnotatedMethods(array $annotatedMethods, array $encryptionConfigs, InterfaceToCallRegistry $interfaceToCallRegistry): array { /** @var AnnotatedMethod $method */ foreach ($annotatedMethods as $method) { @@ -180,24 +198,17 @@ private static function resolveEncryptionConfigsFromAnnotatedMethods(array $anno || $payload->hasAnnotation(Headers::class) || $payload->hasAnnotation(Reference::class) || array_key_exists($payload->getTypeHint(), $encryptionConfigs) + || ! $payload->hasAnnotation(Sensitive::class) ) { continue; } - $isPayloadSensitive = $payload->hasAnnotation(Sensitive::class); - if (! $isPayloadSensitive) { - continue; - } - + $classDefinition = $interfaceToCallRegistry->getClassDefinitionFor($payload->getTypeDescriptor()); $encryptionKey = $payload->findSingleAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey(); - $sensitiveHeaders = array_map(static fn (WithSensitiveHeader $annotation) => $annotation->header, $methodDefinition->getMethodAnnotationsOf(Type::create(WithSensitiveHeader::class)) ?? []); - foreach ($methodDefinition->getInterfaceParameters() as $parameter) { - if ($parameter->hasAnnotation(Header::class) && $parameter->hasAnnotation(Sensitive::class)) { - $sensitiveHeaders[] = $parameter->getName(); - } - } + $sensitiveProperties = array_map(static fn (ClassPropertyDefinition $property): string => $property->getName(), $classDefinition->getProperties()); + $scalarProperties = array_values(array_filter($sensitiveProperties, static fn (string $propertyName): bool => $classDefinition->getProperty($propertyName)->getType()->isScalar())); - $encryptionConfigs[$payload->getTypeHint()] = new MessageEncryptionConfig(encryptionKey: $encryptionKey, isPayloadSensitive: true, sensitiveHeaders: $sensitiveHeaders); + $encryptionConfigs[$payload->getTypeHint()] = new DataProtectorConfig(supportedType: $payload->getTypeDescriptor(), encryptionKey: $encryptionKey, sensitiveProperties: $sensitiveProperties, scalarProperties: $scalarProperties); } return $encryptionConfigs; diff --git a/packages/DataProtection/src/Configuration/MessageEncryptionConfig.php b/packages/DataProtection/src/Configuration/MessageEncryptionConfig.php deleted file mode 100644 index a768afe6c..000000000 --- a/packages/DataProtection/src/Configuration/MessageEncryptionConfig.php +++ /dev/null @@ -1,28 +0,0 @@ - $sensitiveHeaders - */ - public function __construct( - public ?string $encryptionKey, - public bool $isPayloadSensitive, - public array $sensitiveHeaders, - ) { - Assert::allStrings($this->sensitiveHeaders, 'Sensitive Headers should be array of strings'); - } - - public function encryptionKeyName(DataProtectionConfiguration $dataProtectionConfiguration): string - { - return $dataProtectionConfiguration->keyName($this->encryptionKey); - } -} diff --git a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php index 5540564b9..1607e8064 100644 --- a/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundDecryptionChannelInterceptor.php @@ -6,7 +6,7 @@ namespace Ecotone\DataProtection; -use Ecotone\DataProtection\MessageEncryption\MessageEncryptor; +use Ecotone\DataProtection\MessageEncryption\ChannelEncryptor; use Ecotone\Messaging\Channel\AbstractChannelInterceptor; use Ecotone\Messaging\Conversion\MediaType; use Ecotone\Messaging\Message; @@ -17,13 +17,13 @@ class OutboundDecryptionChannelInterceptor extends AbstractChannelInterceptor { /** - * @param array $messageEncryptors + * @param array $messageEncryptors */ public function __construct( - private readonly ?MessageEncryptor $channelEncryptor, + private readonly ?ChannelEncryptor $channelEncryptor, private readonly array $messageEncryptors, ) { - Assert::allInstanceOfType($this->messageEncryptors, MessageEncryptor::class); + Assert::allInstanceOfType($this->messageEncryptors, ChannelEncryptor::class); } public function postReceive(Message $message, MessageChannel $messageChannel): ?Message @@ -43,7 +43,7 @@ public function postReceive(Message $message, MessageChannel $messageChannel): ? return $message; } - private function findMessageEncryptor(Message $message): ?MessageEncryptor + private function findMessageEncryptor(Message $message): ?ChannelEncryptor { if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { return null; diff --git a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php index 27da47cb2..c5eb858c8 100644 --- a/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php +++ b/packages/DataProtection/src/OutboundEncryptionChannelInterceptor.php @@ -6,7 +6,7 @@ namespace Ecotone\DataProtection; -use Ecotone\DataProtection\MessageEncryption\MessageEncryptor; +use Ecotone\DataProtection\MessageEncryption\ChannelEncryptor; use Ecotone\Messaging\Channel\AbstractChannelInterceptor; use Ecotone\Messaging\Conversion\MediaType; use Ecotone\Messaging\Message; @@ -17,13 +17,13 @@ class OutboundEncryptionChannelInterceptor extends AbstractChannelInterceptor { /** - * @param array $messageEncryptors + * @param array $messageEncryptors */ public function __construct( - private readonly ?MessageEncryptor $channelEncryptor, + private readonly ?ChannelEncryptor $channelEncryptor, private readonly array $messageEncryptors, ) { - Assert::allInstanceOfType($this->messageEncryptors, MessageEncryptor::class); + Assert::allInstanceOfType($this->messageEncryptors, ChannelEncryptor::class); } public function preSend(Message $message, MessageChannel $messageChannel): ?Message @@ -43,7 +43,7 @@ public function preSend(Message $message, MessageChannel $messageChannel): ?Mess return $message; } - private function findMessageEncryptor(Message $message): ?MessageEncryptor + private function findMessageEncryptor(Message $message): ?ChannelEncryptor { if (! $message->getHeaders()->containsKey(MessageHeaders::TYPE_ID)) { return null; diff --git a/packages/DataProtection/src/MessageEncryption/MessageEncryptor.php b/packages/DataProtection/src/Protector/ChannelProtector.php similarity index 95% rename from packages/DataProtection/src/MessageEncryption/MessageEncryptor.php rename to packages/DataProtection/src/Protector/ChannelProtector.php index 619583753..d7f9e61ed 100644 --- a/packages/DataProtection/src/MessageEncryption/MessageEncryptor.php +++ b/packages/DataProtection/src/Protector/ChannelProtector.php @@ -4,7 +4,7 @@ * licence Enterprise */ -namespace Ecotone\DataProtection\MessageEncryption; +namespace Ecotone\DataProtection\Protector; use Ecotone\DataProtection\Encryption\Crypto; use Ecotone\DataProtection\Encryption\Key; @@ -12,7 +12,7 @@ use Ecotone\Messaging\Support\Assert; use Ecotone\Messaging\Support\MessageBuilder; -readonly class MessageEncryptor +readonly class ChannelProtector { public function __construct( private Key $encryptionKey, diff --git a/packages/DataProtection/src/Protector/DataDecryptor.php b/packages/DataProtection/src/Protector/DataDecryptor.php new file mode 100644 index 000000000..cfbb61440 --- /dev/null +++ b/packages/DataProtection/src/Protector/DataDecryptor.php @@ -0,0 +1,46 @@ +sensitiveProperties as $property) { + if (!array_key_exists($property, $source)) { + continue; + } + + $source[$property] = Crypto::decrypt(base64_decode($source[$property]), $this->encryptionKey); + + if (! in_array($property, $this->scalarProperties, true)) { + $source[$property] = json_decode($source[$property], true); + } + } + + return json_encode($source); + } + + public function matches(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool + { + return $sourceMediaType->isCompatibleWith(MediaType::createApplicationJsonEncrypted()) && $targetType->isCompatibleWith($this->supportedType); + } +} diff --git a/packages/DataProtection/src/Protector/DataEncryptor.php b/packages/DataProtection/src/Protector/DataEncryptor.php new file mode 100644 index 000000000..277ce475a --- /dev/null +++ b/packages/DataProtection/src/Protector/DataEncryptor.php @@ -0,0 +1,46 @@ +sensitiveProperties as $property) { + if (! array_key_exists($property, $source)) { + continue; + } + + if (! in_array($property, $this->scalarProperties, true)) { + $source[$property] = json_encode($source[$property]); + } + + $source[$property] = base64_encode(Crypto::encrypt($source[$property], $this->encryptionKey)); + } + + return json_encode($source); + } + + public function matches(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool + { + return $sourceType->isCompatibleWith($this->supportedType) && $targetMediaType->isCompatibleWith(MediaType::createApplicationJsonEncrypted()); + } +} diff --git a/packages/DataProtection/src/Protector/DataProtectorConfig.php b/packages/DataProtection/src/Protector/DataProtectorConfig.php new file mode 100644 index 000000000..164a4b8e0 --- /dev/null +++ b/packages/DataProtection/src/Protector/DataProtectorConfig.php @@ -0,0 +1,32 @@ + $sensitiveProperties + */ + public function __construct( + public Type $supportedType, + public ?string $encryptionKey, + public array $sensitiveProperties, + public array $scalarProperties, + ) { + Assert::allStrings($this->sensitiveProperties, 'Sensitive Properties should be array of strings'); + Assert::allStrings($this->scalarProperties, 'Scalar Properties should be array of strings'); + } + + public function encryptionKeyName(DataProtectionConfiguration $dataProtectionConfiguration): string + { + return $dataProtectionConfiguration->keyName($this->encryptionKey); + } +} diff --git a/packages/DataProtection/tests/Integration/EncryptAnnotatedMessagesTest.php b/packages/DataProtection/tests/Integration/EncryptAnnotatedMessagesTest.php index f0a586031..e6f0e9d31 100644 --- a/packages/DataProtection/tests/Integration/EncryptAnnotatedMessagesTest.php +++ b/packages/DataProtection/tests/Integration/EncryptAnnotatedMessagesTest.php @@ -77,10 +77,10 @@ enum: TestEnum::FIRST, self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->primaryKey); + $messagePayload = $this->decryptChannelMessagePayload($channelMessage->getPayload(), $this->primaryKey); $messageHeaders = $channelMessage->getHeaders(); - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); + self::assertEquals('{"class":"{\"argument\":\"value\",\"enum\":\"first\"}","enum":"\"first\"","argument":"value"}', $messagePayload); self::assertEquals($metadataSent['foo'], $messageHeaders->get('foo')); self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); @@ -126,10 +126,10 @@ enum: TestEnum::FIRST, self::assertEquals($metadataSent['baz'], $receivedHeaders['baz']); $channelMessage = $channel->getLastSentMessage(); - $messagePayload = Crypto::decrypt(base64_decode($channelMessage->getPayload()), $this->secondaryKey); + $messagePayload = $this->decryptChannelMessagePayload($channelMessage->getPayload(), $this->secondaryKey); $messageHeaders = $channelMessage->getHeaders(); - self::assertEquals('{"class":{"argument":"value","enum":"first"},"enum":"first","argument":"value"}', $messagePayload); + self::assertEquals('{"class":"{\"argument\":\"value\",\"enum\":\"first\"}","enum":"\"first\"","argument":"value"}', $messagePayload); self::assertEquals($metadataSent['foo'], $messageHeaders->get('foo')); self::assertEquals($metadataSent['bar'], $messageHeaders->get('bar')); self::assertEquals($metadataSent['baz'], $messageHeaders->get('baz')); @@ -348,4 +348,14 @@ classesToResolve: $classesToResolve, ) ); } + + private function decryptChannelMessagePayload(string $payload, Key $primaryKey): string + { + $payload = json_decode($payload, true); + foreach ($payload as $key => $value) { + $payload[$key] = Crypto::decrypt(base64_decode($value), $primaryKey); + } + + return json_encode($payload); + } } diff --git a/packages/Ecotone/src/Messaging/Config/Configuration.php b/packages/Ecotone/src/Messaging/Config/Configuration.php index c9a2fd4c9..46d5ea32c 100644 --- a/packages/Ecotone/src/Messaging/Config/Configuration.php +++ b/packages/Ecotone/src/Messaging/Config/Configuration.php @@ -129,6 +129,8 @@ public function registerConsoleCommand(ConsoleCommandConfiguration $consoleComma */ public function registerConverter(CompilableBuilder $converterBuilder): Configuration; + public function registerDataProtector(CompilableBuilder $dataProtectorBuilder): Configuration; + /** * @param string $referenceName * @return Configuration diff --git a/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php b/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php index 76997c756..3dad7d866 100644 --- a/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php +++ b/packages/Ecotone/src/Messaging/Config/MessagingSystemConfiguration.php @@ -129,6 +129,10 @@ final class MessagingSystemConfiguration implements Configuration * @var CompilableBuilder[] */ private array $converterBuilders = []; + /** + * @var CompilableBuilder[] + */ + private array $dataProtectorBuilders = []; /** * @var string[] */ @@ -831,6 +835,16 @@ public function registerConverter(CompilableBuilder $converterBuilder): Configur return $this; } + /** + * @inheritDoc + */ + public function registerDataProtector(CompilableBuilder $dataProtectorBuilder): Configuration + { + $this->dataProtectorBuilders[] = $dataProtectorBuilder; + + return $this; + } + /** * @inheritDoc */ @@ -913,11 +927,14 @@ public function process(ContainerBuilder $builder): void // TODO: some service configuration should be handled at runtime. Here they are all cached in the container // $messagingBuilder->register('config.defaultSerializationMediaType', MediaType::parseMediaType($this->applicationConfiguration->getDefaultSerializationMediaType())); - $converters = []; + $converters = $dataEncryptors = []; foreach ($this->converterBuilders as $converterBuilder) { $converters[] = $converterBuilder->compile($messagingBuilder); } - $messagingBuilder->register(ConversionService::REFERENCE_NAME, new Definition(AutoCollectionConversionService::class, ['converters' => $converters])); + foreach ($this->dataProtectorBuilders as $encryptorBuilder) { + $dataEncryptors[] = $encryptorBuilder->compile($messagingBuilder); + } + $messagingBuilder->register(ConversionService::REFERENCE_NAME, new Definition(AutoCollectionConversionService::class, ['converters' => $converters, 'dataProtectors' => $dataEncryptors])); $channelInterceptorsByImportance = $this->channelInterceptorBuilders; $channelInterceptorsByChannelName = []; diff --git a/packages/Ecotone/src/Messaging/Conversion/AutoCollectionConversionService.php b/packages/Ecotone/src/Messaging/Conversion/AutoCollectionConversionService.php index 0e38f7404..d40bb423d 100644 --- a/packages/Ecotone/src/Messaging/Conversion/AutoCollectionConversionService.php +++ b/packages/Ecotone/src/Messaging/Conversion/AutoCollectionConversionService.php @@ -4,6 +4,7 @@ namespace Ecotone\Messaging\Conversion; +use Ecotone\DataProtection\Protector\DataEncryptor; use Ecotone\Messaging\Handler\Type; use function is_iterable; @@ -15,9 +16,11 @@ class AutoCollectionConversionService implements ConversionService { /** * @param Converter[] $converters + * @param DataEncryptor[] $dataProtectors * @param array $convertersCache value is index of converter in $this->converters or false if not found + * @param array $protectorsCache value is index of data protector in $this->dataProtectors or false if not found */ - public function __construct(private array $converters, private array $convertersCache = []) + public function __construct(private array $converters, private array $dataProtectors, private array $convertersCache = [], private array $protectorsCache = []) { } @@ -25,9 +28,9 @@ public function __construct(private array $converters, private array $converters * @param Converter[] $converters * @return AutoCollectionConversionService */ - public static function createWith(array $converters): self + public static function createWith(array $converters, array $dataProtectors = []): self { - return new self($converters); + return new self($converters, $dataProtectors); } /** @@ -35,7 +38,7 @@ public static function createWith(array $converters): self */ public static function createEmpty(): self { - return new self([]); + return new self([], []); } public function convert($source, Type $sourcePHPType, MediaType $sourceMediaType, Type $targetPHPType, MediaType $targetMediaType) @@ -44,8 +47,21 @@ public function convert($source, Type $sourcePHPType, MediaType $sourceMediaType return $source; } + // check if a source can be decrypted + if ($sourceMediaType->isCompatibleWith(MediaType::createApplicationJson()) && $dataDecryptor = $this->getDataProtector($sourcePHPType, MediaType::createApplicationJsonEncrypted(), $targetPHPType, $targetMediaType)) { + $source = $dataDecryptor->convert($source, $sourcePHPType, MediaType::createApplicationJsonEncrypted(), $targetPHPType, $targetMediaType); + } + + // run actual conversion if ($converter = $this->getConverter($sourcePHPType, $sourceMediaType, $targetPHPType, $targetMediaType)) { - return $converter->convert($source, $sourcePHPType, $sourceMediaType, $targetPHPType, $targetMediaType, $this); + $convertedValue = $converter->convert($source, $sourcePHPType, $sourceMediaType, $targetPHPType, $targetMediaType, $this); + + // check if a source can be encrypted + if ($targetMediaType->isCompatibleWith(MediaType::createApplicationJson()) && $dataEncryptor = $this->getDataProtector($sourcePHPType, $sourceMediaType, $targetPHPType, MediaType::createApplicationJsonEncrypted())) { + return $dataEncryptor->convert($convertedValue, $sourcePHPType, $sourceMediaType, $targetPHPType, MediaType::createApplicationJsonEncrypted()); + } + + return $convertedValue; } if (is_iterable($source) && $targetPHPType->isIterable() && $targetPHPType instanceof Type\GenericType) { @@ -113,4 +129,26 @@ private function getConverter(Type $sourceType, MediaType $sourceMediaType, Type return null; } + + private function getDataProtector(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): ?Converter + { + $cacheKey = $sourceType->toString() . '|' . $sourceMediaType->toString() . '->' . $targetType->toString() . '|' . $targetMediaType->toString(); + if (isset($this->protectorsCache[$cacheKey])) { + if ($this->protectorsCache[$cacheKey] === false) { + return null; + } + return $this->dataProtectors[$this->protectorsCache[$cacheKey]]; + } + + foreach ($this->dataProtectors as $index => $protector) { + if ($protector->matches($sourceType, $sourceMediaType, $targetType, $targetMediaType)) { + $this->protectorsCache[$cacheKey] = $index; + return $protector; + } + } + + $this->protectorsCache[$cacheKey] = false; + + return null; + } } diff --git a/packages/Ecotone/src/Messaging/Conversion/MediaType.php b/packages/Ecotone/src/Messaging/Conversion/MediaType.php index ea1c62d6d..951ad1c90 100644 --- a/packages/Ecotone/src/Messaging/Conversion/MediaType.php +++ b/packages/Ecotone/src/Messaging/Conversion/MediaType.php @@ -30,6 +30,7 @@ final class MediaType implements DefinedObject public const IMAGE_GIF = 'image/gif'; public const APPLICATION_XML = 'application/xml'; public const APPLICATION_JSON = 'application/json'; + public const APPLICATION_JSON_ENCRYPTED = 'application/json+encrypted'; public const APPLICATION_FORM_URLENCODED = 'application/x-www-form-urlencoded'; public const APPLICATION_ATOM_XML = 'application/atom+xml'; public const APPLICATION_XHTML_XML = 'application/xhtml+xml'; @@ -86,6 +87,15 @@ public static function createApplicationJson(): self return self::parseMediaType(self::APPLICATION_JSON); } + /** + * @return MediaType + * @throws \Ecotone\Messaging\MessagingException + */ + public static function createApplicationJsonEncrypted(): self + { + return self::parseMediaType(self::APPLICATION_JSON_ENCRYPTED); + } + /** * @return MediaType * @throws \Ecotone\Messaging\MessagingException