Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@
"enqueue/dsn": "^0.10.27",
"yoast/phpunit-polyfills": "^4.0.0"
},
"suggest": {
"ext-simplexml": "Required if application/xml is used as serialization media type"
},
"conflict": {
"symfony/doctrine-messenger": ">7.0.5 < 7.1.0",
"enqueue/dbal": "*"
Expand Down
3 changes: 3 additions & 0 deletions packages/DataProtection/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
"phpstan/phpstan": "^2.1",
"wikimedia/composer-merge-plugin": "^2.1"
},
"suggest": {
"ext-simplexml": "Required if application/xml is used as serialization media type"
},
"scripts": {
"tests:phpstan": "vendor/bin/phpstan",
"tests:phpunit": [
Expand Down
5 changes: 4 additions & 1 deletion packages/DataProtection/src/Attribute/Sensitive.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class Sensitive
readonly class Sensitive
{
public function __construct(public string $sensitiveName = '')
{
}
}
47 changes: 34 additions & 13 deletions packages/DataProtection/src/Configuration/DataProtectionModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use Ecotone\DataProtection\Conversion\DataProtectionConversionServiceDecorator;
use Ecotone\DataProtection\Conversion\JsonDecryptionConverter;
use Ecotone\DataProtection\Conversion\JsonEncryptionConverter;
use Ecotone\DataProtection\Conversion\XMLDecryptionConverter;
use Ecotone\DataProtection\Conversion\XMLEncryptionConverter;
use Ecotone\DataProtection\Conversion\XPhpDecryptionConverter;
use Ecotone\DataProtection\Conversion\XPhpEncryptionConverter;
use Ecotone\DataProtection\Encryption\Key;
Expand All @@ -24,6 +26,7 @@
use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\ExtensionObjectResolver;
use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\NoExternalConfigurationModule;
use Ecotone\Messaging\Config\Configuration;
use Ecotone\Messaging\Config\ConfigurationException;
use Ecotone\Messaging\Config\Container\Definition;
use Ecotone\Messaging\Config\Container\Reference;
use Ecotone\Messaging\Config\ModulePackageList;
Expand All @@ -50,8 +53,11 @@ public function __construct(private array $dataProtectorConfigs)

public static function create(AnnotationFinder $annotationRegistrationService, InterfaceToCallRegistry $interfaceToCallRegistry): static
{
$dataProtectorConfigs = self::resolveProtectorConfigsFromAnnotatedClasses([], $annotationRegistrationService->findAnnotatedClasses(Sensitive::class), $interfaceToCallRegistry);
$dataProtectorConfigs = self::resolveProtectorConfigsFromAnnotatedClasses($dataProtectorConfigs, $annotationRegistrationService->findClassesWithAnnotatedProperties(Sensitive::class), $interfaceToCallRegistry);

return new self(
dataProtectorConfigs: self::resolveProtectorConfigsFromAnnotatedClasses($annotationRegistrationService->findAnnotatedClasses(Sensitive::class), $interfaceToCallRegistry)
dataProtectorConfigs: $dataProtectorConfigs
);
}

Expand Down Expand Up @@ -103,7 +109,7 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO
}

$converters = [];
$encryptionConverters = [JsonEncryptionConverter::class, JsonDecryptionConverter::class, XPhpEncryptionConverter::class, XPhpDecryptionConverter::class];
$encryptionConverters = [JsonEncryptionConverter::class, JsonDecryptionConverter::class, XPhpEncryptionConverter::class, XPhpDecryptionConverter::class, XMLEncryptionConverter::class, XMLDecryptionConverter::class];
foreach ($this->dataProtectorConfigs as $protectorConfig) {
foreach ($encryptionConverters as $converterClass) {
$converters[] = new Definition(
Expand All @@ -113,6 +119,7 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO
Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $protectorConfig->encryptionKeyName($dataProtectionConfiguration))),
$protectorConfig->sensitiveProperties,
$protectorConfig->scalarProperties,
$protectorConfig->sensitivePropertyNames,
]
);
}
Expand Down Expand Up @@ -152,29 +159,43 @@ public function getModulePackageName(): string
return ModulePackageList::DATA_PROTECTION_PACKAGE;
}

private static function resolveProtectorConfigsFromAnnotatedClasses(array $sensitiveMessages, InterfaceToCallRegistry $interfaceToCallRegistry): array
private static function resolveProtectorConfigsFromAnnotatedClasses(array $dataProtectorConfigs, array $sensitiveMessages, InterfaceToCallRegistry $interfaceToCallRegistry): array
{
$dataEncryptorConfigs = [];
foreach ($sensitiveMessages as $message) {
if (array_key_exists($message, $dataProtectorConfigs)) {
continue;
}

$classDefinition = $interfaceToCallRegistry->getClassDefinitionFor($messageType = Type::create($message));
$isClassSensitive = $classDefinition->findSingleClassAnnotation(Type::create(Sensitive::class)) !== null;
$encryptionKey = $classDefinition->findSingleClassAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey();

$sensitiveProperties = $classDefinition->getPropertiesWithAnnotation(Type::create(Sensitive::class));
if ($sensitiveProperties === []) {
$sensitiveProperties = $classDefinition->getProperties();
$propertiesToProtect = $classDefinition->getPropertiesWithAnnotation(Type::create(Sensitive::class));
if ($propertiesToProtect !== [] && $isClassSensitive) {
throw ConfigurationException::create('#[Sensitive] attribute can be used only on class level, not on property level.');
}

$scalarProperties = array_values(array_filter($sensitiveProperties, static fn (ClassPropertyDefinition $property): bool => $property->getType()->isScalar()));
if ($propertiesToProtect === []) {
$propertiesToProtect = $classDefinition->getProperties();
}

$scalarProperties = array_values(array_filter($propertiesToProtect, static fn (ClassPropertyDefinition $property): bool => $property->getType()->isScalar()));

$nameMapper = static fn (ClassPropertyDefinition $property): string => $property->getName();
$sensitiveNameMapper = static function (ClassPropertyDefinition $property): string {
$name = $property->findAnnotation(Type::create(Sensitive::class))?->sensitiveName ?? '';

$mapper = static fn (ClassPropertyDefinition $property): string => $property->getName();
return $name ?: $property->getName();
};

$sensitiveProperties = array_map($mapper, $sensitiveProperties);
$scalarProperties = array_map($mapper, $scalarProperties);
$sensitiveProperties = array_map($nameMapper, $propertiesToProtect);
$scalarProperties = array_map($nameMapper, $scalarProperties);
$sensitivePropertyNames = array_combine($sensitiveProperties, array_map($sensitiveNameMapper, $propertiesToProtect));

$dataEncryptorConfigs[$message] = new DataProtectorConfig(supportedType: $messageType, encryptionKey: $encryptionKey, sensitiveProperties: $sensitiveProperties, scalarProperties: $scalarProperties);
$dataProtectorConfigs[$message] = new DataProtectorConfig(supportedType: $messageType, encryptionKey: $encryptionKey, sensitiveProperties: $sensitiveProperties, scalarProperties: $scalarProperties, sensitivePropertyNames: $sensitivePropertyNames);
}

return $dataEncryptorConfigs;
return $dataProtectorConfigs;
}

private function verifyLicense(Configuration $messagingConfiguration): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ public function __construct(
public ?string $encryptionKey,
public array $sensitiveProperties,
public array $scalarProperties,
public array $sensitivePropertyNames,
) {
Assert::allStrings($this->sensitiveProperties, 'Sensitive Properties should be array of strings');
Assert::allStrings($this->scalarProperties, 'Scalar Properties should be array of strings');
Assert::allStrings($this->sensitivePropertyNames, 'Sensitive Properties custom names should be array of strings');
}

public function encryptionKeyName(DataProtectionConfiguration $dataProtectionConfiguration): string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Ecotone\DataProtection\Conversion;

use Ecotone\DataProtection\Encryption\Key;
use Ecotone\Messaging\Conversion\Converter;
use Ecotone\Messaging\Handler\Type;

/**
* licence Enterprise
*/
abstract class AbstractDataProtectionConverter implements Converter
{
public function __construct(
protected Type $supportedType,
protected Key $encryptionKey,
protected array $sensitiveProperties,
protected array $scalarProperties,
protected array $sensitivePropertyNames,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Ecotone\DataProtection\Conversion;

use Ecotone\DataProtection\Encryption\Crypto;

/**
* licence Enterprise
*/
abstract class AbstractDecryptionConverter extends AbstractDataProtectionConverter
{
protected function decrypt(array $data): array
{
foreach ($this->sensitiveProperties as $property) {
$propertyKey = $this->sensitivePropertyNames[$property] ?? $property;
if (! array_key_exists($propertyKey, $data)) {
continue;
}

$data[$propertyKey] = Crypto::decrypt($data[$propertyKey], $this->encryptionKey);

if (! in_array($propertyKey, $this->scalarProperties, true)) {
$data[$propertyKey] = json_decode($data[$propertyKey], true);
}
}

return $data;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Ecotone\DataProtection\Conversion;

use Ecotone\DataProtection\Encryption\Crypto;

/**
* licence Enterprise
*/
abstract class AbstractEncryptionConverter extends AbstractDataProtectionConverter
{
protected function encrypt(array $data): array
{
foreach ($this->sensitiveProperties as $property) {
$propertyKey = $this->sensitivePropertyNames[$property] ?? $property;
if (! array_key_exists($propertyKey, $data)) {
continue;
}

if (! in_array($propertyKey, $this->scalarProperties, true)) {
$data[$propertyKey] = json_encode($data[$propertyKey]);
}

$data[$propertyKey] = Crypto::encrypt($data[$propertyKey], $this->encryptionKey);
}

return $data;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ public function decorate(ConversionService $conversionService): void
public function convert($source, Type $sourcePHPType, MediaType $sourceMediaType, Type $targetPHPType, MediaType $targetMediaType)
{
if ($this->dataProtectionConversionService->canConvert($sourcePHPType, $encryptedSourceMediaType = $sourceMediaType->addParameter('encrypted', 'true'), $targetPHPType, $targetMediaType)) {
$source = $this->dataProtectionConversionService->convert($source, $sourcePHPType, $encryptedSourceMediaType, $targetPHPType, $targetMediaType);
$source = $this->dataProtectionConversionService->convert($source, $sourcePHPType, $encryptedSourceMediaType, $targetPHPType, $targetMediaType, $this->innerConversionService);
}

$source = $this->innerConversionService->convert($source, $sourcePHPType, $sourceMediaType, $targetPHPType, $targetMediaType);
$source = $this->innerConversionService->convert($source, $sourcePHPType, $sourceMediaType, $targetPHPType, $targetMediaType, $this->innerConversionService);

if ($this->dataProtectionConversionService->canConvert($sourcePHPType, $sourceMediaType, $targetPHPType, $encryptedTargetMediaType = $targetMediaType->addParameter('encrypted', 'true'))) {
$source = $this->dataProtectionConversionService->convert($source, $sourcePHPType, $sourceMediaType, $targetPHPType, $encryptedTargetMediaType);
$source = $this->dataProtectionConversionService->convert($source, $sourcePHPType, $sourceMediaType, $targetPHPType, $encryptedTargetMediaType, $this->innerConversionService);
}

return $source;
Expand Down
23 changes: 2 additions & 21 deletions packages/DataProtection/src/Conversion/JsonDecryptionConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,12 @@
/**
* licence Enterprise
*/
readonly class JsonDecryptionConverter implements Converter
class JsonDecryptionConverter extends AbstractDecryptionConverter
{
public function __construct(
private Type $supportedType,
private Key $encryptionKey,
private array $sensitiveProperties,
private array $scalarProperties,
) {
}

public function convert($source, Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType)
{
$data = json_decode($source, true);

foreach ($this->sensitiveProperties as $property) {
if (! array_key_exists($property, $data)) {
continue;
}

$data[$property] = Crypto::decrypt($data[$property], $this->encryptionKey);

if (! in_array($property, $this->scalarProperties, true)) {
$data[$property] = json_decode($data[$property], true);
}
}
$data = $this->decrypt($data);

return json_encode($data);
}
Expand Down
26 changes: 2 additions & 24 deletions packages/DataProtection/src/Conversion/JsonEncryptionConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,18 @@

namespace Ecotone\DataProtection\Conversion;

use Ecotone\DataProtection\Encryption\Crypto;
use Ecotone\DataProtection\Encryption\Key;
use Ecotone\Messaging\Conversion\Converter;
use Ecotone\Messaging\Conversion\MediaType;
use Ecotone\Messaging\Handler\Type;

/**
* licence Enterprise
*/
readonly class JsonEncryptionConverter implements Converter
class JsonEncryptionConverter extends AbstractEncryptionConverter
{
public function __construct(
private Type $supportedType,
private Key $encryptionKey,
private array $sensitiveProperties,
private array $scalarProperties,
) {
}

public function convert($source, Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType)
{
$data = json_decode($source, true);

foreach ($this->sensitiveProperties as $property) {
if (! array_key_exists($property, $data)) {
continue;
}

if (! in_array($property, $this->scalarProperties, true)) {
$data[$property] = json_encode($data[$property]);
}

$data[$property] = Crypto::encrypt($data[$property], $this->encryptionKey);
}
$data = $this->encrypt($data);

return json_encode($data);
}
Expand Down
30 changes: 30 additions & 0 deletions packages/DataProtection/src/Conversion/XMLDecryptionConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Ecotone\DataProtection\Conversion;

use Ecotone\DataProtection\Encryption\Crypto;
use Ecotone\DataProtection\Encryption\Key;
use Ecotone\Messaging\Conversion\Converter;
use Ecotone\Messaging\Conversion\MediaType;
use Ecotone\Messaging\Handler\Type;

/**
* licence Enterprise
*/
class XMLDecryptionConverter extends AbstractDecryptionConverter
{
public function convert($source, Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType)
{
$data = XmlHelper::xmlToArray($source);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to use ConversionService here but it does not support serialization between array and xml string loosing parameter names when converting array into xml.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the #[Sensitive('someCustomName')] solving this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class Person
{
       #[Sensitive("someName")]
       private string name
}

Or if this is about nesting e.g. <result><person><someName></someName></person></result>, so we do not know how to go into deeper nesting?

Because we could then provide nested structure attribute:

#[Nested(expression: "createArray('result', data)")]
class Person
{
       #[Sensitive("someName")]
       private string name
}

or eventually when we add support for functions (new php feature), that could be modified as needed:

#[Nested(expression: function($data) => ["result" => $data]]
class Person
{
       #[Sensitive("someName")]
       private string name
}

Copy link
Copy Markdown
Member Author

@unixslayer unixslayer Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DataProtectionConverter works around serialization on root level. Depending on configuration, not all of properties may need protection. Custom name passed with #[Sensitive] does not solve it as it's not for serialization. It only tells DataProtectionConverter that property name in serialization is different than property in actual object.

Also nested data is not a problem here because it does not matter. Data Protection works only for root and nested properties (non scalar) are always serialized to string. Consider following example:

class SomeMessage
{
    public function __construct(
        #[Sensitive] public SomeClass $sensitive,
        public string $property
    ) {
    }
}

class SomeClass
{
    public function __construct(
        public string $argumentA,
        public string $argumentB,
    ) {
    }
}

After serialization it will have nested structure:

[
    "sensitive" => [
        "argumentA" => "foo",
        "argumentB" => "bar",
    ],
    "property" => "baz",
]

Encryption will return following structure:

[
    "sensitive": "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ=",
    "property": "baz"
]

Once encrypted, data has to be returned as XML. ConversionService returns following XML (handled by JMSConverter)

<?xml version="1.0"?>
<root>
    <entry>TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ=</entry>
    <entry>baz</entry>
</root>

This is the reason why I introduced XMLHelper as JMS was not cooperative.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, i see. That's weird with entry, I would expect it to follow structure of the array. Maybe that requires some config switch on jms side.

Anyways not a blocker :)

$data = $this->decrypt($data);

return XmlHelper::arrayToXml($data);
}

public function matches(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool
{
return $targetType->acceptType($this->supportedType) && $sourceType->isString() && $sourceMediaType->isCompatibleWith(MediaType::createApplicationXml()) && $sourceMediaType->hasParameter('encrypted');
}
}
31 changes: 31 additions & 0 deletions packages/DataProtection/src/Conversion/XMLEncryptionConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Ecotone\DataProtection\Conversion;

use Ecotone\DataProtection\Encryption\Crypto;
use Ecotone\DataProtection\Encryption\Key;
use Ecotone\Messaging\Conversion\ConversionService;
use Ecotone\Messaging\Conversion\Converter;
use Ecotone\Messaging\Conversion\MediaType;
use Ecotone\Messaging\Handler\Type;

/**
* licence Enterprise
*/
class XMLEncryptionConverter extends AbstractEncryptionConverter
{
public function convert($source, Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType)
{
$data = XmlHelper::xmlToArray($source);
$data = $this->encrypt($data);

return XmlHelper::arrayToXml($data);
}

public function matches(Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType): bool
{
return $sourceType->acceptType($this->supportedType) && $targetType->isString() && $targetMediaType->isCompatibleWith(MediaType::createApplicationXml()) && $targetMediaType->hasParameter('encrypted');
}
}
Loading