mirror of
https://github.com/jbcr/Sylius.git
synced 2026-03-24 00:42:08 +01:00
Refactor creating encryption key
Hook key file generation to the installation command
This commit is contained in:
2
.env
2
.env
@@ -43,3 +43,5 @@ SYLIUS_MESSENGER_TRANSPORT_PAYMENT_REQUEST_FAILED_DSN=doctrine://default?queue_n
|
||||
###> symfony/mailer ###
|
||||
MAILER_DSN=null://null
|
||||
###< symfony/mailer ###
|
||||
|
||||
SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH=%kernel.project_dir%/config/encryption/dev.key
|
||||
|
||||
@@ -18,3 +18,5 @@ SYLIUS_MESSENGER_TRANSPORT_PAYMENT_REQUEST_FAILED_DSN=sync://
|
||||
###< symfony/messenger ###
|
||||
|
||||
MAILER_DSN=null://null
|
||||
|
||||
SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH=%kernel.project_dir%/config/encryption/test.key
|
||||
|
||||
@@ -19,3 +19,5 @@ SYLIUS_MESSENGER_TRANSPORT_PAYMENT_REQUEST_FAILED_DSN=sync://
|
||||
###< symfony/messenger ###
|
||||
|
||||
MAILER_DSN=null://null
|
||||
|
||||
SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH=%kernel.project_dir%/config/encryption/test.key
|
||||
|
||||
@@ -19,3 +19,5 @@ SYLIUS_MESSENGER_TRANSPORT_PAYMENT_REQUEST_FAILED_DSN=sync://
|
||||
###< symfony/messenger ###
|
||||
|
||||
MAILER_DSN=null://null
|
||||
|
||||
SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH=%kernel.project_dir%/config/encryption/test.key
|
||||
|
||||
0
config/encryption/.gitkeep
Normal file
0
config/encryption/.gitkeep
Normal file
1
config/encryption/test.key
Normal file
1
config/encryption/test.key
Normal file
@@ -0,0 +1 @@
|
||||
31400500d6649581d6ac178bc41c92acc686dd869e6aa8665b4dad27f8921075e8cbf34059793bf9a0c603cd870f0433fb817afdb68bd75445111f27fe36a3252c8bd26fdbd82801568e9c657b022fd39edabff90518a2e04377e4e813bf3bf7d9411e6e
|
||||
@@ -1,7 +1,6 @@
|
||||
parameters:
|
||||
test_default_state_machine_adapter: 'symfony_workflow'
|
||||
test_sylius_state_machine_adapter: '%env(string:default:test_default_state_machine_adapter:TEST_SYLIUS_STATE_MACHINE_ADAPTER)%'
|
||||
env(SYLIUS_PAYMENT_ENCRYPTION_KEY): 3140050018f411b02c666895e0b163aba52854dbe7f10cd65487b5ca8df7e087253d8da7e9e3ca9629e1b91a015ab282075e0c07eed2199a037385e0f578eaf3f44a6d7bbee69aa6a6a02fbd0d2c1f9e9ffebfac0bd048b9acfb151581e88b5d69afe0f0
|
||||
|
||||
sylius_api:
|
||||
enabled: true
|
||||
|
||||
@@ -9,8 +9,6 @@ parameters:
|
||||
database_driver: pdo_sqlite
|
||||
database_path: "%kernel.project_dir%/var/db.sql"
|
||||
kernel.api_bundle_path: '%kernel.project_dir%/../../'
|
||||
env(SYLIUS_PAYMENT_ENCRYPTION_PASSWORD): 'test-password'
|
||||
env(SYLIUS_PAYMENT_ENCRYPTION_SALT): '9cef0eca7b496271306caf3ef94c70be'
|
||||
|
||||
api_platform:
|
||||
enable_swagger_ui: false
|
||||
|
||||
@@ -46,6 +46,10 @@ final class InstallCommand extends Command
|
||||
'command' => 'sylius:install:jwt-setup',
|
||||
'message' => 'Configuring JWT token.',
|
||||
],
|
||||
[
|
||||
'command' => 'sylius:payment:generate-key',
|
||||
'message' => 'Generating payment encryption key.',
|
||||
],
|
||||
[
|
||||
'command' => 'sylius:install:assets',
|
||||
'message' => 'Installing assets.',
|
||||
|
||||
@@ -5,8 +5,6 @@ parameters:
|
||||
locale: en_US
|
||||
database_driver: pdo_sqlite
|
||||
database_path: "%kernel.project_dir%/var/db.sql"
|
||||
env(SYLIUS_PAYMENT_ENCRYPTION_PASSWORD): 'test-password'
|
||||
env(SYLIUS_PAYMENT_ENCRYPTION_SALT): '9cef0eca7b496271306caf3ef94c70be'
|
||||
|
||||
framework:
|
||||
secret: "ch4mb3r0f5ecr3ts"
|
||||
|
||||
@@ -4,33 +4,85 @@ declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\PaymentBundle\Console\Command;
|
||||
|
||||
use ParagonIE\ConstantTime\Hex;
|
||||
use ParagonIE\Halite\Alerts\CannotPerformOperation;
|
||||
use ParagonIE\Halite\Alerts\InvalidKey;
|
||||
use ParagonIE\Halite\KeyFactory;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'sylius:payment:generate-key',
|
||||
description: 'Generate encryption key for Sylius payment encryption.',
|
||||
description: 'Generate a key for Sylius payment encryption.',
|
||||
)]
|
||||
final class GenerateEncryptionKeyCommand extends Command
|
||||
{
|
||||
protected SymfonyStyle $io;
|
||||
|
||||
public function __construct(
|
||||
private readonly Filesystem $filesystem,
|
||||
private readonly string $keyPath,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$output->writeln('Generating encryption key for Sylius payment encryption');
|
||||
$this->io->writeln('Generating encryption key for Sylius payment encryption');
|
||||
|
||||
if (false === $input->getOption('overwrite') && $this->filesystem->exists($this->keyPath)) {
|
||||
$this->io->writeln(sprintf('Key file "%s" already exists.', $this->keyPath));
|
||||
|
||||
$answer = $this->io->confirm('Do you want to overwrite it?', false);
|
||||
if (false === $answer) {
|
||||
$this->io->info('Key generation has been canceled');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$output->writeln('Key: ' . KeyFactory::export(KeyFactory::generateEncryptionKey())->getString());
|
||||
} catch (\TypeError) {
|
||||
$output->writeln('Key could not be generated. Please, make sure that PHP supports libsodium');
|
||||
$generatedKey = KeyFactory::generateEncryptionKey();
|
||||
} catch (CannotPerformOperation|InvalidKey|\TypeError) {
|
||||
$this->io->error('Key could not be generated. Please, make sure that PHP supports libsodium');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$output->writeln('Remember to update your configuration with this key');
|
||||
try {
|
||||
$this->filesystem->mkdir(\dirname($this->keyPath));
|
||||
$this->filesystem->touch($this->keyPath);
|
||||
$saved = KeyFactory::save($generatedKey, $this->keyPath);
|
||||
} catch (IOException) {
|
||||
$saved = false;
|
||||
}
|
||||
|
||||
if (false === $saved) {
|
||||
$this->io->error(sprintf(
|
||||
'Key could not be saved. Please, make sure that the directory "%s" is writable',
|
||||
\dirname($this->keyPath),
|
||||
));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->io->success(sprintf('Key has been generated and saved in "%s"', $this->keyPath));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrites an existing key file');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
imports:
|
||||
- { resource: "@SyliusPaymentBundle/Resources/config/app/messenger.yaml" }
|
||||
|
||||
parameters:
|
||||
env(SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH): '%kernel.project_dir%/config/encryption/key'
|
||||
|
||||
sylius_payment:
|
||||
payment_request:
|
||||
states_to_be_cancelled_when_payment_method_changed:
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
|
||||
<container
|
||||
xmlns="http://symfony.com/schema/dic/services"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"
|
||||
>
|
||||
<services>
|
||||
<defaults public="true" />
|
||||
|
||||
<service id="sylius.console.command.generate_encryption_key" class="Sylius\Bundle\PaymentBundle\Console\Command\GenerateEncryptionKeyCommand">
|
||||
<argument type="service" id="filesystem" />
|
||||
<argument>%env(resolve:SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH)%</argument>
|
||||
<tag name="console.command" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
@@ -18,7 +18,7 @@
|
||||
>
|
||||
<services>
|
||||
<service id="sylius.encrypter" class="Sylius\Component\Payment\Encryption\Encrypter">
|
||||
<argument>%env(SYLIUS_PAYMENT_ENCRYPTION_KEY)%</argument>
|
||||
<argument>%env(resolve:SYLIUS_PAYMENT_ENCRYPTION_KEY_PATH)%</argument>
|
||||
</service>
|
||||
<service id="Sylius\Component\Payment\Encryption\EncrypterInterface" alias="sylius.encrypter" />
|
||||
|
||||
@@ -46,9 +46,5 @@
|
||||
<tag name="doctrine.event_listener" event="postFlush" />
|
||||
<tag name="doctrine.event_listener" event="postLoad" />
|
||||
</service>
|
||||
|
||||
<service id="sylius.console.command.generate_encryption_salt" class="Sylius\Bundle\PaymentBundle\Console\Command\GenerateEncryptionKeyCommand">
|
||||
<tag name="console.command" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
|
||||
@@ -13,30 +13,110 @@ declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Bundle\PaymentBundle\Tests\Console\Command;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Sylius\Bundle\PaymentBundle\Console\Command\GenerateEncryptionKeyCommand;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
final class GenerateEncryptionKeyCommandTest extends TestCase
|
||||
final class GenerateEncryptionKeyCommandTest extends KernelTestCase
|
||||
{
|
||||
private const ENCRYPTION_KEY_PATH = __DIR__ . '/config/test.key';
|
||||
|
||||
private CommandTester $commandTester;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$kernel = static::createKernel();
|
||||
$kernel->boot();
|
||||
|
||||
$this->commandTester = new CommandTester(new GenerateEncryptionKeyCommand());
|
||||
$command = new GenerateEncryptionKeyCommand(new Filesystem(), self::ENCRYPTION_KEY_PATH);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_generates_encryption_salt(): void
|
||||
public function it_generates_and_saves_the_encryption_key_in_path(): void
|
||||
{
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertEquals(Command::SUCCESS, $this->commandTester->getStatusCode());
|
||||
|
||||
$this->assertStringContainsString('Generating encryption key for Sylius payment encryption', $output);
|
||||
$this->assertStringContainsString('Key:', $output);
|
||||
$this->assertStringContainsString('Please, remember to update your configuration with this key', $output);
|
||||
$this->assertStringContainsString('Key has been generated and saved in', $output);
|
||||
$this->assertStringContainsString(self::ENCRYPTION_KEY_PATH, $this->normalizeString($output));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_does_not_overwrite_existing_key_when_it_is_not_requested(): void
|
||||
{
|
||||
$this->commandTester->setInputs(['Do you want to overwrite it?' => 'n']);
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertEquals(Command::SUCCESS, $this->commandTester->getStatusCode());
|
||||
|
||||
$this->assertStringContainsString('Generating encryption key for Sylius payment encryption', $output);
|
||||
$this->assertStringContainsString('Do you want to overwrite it? (yes/no)', $output);
|
||||
$this->assertStringContainsString(
|
||||
$this->normalizeString(sprintf('"%s" already exists', self::ENCRYPTION_KEY_PATH)),
|
||||
$this->normalizeString($output),
|
||||
);
|
||||
$this->assertStringContainsString('[INFO] Key generation has been canceled', $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_overwrites_existing_key_when_requested(): void
|
||||
{
|
||||
$this->commandTester->setInputs(['Do you want to overwrite it?' => 'y']);
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertEquals(Command::SUCCESS, $this->commandTester->getStatusCode());
|
||||
|
||||
$this->assertStringContainsString('Generating encryption key for Sylius payment encryption', $output);
|
||||
$this->assertStringContainsString('Do you want to overwrite it? (yes/no)', $output);
|
||||
$this->assertStringContainsString(
|
||||
$this->normalizeString(sprintf('"%s" already exists', self::ENCRYPTION_KEY_PATH)),
|
||||
$this->normalizeString($output),
|
||||
);
|
||||
$this->assertStringContainsString('Key has been generated and saved in', $output);
|
||||
$this->assertStringContainsString(self::ENCRYPTION_KEY_PATH, $this->normalizeString($output));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_automatically_overwrites_existing_key_when_overwrite_option_is_passed(): void
|
||||
{
|
||||
$this->commandTester->execute(['--overwrite' => true]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertEquals(Command::SUCCESS, $this->commandTester->getStatusCode());
|
||||
|
||||
$this->assertStringContainsString('Generating encryption key for Sylius payment encryption', $output);
|
||||
$this->assertStringContainsString('Key has been generated and saved in', $output);
|
||||
$this->assertStringContainsString(self::ENCRYPTION_KEY_PATH, $this->normalizeString($output));
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass(): void
|
||||
{
|
||||
self::removeKey();
|
||||
}
|
||||
|
||||
private static function removeKey(): void
|
||||
{
|
||||
if (file_exists(self::ENCRYPTION_KEY_PATH)) {
|
||||
unlink(self::ENCRYPTION_KEY_PATH);
|
||||
rmdir(dirname(self::ENCRYPTION_KEY_PATH));
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeString(string $string): string
|
||||
{
|
||||
return preg_replace('/\s+/', '', $string);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Sylius\Component\Payment\Encryption;
|
||||
|
||||
use ParagonIE\Halite\Alerts\CannotPerformOperation;
|
||||
use ParagonIE\Halite\Alerts\HaliteAlert;
|
||||
use ParagonIE\Halite\Alerts\InvalidKey;
|
||||
use ParagonIE\Halite\KeyFactory;
|
||||
use ParagonIE\Halite\Symmetric\Crypto;
|
||||
use ParagonIE\Halite\Symmetric\EncryptionKey;
|
||||
@@ -30,7 +32,7 @@ final class Encrypter implements EncrypterInterface
|
||||
private ?EncryptionKey $key = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $encryptionKey,
|
||||
private readonly string $encryptionKeyPath,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -61,7 +63,11 @@ final class Encrypter implements EncrypterInterface
|
||||
private function getKey(): EncryptionKey
|
||||
{
|
||||
if (null === $this->key) {
|
||||
$this->key = KeyFactory::importEncryptionKey(new HiddenString($this->encryptionKey));
|
||||
try {
|
||||
$this->key = KeyFactory::loadEncryptionKey($this->encryptionKeyPath);
|
||||
} catch (CannotPerformOperation|InvalidKey $exception) {
|
||||
throw EncryptionException::invalidKey($exception);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->key;
|
||||
|
||||
@@ -31,4 +31,12 @@ final class EncryptionException extends \RuntimeException
|
||||
previous: $previousException,
|
||||
);
|
||||
}
|
||||
|
||||
public static function invalidKey(\Throwable $previousException): self
|
||||
{
|
||||
return new self(
|
||||
message: 'Invalid encryption key.',
|
||||
previous: $previousException,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ final class EncrypterSpec extends ObjectBehavior
|
||||
{
|
||||
function let(): void
|
||||
{
|
||||
$this->beConstructedWith('a_very_strong_password', '6081e27f4be703ebe4626fb40c40cb2c');
|
||||
$this->beConstructedWith(__DIR__ . '/fixtures/encryption_key');
|
||||
}
|
||||
|
||||
function it_is_an_encrypter(): void
|
||||
@@ -31,13 +31,13 @@ final class EncrypterSpec extends ObjectBehavior
|
||||
|
||||
function it_throws_an_exception_if_it_cannot_encrypt(): void
|
||||
{
|
||||
$this->beConstructedWith('', '');
|
||||
$this->beConstructedWith('');
|
||||
$this->shouldThrow(EncryptionException::class)->during('encrypt', ['data']);
|
||||
}
|
||||
|
||||
function it_throws_an_exception_if_it_cannot_decrypt(): void
|
||||
{
|
||||
$this->beConstructedWith('', '');
|
||||
$this->beConstructedWith('');
|
||||
$this->shouldThrow(EncryptionException::class)->during('decrypt', ['data#ENCRYPTED']);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
31400500d37bed69c4fc80633efe2724978b6304197ba4bb15b895a36cc9ef2c833e0ca107738307224758566d75e5f3bf023329caaf360793f91f376ca5d0ac6bbe937e024a8b328ac4fd79649213537f7e377f6da41f10db8f8d7d5c2f52e80412e9f3
|
||||
Reference in New Issue
Block a user