Skip to content

Commit d1865b7

Browse files
authored
Merge pull request #164 from patchlevel/backport-lifecycle-and-cryptography-extension
Backport lifecycle and cryptography extension
2 parents 5fd42f6 + c53de6e commit d1865b7

File tree

61 files changed

+2691
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+2691
-0
lines changed

phpstan-baseline.neon

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,30 @@ parameters:
1818
count: 1
1919
path: src/Cryptography/PersonalDataPayloadCryptographer.php
2020

21+
-
22+
message: '#^Parameter \#3 \$iv of class Patchlevel\\Hydrator\\Extension\\Cryptography\\Cipher\\CipherKey constructor expects non\-empty\-string, string given\.$#'
23+
identifier: argument.type
24+
count: 1
25+
path: src/Extension/Cryptography/BaseCryptographer.php
26+
27+
-
28+
message: '#^Method Patchlevel\\Hydrator\\Extension\\Cryptography\\Cipher\\OpensslCipher\:\:encrypt\(\) should return non\-empty\-string but returns string\.$#'
29+
identifier: return.type
30+
count: 1
31+
path: src/Extension/Cryptography/Cipher/OpensslCipher.php
32+
33+
-
34+
message: '#^Parameter \#1 \$key of class Patchlevel\\Hydrator\\Extension\\Cryptography\\Cipher\\CipherKey constructor expects non\-empty\-string, string given\.$#'
35+
identifier: argument.type
36+
count: 1
37+
path: src/Extension/Cryptography/Cipher/OpensslCipherKeyFactory.php
38+
39+
-
40+
message: '#^Parameter \#3 \$iv of class Patchlevel\\Hydrator\\Extension\\Cryptography\\Cipher\\CipherKey constructor expects non\-empty\-string, string given\.$#'
41+
identifier: argument.type
42+
count: 1
43+
path: src/Extension/Cryptography/Cipher/OpensslCipherKeyFactory.php
44+
2145
-
2246
message: '#^Method Patchlevel\\Hydrator\\Guesser\\BuiltInGuesser\:\:guess\(\) has parameter \$type with generic class Symfony\\Component\\TypeInfo\\Type\\ObjectType but does not specify its types\: T$#'
2347
identifier: missingType.generics
@@ -108,6 +132,12 @@ parameters:
108132
count: 1
109133
path: src/Normalizer/ReflectionTypeUtil.php
110134

135+
-
136+
message: '#^Property Patchlevel\\Hydrator\\Tests\\Unit\\Extension\\Cryptography\\Fixture\\ChildWithSensitiveDataWithIdentifierDto\:\:\$email is never read, only written\.$#'
137+
identifier: property.onlyWritten
138+
count: 1
139+
path: tests/Unit/Extension/Cryptography/Fixture/ChildWithSensitiveDataWithIdentifierDto.php
140+
111141
-
112142
message: '#^Method Patchlevel\\Hydrator\\Tests\\Unit\\Fixture\\DtoWithHooks\:\:postHydrate\(\) is unused\.$#'
113143
identifier: method.unused
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Cryptography\Attribute;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_PROPERTY)]
10+
final class DataSubjectId
11+
{
12+
public function __construct(
13+
public readonly string $name = 'default',
14+
) {
15+
}
16+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Cryptography\Attribute;
6+
7+
use Attribute;
8+
use InvalidArgumentException;
9+
10+
#[Attribute(Attribute::TARGET_PROPERTY)]
11+
final class SensitiveData
12+
{
13+
/** @var (callable(string):mixed)|null */
14+
public readonly mixed $fallbackCallable;
15+
16+
public function __construct(
17+
public readonly mixed $fallback = null,
18+
callable|null $fallbackCallable = null,
19+
public readonly string $subjectIdName = 'default',
20+
) {
21+
$this->fallbackCallable = $fallbackCallable;
22+
23+
if ($this->fallbackCallable !== null && $this->fallback !== null) {
24+
throw new InvalidArgumentException('You can only set one of fallback or fallbackCallable');
25+
}
26+
}
27+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Cryptography;
6+
7+
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\Cipher;
8+
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\CipherKey;
9+
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\CipherKeyFactory;
10+
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\DecryptionFailed;
11+
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\EncryptionFailed;
12+
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\OpensslCipher;
13+
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\OpensslCipherKeyFactory;
14+
use Patchlevel\Hydrator\Extension\Cryptography\Store\CipherKeyNotExists;
15+
use Patchlevel\Hydrator\Extension\Cryptography\Store\CipherKeyStore;
16+
17+
use function array_key_exists;
18+
use function base64_decode;
19+
use function base64_encode;
20+
use function is_array;
21+
22+
/**
23+
* @phpstan-type EncryptedDataV1 array{
24+
* __enc: 'v1',
25+
* data: non-empty-string,
26+
* method?: non-empty-string,
27+
* iv?: non-empty-string,
28+
* }
29+
*/
30+
final class BaseCryptographer implements Cryptographer
31+
{
32+
public function __construct(
33+
private readonly Cipher $cipher,
34+
private readonly CipherKeyStore $cipherKeyStore,
35+
private readonly CipherKeyFactory $cipherKeyFactory,
36+
) {
37+
}
38+
39+
/**
40+
* @return EncryptedDataV1
41+
*
42+
* @throws EncryptionFailed
43+
*/
44+
public function encrypt(string $subjectId, mixed $value): array
45+
{
46+
try {
47+
$cipherKey = $this->cipherKeyStore->get($subjectId);
48+
} catch (CipherKeyNotExists) {
49+
$cipherKey = ($this->cipherKeyFactory)();
50+
$this->cipherKeyStore->store($subjectId, $cipherKey);
51+
}
52+
53+
return [
54+
'__enc' => 'v1',
55+
'data' => $this->cipher->encrypt($cipherKey, $value),
56+
'method' => $cipherKey->method,
57+
'iv' => base64_encode($cipherKey->iv),
58+
];
59+
}
60+
61+
/**
62+
* @param EncryptedDataV1 $encryptedData
63+
*
64+
* @throws CipherKeyNotExists
65+
* @throws DecryptionFailed
66+
*/
67+
public function decrypt(string $subjectId, mixed $encryptedData): mixed
68+
{
69+
$cipherKey = $this->cipherKeyStore->get($subjectId);
70+
71+
return $this->cipher->decrypt(
72+
new CipherKey(
73+
$cipherKey->key,
74+
$encryptedData['method'] ?? $cipherKey->method,
75+
isset($encryptedData['iv']) ? base64_decode($encryptedData['iv']) : $cipherKey->iv,
76+
),
77+
$encryptedData['data'],
78+
);
79+
}
80+
81+
public function supports(mixed $value): bool
82+
{
83+
return is_array($value) && array_key_exists('__enc', $value) && $value['__enc'] === 'v1';
84+
}
85+
86+
/** @param non-empty-string $method */
87+
public static function createWithOpenssl(
88+
CipherKeyStore $cryptoStore,
89+
string $method = OpensslCipherKeyFactory::DEFAULT_METHOD,
90+
): static {
91+
return new self(
92+
new OpensslCipher(),
93+
$cryptoStore,
94+
new OpensslCipherKeyFactory($method),
95+
);
96+
}
97+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Cryptography\Cipher;
6+
7+
interface Cipher
8+
{
9+
/**
10+
* @return non-empty-string
11+
*
12+
* @throws EncryptionFailed
13+
*/
14+
public function encrypt(CipherKey $key, mixed $data): string;
15+
16+
/** @throws DecryptionFailed */
17+
public function decrypt(CipherKey $key, string $data): mixed;
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Cryptography\Cipher;
6+
7+
final class CipherKey
8+
{
9+
/**
10+
* @param non-empty-string $key
11+
* @param non-empty-string $method
12+
* @param non-empty-string $iv
13+
*/
14+
public function __construct(
15+
public readonly string $key,
16+
public readonly string $method,
17+
public readonly string $iv,
18+
) {
19+
}
20+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Cryptography\Cipher;
6+
7+
interface CipherKeyFactory
8+
{
9+
/** @throws CreateCipherKeyFailed */
10+
public function __invoke(): CipherKey;
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Cryptography\Cipher;
6+
7+
use Patchlevel\Hydrator\HydratorException;
8+
use RuntimeException;
9+
10+
final class CreateCipherKeyFailed extends RuntimeException implements HydratorException
11+
{
12+
public function __construct()
13+
{
14+
parent::__construct('Create cipher key failed.');
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Cryptography\Cipher;
6+
7+
use Patchlevel\Hydrator\HydratorException;
8+
use RuntimeException;
9+
10+
final class DecryptionFailed extends RuntimeException implements HydratorException
11+
{
12+
public function __construct()
13+
{
14+
parent::__construct('Decryption failed.');
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Extension\Cryptography\Cipher;
6+
7+
use Patchlevel\Hydrator\HydratorException;
8+
use RuntimeException;
9+
10+
final class EncryptionFailed extends RuntimeException implements HydratorException
11+
{
12+
public function __construct()
13+
{
14+
parent::__construct('Encryption failed.');
15+
}
16+
}

0 commit comments

Comments
 (0)