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 104c9f0
Show file tree
Hide file tree
Showing 35 changed files with 1,176 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)]
final 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)]
final class PersonalData
{
public function __construct(
public readonly mixed $fallback = null,
) {
}
}
13 changes: 13 additions & 0 deletions src/Cryptography/Cipher/Cipher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Cryptography\Cipher;

interface Cipher
{
public function encrypt(CipherKey $key, mixed $data): string;

/** @throws DecryptionFailed */
public function decrypt(CipherKey $key, string $data): mixed;
}
20 changes: 20 additions & 0 deletions src/Cryptography/Cipher/CipherKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Cryptography\Cipher;

final class CipherKey
{
/**
* @param non-empty-string $key
* @param non-empty-string $method
* @param non-empty-string $iv
*/
public function __construct(
public readonly string $key,
public readonly string $method,
public readonly string $iv,
) {
}
}
10 changes: 10 additions & 0 deletions src/Cryptography/Cipher/CipherKeyFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Cryptography\Cipher;

interface CipherKeyFactory
{
public function __invoke(): CipherKey;
}
15 changes: 15 additions & 0 deletions src/Cryptography/Cipher/DecryptionFailed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Cryptography\Cipher;

use RuntimeException;

final class DecryptionFailed extends RuntimeException
{
public function __construct()
{
parent::__construct('decryption failed');
}
}
57 changes: 57 additions & 0 deletions src/Cryptography/Cipher/OpensslCipher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Cryptography\Cipher;

use function base64_decode;
use function base64_encode;
use function json_decode;
use function json_encode;
use function openssl_decrypt;
use function openssl_encrypt;

use const JSON_THROW_ON_ERROR;

final class OpensslCipher implements Cipher
{
public function encrypt(CipherKey $key, mixed $data): string
{
return base64_encode(
openssl_encrypt(

Check failure on line 21 in src/Cryptography/Cipher/OpensslCipher.php

View workflow job for this annotation

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

Parameter #1 $string of function base64_encode expects string, string|false given.
$this->dataEncode($data),
$key->method,
$key->key,
0,
$key->iv,
),
);
}

public function decrypt(CipherKey $key, string $data): mixed
{
$data = openssl_decrypt(
base64_decode($data),
$key->method,
$key->key,
0,
$key->iv,
);

if ($data === false) {
throw new DecryptionFailed();
}

return $this->dataDecode($data);
}

private function dataEncode(mixed $data): string
{
return json_encode($data, JSON_THROW_ON_ERROR);
}

private function dataDecode(string $data): mixed
{
return json_decode($data, true, 512, JSON_THROW_ON_ERROR);
}
}
30 changes: 30 additions & 0 deletions src/Cryptography/Cipher/OpensslCipherKeyFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Cryptography\Cipher;

use function openssl_cipher_iv_length;
use function openssl_random_pseudo_bytes;

final class OpensslCipherKeyFactory implements CipherKeyFactory
{
public const DEFAULT_LENGTH = 32;

public const DEFAULT_METHOD = 'aes128';

public function __construct(
private readonly int $length = self::DEFAULT_LENGTH,
private readonly string $method = self::DEFAULT_METHOD,
) {
}

public function __invoke(): CipherKey
{
return new CipherKey(
openssl_random_pseudo_bytes($this->length),

Check failure on line 25 in src/Cryptography/Cipher/OpensslCipherKeyFactory.php

View workflow job for this annotation

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

Parameter #1 $key of class Patchlevel\EventSourcing\Cryptography\Cipher\CipherKey constructor expects non-empty-string, string given.

Check failure on line 25 in src/Cryptography/Cipher/OpensslCipherKeyFactory.php

View workflow job for this annotation

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

ArgumentTypeCoercion

src/Cryptography/Cipher/OpensslCipherKeyFactory.php:25:13: ArgumentTypeCoercion: Argument 1 of Patchlevel\EventSourcing\Cryptography\Cipher\CipherKey::__construct expects non-empty-string, but parent type string provided (see https://psalm.dev/193)
$this->method,

Check failure on line 26 in src/Cryptography/Cipher/OpensslCipherKeyFactory.php

View workflow job for this annotation

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

Parameter #2 $method of class Patchlevel\EventSourcing\Cryptography\Cipher\CipherKey constructor expects non-empty-string, string given.

Check failure on line 26 in src/Cryptography/Cipher/OpensslCipherKeyFactory.php

View workflow job for this annotation

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

ArgumentTypeCoercion

src/Cryptography/Cipher/OpensslCipherKeyFactory.php:26:13: ArgumentTypeCoercion: Argument 2 of Patchlevel\EventSourcing\Cryptography\Cipher\CipherKey::__construct expects non-empty-string, but parent type string provided (see https://psalm.dev/193)
openssl_random_pseudo_bytes(openssl_cipher_iv_length($this->method)),

Check failure on line 27 in src/Cryptography/Cipher/OpensslCipherKeyFactory.php

View workflow job for this annotation

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

Parameter #1 $length of function openssl_random_pseudo_bytes expects int, int|false given.

Check failure on line 27 in src/Cryptography/Cipher/OpensslCipherKeyFactory.php

View workflow job for this annotation

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

Parameter #3 $iv of class Patchlevel\EventSourcing\Cryptography\Cipher\CipherKey constructor expects non-empty-string, string given.

Check failure on line 27 in src/Cryptography/Cipher/OpensslCipherKeyFactory.php

View workflow job for this annotation

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

ArgumentTypeCoercion

src/Cryptography/Cipher/OpensslCipherKeyFactory.php:27:13: ArgumentTypeCoercion: Argument 3 of Patchlevel\EventSourcing\Cryptography\Cipher\CipherKey::__construct expects non-empty-string, but parent type string provided (see https://psalm.dev/193)
);
}
}
132 changes: 132 additions & 0 deletions src/Cryptography/DefaultEventPayloadCryptographer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Cryptography;

use Patchlevel\EventSourcing\Cryptography\Cipher\Cipher;
use Patchlevel\EventSourcing\Cryptography\Cipher\CipherKeyFactory;
use Patchlevel\EventSourcing\Cryptography\Cipher\DecryptionFailed;
use Patchlevel\EventSourcing\Cryptography\Cipher\OpensslCipher;
use Patchlevel\EventSourcing\Cryptography\Cipher\OpensslCipherKeyFactory;
use Patchlevel\EventSourcing\Cryptography\Store\CipherKeyNotExists;
use Patchlevel\EventSourcing\Cryptography\Store\CipherKeyStore;
use Patchlevel\EventSourcing\Metadata\Event\EventMetadataFactory;

final class DefaultEventPayloadCryptographer implements EventPayloadCryptographer
{
public function __construct(
private readonly EventMetadataFactory $eventMetadataFactory,
private readonly CipherKeyStore $cryptoStore,
private readonly Cipher $cipher,
private readonly CipherKeyFactory $cipherKeyFactory,
) {
}

/**
* @param class-string $class
* @param array<string, mixed> $data
*
* @return array<string, mixed>
*/
public function encrypt(string $class, array $data): array
{
$subjectId = $this->subjectId($class, $data);

if ($subjectId === null) {
return $data;
}

try {
$cipherKey = $this->cryptoStore->get($subjectId);
} catch (CipherKeyNotExists) {
$cipherKey = ($this->cipherKeyFactory)();
$this->cryptoStore->store($subjectId, $cipherKey);
}

$metadata = $this->eventMetadataFactory->metadata($class);

foreach ($metadata->propertyMetadata as $propertyMetadata) {
if (!$propertyMetadata->isPersonalData) {
continue;
}

$data[$propertyMetadata->fieldName] = $this->cipher->encrypt(
$cipherKey,
$data[$propertyMetadata->fieldName],
);
}

return $data;
}

/**
* @param class-string $class
* @param array<string, mixed> $data
*
* @return array<string, mixed>
*/
public function decrypt(string $class, array $data): array
{
$subjectId = $this->subjectId($class, $data);

if ($subjectId === null) {
return $data;
}

try {
$cipherKey = $this->cryptoStore->get($subjectId);
} catch (CipherKeyNotExists) {
$cipherKey = null;
}

$metadata = $this->eventMetadataFactory->metadata($class);

foreach ($metadata->propertyMetadata as $propertyMetadata) {
if (!$propertyMetadata->isPersonalData) {
continue;
}

if (!$cipherKey) {
$data[$propertyMetadata->fieldName] = $propertyMetadata->personalDataFallback;

Check failure on line 91 in src/Cryptography/DefaultEventPayloadCryptographer.php

View workflow job for this annotation

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

MixedAssignment

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

try {
$data[$propertyMetadata->fieldName] = $this->cipher->decrypt(

Check failure on line 96 in src/Cryptography/DefaultEventPayloadCryptographer.php

View workflow job for this annotation

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

MixedAssignment

src/Cryptography/DefaultEventPayloadCryptographer.php:96:17: MixedAssignment: Unable to determine the type of this assignment (see https://psalm.dev/032)
$cipherKey,
$data[$propertyMetadata->fieldName],

Check failure on line 98 in src/Cryptography/DefaultEventPayloadCryptographer.php

View workflow job for this annotation

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

Parameter #2 $data of method Patchlevel\EventSourcing\Cryptography\Cipher\Cipher::decrypt() expects string, mixed given.

Check failure on line 98 in src/Cryptography/DefaultEventPayloadCryptographer.php

View workflow job for this annotation

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

MixedArgument

src/Cryptography/DefaultEventPayloadCryptographer.php:98:21: MixedArgument: Argument 2 of Patchlevel\EventSourcing\Cryptography\Cipher\Cipher::decrypt cannot be mixed, expecting string (see https://psalm.dev/030)
);
} catch (DecryptionFailed) {
$data[$propertyMetadata->fieldName] = $propertyMetadata->personalDataFallback;

Check failure on line 101 in src/Cryptography/DefaultEventPayloadCryptographer.php

View workflow job for this annotation

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

MixedAssignment

src/Cryptography/DefaultEventPayloadCryptographer.php:101:17: MixedAssignment: Unable to determine the type of this assignment (see https://psalm.dev/032)
}
}

return $data;
}

/**
* @param class-string $class
* @param array<string, mixed> $data
*/
private function subjectId(string $class, array $data): string|null

Check failure on line 112 in src/Cryptography/DefaultEventPayloadCryptographer.php

View workflow job for this annotation

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

MixedInferredReturnType

src/Cryptography/DefaultEventPayloadCryptographer.php:112:61: MixedInferredReturnType: Could not verify return type 'null|string' for Patchlevel\EventSourcing\Cryptography\DefaultEventPayloadCryptographer::subjectId (see https://psalm.dev/047)
{
$metadata = $this->eventMetadataFactory->metadata($class);

if ($metadata->dataSubjectIdField === null) {
return null;
}

return $data[$metadata->dataSubjectIdField];

Check failure on line 120 in src/Cryptography/DefaultEventPayloadCryptographer.php

View workflow job for this annotation

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

Method Patchlevel\EventSourcing\Cryptography\DefaultEventPayloadCryptographer::subjectId() should return string|null but returns mixed.

Check failure on line 120 in src/Cryptography/DefaultEventPayloadCryptographer.php

View workflow job for this annotation

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

MixedReturnStatement

src/Cryptography/DefaultEventPayloadCryptographer.php:120:16: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138)
}

public static function createWithOpenssl(EventMetadataFactory $eventMetadataFactory, CipherKeyStore $cryptoStore): static
{
return new self(
$eventMetadataFactory,
$cryptoStore,
new OpensslCipher(),
new OpensslCipherKeyFactory(),
);
}
}
24 changes: 24 additions & 0 deletions src/Cryptography/EventPayloadCryptographer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Cryptography;

interface EventPayloadCryptographer
{
/**
* @param class-string $class
* @param array<string, mixed> $data
*
* @return array<string, mixed>
*/
public function encrypt(string $class, array $data): array;

/**
* @param class-string $class
* @param array<string, mixed> $data
*
* @return array<string, mixed>
*/
public function decrypt(string $class, array $data): array;
}
17 changes: 17 additions & 0 deletions src/Cryptography/Store/CipherKeyNotExists.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Cryptography\Store;

use RuntimeException;

use function sprintf;

final class CipherKeyNotExists extends RuntimeException
{
public function __construct(string $id)
{
parent::__construct(sprintf('Cipher key with subject id "%s" not found', $id));
}
}
17 changes: 17 additions & 0 deletions src/Cryptography/Store/CipherKeyStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Cryptography\Store;

use Patchlevel\EventSourcing\Cryptography\Cipher\CipherKey;

interface CipherKeyStore
{
/** @throws CipherKeyNotExists */
public function get(string $id): CipherKey;

public function store(string $id, CipherKey $key): void;

public function remove(string $id): void;
}
Loading

0 comments on commit 104c9f0

Please sign in to comment.