Skip to content

Commit 8e47cf5

Browse files
authored
Merge pull request #835 from patchlevel/add-new-doctrine-cipher-key-store
add doctrine cipher key store for hydrator extension
2 parents ab29c25 + a4aad60 commit 8e47cf5

5 files changed

Lines changed: 340 additions & 5 deletions

File tree

phpstan-baseline.neon

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

21+
-
22+
message: '#^Offset ''crypto_iv'' does not exist on array\{id\: non\-empty\-string, subject_id\: non\-empty\-string, crypto_key\: non\-empty\-string, crypto_method\: non\-empty\-string, created_at\: non\-empty\-string\}\.$#'
23+
identifier: offsetAccess.notFound
24+
count: 1
25+
path: src/Cryptography/ExtensionDoctrineCipherKeyStore.php
26+
27+
-
28+
message: '#^Parameter \#1 \$string of function base64_decode expects string, mixed given\.$#'
29+
identifier: argument.type
30+
count: 1
31+
path: src/Cryptography/ExtensionDoctrineCipherKeyStore.php
32+
33+
-
34+
message: '#^Parameter \#2 \$subjectId 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/Cryptography/ExtensionDoctrineCipherKeyStore.php
38+
39+
-
40+
message: '#^Parameter \#3 \$key 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/Cryptography/ExtensionDoctrineCipherKeyStore.php
44+
45+
-
46+
message: '#^Parameter \#4 \$method of class Patchlevel\\Hydrator\\Extension\\Cryptography\\Cipher\\CipherKey constructor expects non\-empty\-string, string given\.$#'
47+
identifier: argument.type
48+
count: 1
49+
path: src/Cryptography/ExtensionDoctrineCipherKeyStore.php
50+
51+
-
52+
message: '#^Parameter \#5 \$createdAt of class Patchlevel\\Hydrator\\Extension\\Cryptography\\Cipher\\CipherKey constructor expects DateTimeImmutable, mixed given\.$#'
53+
identifier: argument.type
54+
count: 2
55+
path: src/Cryptography/ExtensionDoctrineCipherKeyStore.php
56+
2157
-
2258
message: '#^Call to function method_exists\(\) with ReflectionFunction and ''isAnonymous'' will always evaluate to true\.$#'
2359
identifier: function.alreadyNarrowedType
@@ -285,19 +321,19 @@ parameters:
285321
-
286322
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertArrayHasKey\(\) with 0 and array\{array\<string, mixed\>\} will always evaluate to true\.$#'
287323
identifier: staticMethod.alreadyNarrowedType
288-
count: 1
324+
count: 4
289325
path: tests/Integration/PersonalData/PersonalDataTest.php
290326

291327
-
292328
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) with ''Patchlevel\\\\EventSourcing\\\\Tests\\\\Integration\\\\PersonalData\\\\Profile'' and Patchlevel\\EventSourcing\\Tests\\Integration\\PersonalData\\Profile will always evaluate to true\.$#'
293329
identifier: staticMethod.alreadyNarrowedType
294-
count: 6
330+
count: 8
295331
path: tests/Integration/PersonalData/PersonalDataTest.php
296332

297333
-
298334
message: '#^Parameter \#2 \$haystack of static method PHPUnit\\Framework\\Assert\:\:assertStringNotContainsString\(\) expects string, mixed given\.$#'
299335
identifier: argument.type
300-
count: 1
336+
count: 3
301337
path: tests/Integration/PersonalData/PersonalDataTest.php
302338

303339
-
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Cryptography;
6+
7+
use Doctrine\DBAL\Connection;
8+
use Doctrine\DBAL\Schema\Schema;
9+
use Doctrine\DBAL\Types\Type;
10+
use Doctrine\DBAL\Types\Types;
11+
use Patchlevel\EventSourcing\Schema\DoctrineHelper;
12+
use Patchlevel\EventSourcing\Schema\DoctrineSchemaConfigurator;
13+
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\CipherKey;
14+
use Patchlevel\Hydrator\Extension\Cryptography\Store\CipherKeyNotExists;
15+
use Patchlevel\Hydrator\Extension\Cryptography\Store\CipherKeyStore;
16+
17+
use function base64_decode;
18+
use function base64_encode;
19+
20+
/**
21+
* @phpstan-type Row = array{
22+
* id: non-empty-string,
23+
* subject_id: non-empty-string,
24+
* crypto_key: non-empty-string,
25+
* crypto_method: non-empty-string,
26+
* created_at: non-empty-string
27+
* }
28+
*/
29+
final class ExtensionDoctrineCipherKeyStore implements CipherKeyStore, DoctrineSchemaConfigurator
30+
{
31+
private Type $dateTimeType;
32+
33+
public function __construct(
34+
private readonly Connection $connection,
35+
private readonly string $tableName = 'cryptography_keys',
36+
) {
37+
$this->dateTimeType = Type::getType(Types::DATETIMETZ_IMMUTABLE);
38+
}
39+
40+
public function get(string $id): CipherKey
41+
{
42+
/** @var Row|false $result */
43+
$result = $this->connection->fetchAssociative(
44+
"SELECT * FROM {$this->tableName} WHERE id = :id",
45+
['id' => $id],
46+
);
47+
48+
if ($result === false) {
49+
throw CipherKeyNotExists::forKeyId($id);
50+
}
51+
52+
return new CipherKey(
53+
$result['id'],
54+
$result['subject_id'],
55+
base64_decode($result['crypto_key']),
56+
$result['crypto_method'],
57+
$this->dateTimeType->convertToPHPValue($result['created_at'], $this->connection->getDatabasePlatform()),
58+
);
59+
}
60+
61+
public function currentKeyFor(string $subjectId): CipherKey
62+
{
63+
/** @var Row|false $result */
64+
$result = $this->connection->fetchAssociative(
65+
"SELECT * FROM {$this->tableName} WHERE subject_id = :subject_id",
66+
['subject_id' => $subjectId],
67+
);
68+
69+
if ($result === false) {
70+
throw CipherKeyNotExists::forSubjectId($subjectId);
71+
}
72+
73+
return new CipherKey(
74+
$result['id'],
75+
base64_decode($result['crypto_key']),
76+
$result['crypto_method'],
77+
base64_decode($result['crypto_iv']),
78+
$this->dateTimeType->convertToPHPValue($result['created_at'], $this->connection->getDatabasePlatform()),
79+
);
80+
}
81+
82+
public function store(CipherKey $key): void
83+
{
84+
$this->connection->insert($this->tableName, [
85+
'id' => $key->id,
86+
'subject_id' => $key->subjectId,
87+
'crypto_key' => base64_encode($key->key),
88+
'crypto_method' => $key->method,
89+
'created_at' => $this->dateTimeType->convertToDatabaseValue($key->createdAt, $this->connection->getDatabasePlatform()),
90+
]);
91+
}
92+
93+
public function remove(string $id): void
94+
{
95+
$this->connection->delete($this->tableName, ['id' => $id]);
96+
}
97+
98+
public function removeWithSubjectId(string $subjectId): void
99+
{
100+
$this->connection->delete($this->tableName, ['subject_id' => $subjectId]);
101+
}
102+
103+
public function configureSchema(Schema $schema, Connection $connection): void
104+
{
105+
if (!DoctrineHelper::sameDatabase($this->connection, $connection)) {
106+
return;
107+
}
108+
109+
$table = $schema->createTable($this->tableName);
110+
$table->addColumn('id', 'string')
111+
->setNotnull(true)
112+
->setLength(255);
113+
$table->addColumn('subject_id', 'string')
114+
->setNotnull(true)
115+
->setLength(255);
116+
$table->addColumn('crypto_key', 'string')
117+
->setNotnull(true)
118+
->setLength(255);
119+
$table->addColumn('crypto_method', 'string')
120+
->setNotnull(true)
121+
->setLength(255);
122+
$table->addColumn('created_at', 'datetimetz_immutable')
123+
->setNotnull(true);
124+
$table->setPrimaryKey(['id']);
125+
$table->addIndex(['subject_id']);
126+
}
127+
}

tests/Integration/PersonalData/Events/NameChanged.php

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

77
use Patchlevel\EventSourcing\Attribute\Event;
88
use Patchlevel\EventSourcing\Tests\Integration\PersonalData\ProfileId;
9-
use Patchlevel\Hydrator\Attribute\DataSubjectId;
9+
use Patchlevel\Hydrator\Attribute\DataSubjectId as LegacyDataSubjectId;
1010
use Patchlevel\Hydrator\Attribute\PersonalData;
11+
use Patchlevel\Hydrator\Extension\Cryptography\Attribute\DataSubjectId;
12+
use Patchlevel\Hydrator\Extension\Cryptography\Attribute\SensitiveData;
1113

1214
#[Event('profile.name_changed')]
1315
final class NameChanged
1416
{
1517
public function __construct(
1618
#[DataSubjectId]
19+
#[LegacyDataSubjectId]
1720
public readonly ProfileId $aggregateId,
21+
#[SensitiveData(fallback: 'unknown')]
1822
#[PersonalData(fallback: 'unknown')]
1923
public readonly string $name,
2024
) {

tests/Integration/PersonalData/Events/ProfileCreated.php

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

77
use Patchlevel\EventSourcing\Attribute\Event;
88
use Patchlevel\EventSourcing\Tests\Integration\PersonalData\ProfileId;
9-
use Patchlevel\Hydrator\Attribute\DataSubjectId;
9+
use Patchlevel\Hydrator\Attribute\DataSubjectId as LegacyDataSubjectId;
1010
use Patchlevel\Hydrator\Attribute\PersonalData;
11+
use Patchlevel\Hydrator\Extension\Cryptography\Attribute\DataSubjectId;
12+
use Patchlevel\Hydrator\Extension\Cryptography\Attribute\SensitiveData;
1113

1214
#[Event('profile.created')]
1315
final class ProfileCreated
1416
{
1517
public function __construct(
1618
#[DataSubjectId]
19+
#[LegacyDataSubjectId]
1720
public ProfileId $profileId,
21+
#[SensitiveData(fallback: 'unknown')]
1822
#[PersonalData(fallback: 'unknown')]
1923
public string $name,
2024
) {

0 commit comments

Comments
 (0)