Skip to content

Commit

Permalink
poc personal data
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidBadura committed Mar 19, 2024
1 parent 3b6277f commit 0ee4d1d
Show file tree
Hide file tree
Showing 31 changed files with 979 additions and 11 deletions.
12 changes: 12 additions & 0 deletions src/Attribute/DataSubjectId.php
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

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

ClassNotFinal

src/Attribute/DataSubjectId.php:9:1: ClassNotFinal: DataSubjectId has not been marked as final nor is marked for inheritance.
class DataSubjectId
{
}
16 changes: 16 additions & 0 deletions src/Attribute/PersonalData.php
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

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

ClassNotFinal

src/Attribute/PersonalData.php:9:1: ClassNotFinal: PersonalData has not been marked as final nor is marked for inheritance.
class PersonalData
{
public function __construct(
public readonly mixed $fallback = null,
) {
}
}
45 changes: 45 additions & 0 deletions src/Crypto/ArrayCacheCryptoStore.php
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

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

ClassNotFinal

src/Crypto/ArrayCacheCryptoStore.php:9:1: ClassNotFinal: ArrayCacheCryptoStore has not been marked as final nor is marked for inheritance.
{
private array $keys = [];

Check failure on line 11 in src/Crypto/ArrayCacheCryptoStore.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)

Property Patchlevel\EventSourcing\Crypto\ArrayCacheCryptoStore::$keys type has no value type specified in iterable type array.

public function __construct(
private readonly CryptoStore $parent,
) {
}

public function find(string $id): EncryptionKey|null

Check failure on line 18 in src/Crypto/ArrayCacheCryptoStore.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedInferredReturnType

src/Crypto/ArrayCacheCryptoStore.php:18:39: MixedInferredReturnType: Could not verify return type 'Patchlevel\EventSourcing\Crypto\EncryptionKey|null' for Patchlevel\EventSourcing\Crypto\ArrayCacheCryptoStore::find (see https://psalm.dev/047)
{
if (array_key_exists($id, $this->keys)) {
return $this->keys[$id];

Check failure on line 21 in src/Crypto/ArrayCacheCryptoStore.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedReturnStatement

src/Crypto/ArrayCacheCryptoStore.php:21:20: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)
}

$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 = [];
}
}
14 changes: 14 additions & 0 deletions src/Crypto/CryptoManager.php
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

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)

Method Patchlevel\EventSourcing\Crypto\CryptoManager::encrypt() has parameter $data with no value type specified in iterable type array.

Check failure on line 10 in src/Crypto/CryptoManager.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)

Method Patchlevel\EventSourcing\Crypto\CryptoManager::encrypt() return type has no value type specified in iterable type array.

/** @param class-string $class */
public function decrypt(string $class, array $data): array;

Check failure on line 13 in src/Crypto/CryptoManager.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)

Method Patchlevel\EventSourcing\Crypto\CryptoManager::decrypt() has parameter $data with no value type specified in iterable type array.

Check failure on line 13 in src/Crypto/CryptoManager.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)

Method Patchlevel\EventSourcing\Crypto\CryptoManager::decrypt() return type has no value type specified in iterable type array.
}
12 changes: 12 additions & 0 deletions src/Crypto/CryptoService.php
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;
}
14 changes: 14 additions & 0 deletions src/Crypto/CryptoStore.php
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;
}
101 changes: 101 additions & 0 deletions src/Crypto/DefaultCryptoManager.php
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

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

ClassNotFinal

src/Crypto/DefaultCryptoManager.php:9:1: ClassNotFinal: DefaultCryptoManager has not been marked as final nor is marked for inheritance.
{
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

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)

Method Patchlevel\EventSourcing\Crypto\DefaultCryptoManager::encrypt() has parameter $data with no value type specified in iterable type array.

Check failure on line 19 in src/Crypto/DefaultCryptoManager.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)

Method Patchlevel\EventSourcing\Crypto\DefaultCryptoManager::encrypt() return type has no value type specified in iterable type array.
{
$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

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)

Method Patchlevel\EventSourcing\Crypto\DefaultCryptoManager::decrypt() has parameter $data with no value type specified in iterable type array.

Check failure on line 50 in src/Crypto/DefaultCryptoManager.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)

Method Patchlevel\EventSourcing\Crypto\DefaultCryptoManager::decrypt() return type has no value type specified in iterable type array.
{
$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

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedAssignment

src/Crypto/DefaultCryptoManager.php:68:17: MixedAssignment: Unable to determine the type of this assignment (see https://psalm.dev/032)
continue;
}

$data[$propertyMetadata->fieldName] = $this->cryptoService->decrypt(

Check failure on line 72 in src/Crypto/DefaultCryptoManager.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedAssignment

src/Crypto/DefaultCryptoManager.php:72:13: MixedAssignment: Unable to determine the type of this assignment (see https://psalm.dev/032)
$encryptionKey,
$data[$propertyMetadata->fieldName],

Check failure on line 74 in src/Crypto/DefaultCryptoManager.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedArgument

src/Crypto/DefaultCryptoManager.php:74:17: MixedArgument: Argument 2 of Patchlevel\EventSourcing\Crypto\CryptoService::decrypt cannot be mixed, expecting string (see https://psalm.dev/030)
);
}

return $data;
}

private function subjectId(string $class, array $data): string|null

Check failure on line 81 in src/Crypto/DefaultCryptoManager.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MixedInferredReturnType

src/Crypto/DefaultCryptoManager.php:81:61: MixedInferredReturnType: Could not verify return type 'null|string' for Patchlevel\EventSourcing\Crypto\DefaultCryptoManager::subjectId (see https://psalm.dev/047)

Check failure on line 81 in src/Crypto/DefaultCryptoManager.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.3, ubuntu-latest)

Method Patchlevel\EventSourcing\Crypto\DefaultCryptoManager::subjectId() has parameter $data with no value type specified in iterable type array.
{
$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(),
);
}
}
76 changes: 76 additions & 0 deletions src/Crypto/DoctrineCryptoStore.php
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']);
}
}
15 changes: 15 additions & 0 deletions src/Crypto/EncryptionKey.php
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,
) {
}
}
10 changes: 10 additions & 0 deletions src/Crypto/EncryptionKeyFactory.php
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;
}
26 changes: 26 additions & 0 deletions src/Crypto/InMemoryCryptoStore.php
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]);
}
}
Loading

0 comments on commit 0ee4d1d

Please sign in to comment.