Skip to content

Commit f51ed59

Browse files
committed
rewrite cryptography
1 parent 93dd685 commit f51ed59

19 files changed

Lines changed: 857 additions & 836 deletions

phpstan.neon.dist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ services:
1515
class: Patchlevel\Hydrator\Tests\Architecture\FinalClassesTest
1616
tags:
1717
- phpat.test
18+
-
19+
class: Patchlevel\Hydrator\Tests\Architecture\ExceptionImplementsHydratorExceptionTest
20+
tags:
21+
- phpat.test

src/Cryptography/Cipher/CreateCipherKeyFailed.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
namespace Patchlevel\Hydrator\Cryptography\Cipher;
66

7+
use Patchlevel\Hydrator\HydratorException;
78
use RuntimeException;
89

9-
final class CreateCipherKeyFailed extends RuntimeException
10+
final class CreateCipherKeyFailed extends RuntimeException implements HydratorException
1011
{
1112
public function __construct()
1213
{

src/Cryptography/Cipher/DecryptionFailed.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
namespace Patchlevel\Hydrator\Cryptography\Cipher;
66

7+
use Patchlevel\Hydrator\HydratorException;
78
use RuntimeException;
89

9-
final class DecryptionFailed extends RuntimeException
10+
final class DecryptionFailed extends RuntimeException implements HydratorException
1011
{
1112
public function __construct()
1213
{

src/Cryptography/Cipher/EncryptionFailed.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
namespace Patchlevel\Hydrator\Cryptography\Cipher;
66

7+
use Patchlevel\Hydrator\HydratorException;
78
use RuntimeException;
89

9-
final class EncryptionFailed extends RuntimeException
10+
final class EncryptionFailed extends RuntimeException implements HydratorException
1011
{
1112
public function __construct()
1213
{

src/Cryptography/Cipher/MethodNotSupported.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
namespace Patchlevel\Hydrator\Cryptography\Cipher;
66

7+
use Patchlevel\Hydrator\HydratorException;
78
use RuntimeException;
89

910
use function sprintf;
1011

11-
final class MethodNotSupported extends RuntimeException
12+
final class MethodNotSupported extends RuntimeException implements HydratorException
1213
{
1314
public function __construct(string $method)
1415
{

src/Cryptography/CryptographyMiddleware.php

Lines changed: 198 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,34 @@
44

55
namespace Patchlevel\Hydrator\Cryptography;
66

7+
use Closure;
8+
use Patchlevel\Hydrator\Cryptography\Cipher\Cipher;
9+
use Patchlevel\Hydrator\Cryptography\Cipher\CipherKeyFactory;
10+
use Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed;
11+
use Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipher;
12+
use Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipherKeyFactory;
13+
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists;
14+
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;
715
use Patchlevel\Hydrator\Metadata\ClassMetadata;
816
use Patchlevel\Hydrator\Middleware\Middleware;
917
use Patchlevel\Hydrator\Middleware\Stack;
18+
use Stringable;
19+
20+
use function array_key_exists;
21+
use function assert;
22+
use function is_array;
23+
use function is_int;
24+
use function is_string;
1025

1126
final class CryptographyMiddleware implements Middleware
1227
{
28+
private const DEFAULT_ENCRYPTED_FIELD_NAME_PREFIX = '!';
29+
1330
public function __construct(
14-
private readonly PayloadCryptographer $cryptography,
31+
private readonly Cipher $cipher,
32+
private readonly CipherKeyStore $cipherKeyStore,
33+
private readonly CipherKeyFactory $cipherKeyFactory,
34+
private readonly string|null $encryptedFieldNamePrefix = self::DEFAULT_ENCRYPTED_FIELD_NAME_PREFIX,
1535
) {
1636
}
1737

@@ -26,9 +46,72 @@ public function __construct(
2646
*/
2747
public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object
2848
{
49+
$subjectIds = $context[SubjectIds::class] ?? new SubjectIds();
50+
51+
assert($subjectIds instanceof SubjectIds);
52+
53+
$mapping = $metadata->extras[SubjectIdFieldMapping::class] ?? null;
54+
55+
if ($mapping instanceof SubjectIdFieldMapping) {
56+
$subjectIds = $this->resolveSubjectIds($metadata, $mapping, $data)
57+
->merge($subjectIds);
58+
}
59+
60+
$context[SubjectIds::class] = $subjectIds;
61+
62+
foreach ($metadata->properties as $propertyMetadata) {
63+
$sensitiveDataInfo = $propertyMetadata->extras[SensitiveDataInfo::class] ?? null;
64+
65+
if (!$sensitiveDataInfo instanceof SensitiveDataInfo) {
66+
continue;
67+
}
68+
69+
$subjectId = $subjectIds->get($sensitiveDataInfo->subjectIdName);
70+
71+
try {
72+
$cipherKey = $this->cipherKeyStore->get($subjectId);
73+
} catch (CipherKeyNotExists) {
74+
$cipherKey = null;
75+
}
76+
77+
if (
78+
$this->encryptedFieldNamePrefix && array_key_exists(
79+
$this->encryptedFieldNamePrefix . $propertyMetadata->fieldName,
80+
$data,
81+
)
82+
) {
83+
$rawData = $data[$this->encryptedFieldNamePrefix . $propertyMetadata->fieldName];
84+
unset($data[$this->encryptedFieldNamePrefix . $propertyMetadata->fieldName]);
85+
} elseif (!$this->encryptedFieldNamePrefix) {
86+
$rawData = $data[$propertyMetadata->fieldName];
87+
} else {
88+
continue;
89+
}
90+
91+
if (!is_string($rawData)) {
92+
$data[$propertyMetadata->fieldName] = $rawData;
93+
94+
continue;
95+
}
96+
97+
if (!$cipherKey) {
98+
$data[$propertyMetadata->fieldName] = $this->fallback($sensitiveDataInfo, $subjectId, $rawData);
99+
continue;
100+
}
101+
102+
try {
103+
$data[$propertyMetadata->fieldName] = $this->cipher->decrypt(
104+
$cipherKey,
105+
$rawData,
106+
);
107+
} catch (DecryptionFailed) {
108+
$data[$propertyMetadata->fieldName] = $this->fallback($sensitiveDataInfo, $subjectId, $rawData);
109+
}
110+
}
111+
29112
return $stack->next()->hydrate(
30113
$metadata,
31-
$this->cryptography->decrypt($metadata, $data),
114+
$data,
32115
$context,
33116
$stack,
34117
);
@@ -45,9 +128,119 @@ public function hydrate(ClassMetadata $metadata, array $data, array $context, St
45128
*/
46129
public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array
47130
{
48-
return $this->cryptography->encrypt(
49-
$metadata,
50-
$stack->next()->extract($metadata, $object, $context, $stack),
131+
$subjectIds = $context[SubjectIds::class] ?? new SubjectIds();
132+
133+
assert($subjectIds instanceof SubjectIds);
134+
135+
$mapping = $metadata->extras[SubjectIdFieldMapping::class] ?? null;
136+
137+
if ($mapping instanceof SubjectIdFieldMapping) {
138+
$subjectIds = $this->resolveSubjectIds($metadata, $mapping, $object)
139+
->merge($subjectIds);
140+
}
141+
142+
$context[SubjectIds::class] = $subjectIds;
143+
144+
$data = $stack->next()->extract($metadata, $object, $context, $stack);
145+
146+
foreach ($metadata->properties as $propertyMetadata) {
147+
$sensitiveDataInfo = $propertyMetadata->extras[SensitiveDataInfo::class] ?? null;
148+
149+
if (!$sensitiveDataInfo instanceof SensitiveDataInfo) {
150+
continue;
151+
}
152+
153+
$subjectId = $subjectIds->get($sensitiveDataInfo->subjectIdName);
154+
155+
try {
156+
$cipherKey = $this->cipherKeyStore->get($subjectId);
157+
} catch (CipherKeyNotExists) {
158+
$cipherKey = ($this->cipherKeyFactory)();
159+
$this->cipherKeyStore->store($subjectId, $cipherKey);
160+
}
161+
162+
$targetFieldName = $this->encryptedFieldNamePrefix
163+
? $this->encryptedFieldNamePrefix . $propertyMetadata->fieldName
164+
: $propertyMetadata->fieldName;
165+
166+
$data[$targetFieldName] = $this->cipher->encrypt(
167+
$cipherKey,
168+
$data[$propertyMetadata->fieldName],
169+
);
170+
171+
if (!$this->encryptedFieldNamePrefix) {
172+
continue;
173+
}
174+
175+
unset($data[$propertyMetadata->fieldName]);
176+
}
177+
178+
return $data;
179+
}
180+
181+
/** @param array<string, mixed>|object $data */
182+
private function resolveSubjectIds(
183+
ClassMetadata $metadata,
184+
SubjectIdFieldMapping $mapping,
185+
array|object $data,
186+
): SubjectIds {
187+
$result = [];
188+
189+
foreach ($mapping->nameToField as $name => $fieldName) {
190+
if (is_array($data)) {
191+
if (!array_key_exists($fieldName, $data)) {
192+
throw new MissingSubjectIdField($metadata->className, $fieldName);
193+
}
194+
195+
$subjectId = $data[$fieldName];
196+
} else {
197+
$property = $metadata->propertyForField($fieldName);
198+
199+
if ($property->normalizer) {
200+
$subjectId = $property->normalizer->normalize($property->getValue($data));
201+
} else {
202+
$subjectId = $property->getValue($data);
203+
}
204+
}
205+
206+
if (is_int($subjectId)) {
207+
$subjectId = (string)$subjectId;
208+
}
209+
210+
if ($subjectId instanceof Stringable) {
211+
$subjectId = $subjectId->__toString();
212+
}
213+
214+
if (!is_string($subjectId)) {
215+
throw new UnsupportedSubjectId($metadata->className, $fieldName, $subjectId);
216+
}
217+
218+
$result[$name] = $subjectId;
219+
}
220+
221+
return new SubjectIds($result);
222+
}
223+
224+
private function fallback(SensitiveDataInfo $sensitiveDataInfo, string $subjectId, mixed $rawData): mixed
225+
{
226+
if ($sensitiveDataInfo->fallback instanceof Closure) {
227+
return ($sensitiveDataInfo->fallback)($subjectId, $rawData);
228+
}
229+
230+
return $sensitiveDataInfo->fallback;
231+
}
232+
233+
/** @param non-empty-string $method */
234+
public static function createWithOpenssl(
235+
CipherKeyStore $cryptoStore,
236+
string $method = OpensslCipherKeyFactory::DEFAULT_METHOD,
237+
string|null $encryptedFieldNamePrefix = self::DEFAULT_ENCRYPTED_FIELD_NAME_PREFIX,
238+
): static {
239+
return new self(
240+
new OpensslCipher(),
241+
$cryptoStore,
242+
new OpensslCipherKeyFactory($method),
243+
$encryptedFieldNamePrefix,
51244
);
52245
}
53246
}

src/Cryptography/MissingSubjectId.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44

55
namespace Patchlevel\Hydrator\Cryptography;
66

7+
use Patchlevel\Hydrator\HydratorException;
78
use RuntimeException;
89

910
use function sprintf;
1011

11-
final class MissingSubjectId extends RuntimeException
12+
final class MissingSubjectId extends RuntimeException implements HydratorException
1213
{
13-
/** @param class-string $class */
14-
public function __construct(string $class, string $fieldName)
14+
public function __construct(string $name)
1515
{
16-
parent::__construct(sprintf('Missing subject id for %s in field %s.', $class, $fieldName));
16+
parent::__construct(sprintf('Missing subject id %s.', $name));
1717
}
1818
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Cryptography;
6+
7+
use Patchlevel\Hydrator\HydratorException;
8+
use RuntimeException;
9+
10+
use function sprintf;
11+
12+
final class MissingSubjectIdField extends RuntimeException implements HydratorException
13+
{
14+
/** @param class-string $class */
15+
public function __construct(string $class, string $fieldName)
16+
{
17+
parent::__construct(sprintf('Missing subject id for %s in field %s.', $class, $fieldName));
18+
}
19+
}

src/Cryptography/PayloadCryptographer.php

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)