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
75 changes: 75 additions & 0 deletions src/Cryptography/CryptoNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace Patchlevel\Hydrator\Cryptography;

use Closure;
use Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed;
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists;
use Patchlevel\Hydrator\Normalizer\ContextAwareNormalizer;
use Patchlevel\Hydrator\Normalizer\InvalidArgument;
use Patchlevel\Hydrator\Normalizer\Normalizer;

final class CryptoNormalizer implements Normalizer, ContextAwareNormalizer
{
public function __construct(
private readonly Cryptographer $cryptographer,
private readonly string $subjectIdName,
private readonly mixed $fallback = null,
private readonly Normalizer|null $normalizer = null,
) {
}

/**
* @param array<string, mixed> $context
*
* @throws InvalidArgument
*/
public function normalize(mixed $value, array $context = []): mixed
{
if ($this->normalizer !== null) {
$value = $this->normalizer->normalize($value, $context);

Check failure on line 30 in src/Cryptography/CryptoNormalizer.php

View workflow job for this annotation

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

Method Patchlevel\Hydrator\Normalizer\Normalizer::normalize() invoked with 2 parameters, 1 required.
}

if ($value === null) {
return null;
}

$subjectId = $context[SubjectIds::class]->get($this->subjectIdName);

Check failure on line 37 in src/Cryptography/CryptoNormalizer.php

View workflow job for this annotation

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

Cannot call method get() on mixed.

return ['__enc' => $this->cryptographer->encrypt($subjectId, $value)];

Check failure on line 39 in src/Cryptography/CryptoNormalizer.php

View workflow job for this annotation

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

Parameter #1 $subjectId of method Patchlevel\Hydrator\Cryptography\Cryptographer::encrypt() expects string, mixed given.
}

/**
* @param array<string, mixed> $context
*
* @throws InvalidArgument
*/
public function denormalize(mixed $value, array $context = []): mixed
{
if (!is_array($value) || !array_key_exists('__enc', $value)) {
if ($this->normalizer === null) {
return $value;
}

return $this->normalizer->denormalize($value, $context);

Check failure on line 54 in src/Cryptography/CryptoNormalizer.php

View workflow job for this annotation

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

Method Patchlevel\Hydrator\Normalizer\Normalizer::denormalize() invoked with 2 parameters, 1 required.
}

$subjectId = $context[SubjectIds::class]->get($this->subjectIdName);

Check failure on line 57 in src/Cryptography/CryptoNormalizer.php

View workflow job for this annotation

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

Cannot call method get() on mixed.

try {
$data = $this->cryptographer->decrypt($subjectId, $value['__enc']);

Check failure on line 60 in src/Cryptography/CryptoNormalizer.php

View workflow job for this annotation

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

Parameter #2 $value of method Patchlevel\Hydrator\Cryptography\Cryptographer::decrypt() expects string, mixed given.

Check failure on line 60 in src/Cryptography/CryptoNormalizer.php

View workflow job for this annotation

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

Parameter #1 $subjectId of method Patchlevel\Hydrator\Cryptography\Cryptographer::decrypt() expects string, mixed given.
} catch (DecryptionFailed|CipherKeyNotExists) {
if ($this->fallback instanceof Closure) {
return ($this->fallback)($subjectId, $value['__enc']);
}

return $this->fallback;
}

if ($this->normalizer === null) {
return $data;
}

return $this->normalizer->denormalize($data, $context);

Check failure on line 73 in src/Cryptography/CryptoNormalizer.php

View workflow job for this annotation

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

Method Patchlevel\Hydrator\Normalizer\Normalizer::denormalize() invoked with 2 parameters, 1 required.
}
}
62 changes: 62 additions & 0 deletions src/Cryptography/Cryptographer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace Patchlevel\Hydrator\Cryptography;

use Patchlevel\Hydrator\Cryptography\Cipher\Cipher;
use Patchlevel\Hydrator\Cryptography\Cipher\CipherKeyFactory;
use Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed;
use Patchlevel\Hydrator\Cryptography\Cipher\EncryptionFailed;
use Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipher;
use Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipherKeyFactory;
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists;
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore;

final class Cryptographer
{
public function __construct(
private readonly Cipher $cipher,
private readonly CipherKeyStore $cipherKeyStore,
private readonly CipherKeyFactory $cipherKeyFactory,
)
{
}

/**
* @throws EncryptionFailed
*/
public function encrypt(string $subjectId, mixed $value): string
{
try {
$cipherKey = $this->cipherKeyStore->get($subjectId);
} catch (CipherKeyNotExists) {
$cipherKey = ($this->cipherKeyFactory)();
$this->cipherKeyStore->store($subjectId, $cipherKey);
}

return $this->cipher->encrypt($cipherKey, $value);
}

/**
* @throws CipherKeyNotExists
* @throws DecryptionFailed
*/
public function decrypt(string $subjectId, string $value): mixed
{
$cipherKey = $this->cipherKeyStore->get($subjectId);

return $this->cipher->decrypt($cipherKey, $value);
}


/** @param non-empty-string $method */
public static function createWithOpenssl(
CipherKeyStore $cryptoStore,
string $method = OpensslCipherKeyFactory::DEFAULT_METHOD,
): static {
return new self(
new OpensslCipher(),
$cryptoStore,
new OpensslCipherKeyFactory($method),
);
}
}
31 changes: 11 additions & 20 deletions src/Cryptography/CryptographyMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
use Patchlevel\Hydrator\Attribute\SensitiveData;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Metadata\MetadataFactory;
use ReflectionProperty;

use function array_key_exists;

final class CryptographyMetadataFactory implements MetadataFactory
{
public function __construct(
private readonly Cryptographer $cryptographer,
private readonly MetadataFactory $metadataFactory,
) {
}
Expand Down Expand Up @@ -46,17 +46,24 @@ public function metadata(string $class): ClassMetadata
$isSubjectId = true;
}

$sensitiveDataInfo = $this->sensitiveDataInfo($property->reflection);
$attributeReflectionList = $property->reflection->getAttributes(SensitiveData::class);

if (!$sensitiveDataInfo) {
if ($attributeReflectionList === []) {
continue;
}

if ($isSubjectId) {
throw new SubjectIdAndSensitiveDataConflict($metadata->className, $property->propertyName);
}

$property->extras[SensitiveDataInfo::class] = $sensitiveDataInfo;
$attribute = $attributeReflectionList[0]->newInstance();

$property->normalizer = new CryptoNormalizer(
$this->cryptographer,
$attribute->subjectIdName,
$attribute->fallbackCallable !== null ? ($attribute->fallbackCallable)(...) : $attribute->fallback,
$property->normalizer,
);
}

if ($subjectIdMapping !== []) {
Expand All @@ -65,20 +72,4 @@ public function metadata(string $class): ClassMetadata

return $metadata;
}

private function sensitiveDataInfo(ReflectionProperty $reflectionProperty): SensitiveDataInfo|null
{
$attributeReflectionList = $reflectionProperty->getAttributes(SensitiveData::class);

if ($attributeReflectionList === []) {
return null;
}

$attribute = $attributeReflectionList[0]->newInstance();

return new SensitiveDataInfo(
$attribute->subjectIdName,
$attribute->fallbackCallable !== null ? ($attribute->fallbackCallable)(...) : $attribute->fallback,
);
}
}
Loading
Loading