-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\EventSourcing\Attribute; | ||
|
||
use Attribute; | ||
|
||
#[Attribute(Attribute::TARGET_PROPERTY)] | ||
Check failure on line 9 in src/Attribute/DataSubjectId.php GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)ClassNotFinal
|
||
class DataSubjectId | ||
{ | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\EventSourcing\Attribute; | ||
|
||
use Attribute; | ||
|
||
#[Attribute(Attribute::TARGET_PROPERTY)] | ||
Check failure on line 9 in src/Attribute/PersonalData.php GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)ClassNotFinal
|
||
class PersonalData | ||
{ | ||
public function __construct( | ||
public readonly mixed $fallback = null, | ||
) { | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\EventSourcing\Crypto; | ||
|
||
use function array_key_exists; | ||
|
||
class ArrayCacheCryptoStore implements CryptoStore | ||
Check failure on line 9 in src/Crypto/ArrayCacheCryptoStore.php GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)ClassNotFinal
|
||
{ | ||
private array $keys = []; | ||
Check failure on line 11 in src/Crypto/ArrayCacheCryptoStore.php GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)
|
||
|
||
public function __construct( | ||
private readonly CryptoStore $parent, | ||
) { | ||
} | ||
|
||
public function find(string $id): EncryptionKey|null | ||
Check failure on line 18 in src/Crypto/ArrayCacheCryptoStore.php GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)MixedInferredReturnType
|
||
{ | ||
if (array_key_exists($id, $this->keys)) { | ||
return $this->keys[$id]; | ||
Check failure on line 21 in src/Crypto/ArrayCacheCryptoStore.php GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)MixedReturnStatement
|
||
} | ||
|
||
$this->keys[$id] = $this->parent->find($id); | ||
|
||
return $this->keys[$id]; | ||
} | ||
|
||
public function store(string $id, EncryptionKey $key): void | ||
{ | ||
$this->keys[$id] = $key; | ||
$this->parent->store($id, $key); | ||
} | ||
|
||
public function remove(string $id): void | ||
{ | ||
unset($this->keys[$id]); | ||
$this->parent->remove($id); | ||
} | ||
|
||
public function clear(): void | ||
{ | ||
$this->keys = []; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\EventSourcing\Crypto; | ||
|
||
interface CryptoManager | ||
{ | ||
/** @param class-string $class */ | ||
public function encrypt(string $class, array $data): array; | ||
Check failure on line 10 in src/Crypto/CryptoManager.php GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)
|
||
|
||
/** @param class-string $class */ | ||
public function decrypt(string $class, array $data): array; | ||
Check failure on line 13 in src/Crypto/CryptoManager.php GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\EventSourcing\Crypto; | ||
|
||
interface CryptoService | ||
{ | ||
public function encrypt(EncryptionKey $key, mixed $data): string; | ||
|
||
public function decrypt(EncryptionKey $key, string $data): mixed; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\EventSourcing\Crypto; | ||
|
||
interface CryptoStore | ||
{ | ||
public function find(string $id): EncryptionKey|null; | ||
|
||
public function store(string $id, EncryptionKey $key): void; | ||
|
||
public function remove(string $id): void; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\EventSourcing\Crypto; | ||
|
||
use Patchlevel\EventSourcing\Metadata\Event\EventMetadataFactory; | ||
|
||
class DefaultCryptoManager implements CryptoManager | ||
Check failure on line 9 in src/Crypto/DefaultCryptoManager.php GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)ClassNotFinal
|
||
{ | ||
public function __construct( | ||
private readonly EventMetadataFactory $eventMetadataFactory, | ||
private readonly CryptoStore $cryptoStore, | ||
private readonly CryptoService $cryptoService, | ||
private readonly EncryptionKeyFactory $encryptionKeyFactory, | ||
) { | ||
} | ||
|
||
public function encrypt(string $class, array $data): array | ||
Check failure on line 19 in src/Crypto/DefaultCryptoManager.php GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)
Check failure on line 19 in src/Crypto/DefaultCryptoManager.php GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)
|
||
{ | ||
$subjectId = $this->subjectId($class, $data); | ||
|
||
if ($subjectId === null) { | ||
return $data; | ||
} | ||
|
||
$encryptionKey = $this->cryptoStore->find($subjectId); | ||
|
||
if ($encryptionKey === null) { | ||
$encryptionKey = ($this->encryptionKeyFactory)(); | ||
$this->cryptoStore->store($subjectId, $encryptionKey); | ||
} | ||
|
||
$metadata = $this->eventMetadataFactory->metadata($class); | ||
|
||
foreach ($metadata->propertyMetadata as $propertyMetadata) { | ||
if (!$propertyMetadata->isPersonalData) { | ||
continue; | ||
} | ||
|
||
$data[$propertyMetadata->fieldName] = $this->cryptoService->encrypt( | ||
$encryptionKey, | ||
$data[$propertyMetadata->fieldName], | ||
); | ||
} | ||
|
||
return $data; | ||
} | ||
|
||
public function decrypt(string $class, array $data): array | ||
Check failure on line 50 in src/Crypto/DefaultCryptoManager.php GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)
Check failure on line 50 in src/Crypto/DefaultCryptoManager.php GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)
|
||
{ | ||
$subjectId = $this->subjectId($class, $data); | ||
|
||
if ($subjectId === null) { | ||
return $data; | ||
} | ||
|
||
$encryptionKey = $this->cryptoStore->find($subjectId); | ||
|
||
$metadata = $this->eventMetadataFactory->metadata($class); | ||
|
||
foreach ($metadata->propertyMetadata as $propertyMetadata) { | ||
if (!$propertyMetadata->isPersonalData) { | ||
continue; | ||
} | ||
|
||
if (!$encryptionKey) { | ||
$data[$propertyMetadata->fieldName] = $propertyMetadata->personalDataFallback; | ||
Check failure on line 68 in src/Crypto/DefaultCryptoManager.php GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)MixedAssignment
|
||
continue; | ||
} | ||
|
||
$data[$propertyMetadata->fieldName] = $this->cryptoService->decrypt( | ||
Check failure on line 72 in src/Crypto/DefaultCryptoManager.php GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)MixedAssignment
|
||
$encryptionKey, | ||
$data[$propertyMetadata->fieldName], | ||
Check failure on line 74 in src/Crypto/DefaultCryptoManager.php GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)MixedArgument
|
||
); | ||
} | ||
|
||
return $data; | ||
} | ||
|
||
private function subjectId(string $class, array $data): string|null | ||
Check failure on line 81 in src/Crypto/DefaultCryptoManager.php GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)MixedInferredReturnType
Check failure on line 81 in src/Crypto/DefaultCryptoManager.php GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)
|
||
{ | ||
$metadata = $this->eventMetadataFactory->metadata($class); | ||
|
||
if ($metadata->dataSubjectIdField === null) { | ||
return null; | ||
} | ||
|
||
return $data[$metadata->dataSubjectIdField]; | ||
} | ||
|
||
public static function createDefault(EventMetadataFactory $eventMetadataFactory, CryptoStore $cryptoStore): static | ||
{ | ||
return new self( | ||
$eventMetadataFactory, | ||
$cryptoStore, | ||
new OpensslCryptoService(), | ||
new OpensslEncryptionKeyFactory(), | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\EventSourcing\Crypto; | ||
|
||
use Doctrine\DBAL\Connection; | ||
use Doctrine\DBAL\Schema\Schema; | ||
use Patchlevel\EventSourcing\Schema\DoctrineSchemaConfigurator; | ||
|
||
use function bin2hex; | ||
use function hex2bin; | ||
|
||
class DoctrineCryptoStore implements CryptoStore, DoctrineSchemaConfigurator | ||
{ | ||
public function __construct( | ||
private readonly Connection $connection, | ||
private readonly string $tableName = 'crypto_keys', | ||
) { | ||
} | ||
|
||
public function find(string $id): EncryptionKey|null | ||
{ | ||
$result = $this->connection->fetchAssociative( | ||
"SELECT * FROM {$this->tableName} WHERE id = :id", | ||
['id' => $id], | ||
); | ||
|
||
if ($result === false) { | ||
return null; | ||
} | ||
|
||
return new EncryptionKey( | ||
hex2bin($result['key']), | ||
$result['method'], | ||
hex2bin($result['iv']), | ||
); | ||
} | ||
|
||
public function store(string $id, EncryptionKey $key): void | ||
{ | ||
$this->connection->insert($this->tableName, [ | ||
'id' => $id, | ||
'key' => bin2hex($key->key), | ||
'method' => $key->method, | ||
'iv' => bin2hex($key->iv), | ||
]); | ||
} | ||
|
||
public function remove(string $id): void | ||
{ | ||
$this->connection->delete($this->tableName, ['id' => $id]); | ||
} | ||
|
||
public function configureSchema(Schema $schema, Connection $connection): void | ||
{ | ||
if ($connection !== $this->connection) { | ||
return; | ||
} | ||
|
||
$table = $schema->createTable($this->tableName); | ||
$table->addColumn('id', 'string') | ||
->setNotnull(true) | ||
->setLength(255); | ||
$table->addColumn('key', 'string') | ||
->setNotnull(true) | ||
->setLength(255); | ||
$table->addColumn('method', 'string') | ||
->setNotnull(true) | ||
->setLength(255); | ||
$table->addColumn('iv', 'string') | ||
->setNotnull(true) | ||
->setLength(255); | ||
$table->setPrimaryKey(['id']); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\EventSourcing\Crypto; | ||
|
||
final class EncryptionKey | ||
{ | ||
public function __construct( | ||
public readonly string $key, | ||
public readonly string $method, | ||
public readonly string $iv, | ||
) { | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\EventSourcing\Crypto; | ||
|
||
interface EncryptionKeyFactory | ||
{ | ||
public function __invoke(): EncryptionKey; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Patchlevel\EventSourcing\Crypto; | ||
|
||
class InMemoryCryptoStore implements CryptoStore | ||
{ | ||
/** @var array<string, EncryptionKey> */ | ||
private array $keys = []; | ||
|
||
public function find(string $id): EncryptionKey|null | ||
{ | ||
return $this->keys[$id] ?? null; | ||
} | ||
|
||
public function store(string $id, EncryptionKey $key): void | ||
{ | ||
$this->keys[$id] = $key; | ||
} | ||
|
||
public function remove(string $id): void | ||
{ | ||
unset($this->keys[$id]); | ||
} | ||
} |