Skip to content

Commit 400472f

Browse files
authored
Merge pull request #122 from patchlevel/decouple-cryptography
Decouple cryptography, add extra property in Metadata
2 parents 1c92111 + 6960275 commit 400472f

16 files changed

+419
-437
lines changed

baseline.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@
3434
<code><![CDATA[new ReflectionClass($data['className'])]]></code>
3535
</InvalidPropertyAssignmentValue>
3636
</file>
37-
<file src="src/Metadata/PropertyMetadata.php">
38-
<RiskyTruthyFalsyComparison>
39-
<code><![CDATA[$this->sensitiveDataFallbackCallable]]></code>
40-
</RiskyTruthyFalsyComparison>
41-
</file>
4237
<file src="src/MetadataHydrator.php">
4338
<InvalidOperand>
4439
<code><![CDATA[$guessers]]></code>
@@ -93,6 +88,11 @@
9388
<code><![CDATA[Generator]]></code>
9489
</MixedReturnStatement>
9590
</file>
91+
<file src="tests/Unit/Cryptography/CryptographyMetadataFactoryTest.php">
92+
<MixedAssignment>
93+
<code><![CDATA[$sensitiveDataInfo]]></code>
94+
</MixedAssignment>
95+
</file>
9696
<file src="tests/Unit/Cryptography/CryptographySubscriberTest.php">
9797
<InvalidArgument>
9898
<code><![CDATA[$metadata]]></code>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Cryptography;
6+
7+
use Patchlevel\Hydrator\Attribute\DataSubjectId;
8+
use Patchlevel\Hydrator\Attribute\SensitiveData;
9+
use Patchlevel\Hydrator\Metadata\ClassMetadata;
10+
use Patchlevel\Hydrator\Metadata\MetadataFactory;
11+
use ReflectionProperty;
12+
13+
use function array_key_exists;
14+
15+
final class CryptographyMetadataFactory implements MetadataFactory
16+
{
17+
public function __construct(
18+
private readonly MetadataFactory $metadataFactory,
19+
) {
20+
}
21+
22+
public function metadata(string $class): ClassMetadata
23+
{
24+
$metadata = $this->metadataFactory->metadata($class);
25+
26+
$subjectIdMapping = [];
27+
28+
foreach ($metadata->properties as $property) {
29+
$isSubjectId = false;
30+
$attributeReflectionList = $property->reflection->getAttributes(DataSubjectId::class);
31+
32+
if ($attributeReflectionList) {
33+
$subjectIdIdentifier = $attributeReflectionList[0]->newInstance()->name;
34+
35+
if (array_key_exists($subjectIdIdentifier, $subjectIdMapping)) {
36+
throw new DuplicateSubjectIdIdentifier(
37+
$metadata->className(),
38+
$metadata->propertyForField($subjectIdMapping[$subjectIdIdentifier])->propertyName(),
39+
$property->propertyName(),
40+
$subjectIdIdentifier,
41+
);
42+
}
43+
44+
$subjectIdMapping[$subjectIdIdentifier] = $property->fieldName;
45+
46+
$isSubjectId = true;
47+
}
48+
49+
$sensitiveDataInfo = $this->sensitiveDataInfo($property->reflection);
50+
51+
if (!$sensitiveDataInfo) {
52+
continue;
53+
}
54+
55+
if ($isSubjectId) {
56+
throw new SubjectIdAndSensitiveDataConflict($metadata->className(), $property->propertyName());
57+
}
58+
59+
$property->extras[SensitiveDataInfo::class] = $sensitiveDataInfo;
60+
}
61+
62+
if ($subjectIdMapping !== []) {
63+
$metadata->extras[SubjectIdFieldMapping::class] = new SubjectIdFieldMapping($subjectIdMapping);
64+
}
65+
66+
return $metadata;
67+
}
68+
69+
private function sensitiveDataInfo(ReflectionProperty $reflectionProperty): SensitiveDataInfo|null
70+
{
71+
$attributeReflectionList = $reflectionProperty->getAttributes(SensitiveData::class);
72+
73+
if ($attributeReflectionList === []) {
74+
return null;
75+
}
76+
77+
$attribute = $attributeReflectionList[0]->newInstance();
78+
79+
return new SensitiveDataInfo(
80+
$attribute->subjectIdName,
81+
$attribute->fallbackCallable !== null ? ($attribute->fallbackCallable)(...) : $attribute->fallback,
82+
);
83+
}
84+
}

src/Metadata/DuplicateSubjectIdIdentifier.php renamed to src/Cryptography/DuplicateSubjectIdIdentifier.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace Patchlevel\Hydrator\Metadata;
5+
namespace Patchlevel\Hydrator\Cryptography;
66

7+
use Patchlevel\Hydrator\Metadata\MetadataException;
78
use RuntimeException;
89

910
use function sprintf;

src/Cryptography/NotSensitiveData.php

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Cryptography;
6+
7+
final class SensitiveDataInfo
8+
{
9+
public function __construct(
10+
public readonly string $subjectIdName,
11+
public readonly mixed $fallback = null,
12+
) {
13+
}
14+
}

src/Cryptography/SensitiveDataPayloadCryptographer.php

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Patchlevel\Hydrator\Cryptography;
66

7+
use Closure;
78
use Patchlevel\Hydrator\Cryptography\Cipher\Cipher;
89
use Patchlevel\Hydrator\Cryptography\Cipher\CipherKeyFactory;
910
use Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed;
@@ -12,14 +13,15 @@
1213
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists;
1314
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;
1415
use Patchlevel\Hydrator\Metadata\ClassMetadata;
15-
use Patchlevel\Hydrator\Metadata\PropertyMetadata;
1616

1717
use function array_key_exists;
1818
use function is_int;
1919
use function is_string;
2020

2121
final class SensitiveDataPayloadCryptographer implements PayloadCryptographer
2222
{
23+
private const ENCRYPTED_PREFIX = '!';
24+
2325
public function __construct(
2426
private readonly CipherKeyStore $cipherKeyStore,
2527
private readonly CipherKeyFactory $cipherKeyFactory,
@@ -36,12 +38,26 @@ public function __construct(
3638
*/
3739
public function encrypt(ClassMetadata $metadata, array $data): array
3840
{
41+
$mapping = $metadata->extras[SubjectIdFieldMapping::class] ?? null;
42+
43+
if (!$mapping instanceof SubjectIdFieldMapping) {
44+
return $data;
45+
}
46+
47+
$subjectIds = $this->getSubjectIds($metadata, $mapping, $data);
48+
3949
foreach ($metadata->properties as $propertyMetadata) {
40-
if (!$propertyMetadata->isSensitiveData()) {
50+
$sensitiveDataInfo = $propertyMetadata->extras[SensitiveDataInfo::class] ?? null;
51+
52+
if (!$sensitiveDataInfo instanceof SensitiveDataInfo) {
4153
continue;
4254
}
4355

44-
$subjectId = $this->subjectId($propertyMetadata, $metadata, $data);
56+
$subjectId = $subjectIds[$sensitiveDataInfo->subjectIdName] ?? null;
57+
58+
if ($subjectId === null) {
59+
throw new MissingSubjectId($metadata->className(), $sensitiveDataInfo->subjectIdName);
60+
}
4561

4662
try {
4763
$cipherKey = $this->cipherKeyStore->get($subjectId);
@@ -51,7 +67,7 @@ public function encrypt(ClassMetadata $metadata, array $data): array
5167
}
5268

5369
$targetFieldName = $this->useEncryptedFieldName
54-
? $propertyMetadata->encryptedFieldName()
70+
? self::ENCRYPTED_PREFIX . $propertyMetadata->fieldName
5571
: $propertyMetadata->fieldName;
5672

5773
$data[$targetFieldName] = $this->cipher->encrypt(
@@ -76,30 +92,44 @@ public function encrypt(ClassMetadata $metadata, array $data): array
7692
*/
7793
public function decrypt(ClassMetadata $metadata, array $data): array
7894
{
95+
$mapping = $metadata->extras[SubjectIdFieldMapping::class] ?? null;
96+
97+
if (!$mapping instanceof SubjectIdFieldMapping) {
98+
return $data;
99+
}
100+
101+
$subjectIds = $this->getSubjectIds($metadata, $mapping, $data);
102+
79103
foreach ($metadata->properties as $propertyMetadata) {
80-
if (!$propertyMetadata->isSensitiveData()) {
104+
$sensitiveDataInfo = $propertyMetadata->extras[SensitiveDataInfo::class] ?? null;
105+
106+
if (!$sensitiveDataInfo instanceof SensitiveDataInfo) {
81107
continue;
82108
}
83109

84-
$subjectId = $this->subjectId($propertyMetadata, $metadata, $data);
110+
$subjectId = $subjectIds[$sensitiveDataInfo->subjectIdName] ?? null;
111+
112+
if ($subjectId === null) {
113+
throw new MissingSubjectId($metadata->className(), $sensitiveDataInfo->subjectIdName);
114+
}
85115

86116
try {
87117
$cipherKey = $this->cipherKeyStore->get($subjectId);
88118
} catch (CipherKeyNotExists) {
89119
$cipherKey = null;
90120
}
91121

92-
if ($this->useEncryptedFieldName && array_key_exists($propertyMetadata->encryptedFieldName(), $data)) {
93-
$rawData = $data[$propertyMetadata->encryptedFieldName()];
94-
unset($data[$propertyMetadata->encryptedFieldName()]);
122+
if ($this->useEncryptedFieldName && array_key_exists(self::ENCRYPTED_PREFIX . $propertyMetadata->fieldName, $data)) {
123+
$rawData = $data[self::ENCRYPTED_PREFIX . $propertyMetadata->fieldName];
124+
unset($data[self::ENCRYPTED_PREFIX . $propertyMetadata->fieldName]);
95125
} elseif (!$this->useEncryptedFieldName || $this->fallbackToFieldName) {
96126
$rawData = $data[$propertyMetadata->fieldName];
97127
} else {
98128
continue;
99129
}
100130

101131
if (!$cipherKey) {
102-
$data[$propertyMetadata->fieldName] = $this->fallback($propertyMetadata, $subjectId, $rawData);
132+
$data[$propertyMetadata->fieldName] = $this->fallback($sensitiveDataInfo, $subjectId, $rawData);
103133
continue;
104134
}
105135

@@ -109,54 +139,50 @@ public function decrypt(ClassMetadata $metadata, array $data): array
109139
$rawData,
110140
);
111141
} catch (DecryptionFailed) {
112-
$data[$propertyMetadata->fieldName] = $this->fallback($propertyMetadata, $subjectId, $rawData);
142+
$data[$propertyMetadata->fieldName] = $this->fallback($sensitiveDataInfo, $subjectId, $rawData);
113143
}
114144
}
115145

116146
return $data;
117147
}
118148

119-
/** @param array<string, mixed> $data */
120-
private function subjectId(PropertyMetadata $propertyMetadata, ClassMetadata $metadata, array $data): string
149+
/**
150+
* @param array<string, mixed> $data
151+
*
152+
* @return array<string, string>
153+
*/
154+
private function getSubjectIds(ClassMetadata $metadata, SubjectIdFieldMapping $mapping, array $data): array
121155
{
122-
if (!$propertyMetadata->isSensitiveData()) {
123-
throw new NotSensitiveData($metadata->className(), $propertyMetadata->propertyName());
124-
}
125-
126-
$sensitiveDataSubjectIdName = $propertyMetadata->sensitiveDataSubjectIdName;
127-
128-
if (!$metadata->hasSubjectIdIdentifier($sensitiveDataSubjectIdName)) {
129-
throw new MissingSubjectId($metadata->className(), $propertyMetadata->propertyName());
130-
}
156+
$result = [];
131157

132-
$fieldName = $metadata->getSubjectIdFieldName($sensitiveDataSubjectIdName);
158+
foreach ($mapping->nameToField as $name => $fieldName) {
159+
if (!array_key_exists($fieldName, $data)) {
160+
throw new MissingSubjectId($metadata->className(), $fieldName);
161+
}
133162

134-
if (!array_key_exists($fieldName, $data)) {
135-
throw new MissingSubjectId($metadata->className(), $fieldName);
136-
}
163+
$subjectId = $data[$fieldName];
137164

138-
$subjectId = $data[$fieldName];
165+
if (is_int($subjectId)) {
166+
$subjectId = (string)$subjectId;
167+
}
139168

140-
if (is_int($subjectId)) {
141-
$subjectId = (string)$subjectId;
142-
}
169+
if (!is_string($subjectId)) {
170+
throw new UnsupportedSubjectId($metadata->className(), $fieldName, $subjectId);
171+
}
143172

144-
if (!is_string($subjectId)) {
145-
throw new UnsupportedSubjectId($metadata->className(), $fieldName, $subjectId);
173+
$result[$name] = $subjectId;
146174
}
147175

148-
return $subjectId;
176+
return $result;
149177
}
150178

151-
private function fallback(PropertyMetadata $propertyMetadata, string $subjectId, mixed $rawData): mixed
179+
private function fallback(SensitiveDataInfo $sensitiveDataInfo, string $subjectId, mixed $rawData): mixed
152180
{
153-
$callback = $propertyMetadata->sensitiveDataFallbackCallable();
154-
155-
if (!$callback) {
156-
return $propertyMetadata->sensitiveDataFallback;
181+
if ($sensitiveDataInfo->fallback instanceof Closure) {
182+
return ($sensitiveDataInfo->fallback)($subjectId, $rawData);
157183
}
158184

159-
return $callback($subjectId, $rawData);
185+
return $sensitiveDataInfo->fallback;
160186
}
161187

162188
/** @param non-empty-string $method */

src/Metadata/SubjectIdAndSensitiveDataConflict.php renamed to src/Cryptography/SubjectIdAndSensitiveDataConflict.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace Patchlevel\Hydrator\Metadata;
5+
namespace Patchlevel\Hydrator\Cryptography;
66

7+
use Patchlevel\Hydrator\Metadata\MetadataException;
78
use RuntimeException;
89

910
use function sprintf;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Cryptography;
6+
7+
final class SubjectIdFieldMapping
8+
{
9+
/** @param array<string, string> $nameToField */
10+
public function __construct(
11+
public readonly array $nameToField,
12+
) {
13+
}
14+
}

0 commit comments

Comments
 (0)