Create an encryption service

This commit is contained in:
Jan Goralski 2024-10-11 14:03:09 +02:00
parent 54f85b5ed6
commit bb92fc1370
No known key found for this signature in database
GPG key ID: 95D91BA380F31EDD
6 changed files with 195 additions and 0 deletions

View file

@ -59,6 +59,7 @@
"lexik/jwt-authentication-bundle": "^3.1",
"liip/imagine-bundle": "^2.13",
"pagerfanta/pagerfanta": "^4.0",
"paragonie/halite": "^5.0",
"payum/offline": "^1.7.5",
"payum/payum-bundle": "^2.6",
"php-http/discovery": "^1.20",

View file

@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Component\Payment\Encryption;
use ParagonIE\Halite\Alerts\HaliteAlert;
use ParagonIE\Halite\KeyFactory;
use ParagonIE\Halite\Symmetric\Crypto;
use ParagonIE\Halite\Symmetric\EncryptionKey;
use ParagonIE\HiddenString\HiddenString;
use Sylius\Component\Payment\Encryption\Exception\EncryptionException;
final class Encrypter implements EncrypterInterface
{
private const ENCRYPTION_SUFFIX = '#ENCRYPTED';
private const ENCRYPTION_SUFFIX_LENGTH = 10;
private ?EncryptionKey $key = null;
public function __construct(
#[\SensitiveParameter]
private readonly string $keyPhrase,
#[\SensitiveParameter]
private readonly string $keySalt,
) {
}
public function encrypt(string $data): string
{
try {
return Crypto::encrypt(new HiddenString($data), $this->getKey()) . self::ENCRYPTION_SUFFIX;
} catch (\TypeError|\SodiumException|HaliteAlert $exception) {
throw EncryptionException::cannotEncrypt($exception);
}
}
public function decrypt(string $data): string
{
if (!str_ends_with($data, self::ENCRYPTION_SUFFIX)) {
return $data;
}
try {
$data = substr($data, 0, -self::ENCRYPTION_SUFFIX_LENGTH);
return Crypto::decrypt($data, $this->getKey())->getString();
} catch (\TypeError|\SodiumException|HaliteAlert $exception) {
throw EncryptionException::cannotDecrypt($exception);
}
}
private function getKey(): EncryptionKey
{
if (null === $this->key) {
$this->key = KeyFactory::deriveEncryptionKey(new HiddenString($this->keyPhrase), $this->keySalt);
}
return $this->key;
}
}

View file

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Component\Payment\Encryption;
use Sylius\Component\Payment\Encryption\Exception\EncryptionException;
interface EncrypterInterface
{
/** @throws EncryptionException */
public function encrypt(string $data): string;
/** @throws EncryptionException */
public function decrypt(string $data): string;
}

View file

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Sylius\Component\Payment\Encryption\Exception;
final class EncryptionException extends \RuntimeException
{
public static function cannotEncrypt(\Throwable $previousException): self
{
return new self(
message: 'Cannot encrypt data.',
previous: $previousException,
);
}
public static function cannotDecrypt(\Throwable $previousException): self
{
return new self(
message: 'Cannot decrypt data.',
previous: $previousException,
);
}
}

View file

@ -27,6 +27,7 @@
],
"require": {
"php": "^8.2",
"paragonie/halite": "^5.0",
"sylius/registry": "^1.6",
"sylius/resource": "^1.12",
"symfony/uid": "^6.4 || ^7.1"

View file

@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace spec\Sylius\Component\Payment\Encryption;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Payment\Encryption\EncrypterInterface;
use Sylius\Component\Payment\Encryption\Exception\EncryptionException;
final class EncrypterSpec extends ObjectBehavior
{
function let(): void
{
$this->beConstructedWith('a_very_strong_password', 'very_strong_salt');
}
function it_is_an_encrypter(): void
{
$this->shouldImplement(EncrypterInterface::class);
}
function it_throws_an_exception_if_it_cannot_encrypt(): void
{
$this->beConstructedWith('', '');
$this->shouldThrow(EncryptionException::class)->during('encrypt', ['data']);
}
function it_throws_an_exception_if_it_cannot_decrypt(): void
{
$this->beConstructedWith('', '');
$this->shouldThrow(EncryptionException::class)->during('decrypt', ['data#ENCRYPTED']);
}
function it_encrypts_data(): void
{
$this->encrypt('data')->shouldBeString();
$this->encrypt('data')->shouldNotBe('data');
$this->encrypt('data')->shouldEndWith('#ENCRYPTED');
}
function it_decrypts_data(): void
{
$data = 'data';
$encryptedData = $this->getWrappedObject()->encrypt($data);
$this->decrypt($encryptedData)->shouldNotEndWith('#ENCRYPTED');
$this->decrypt($encryptedData)->shouldBe($data);
}
function it_does_nothing_when_data_is_not_marked_as_encrypted(): void
{
$this->decrypt('data')->shouldBe('data');
}
}