Skip to content

Commit 3886a53

Browse files
committed
rewrite cryptography
1 parent ec410f9 commit 3886a53

29 files changed

Lines changed: 752 additions & 539 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/Attribute/SensitiveData.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#[Attribute(Attribute::TARGET_PROPERTY)]
1111
final class SensitiveData
1212
{
13-
/** @var (callable(string, mixed):mixed)|null */
13+
/** @var (callable(string):mixed)|null */
1414
public readonly mixed $fallbackCallable;
1515

1616
public function __construct(
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace Patchlevel\Hydrator\Cryptography;
4+
5+
use Patchlevel\Hydrator\Cryptography\Cipher\Cipher;
6+
use Patchlevel\Hydrator\Cryptography\Cipher\CipherKey;
7+
use Patchlevel\Hydrator\Cryptography\Cipher\CipherKeyFactory;
8+
use Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed;
9+
use Patchlevel\Hydrator\Cryptography\Cipher\EncryptionFailed;
10+
use Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipher;
11+
use Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipherKeyFactory;
12+
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists;
13+
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;
14+
15+
/**
16+
* @phpstan-type EncryptedDataV1 array{
17+
* __enc: 'v1',
18+
* data: non-empty-string,
19+
* method?: non-empty-string,
20+
* iv?: non-empty-string,
21+
* }
22+
*/
23+
final class BaseCryptographer implements Cryptographer
24+
{
25+
public function __construct(
26+
private readonly Cipher $cipher,
27+
private readonly CipherKeyStore $cipherKeyStore,
28+
private readonly CipherKeyFactory $cipherKeyFactory,
29+
)
30+
{
31+
}
32+
33+
/**
34+
* @throws EncryptionFailed
35+
*
36+
* @return EncryptedDataV1
37+
*/
38+
public function encrypt(string $subjectId, mixed $value): array
39+
{
40+
try {
41+
$cipherKey = $this->cipherKeyStore->get($subjectId);
42+
} catch (CipherKeyNotExists) {
43+
$cipherKey = ($this->cipherKeyFactory)();
44+
$this->cipherKeyStore->store($subjectId, $cipherKey);
45+
}
46+
47+
return [
48+
'__enc' => 'v1',
49+
'data' => $this->cipher->encrypt($cipherKey, $value),
50+
'method' => $cipherKey->method,
51+
'iv' => $cipherKey->iv,
52+
];
53+
}
54+
55+
/**
56+
* @param EncryptedDataV1 $encryptedData
57+
*
58+
* @throws CipherKeyNotExists
59+
* @throws DecryptionFailed
60+
*/
61+
public function decrypt(string $subjectId, mixed $encryptedData): mixed
62+
{
63+
$cipherKey = $this->cipherKeyStore->get($subjectId);
64+
65+
return $this->cipher->decrypt(
66+
new CipherKey(
67+
$cipherKey->key,
68+
$encryptedData['method'] ?? $cipherKey->method,
69+
$encryptedData['iv'] ?? $cipherKey->iv
70+
),
71+
$encryptedData['data']
72+
);
73+
}
74+
75+
public function supports(mixed $value): bool
76+
{
77+
return is_array($value) && array_key_exists('__enc', $value) && $value['__enc'] === 'v1';
78+
}
79+
80+
/** @param non-empty-string $method */
81+
public static function createWithOpenssl(
82+
CipherKeyStore $cryptoStore,
83+
string $method = OpensslCipherKeyFactory::DEFAULT_METHOD,
84+
): static {
85+
return new self(
86+
new OpensslCipher(),
87+
$cryptoStore,
88+
new OpensslCipherKeyFactory($method),
89+
);
90+
}
91+
}

src/Cryptography/Cipher/Cipher.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66

77
interface Cipher
88
{
9-
/** @throws EncryptionFailed */
9+
/**
10+
* @return non-empty-string
11+
*
12+
* @throws EncryptionFailed
13+
*/
1014
public function encrypt(CipherKey $key, mixed $data): string;
1115

1216
/** @throws DecryptionFailed */

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/Cipher/OpensslCipher.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
final class OpensslCipher implements Cipher
1919
{
20+
/**
21+
* @return non-empty-string
22+
*/
2023
public function encrypt(CipherKey $key, mixed $data): string
2124
{
2225
$encryptedData = @openssl_encrypt(
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
return $this->cryptographer->encrypt($context[SubjectIds::class]->get($this->subjectIdName), $value);
38+
}
39+
40+
/**
41+
* @param array<string, mixed> $context
42+
*
43+
* @throws InvalidArgument
44+
*/
45+
public function denormalize(mixed $value, array $context = []): mixed
46+
{
47+
if (!$this->cryptographer->supports($value)) {
48+
if ($this->normalizer === null) {
49+
return $value;
50+
}
51+
52+
return $this->normalizer->denormalize($value, $context);
53+
}
54+
55+
$subjectId = $context[SubjectIds::class]->get($this->subjectIdName);
56+
57+
try {
58+
$data = $this->cryptographer->decrypt($subjectId, $value);
59+
} catch (DecryptionFailed|CipherKeyNotExists) {
60+
if ($this->fallback instanceof Closure) {
61+
return ($this->fallback)($subjectId);
62+
}
63+
64+
return $this->fallback;
65+
}
66+
67+
if ($this->normalizer === null) {
68+
return $data;
69+
}
70+
71+
return $this->normalizer->denormalize($data, $context);
72+
}
73+
}

0 commit comments

Comments
 (0)