Skip to content

Commit bfe8713

Browse files
authored
data protection improvements (#645)
* XML support in data protection * configure message protection when only property is annotated * handle changing property names with custom converters * cr changes - move ext-simplexml into suggested as it is used in XML encryption - forbid to use `#[Sensitive]` on both class and its properties - cache classes with annotated properties
1 parent 106f12d commit bfe8713

33 files changed

Lines changed: 875 additions & 154 deletions

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@
177177
"enqueue/dsn": "^0.10.27",
178178
"yoast/phpunit-polyfills": "^4.0.0"
179179
},
180+
"suggest": {
181+
"ext-simplexml": "Required if application/xml is used as serialization media type"
182+
},
180183
"conflict": {
181184
"symfony/doctrine-messenger": ">7.0.5 < 7.1.0",
182185
"enqueue/dbal": "*"

packages/DataProtection/composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
"phpstan/phpstan": "^2.1",
4949
"wikimedia/composer-merge-plugin": "^2.1"
5050
},
51+
"suggest": {
52+
"ext-simplexml": "Required if application/xml is used as serialization media type"
53+
},
5154
"scripts": {
5255
"tests:phpstan": "vendor/bin/phpstan",
5356
"tests:phpunit": [

packages/DataProtection/src/Attribute/Sensitive.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
use Attribute;
1010

1111
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
12-
class Sensitive
12+
readonly class Sensitive
1313
{
14+
public function __construct(public string $sensitiveName = '')
15+
{
16+
}
1417
}

packages/DataProtection/src/Configuration/DataProtectionModule.php

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Ecotone\DataProtection\Conversion\DataProtectionConversionServiceDecorator;
1616
use Ecotone\DataProtection\Conversion\JsonDecryptionConverter;
1717
use Ecotone\DataProtection\Conversion\JsonEncryptionConverter;
18+
use Ecotone\DataProtection\Conversion\XMLDecryptionConverter;
19+
use Ecotone\DataProtection\Conversion\XMLEncryptionConverter;
1820
use Ecotone\DataProtection\Conversion\XPhpDecryptionConverter;
1921
use Ecotone\DataProtection\Conversion\XPhpEncryptionConverter;
2022
use Ecotone\DataProtection\Encryption\Key;
@@ -24,6 +26,7 @@
2426
use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\ExtensionObjectResolver;
2527
use Ecotone\Messaging\Config\Annotation\ModuleConfiguration\NoExternalConfigurationModule;
2628
use Ecotone\Messaging\Config\Configuration;
29+
use Ecotone\Messaging\Config\ConfigurationException;
2730
use Ecotone\Messaging\Config\Container\Definition;
2831
use Ecotone\Messaging\Config\Container\Reference;
2932
use Ecotone\Messaging\Config\ModulePackageList;
@@ -50,8 +53,11 @@ public function __construct(private array $dataProtectorConfigs)
5053

5154
public static function create(AnnotationFinder $annotationRegistrationService, InterfaceToCallRegistry $interfaceToCallRegistry): static
5255
{
56+
$dataProtectorConfigs = self::resolveProtectorConfigsFromAnnotatedClasses([], $annotationRegistrationService->findAnnotatedClasses(Sensitive::class), $interfaceToCallRegistry);
57+
$dataProtectorConfigs = self::resolveProtectorConfigsFromAnnotatedClasses($dataProtectorConfigs, $annotationRegistrationService->findClassesWithAnnotatedProperties(Sensitive::class), $interfaceToCallRegistry);
58+
5359
return new self(
54-
dataProtectorConfigs: self::resolveProtectorConfigsFromAnnotatedClasses($annotationRegistrationService->findAnnotatedClasses(Sensitive::class), $interfaceToCallRegistry)
60+
dataProtectorConfigs: $dataProtectorConfigs
5561
);
5662
}
5763

@@ -103,7 +109,7 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO
103109
}
104110

105111
$converters = [];
106-
$encryptionConverters = [JsonEncryptionConverter::class, JsonDecryptionConverter::class, XPhpEncryptionConverter::class, XPhpDecryptionConverter::class];
112+
$encryptionConverters = [JsonEncryptionConverter::class, JsonDecryptionConverter::class, XPhpEncryptionConverter::class, XPhpDecryptionConverter::class, XMLEncryptionConverter::class, XMLDecryptionConverter::class];
107113
foreach ($this->dataProtectorConfigs as $protectorConfig) {
108114
foreach ($encryptionConverters as $converterClass) {
109115
$converters[] = new Definition(
@@ -113,6 +119,7 @@ public function prepare(Configuration $messagingConfiguration, array $extensionO
113119
Reference::to(sprintf(self::KEY_SERVICE_ID_FORMAT, $protectorConfig->encryptionKeyName($dataProtectionConfiguration))),
114120
$protectorConfig->sensitiveProperties,
115121
$protectorConfig->scalarProperties,
122+
$protectorConfig->sensitivePropertyNames,
116123
]
117124
);
118125
}
@@ -152,29 +159,43 @@ public function getModulePackageName(): string
152159
return ModulePackageList::DATA_PROTECTION_PACKAGE;
153160
}
154161

155-
private static function resolveProtectorConfigsFromAnnotatedClasses(array $sensitiveMessages, InterfaceToCallRegistry $interfaceToCallRegistry): array
162+
private static function resolveProtectorConfigsFromAnnotatedClasses(array $dataProtectorConfigs, array $sensitiveMessages, InterfaceToCallRegistry $interfaceToCallRegistry): array
156163
{
157-
$dataEncryptorConfigs = [];
158164
foreach ($sensitiveMessages as $message) {
165+
if (array_key_exists($message, $dataProtectorConfigs)) {
166+
continue;
167+
}
168+
159169
$classDefinition = $interfaceToCallRegistry->getClassDefinitionFor($messageType = Type::create($message));
170+
$isClassSensitive = $classDefinition->findSingleClassAnnotation(Type::create(Sensitive::class)) !== null;
160171
$encryptionKey = $classDefinition->findSingleClassAnnotation(Type::create(WithEncryptionKey::class))?->encryptionKey();
161172

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

167-
$scalarProperties = array_values(array_filter($sensitiveProperties, static fn (ClassPropertyDefinition $property): bool => $property->getType()->isScalar()));
178+
if ($propertiesToProtect === []) {
179+
$propertiesToProtect = $classDefinition->getProperties();
180+
}
181+
182+
$scalarProperties = array_values(array_filter($propertiesToProtect, static fn (ClassPropertyDefinition $property): bool => $property->getType()->isScalar()));
183+
184+
$nameMapper = static fn (ClassPropertyDefinition $property): string => $property->getName();
185+
$sensitiveNameMapper = static function (ClassPropertyDefinition $property): string {
186+
$name = $property->findAnnotation(Type::create(Sensitive::class))?->sensitiveName ?? '';
168187

169-
$mapper = static fn (ClassPropertyDefinition $property): string => $property->getName();
188+
return $name ?: $property->getName();
189+
};
170190

171-
$sensitiveProperties = array_map($mapper, $sensitiveProperties);
172-
$scalarProperties = array_map($mapper, $scalarProperties);
191+
$sensitiveProperties = array_map($nameMapper, $propertiesToProtect);
192+
$scalarProperties = array_map($nameMapper, $scalarProperties);
193+
$sensitivePropertyNames = array_combine($sensitiveProperties, array_map($sensitiveNameMapper, $propertiesToProtect));
173194

174-
$dataEncryptorConfigs[$message] = new DataProtectorConfig(supportedType: $messageType, encryptionKey: $encryptionKey, sensitiveProperties: $sensitiveProperties, scalarProperties: $scalarProperties);
195+
$dataProtectorConfigs[$message] = new DataProtectorConfig(supportedType: $messageType, encryptionKey: $encryptionKey, sensitiveProperties: $sensitiveProperties, scalarProperties: $scalarProperties, sensitivePropertyNames: $sensitivePropertyNames);
175196
}
176197

177-
return $dataEncryptorConfigs;
198+
return $dataProtectorConfigs;
178199
}
179200

180201
private function verifyLicense(Configuration $messagingConfiguration): void

packages/DataProtection/src/Configuration/DataProtectorConfig.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ public function __construct(
1919
public ?string $encryptionKey,
2020
public array $sensitiveProperties,
2121
public array $scalarProperties,
22+
public array $sensitivePropertyNames,
2223
) {
2324
Assert::allStrings($this->sensitiveProperties, 'Sensitive Properties should be array of strings');
2425
Assert::allStrings($this->scalarProperties, 'Scalar Properties should be array of strings');
26+
Assert::allStrings($this->sensitivePropertyNames, 'Sensitive Properties custom names should be array of strings');
2527
}
2628

2729
public function encryptionKeyName(DataProtectionConfiguration $dataProtectionConfiguration): string
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecotone\DataProtection\Conversion;
6+
7+
use Ecotone\DataProtection\Encryption\Key;
8+
use Ecotone\Messaging\Conversion\Converter;
9+
use Ecotone\Messaging\Handler\Type;
10+
11+
/**
12+
* licence Enterprise
13+
*/
14+
abstract class AbstractDataProtectionConverter implements Converter
15+
{
16+
public function __construct(
17+
protected Type $supportedType,
18+
protected Key $encryptionKey,
19+
protected array $sensitiveProperties,
20+
protected array $scalarProperties,
21+
protected array $sensitivePropertyNames,
22+
) {
23+
}
24+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecotone\DataProtection\Conversion;
6+
7+
use Ecotone\DataProtection\Encryption\Crypto;
8+
9+
/**
10+
* licence Enterprise
11+
*/
12+
abstract class AbstractDecryptionConverter extends AbstractDataProtectionConverter
13+
{
14+
protected function decrypt(array $data): array
15+
{
16+
foreach ($this->sensitiveProperties as $property) {
17+
$propertyKey = $this->sensitivePropertyNames[$property] ?? $property;
18+
if (! array_key_exists($propertyKey, $data)) {
19+
continue;
20+
}
21+
22+
$data[$propertyKey] = Crypto::decrypt($data[$propertyKey], $this->encryptionKey);
23+
24+
if (! in_array($propertyKey, $this->scalarProperties, true)) {
25+
$data[$propertyKey] = json_decode($data[$propertyKey], true);
26+
}
27+
}
28+
29+
return $data;
30+
}
31+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecotone\DataProtection\Conversion;
6+
7+
use Ecotone\DataProtection\Encryption\Crypto;
8+
9+
/**
10+
* licence Enterprise
11+
*/
12+
abstract class AbstractEncryptionConverter extends AbstractDataProtectionConverter
13+
{
14+
protected function encrypt(array $data): array
15+
{
16+
foreach ($this->sensitiveProperties as $property) {
17+
$propertyKey = $this->sensitivePropertyNames[$property] ?? $property;
18+
if (! array_key_exists($propertyKey, $data)) {
19+
continue;
20+
}
21+
22+
if (! in_array($propertyKey, $this->scalarProperties, true)) {
23+
$data[$propertyKey] = json_encode($data[$propertyKey]);
24+
}
25+
26+
$data[$propertyKey] = Crypto::encrypt($data[$propertyKey], $this->encryptionKey);
27+
}
28+
29+
return $data;
30+
}
31+
}

packages/DataProtection/src/Conversion/DataProtectionConversionServiceDecorator.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ public function decorate(ConversionService $conversionService): void
2727
public function convert($source, Type $sourcePHPType, MediaType $sourceMediaType, Type $targetPHPType, MediaType $targetMediaType)
2828
{
2929
if ($this->dataProtectionConversionService->canConvert($sourcePHPType, $encryptedSourceMediaType = $sourceMediaType->addParameter('encrypted', 'true'), $targetPHPType, $targetMediaType)) {
30-
$source = $this->dataProtectionConversionService->convert($source, $sourcePHPType, $encryptedSourceMediaType, $targetPHPType, $targetMediaType);
30+
$source = $this->dataProtectionConversionService->convert($source, $sourcePHPType, $encryptedSourceMediaType, $targetPHPType, $targetMediaType, $this->innerConversionService);
3131
}
3232

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

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

3939
return $source;

packages/DataProtection/src/Conversion/JsonDecryptionConverter.php

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,12 @@
1313
/**
1414
* licence Enterprise
1515
*/
16-
readonly class JsonDecryptionConverter implements Converter
16+
class JsonDecryptionConverter extends AbstractDecryptionConverter
1717
{
18-
public function __construct(
19-
private Type $supportedType,
20-
private Key $encryptionKey,
21-
private array $sensitiveProperties,
22-
private array $scalarProperties,
23-
) {
24-
}
25-
2618
public function convert($source, Type $sourceType, MediaType $sourceMediaType, Type $targetType, MediaType $targetMediaType)
2719
{
2820
$data = json_decode($source, true);
29-
30-
foreach ($this->sensitiveProperties as $property) {
31-
if (! array_key_exists($property, $data)) {
32-
continue;
33-
}
34-
35-
$data[$property] = Crypto::decrypt($data[$property], $this->encryptionKey);
36-
37-
if (! in_array($property, $this->scalarProperties, true)) {
38-
$data[$property] = json_decode($data[$property], true);
39-
}
40-
}
21+
$data = $this->decrypt($data);
4122

4223
return json_encode($data);
4324
}

0 commit comments

Comments
 (0)