Skip to content
Open
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
24 changes: 6 additions & 18 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
parameters:
ignoreErrors:
-
message: '#^Parameter \#1 \$key of class Patchlevel\\Hydrator\\Cryptography\\Cipher\\CipherKey constructor expects non\-empty\-string, string given\.$#'
identifier: argument.type
message: '#^Method Patchlevel\\Hydrator\\Cryptography\\Cipher\\OpensslCipher\:\:encrypt\(\) should return non\-empty\-string but returns string\.$#'
identifier: return.type
count: 1
path: src/Cryptography/Cipher/OpensslCipherKeyFactory.php
path: src/Cryptography/Cipher/OpensslCipher.php

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

-
message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\Cryptography\\Cipher\\Cipher\:\:decrypt\(\) expects string, mixed given\.$#'
message: '#^Parameter \#3 \$iv of class Patchlevel\\Hydrator\\Cryptography\\Cipher\\CipherKey constructor expects non\-empty\-string, string given\.$#'
identifier: argument.type
count: 1
path: src/Cryptography/SensitiveDataPayloadCryptographer.php
path: src/Cryptography/Cipher/OpensslCipherKeyFactory.php

-
message: '#^Method Patchlevel\\Hydrator\\Guesser\\BuiltInGuesser\:\:guess\(\) has parameter \$type with generic class Symfony\\Component\\TypeInfo\\Type\\ObjectType but does not specify its types\: T$#'
Expand Down Expand Up @@ -120,12 +120,6 @@ parameters:
count: 2
path: tests/Unit/Normalizer/ArrayNormalizerTest.php

-
message: '#^Parameter \#1 \$normalizer of class Patchlevel\\Hydrator\\Normalizer\\ArrayNormalizer constructor expects Patchlevel\\Hydrator\\Normalizer\\Normalizer, PHPUnit\\Framework\\MockObject\\MockObject given\.$#'
identifier: argument.type
count: 1
path: tests/Unit/Normalizer/ArrayNormalizerTest.php

-
message: '#^Cannot cast mixed to int\.$#'
identifier: cast.int
Expand All @@ -137,9 +131,3 @@ parameters:
identifier: cast.string
count: 2
path: tests/Unit/Normalizer/ArrayShapeNormalizerTest.php

-
message: '#^Parameter \#1 \$normalizerMap of class Patchlevel\\Hydrator\\Normalizer\\ArrayShapeNormalizer constructor expects array\<Patchlevel\\Hydrator\\Normalizer\\Normalizer\>, array\<string, PHPUnit\\Framework\\MockObject\\MockObject\> given\.$#'
identifier: argument.type
count: 1
path: tests/Unit/Normalizer/ArrayShapeNormalizerTest.php
4 changes: 4 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ services:
class: Patchlevel\Hydrator\Tests\Architecture\FinalClassesTest
tags:
- phpat.test
-
class: Patchlevel\Hydrator\Tests\Architecture\ExceptionImplementsHydratorExceptionTest
tags:
- phpat.test
2 changes: 1 addition & 1 deletion src/Attribute/SensitiveData.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#[Attribute(Attribute::TARGET_PROPERTY)]
final class SensitiveData
{
/** @var (callable(string, mixed):mixed)|null */
/** @var (callable(string):mixed)|null */
public readonly mixed $fallbackCallable;

public function __construct(
Expand Down
95 changes: 95 additions & 0 deletions src/Cryptography/BaseCryptographer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Cryptography;

use Patchlevel\Hydrator\Cryptography\Cipher\Cipher;
use Patchlevel\Hydrator\Cryptography\Cipher\CipherKey;
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;

use function array_key_exists;
use function is_array;

/**
* @phpstan-type EncryptedDataV1 array{
* __enc: 'v1',
* data: non-empty-string,
* method?: non-empty-string,
* iv?: non-empty-string,
* }
*/
final class BaseCryptographer implements Cryptographer
{
public function __construct(
private readonly Cipher $cipher,
private readonly CipherKeyStore $cipherKeyStore,
private readonly CipherKeyFactory $cipherKeyFactory,
) {
}

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

return [
'__enc' => 'v1',
'data' => $this->cipher->encrypt($cipherKey, $value),
'method' => $cipherKey->method,
'iv' => $cipherKey->iv,
];
}

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

return $this->cipher->decrypt(
new CipherKey(
$cipherKey->key,
$encryptedData['method'] ?? $cipherKey->method,
$encryptedData['iv'] ?? $cipherKey->iv,
),
$encryptedData['data'],
);
}

public function supports(mixed $value): bool
{
return is_array($value) && array_key_exists('__enc', $value) && $value['__enc'] === 'v1';
}

/** @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),
);
}
}
6 changes: 5 additions & 1 deletion src/Cryptography/Cipher/Cipher.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

interface Cipher
{
/** @throws EncryptionFailed */
/**
* @return non-empty-string
*
* @throws EncryptionFailed
*/
public function encrypt(CipherKey $key, mixed $data): string;

/** @throws DecryptionFailed */
Expand Down
3 changes: 2 additions & 1 deletion src/Cryptography/Cipher/CreateCipherKeyFailed.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

namespace Patchlevel\Hydrator\Cryptography\Cipher;

use Patchlevel\Hydrator\HydratorException;
use RuntimeException;

final class CreateCipherKeyFailed extends RuntimeException
final class CreateCipherKeyFailed extends RuntimeException implements HydratorException
{
public function __construct()
{
Expand Down
3 changes: 2 additions & 1 deletion src/Cryptography/Cipher/DecryptionFailed.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

namespace Patchlevel\Hydrator\Cryptography\Cipher;

use Patchlevel\Hydrator\HydratorException;
use RuntimeException;

final class DecryptionFailed extends RuntimeException
final class DecryptionFailed extends RuntimeException implements HydratorException
{
public function __construct()
{
Expand Down
3 changes: 2 additions & 1 deletion src/Cryptography/Cipher/EncryptionFailed.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

namespace Patchlevel\Hydrator\Cryptography\Cipher;

use Patchlevel\Hydrator\HydratorException;
use RuntimeException;

final class EncryptionFailed extends RuntimeException
final class EncryptionFailed extends RuntimeException implements HydratorException
{
public function __construct()
{
Expand Down
3 changes: 2 additions & 1 deletion src/Cryptography/Cipher/MethodNotSupported.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

namespace Patchlevel\Hydrator\Cryptography\Cipher;

use Patchlevel\Hydrator\HydratorException;
use RuntimeException;

use function sprintf;

final class MethodNotSupported extends RuntimeException
final class MethodNotSupported extends RuntimeException implements HydratorException
{
public function __construct(string $method)
{
Expand Down
1 change: 1 addition & 0 deletions src/Cryptography/Cipher/OpensslCipher.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

final class OpensslCipher implements Cipher
{
/** @return non-empty-string */
public function encrypt(CipherKey $key, mixed $data): string
{
$encryptedData = @openssl_encrypt(
Expand Down Expand Up @@ -62,6 +63,6 @@

private function dataDecode(string $data): mixed
{
return json_decode($data, true, 512, JSON_THROW_ON_ERROR);

Check warning on line 66 in src/Cryptography/Cipher/OpensslCipher.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.4, ubuntu-latest)

Escaped Mutant for Mutator "DecrementInteger": @@ @@ private function dataDecode(string $data): mixed { - return json_decode($data, true, 512, JSON_THROW_ON_ERROR); + return json_decode($data, true, 511, JSON_THROW_ON_ERROR); } }

Check warning on line 66 in src/Cryptography/Cipher/OpensslCipher.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.4, ubuntu-latest)

Escaped Mutant for Mutator "IncrementInteger": @@ @@ private function dataDecode(string $data): mixed { - return json_decode($data, true, 512, JSON_THROW_ON_ERROR); + return json_decode($data, true, 513, JSON_THROW_ON_ERROR); } }
}
}
23 changes: 23 additions & 0 deletions src/Cryptography/Cryptographer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Cryptography;

use Patchlevel\Hydrator\Cryptography\Cipher\DecryptionFailed;
use Patchlevel\Hydrator\Cryptography\Cipher\EncryptionFailed;
use Patchlevel\Hydrator\Cryptography\Store\CipherKeyNotExists;

interface Cryptographer
{
/** @throws EncryptionFailed */
public function encrypt(string $subjectId, mixed $value): mixed;

/**
* @throws CipherKeyNotExists
* @throws DecryptionFailed
*/
public function decrypt(string $subjectId, mixed $encryptedData): mixed;

public function supports(mixed $value): bool;
}
2 changes: 1 addition & 1 deletion src/Cryptography/CryptographyExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
final class CryptographyExtension implements Extension
{
public function __construct(
private readonly PayloadCryptographer $cryptography,
private readonly Cryptographer $cryptography,
) {
}

Expand Down
Loading
Loading