Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,42 @@ parameters:
count: 1
path: src/Cryptography/DoctrineCipherKeyStore.php

-
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\}\.$#'
identifier: offsetAccess.notFound
count: 1
path: src/Cryptography/ExtensionDoctrineCipherKeyStore.php

-
message: '#^Parameter \#1 \$string of function base64_decode expects string, mixed given\.$#'
identifier: argument.type
count: 1
path: src/Cryptography/ExtensionDoctrineCipherKeyStore.php

-
message: '#^Parameter \#2 \$subjectId of class Patchlevel\\Hydrator\\Extension\\Cryptography\\Cipher\\CipherKey constructor expects non\-empty\-string, string given\.$#'
identifier: argument.type
count: 1
path: src/Cryptography/ExtensionDoctrineCipherKeyStore.php

-
message: '#^Parameter \#3 \$key of class Patchlevel\\Hydrator\\Extension\\Cryptography\\Cipher\\CipherKey constructor expects non\-empty\-string, string given\.$#'
identifier: argument.type
count: 1
path: src/Cryptography/ExtensionDoctrineCipherKeyStore.php

-
message: '#^Parameter \#4 \$method of class Patchlevel\\Hydrator\\Extension\\Cryptography\\Cipher\\CipherKey constructor expects non\-empty\-string, string given\.$#'
identifier: argument.type
count: 1
path: src/Cryptography/ExtensionDoctrineCipherKeyStore.php

-
message: '#^Parameter \#5 \$createdAt of class Patchlevel\\Hydrator\\Extension\\Cryptography\\Cipher\\CipherKey constructor expects DateTimeImmutable, mixed given\.$#'
identifier: argument.type
count: 2
path: src/Cryptography/ExtensionDoctrineCipherKeyStore.php

-
message: '#^Call to function method_exists\(\) with ReflectionFunction and ''isAnonymous'' will always evaluate to true\.$#'
identifier: function.alreadyNarrowedType
Expand Down Expand Up @@ -285,19 +321,19 @@ parameters:
-
message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertArrayHasKey\(\) with 0 and array\{array\<string, mixed\>\} will always evaluate to true\.$#'
identifier: staticMethod.alreadyNarrowedType
count: 1
count: 4
path: tests/Integration/PersonalData/PersonalDataTest.php

-
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\.$#'
identifier: staticMethod.alreadyNarrowedType
count: 6
count: 8
path: tests/Integration/PersonalData/PersonalDataTest.php

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

-
Expand Down
127 changes: 127 additions & 0 deletions src/Cryptography/ExtensionDoctrineCipherKeyStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Cryptography;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Patchlevel\EventSourcing\Schema\DoctrineHelper;
use Patchlevel\EventSourcing\Schema\DoctrineSchemaConfigurator;
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\CipherKey;
use Patchlevel\Hydrator\Extension\Cryptography\Store\CipherKeyNotExists;
use Patchlevel\Hydrator\Extension\Cryptography\Store\CipherKeyStore;

use function base64_decode;
use function base64_encode;

/**
* @phpstan-type Row = 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
* }
*/
final class ExtensionDoctrineCipherKeyStore implements CipherKeyStore, DoctrineSchemaConfigurator
{
private Type $dateTimeType;

public function __construct(
private readonly Connection $connection,
private readonly string $tableName = 'cryptography_keys',
) {
$this->dateTimeType = Type::getType(Types::DATETIMETZ_IMMUTABLE);
}

public function get(string $id): CipherKey
{
/** @var Row|false $result */
$result = $this->connection->fetchAssociative(
"SELECT * FROM {$this->tableName} WHERE id = :id",
['id' => $id],
);

if ($result === false) {
throw CipherKeyNotExists::forKeyId($id);
}

return new CipherKey(
$result['id'],
$result['subject_id'],
base64_decode($result['crypto_key']),
$result['crypto_method'],
$this->dateTimeType->convertToPHPValue($result['created_at'], $this->connection->getDatabasePlatform()),
);
}

public function currentKeyFor(string $subjectId): CipherKey
{
/** @var Row|false $result */
$result = $this->connection->fetchAssociative(
"SELECT * FROM {$this->tableName} WHERE subject_id = :subject_id",
['subject_id' => $subjectId],
);

if ($result === false) {
throw CipherKeyNotExists::forSubjectId($subjectId);
}

return new CipherKey(
$result['id'],
base64_decode($result['crypto_key']),
$result['crypto_method'],
base64_decode($result['crypto_iv']),
$this->dateTimeType->convertToPHPValue($result['created_at'], $this->connection->getDatabasePlatform()),
);
}

public function store(CipherKey $key): void
{
$this->connection->insert($this->tableName, [
'id' => $key->id,
'subject_id' => $key->subjectId,
'crypto_key' => base64_encode($key->key),
'crypto_method' => $key->method,
'created_at' => $this->dateTimeType->convertToDatabaseValue($key->createdAt, $this->connection->getDatabasePlatform()),
]);
}

public function remove(string $id): void
{
$this->connection->delete($this->tableName, ['id' => $id]);
}

public function removeWithSubjectId(string $subjectId): void
{
$this->connection->delete($this->tableName, ['subject_id' => $subjectId]);
}

public function configureSchema(Schema $schema, Connection $connection): void
{
if (!DoctrineHelper::sameDatabase($this->connection, $connection)) {
return;
}

$table = $schema->createTable($this->tableName);
$table->addColumn('id', 'string')
->setNotnull(true)
->setLength(255);
$table->addColumn('subject_id', 'string')
->setNotnull(true)
->setLength(255);
$table->addColumn('crypto_key', 'string')
->setNotnull(true)
->setLength(255);
$table->addColumn('crypto_method', 'string')
->setNotnull(true)
->setLength(255);
$table->addColumn('created_at', 'datetimetz_immutable')
->setNotnull(true);
$table->setPrimaryKey(['id']);
$table->addIndex(['subject_id']);
}
}
6 changes: 5 additions & 1 deletion tests/Integration/PersonalData/Events/NameChanged.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@

use Patchlevel\EventSourcing\Attribute\Event;
use Patchlevel\EventSourcing\Tests\Integration\PersonalData\ProfileId;
use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\DataSubjectId as LegacyDataSubjectId;
use Patchlevel\Hydrator\Attribute\PersonalData;
use Patchlevel\Hydrator\Extension\Cryptography\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Extension\Cryptography\Attribute\SensitiveData;

#[Event('profile.name_changed')]
final class NameChanged
{
public function __construct(
#[DataSubjectId]
#[LegacyDataSubjectId]
public readonly ProfileId $aggregateId,
#[SensitiveData(fallback: 'unknown')]
#[PersonalData(fallback: 'unknown')]
public readonly string $name,
) {
Expand Down
6 changes: 5 additions & 1 deletion tests/Integration/PersonalData/Events/ProfileCreated.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@

use Patchlevel\EventSourcing\Attribute\Event;
use Patchlevel\EventSourcing\Tests\Integration\PersonalData\ProfileId;
use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\DataSubjectId as LegacyDataSubjectId;
use Patchlevel\Hydrator\Attribute\PersonalData;
use Patchlevel\Hydrator\Extension\Cryptography\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Extension\Cryptography\Attribute\SensitiveData;

#[Event('profile.created')]
final class ProfileCreated
{
public function __construct(
#[DataSubjectId]
#[LegacyDataSubjectId]
public ProfileId $profileId,
#[SensitiveData(fallback: 'unknown')]
#[PersonalData(fallback: 'unknown')]
public string $name,
) {
Expand Down
Loading
Loading