Skip to content

Commit

Permalink
Merge pull request #42 from patchlevel/cryptography
Browse files Browse the repository at this point in the history
add cryptography
  • Loading branch information
DavidBadura authored Apr 11, 2024
2 parents 5de321d + 5a15c3a commit 0d5518e
Show file tree
Hide file tree
Showing 45 changed files with 1,765 additions and 9 deletions.
112 changes: 111 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

# Hydrator

With this library you can hydrate objects from array into objects and back again.
With this library you can hydrate objects from array into objects and back again
with a focus on data processing from and into a database.
It has now been outsourced by the [event-sourcing](https://github.com/patchlevel/event-sourcing) library as a separate library.

## Installation
Expand Down Expand Up @@ -333,3 +334,112 @@ readonly class ProfileCreated
}
}
```

### Cryptography

The library also offers the possibility to encrypt and decrypt personal data.

#### PersonalData

First of all, we have to mark the fields that contain personal data.
For our example, we use events, but you can do the same with aggregates.

```php
use Patchlevel\Hydrator\Attribute\PersonalData;

final class DTO
{
#[PersonalData]
public readonly string|null $email;
}
```

If the information could not be decrypted, then a fallback value is inserted.
The default fallback value is `null`.
You can change this by setting the `fallback` parameter.
In this case `unknown` is added:

```php
use Patchlevel\Hydrator\Attribute\PersonalData;

final class DTO
{
public function __construct(
#[PersonalData(fallback: 'unknown')]
public readonly string $email,
) {
}
}
```

> [!DANGER]
> You have to deal with this case in your business logic such as aggregates and subscriptions.
> [!WARNING]
> You need to define a subject ID to use the personal data attribute.
#### DataSubjectId

In order for the correct key to be used, a subject ID must be defined.
Without Subject Id, no personal data can be encrypted or decrypted.

```php
use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\PersonalData;

final class EmailChanged
{
public function __construct(
#[DataSubjectId]
public readonly string $personId,
#[PersonalData(fallback: 'unknown')]
public readonly string|null $email,
) {
}
}
```

> [!WARNING]
> A subject ID can not be a personal data.
#### Configure Cryptography

Here we show you how to configure the cryptography.

```php
use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer;
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;
use Patchlevel\Hydrator\Metadata\Event\EventMetadataFactory;
use Patchlevel\Hydrator\MetadataHydrator;

$cipherKeyStore = new InMemoryCipherKeyStore();
$cryptographer = PersonalDataPayloadCryptographer::createWithOpenssl($cipherKeyStore);
$hydrator = new MetadataHydrator(cryptographer: $cryptographer);
```

#### Cipher Key Store

The keys must be stored somewhere. For testing purposes, we offer an in-memory implementation.

```php
use Patchlevel\Hydrator\Cryptography\Cipher\CipherKey;
use Patchlevel\Hydrator\Cryptography\Store\InMemoryCipherKeyStore;

$cipherKeyStore = new InMemoryCipherKeyStore();

/** @var CipherKey $cipherKey */
$cipherKeyStore->store('foo-id', $cipherKey);
$cipherKey = $cipherKeyStore->get('foo-id');
$cipherKeyStore->remove('foo-id');
```

Because we don't know where you want to store the keys, we don't offer any other implementations.
You should use a database or a key store for this. To do this, you have to implement the `CipherKeyStore` interface.

#### Remove personal data

To remove personal data, you need only remove the key from the store.

```php
$cipherKeyStore->remove('foo-id');
```
39 changes: 38 additions & 1 deletion baseline.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.22.2@d768d914152dbbf3486c36398802f74e80cfde48">
<files psalm-version="5.23.1@8471a896ccea3526b26d082f4461eeea467f10a4">
<file src="src/Cryptography/Cipher/OpensslCipherKeyFactory.php">
<ArgumentTypeCoercion>
<code><![CDATA[openssl_random_pseudo_bytes($this->ivLength)]]></code>
<code><![CDATA[openssl_random_pseudo_bytes($this->keyLength)]]></code>
</ArgumentTypeCoercion>
<LessSpecificReturnStatement>
<code><![CDATA[openssl_get_cipher_methods(true)]]></code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType>
<code><![CDATA[list<string>]]></code>
</MoreSpecificReturnType>
</file>
<file src="src/Cryptography/PersonalDataPayloadCryptographer.php">
<MixedArgument>
<code><![CDATA[$data[$propertyMetadata->fieldName()]]]></code>
</MixedArgument>
<MixedAssignment>
<code><![CDATA[$data[$propertyMetadata->fieldName()]]]></code>
<code><![CDATA[$data[$propertyMetadata->fieldName()]]]></code>
<code><![CDATA[$data[$propertyMetadata->fieldName()]]]></code>
</MixedAssignment>
</file>
<file src="src/Metadata/AttributeMetadataFactory.php">
<MixedAssignment>
<code><![CDATA[$personalDataFallback]]></code>
</MixedAssignment>
</file>
<file src="src/Metadata/ClassMetadata.php">
<InvalidPropertyAssignmentValue>
<code><![CDATA[new ReflectionClass($data['className'])]]></code>
</InvalidPropertyAssignmentValue>
</file>
<file src="src/Normalizer/ObjectNormalizer.php">
<MixedArgument>
<code><![CDATA[$value]]></code>
Expand All @@ -8,4 +40,9 @@
<code><![CDATA[$value]]></code>
</MixedArgumentTypeCoercion>
</file>
<file src="tests/Unit/Cryptography/Cipher/OpensslCipherTest.php">
<MixedAssignment>
<code><![CDATA[$return]]></code>
</MixedAssignment>
</file>
</files>
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
}
],
"require": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.0"
"php": "~8.1.0 || ~8.2.0 || ~8.3.0",
"ext-openssl": "*"
},
"require-dev": {
"cspray/phinal": "^2.0.0",
Expand Down
20 changes: 20 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#1 \\$key of class Patchlevel\\\\Hydrator\\\\Cryptography\\\\Cipher\\\\CipherKey constructor expects non\\-empty\\-string, string given\\.$#"
count: 1
path: src/Cryptography/Cipher/OpensslCipherKeyFactory.php

-
message: "#^Parameter \\#3 \\$iv of class Patchlevel\\\\Hydrator\\\\Cryptography\\\\Cipher\\\\CipherKey constructor expects non\\-empty\\-string, string given\\.$#"
count: 1
path: src/Cryptography/Cipher/OpensslCipherKeyFactory.php

-
message: "#^Parameter \\#2 \\$data of method Patchlevel\\\\Hydrator\\\\Cryptography\\\\Cipher\\\\Cipher\\:\\:decrypt\\(\\) expects string, mixed given\\.$#"
count: 1
path: src/Cryptography/PersonalDataPayloadCryptographer.php

-
message: "#^Property Patchlevel\\\\Hydrator\\\\Metadata\\\\ClassMetadata\\<T of object\\>\\:\\:\\$reflection \\(ReflectionClass\\<T of object\\>\\) does not accept ReflectionClass\\<object\\>\\.$#"
count: 1
path: src/Metadata/ClassMetadata.php

-
message: "#^Dead catch \\- Patchlevel\\\\Hydrator\\\\CircularReference is never thrown in the try block\\.$#"
count: 1
Expand Down
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\Hydrator\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\Hydrator\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
final class PersonalData
{
public function __construct(
public readonly mixed $fallback = null,
) {
}
}
14 changes: 14 additions & 0 deletions src/Cryptography/Cipher/Cipher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Cryptography\Cipher;

interface Cipher
{
/** @throws EncryptionFailed */
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\Hydrator\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,
) {
}
}
11 changes: 11 additions & 0 deletions src/Cryptography/Cipher/CipherKeyFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Cryptography\Cipher;

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

declare(strict_types=1);

namespace Patchlevel\Hydrator\Cryptography\Cipher;

use RuntimeException;

final class CreateCipherKeyFailed extends RuntimeException
{
public function __construct()
{
parent::__construct('Create cipher key failed.');
}
}
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\Hydrator\Cryptography\Cipher;

use RuntimeException;

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

declare(strict_types=1);

namespace Patchlevel\Hydrator\Cryptography\Cipher;

use RuntimeException;

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

declare(strict_types=1);

namespace Patchlevel\Hydrator\Cryptography\Cipher;

use RuntimeException;

use function sprintf;

final class MethodNotSupported extends RuntimeException
{
public function __construct(string $method)
{
parent::__construct(sprintf('Method %s not supported.', $method));
}
}
Loading

0 comments on commit 0d5518e

Please sign in to comment.