Skip to content

Commit 2b92cad

Browse files
committed
crypto normalizer
1 parent f51ed59 commit 2b92cad

6 files changed

Lines changed: 173 additions & 175 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace Patchlevel\Hydrator\Cryptography;
4+
5+
use Closure;
6+
use Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed;
7+
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists;
8+
use Patchlevel\Hydrator\Normalizer\ContextAwareNormalizer;
9+
use Patchlevel\Hydrator\Normalizer\InvalidArgument;
10+
use Patchlevel\Hydrator\Normalizer\Normalizer;
11+
12+
final class CryptoNormalizer implements Normalizer, ContextAwareNormalizer
13+
{
14+
public function __construct(
15+
private readonly Cryptographer $cryptographer,
16+
private readonly string $subjectIdName,
17+
private readonly mixed $fallback = null,
18+
private readonly Normalizer|null $normalizer = null,
19+
) {
20+
}
21+
22+
/**
23+
* @param array<string, mixed> $context
24+
*
25+
* @throws InvalidArgument
26+
*/
27+
public function normalize(mixed $value, array $context = []): mixed
28+
{
29+
if ($this->normalizer !== null) {
30+
$value = $this->normalizer->normalize($value, $context);
31+
}
32+
33+
if ($value === null) {
34+
return null;
35+
}
36+
37+
$subjectId = $context[SubjectIds::class]->get($this->subjectIdName);
38+
39+
return ['__enc' => $this->cryptographer->encrypt($subjectId, $value)];
40+
}
41+
42+
/**
43+
* @param array<string, mixed> $context
44+
*
45+
* @throws InvalidArgument
46+
*/
47+
public function denormalize(mixed $value, array $context = []): mixed
48+
{
49+
if (!is_array($value) || !array_key_exists('__enc', $value)) {
50+
if ($this->normalizer === null) {
51+
return $value;
52+
}
53+
54+
return $this->normalizer->denormalize($value, $context);
55+
}
56+
57+
$subjectId = $context[SubjectIds::class]->get($this->subjectIdName);
58+
59+
try {
60+
$data = $this->cryptographer->decrypt($subjectId, $value['__enc']);
61+
} catch (DecryptionFailed|CipherKeyNotExists) {
62+
if ($this->fallback instanceof Closure) {
63+
return ($this->fallback)($subjectId, $value['__enc']);
64+
}
65+
66+
return $this->fallback;
67+
}
68+
69+
if ($this->normalizer === null) {
70+
return $data;
71+
}
72+
73+
return $this->normalizer->denormalize($data, $context);
74+
}
75+
}

src/Cryptography/Cryptographer.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Patchlevel\Hydrator\Cryptography;
4+
5+
use Patchlevel\Hydrator\Cryptography\Cipher\Cipher;
6+
use Patchlevel\Hydrator\Cryptography\Cipher\CipherKeyFactory;
7+
use Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed;
8+
use Patchlevel\Hydrator\Cryptography\Cipher\EncryptionFailed;
9+
use Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipher;
10+
use Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipherKeyFactory;
11+
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists;
12+
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;
13+
14+
final class Cryptographer
15+
{
16+
public function __construct(
17+
private readonly Cipher $cipher,
18+
private readonly CipherKeyStore $cipherKeyStore,
19+
private readonly CipherKeyFactory $cipherKeyFactory,
20+
)
21+
{
22+
}
23+
24+
/**
25+
* @throws EncryptionFailed
26+
*/
27+
public function encrypt(string $subjectId, mixed $value): string
28+
{
29+
try {
30+
$cipherKey = $this->cipherKeyStore->get($subjectId);
31+
} catch (CipherKeyNotExists) {
32+
$cipherKey = ($this->cipherKeyFactory)();
33+
$this->cipherKeyStore->store($subjectId, $cipherKey);
34+
}
35+
36+
return $this->cipher->encrypt($cipherKey, $value);
37+
}
38+
39+
/**
40+
* @throws CipherKeyNotExists
41+
* @throws DecryptionFailed
42+
*/
43+
public function decrypt(string $subjectId, string $value): mixed
44+
{
45+
$cipherKey = $this->cipherKeyStore->get($subjectId);
46+
47+
return $this->cipher->decrypt($cipherKey, $value);
48+
}
49+
50+
51+
/** @param non-empty-string $method */
52+
public static function createWithOpenssl(
53+
CipherKeyStore $cryptoStore,
54+
string $method = OpensslCipherKeyFactory::DEFAULT_METHOD,
55+
): static {
56+
return new self(
57+
new OpensslCipher(),
58+
$cryptoStore,
59+
new OpensslCipherKeyFactory($method),
60+
);
61+
}
62+
}

src/Cryptography/CryptographyMetadataFactory.php

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
use Patchlevel\Hydrator\Attribute\SensitiveData;
99
use Patchlevel\Hydrator\Metadata\ClassMetadata;
1010
use Patchlevel\Hydrator\Metadata\MetadataFactory;
11-
use ReflectionProperty;
1211

1312
use function array_key_exists;
1413

1514
final class CryptographyMetadataFactory implements MetadataFactory
1615
{
1716
public function __construct(
17+
private readonly Cryptographer $cryptographer,
1818
private readonly MetadataFactory $metadataFactory,
1919
) {
2020
}
@@ -46,17 +46,24 @@ public function metadata(string $class): ClassMetadata
4646
$isSubjectId = true;
4747
}
4848

49-
$sensitiveDataInfo = $this->sensitiveDataInfo($property->reflection);
49+
$attributeReflectionList = $property->reflection->getAttributes(SensitiveData::class);
5050

51-
if (!$sensitiveDataInfo) {
51+
if ($attributeReflectionList === []) {
5252
continue;
5353
}
5454

5555
if ($isSubjectId) {
5656
throw new SubjectIdAndSensitiveDataConflict($metadata->className, $property->propertyName);
5757
}
5858

59-
$property->extras[SensitiveDataInfo::class] = $sensitiveDataInfo;
59+
$attribute = $attributeReflectionList[0]->newInstance();
60+
61+
$property->normalizer = new CryptoNormalizer(
62+
$this->cryptographer,
63+
$attribute->subjectIdName,
64+
$attribute->fallbackCallable !== null ? ($attribute->fallbackCallable)(...) : $attribute->fallback,
65+
$property->normalizer,
66+
);
6067
}
6168

6269
if ($subjectIdMapping !== []) {
@@ -65,20 +72,4 @@ public function metadata(string $class): ClassMetadata
6572

6673
return $metadata;
6774
}
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-
}
8475
}

0 commit comments

Comments
 (0)