Skip to content

Commit 0622079

Browse files
committed
add doctrine cipher key store for hydrator extension
1 parent ab29c25 commit 0622079

5 files changed

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

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)