Add secrets management
This commit is contained in:
parent
8c8f62390a
commit
02b5d740e5
@ -17,6 +17,7 @@ CHANGELOG
|
||||
* Added new `error_controller` configuration to handle system exceptions
|
||||
* Added sort option for `translation:update` command.
|
||||
* [BC Break] The `framework.messenger.routing.senders` config key is not deep merged anymore.
|
||||
* Added secrets management.
|
||||
|
||||
4.3.0
|
||||
-----
|
||||
|
@ -1,27 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Command;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\SecretStorageInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Exception\EncryptionKeyNotFoundException;
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Storage\MutableSecretStorageInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* @author Tobias Schultze <http://tobion.de>
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
final class SecretsAddCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'secrets:add';
|
||||
|
||||
/**
|
||||
* @var SecretStorageInterface
|
||||
*/
|
||||
private $secretStorage;
|
||||
private $secretsStorage;
|
||||
|
||||
public function __construct(SecretStorageInterface $secretStorage)
|
||||
public function __construct(MutableSecretStorageInterface $secretsStorage)
|
||||
{
|
||||
$this->secretStorage = $secretStorage;
|
||||
$this->secretsStorage = $secretsStorage;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
@ -29,40 +39,32 @@ final class SecretsAddCommand extends Command
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setDescription('Adds a secret with the key.')
|
||||
->addArgument(
|
||||
'key',
|
||||
InputArgument::REQUIRED
|
||||
)
|
||||
->addArgument(
|
||||
'secret',
|
||||
InputArgument::REQUIRED
|
||||
->setDefinition([
|
||||
new InputArgument('name', InputArgument::REQUIRED, 'The name of the secret'),
|
||||
])
|
||||
->setDescription('Adds a secret in the storage.')
|
||||
->setHelp(<<<'EOF'
|
||||
The <info>%command.name%</info> command stores a secret.
|
||||
|
||||
%command.full_name% <name>
|
||||
EOF
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$key = $input->getArgument('key');
|
||||
$secret = $input->getArgument('secret');
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$this->secretStorage->putSecret($key, $secret);
|
||||
}
|
||||
$name = $input->getArgument('name');
|
||||
$secret = $io->askHidden('Value of the secret');
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
try {
|
||||
$this->secretsStorage->setSecret($name, $secret);
|
||||
} catch (EncryptionKeyNotFoundException $e) {
|
||||
throw new \LogicException(sprintf('No encryption keys found. You should call the "%s" command.', SecretsGenerateKeyCommand::getDefaultName()));
|
||||
}
|
||||
|
||||
$question = new Question('Key of the secret: ', $input->getArgument('key'));
|
||||
|
||||
$key = $helper->ask($input, $output, $question);
|
||||
$input->setArgument('key', $key);
|
||||
|
||||
$question = new Question('Plaintext secret value: ', $input->getArgument('secret'));
|
||||
$question->setHidden(true);
|
||||
|
||||
$secret = $helper->ask($input, $output, $question);
|
||||
$input->setArgument('secret', $secret);
|
||||
$io->success('Secret was successfully stored.');
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +1,97 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Command;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Exception\EncryptionKeyNotFoundException;
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Encoder\EncoderInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Storage\MutableSecretStorageInterface;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @author Tobias Schultze <http://tobion.de>
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
final class SecretsGenerateKeyCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'secrets:generate-key';
|
||||
private $secretsStorage;
|
||||
private $encoder;
|
||||
|
||||
public function __construct(EncoderInterface $encoder, MutableSecretStorageInterface $secretsStorage)
|
||||
{
|
||||
$this->secretsStorage = $secretsStorage;
|
||||
$this->encoder = $encoder;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setDescription('Prints a randomly generated encryption key.')
|
||||
->setDefinition([
|
||||
new InputOption('rekey', 'r', InputOption::VALUE_NONE, 'Re-encrypt previous secret with the new key.'),
|
||||
])
|
||||
->setDescription('Generates a new encryption key.')
|
||||
->setHelp(<<<'EOF'
|
||||
The <info>%command.name%</info> command generates a new encryption key.
|
||||
|
||||
%command.full_name%
|
||||
|
||||
If a previous encryption key already exists, the command must be called with
|
||||
the <info>--rekey</info> option in order to override that key and re-encrypt
|
||||
previous secrets.
|
||||
|
||||
%command.full_name% --rekey
|
||||
EOF
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$encryptionKey = sodium_crypto_stream_keygen();
|
||||
$rekey = $input->getOption('rekey');
|
||||
|
||||
$output->write($encryptionKey, false, OutputInterface::OUTPUT_RAW);
|
||||
$previousSecrets = [];
|
||||
try {
|
||||
foreach ($this->secretsStorage->listSecrets(true) as $name => $decryptedSecret) {
|
||||
$previousSecrets[$name] = $decryptedSecret;
|
||||
}
|
||||
} catch (EncryptionKeyNotFoundException $e) {
|
||||
if (!$rekey) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
sodium_memzero($encryptionKey);
|
||||
$keys = $this->encoder->generateKeys($rekey);
|
||||
foreach ($previousSecrets as $name => $decryptedSecret) {
|
||||
$this->secretsStorage->setSecret($name, $decryptedSecret);
|
||||
}
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
switch (\count($keys)) {
|
||||
case 0:
|
||||
$io->success('Keys have been generated.');
|
||||
break;
|
||||
case 1:
|
||||
$io->success(sprintf('A key has been generated in "%s".', $keys[0]));
|
||||
$io->caution('DO NOT COMMIT that file!');
|
||||
break;
|
||||
default:
|
||||
$io->success(sprintf("Keys have been generated in :\n -%s", implode("\n -", $keys)));
|
||||
$io->caution('DO NOT COMMIT those files!');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Command;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\SecretStorageInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Exception\EncryptionKeyNotFoundException;
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Storage\SecretStorageInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* @author Tobias Schultze <http://tobion.de>
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
final class SecretsListCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'secrets:list';
|
||||
protected static $defaultName = 'debug:secrets';
|
||||
|
||||
/**
|
||||
* @var SecretStorageInterface
|
||||
*/
|
||||
private $secretStorage;
|
||||
|
||||
public function __construct(SecretStorageInterface $secretStorage)
|
||||
@ -27,19 +39,54 @@ final class SecretsListCommand extends Command
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setDefinition([
|
||||
new InputOption('reveal', 'r', InputOption::VALUE_NONE, 'Display decrypted values alongside names'),
|
||||
])
|
||||
->setDescription('Lists all secrets.')
|
||||
->setHelp(<<<'EOF'
|
||||
The <info>%command.name%</info> command list all stored secrets.
|
||||
|
||||
%command.full_name%
|
||||
|
||||
When the the option <info>--reveal</info> is provided, the decrypted secrets are also displayed.
|
||||
|
||||
%command.full_name% --reveal
|
||||
EOF
|
||||
)
|
||||
;
|
||||
|
||||
$this
|
||||
->setDescription('Lists all secrets.')
|
||||
->addOption('reveal', 'r', InputOption::VALUE_NONE, 'Display decrypted values alongside names');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$table = new Table($output);
|
||||
$table->setHeaders(['key', 'plaintext secret']);
|
||||
$reveal = $input->getOption('reveal');
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
foreach ($this->secretStorage->listSecrets() as $key => $secret) {
|
||||
$table->addRow([$key, $secret]);
|
||||
try {
|
||||
$secrets = $this->secretStorage->listSecrets($reveal);
|
||||
} catch (EncryptionKeyNotFoundException $e) {
|
||||
throw new \LogicException(sprintf('Unable to decrypt secrets, the encryption key "%s" is missing.', $e->getKeyLocation()));
|
||||
}
|
||||
|
||||
$table->render();
|
||||
if ($reveal) {
|
||||
$rows = [];
|
||||
foreach ($secrets as $name => $value) {
|
||||
$rows[] = [$name, $value];
|
||||
}
|
||||
$io->table(['name', 'secret'], $rows);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($secrets as $name => $_) {
|
||||
$rows[] = [$name];
|
||||
}
|
||||
|
||||
$io->comment(sprintf('To reveal the values of the secrets use <info>php %s %s --reveal</info>', $_SERVER['PHP_SELF'], $this->getName()));
|
||||
$io->table(['name'], $rows);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Command;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Storage\MutableSecretStorageInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
final class SecretsRemoveCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'secrets:remove';
|
||||
|
||||
private $secretsStorage;
|
||||
|
||||
public function __construct(MutableSecretStorageInterface $secretsStorage)
|
||||
{
|
||||
$this->secretsStorage = $secretsStorage;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setDefinition([
|
||||
new InputArgument('name', InputArgument::REQUIRED, 'The name of the secret'),
|
||||
])
|
||||
->setDescription('Removes a secret from the storage.')
|
||||
->setHelp(<<<'EOF'
|
||||
The <info>%command.name%</info> command remove a secret.
|
||||
|
||||
%command.full_name% <name>
|
||||
EOF
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$this->secretsStorage->removeSecret($input->getArgument('name'));
|
||||
|
||||
$io->success('Secret was successfully removed.');
|
||||
}
|
||||
}
|
@ -127,11 +127,8 @@ class Configuration implements ConfigurationInterface
|
||||
->arrayNode('secrets')
|
||||
->canBeEnabled()
|
||||
->children()
|
||||
->scalarNode('encrypted_secrets_dir')->end()
|
||||
->scalarNode('encryption_key')->end()
|
||||
//->scalarNode('public_key')->end()
|
||||
//->scalarNode('private_key')->end()
|
||||
->scalarNode('decrypted_secrets_cache')->end()
|
||||
->scalarNode('encrypted_secrets_dir')->defaultValue('%kernel.project_dir%/config/secrets/%kernel.environment%')->cannotBeEmpty()->end()
|
||||
->scalarNode('encryption_key')->defaultValue('%kernel.project_dir%/config/secrets/encryption_%kernel.environment%.key')->cannotBeEmpty()->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
|
@ -25,6 +25,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader;
|
||||
use Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher;
|
||||
use Symfony\Bundle\FrameworkBundle\Routing\RouteLoaderInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Storage\SecretStorageInterface;
|
||||
use Symfony\Bundle\FullStack;
|
||||
use Symfony\Component\Asset\PackageInterface;
|
||||
use Symfony\Component\BrowserKit\AbstractBrowser;
|
||||
@ -1446,14 +1447,22 @@ class FrameworkExtension extends Extension
|
||||
{
|
||||
if (!$this->isConfigEnabled($container, $config)) {
|
||||
$container->removeDefinition('console.command.secrets_add');
|
||||
$container->removeDefinition('console.command.secrets_list');
|
||||
$container->removeDefinition('console.command.secrets_remove');
|
||||
$container->removeDefinition('console.command.secrets_generate_key');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$loader->load('secrets.xml');
|
||||
|
||||
$container->setAlias(SecretStorageInterface::class, new Alias('secrets.storage.cache', false));
|
||||
|
||||
$container->getDefinition('secrets.storage.files')->replaceArgument(0, $config['encrypted_secrets_dir']);
|
||||
$container->getDefinition('secrets.storage.files')->replaceArgument(1, $config['encryption_key']);
|
||||
$container->getDefinition('secrets.encoder.sodium')->replaceArgument(0, $config['encryption_key']);
|
||||
|
||||
$container->registerForAutoconfiguration(SecretStorageInterface::class)
|
||||
->addTag('secret_storage');
|
||||
}
|
||||
|
||||
private function registerSecurityCsrfConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
|
||||
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Exception;
|
||||
|
||||
class EncryptionKeyNotFoundException extends \RuntimeException
|
||||
{
|
||||
private $keyLocation;
|
||||
|
||||
public function __construct(string $keyLocation)
|
||||
{
|
||||
$this->keyLocation = $keyLocation;
|
||||
parent::__construct(sprintf('Encryption key not found in "%s".', $keyLocation));
|
||||
}
|
||||
|
||||
public function getKeyLocation(): string
|
||||
{
|
||||
return $this->keyLocation;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Exception;
|
||||
|
||||
class SecretNotFoundException extends \RuntimeException
|
||||
{
|
||||
private $name;
|
||||
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
parent::__construct(sprintf('The secret "%s" does not exist.', $name));
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
@ -202,17 +202,24 @@
|
||||
</service>
|
||||
|
||||
<service id="console.command.secrets_add" class="Symfony\Bundle\FrameworkBundle\Command\SecretsAddCommand">
|
||||
<argument type="service" id="secrets.storage.cache" />
|
||||
<argument type="service" id="secrets.storage.files" />
|
||||
<tag name="console.command" command="secrets:add" />
|
||||
</service>
|
||||
|
||||
<service id="console.command.secrets_remove" class="Symfony\Bundle\FrameworkBundle\Command\SecretsRemoveCommand">
|
||||
<argument type="service" id="secrets.storage.files" />
|
||||
<tag name="console.command" command="secrets:remove" />
|
||||
</service>
|
||||
|
||||
<service id="console.command.secrets_generate_key" class="Symfony\Bundle\FrameworkBundle\Command\SecretsGenerateKeyCommand">
|
||||
<argument type="service" id="secrets.encoder.sodium"/>
|
||||
<argument type="service" id="secrets.storage.files"/>
|
||||
<tag name="console.command" command="secrets:generate-key" />
|
||||
</service>
|
||||
|
||||
<service id="console.command.secrets_list" class="Symfony\Bundle\FrameworkBundle\Command\SecretsListCommand">
|
||||
<argument type="service" id="secrets.storage.cache" />
|
||||
<tag name="console.command" command="secrets:list" />
|
||||
<tag name="console.command" command="debug:secrets" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
||||
|
@ -5,13 +5,22 @@
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||
|
||||
<services>
|
||||
<service id="secrets.storage.files" class="Symfony\Bundle\FrameworkBundle\Secret\FilesSecretStorage">
|
||||
<argument />
|
||||
<service id="secrets.encoder.sodium" class="Symfony\Bundle\FrameworkBundle\Secret\Encoder\SodiumEncoder">
|
||||
<argument />
|
||||
</service>
|
||||
|
||||
<service id="secrets.storage.cache" class="Symfony\Bundle\FrameworkBundle\Secret\CachedSecretStorage">
|
||||
<argument type="service" id="secrets.storage.files" />
|
||||
<service id="secrets.storage.files" class="Symfony\Bundle\FrameworkBundle\Secret\Storage\FilesSecretStorage">
|
||||
<argument />
|
||||
<argument type="service" id="secrets.encoder.sodium"/>
|
||||
<tag name="secret_storage" priority="1000"/>
|
||||
</service>
|
||||
|
||||
<service id="secrets.storage.chain" class="Symfony\Bundle\FrameworkBundle\Secret\Storage\ChainSecretStorage">
|
||||
<argument type="tagged_iterator" tag="secret_storage"/>
|
||||
</service>
|
||||
|
||||
<service id="secrets.storage.cache" class="Symfony\Bundle\FrameworkBundle\Secret\Storage\CachedSecretStorage">
|
||||
<argument type="service" id="secrets.storage.chain" />
|
||||
<argument type="service" id="cache.system" />
|
||||
</service>
|
||||
|
||||
|
@ -1,78 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Secret;
|
||||
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
class CachedSecretStorage implements SecretStorageInterface
|
||||
{
|
||||
/**
|
||||
* @var SecretStorageInterface
|
||||
*/
|
||||
private $decoratedStorage;
|
||||
/**
|
||||
* @var CacheItemPoolInterface
|
||||
*/
|
||||
private $cache;
|
||||
|
||||
public function __construct(SecretStorageInterface $decoratedStorage, CacheItemPoolInterface $cache)
|
||||
{
|
||||
$this->decoratedStorage = $decoratedStorage;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
public function getSecret(string $key): string
|
||||
{
|
||||
$cacheItem = $this->cache->getItem('secrets.php');
|
||||
|
||||
if ($cacheItem->isHit()) {
|
||||
$secrets = $cacheItem->get();
|
||||
if (isset($secrets[$key])) {
|
||||
return $secrets[$key];
|
||||
}
|
||||
}
|
||||
|
||||
$this->regenerateCache($cacheItem);
|
||||
|
||||
return $this->decoratedStorage->getSecret($key);
|
||||
}
|
||||
|
||||
public function putSecret(string $key, string $secret): void
|
||||
{
|
||||
$this->decoratedStorage->putSecret($key, $secret);
|
||||
$this->regenerateCache();
|
||||
}
|
||||
|
||||
public function deleteSecret(string $key): void
|
||||
{
|
||||
$this->decoratedStorage->deleteSecret($key);
|
||||
$this->regenerateCache();
|
||||
}
|
||||
|
||||
public function listSecrets(): iterable
|
||||
{
|
||||
$cacheItem = $this->cache->getItem('secrets.php');
|
||||
|
||||
if ($cacheItem->isHit()) {
|
||||
return $cacheItem->get();
|
||||
}
|
||||
|
||||
return $this->regenerateCache($cacheItem);
|
||||
}
|
||||
|
||||
private function regenerateCache(?CacheItemInterface $cacheItem = null): array
|
||||
{
|
||||
$cacheItem = $cacheItem ?? $this->cache->getItem('secrets.php');
|
||||
|
||||
$secrets = [];
|
||||
foreach ($this->decoratedStorage->listSecrets() as $key => $secret) {
|
||||
$secrets[$key] = $secret;
|
||||
}
|
||||
|
||||
$cacheItem->set($secrets);
|
||||
$this->cache->save($cacheItem);
|
||||
|
||||
return $secrets;
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Secret\Encoder;
|
||||
|
||||
/**
|
||||
* EncoderInterface defines an interface to encrypt and decrypt secrets.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface EncoderInterface
|
||||
{
|
||||
/**
|
||||
* Generate the keys and material necessary for its operation.
|
||||
*
|
||||
* @param bool $override Override previous keys if already exists
|
||||
*
|
||||
* @return string[] List of resources created
|
||||
*/
|
||||
public function generateKeys(bool $override = false): array;
|
||||
|
||||
/**
|
||||
* Encrypt a secret.
|
||||
*/
|
||||
public function encrypt(string $secret): string;
|
||||
|
||||
/**
|
||||
* Decrypt a secret.
|
||||
*/
|
||||
public function decrypt(string $encryptedSecret): string;
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Secret\Encoder;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Exception\EncryptionKeyNotFoundException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
/**
|
||||
* @author Tobias Schultze <http://tobion.de>
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class SodiumEncoder implements EncoderInterface
|
||||
{
|
||||
private $encryptionKey;
|
||||
private $encryptionKeyPath;
|
||||
|
||||
public function __construct(string $encryptionKeyPath)
|
||||
{
|
||||
if (!\function_exists('\sodium_crypto_stream_xor')) {
|
||||
throw new \RuntimeException('The "sodium" PHP extension is not loaded.');
|
||||
}
|
||||
|
||||
$this->encryptionKeyPath = $encryptionKeyPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function generateKeys(bool $override = false): array
|
||||
{
|
||||
if (!$override && file_exists($this->encryptionKeyPath)) {
|
||||
throw new \LogicException(sprintf('A key already exists in "%s".', $this->encryptionKeyPath));
|
||||
}
|
||||
|
||||
$this->encryptionKey = null;
|
||||
|
||||
$encryptionKey = sodium_crypto_stream_keygen();
|
||||
(new Filesystem())->dumpFile($this->encryptionKeyPath, $encryptionKey);
|
||||
sodium_memzero($encryptionKey);
|
||||
|
||||
return [$this->encryptionKeyPath];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function encrypt(string $secret): string
|
||||
{
|
||||
$nonce = random_bytes(\SODIUM_CRYPTO_STREAM_NONCEBYTES);
|
||||
|
||||
$key = $this->getKey();
|
||||
$encryptedSecret = sodium_crypto_stream_xor($secret, $nonce, $key);
|
||||
sodium_memzero($secret);
|
||||
sodium_memzero($key);
|
||||
|
||||
return $this->encode($nonce, $encryptedSecret);
|
||||
}
|
||||
|
||||
public function decrypt(string $encryptedSecret): string
|
||||
{
|
||||
[$nonce, $encryptedSecret] = $this->decode($encryptedSecret);
|
||||
|
||||
$key = $this->getKey();
|
||||
$secret = sodium_crypto_stream_xor($encryptedSecret, $nonce, $key);
|
||||
sodium_memzero($key);
|
||||
|
||||
return $secret;
|
||||
}
|
||||
|
||||
private function getKey(): string
|
||||
{
|
||||
if (isset($this->encryptionKey)) {
|
||||
return $this->encryptionKey;
|
||||
}
|
||||
if (!is_file($this->encryptionKeyPath)) {
|
||||
throw new EncryptionKeyNotFoundException($this->encryptionKeyPath);
|
||||
}
|
||||
|
||||
return $this->encryptionKey = file_get_contents($this->encryptionKeyPath);
|
||||
}
|
||||
|
||||
private function encode(string $nonce, string $encryptedSecret): string
|
||||
{
|
||||
return $nonce.$encryptedSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array [$nonce, $encryptedSecret]
|
||||
*/
|
||||
private function decode(string $message): array
|
||||
{
|
||||
if (\strlen($message) < \SODIUM_CRYPTO_STREAM_NONCEBYTES) {
|
||||
throw new \UnexpectedValueException(sprintf('Invalid encrypted secret, message should be at least %s chars long.', \SODIUM_CRYPTO_STREAM_NONCEBYTES));
|
||||
}
|
||||
|
||||
$nonce = substr($message, 0, \SODIUM_CRYPTO_STREAM_NONCEBYTES);
|
||||
$encryptedSecret = substr($message, \SODIUM_CRYPTO_STREAM_NONCEBYTES);
|
||||
|
||||
return [$nonce, $encryptedSecret];
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Secret;
|
||||
|
||||
class EncryptedMessage
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $ciphertext;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $nonce;
|
||||
|
||||
public function __construct(string $ciphertext, string $nonce)
|
||||
{
|
||||
$this->ciphertext = $ciphertext;
|
||||
$this->nonce = $nonce;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->nonce.$this->ciphertext;
|
||||
}
|
||||
|
||||
public function getCiphertext(): string
|
||||
{
|
||||
return $this->ciphertext;
|
||||
}
|
||||
|
||||
public function getNonce(): string
|
||||
{
|
||||
return $this->nonce;
|
||||
}
|
||||
|
||||
public static function createFromString(string $message): self
|
||||
{
|
||||
if (\strlen($message) < SODIUM_CRYPTO_STREAM_NONCEBYTES) {
|
||||
throw new \RuntimeException('Invalid ciphertext. Message is too short.');
|
||||
}
|
||||
|
||||
$nonce = substr($message, 0, SODIUM_CRYPTO_STREAM_NONCEBYTES);
|
||||
$ciphertext = substr($message, SODIUM_CRYPTO_STREAM_NONCEBYTES);
|
||||
|
||||
return new self($ciphertext, $nonce);
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Secret;
|
||||
|
||||
class FilesSecretStorage implements SecretStorageInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $secretsFolder;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $encryptionKey;
|
||||
|
||||
public function __construct(string $secretsFolder, string $encryptionKey)
|
||||
{
|
||||
$this->secretsFolder = $secretsFolder;
|
||||
$this->encryptionKey = $encryptionKey;
|
||||
}
|
||||
|
||||
public function getSecret(string $key): string
|
||||
{
|
||||
return $this->decryptFile($this->getFilePath($key));
|
||||
}
|
||||
|
||||
public function putSecret(string $key, string $secret): void
|
||||
{
|
||||
$nonce = random_bytes(SODIUM_CRYPTO_STREAM_NONCEBYTES);
|
||||
$ciphertext = sodium_crypto_stream_xor($secret, $nonce, $this->encryptionKey);
|
||||
|
||||
sodium_memzero($secret);
|
||||
|
||||
$message = new EncryptedMessage($ciphertext, $nonce);
|
||||
|
||||
file_put_contents($this->getFilePath($key), (string) $message);
|
||||
}
|
||||
|
||||
public function deleteSecret(string $key): void
|
||||
{
|
||||
unlink($this->getFilePath($key));
|
||||
}
|
||||
|
||||
public function listSecrets(): iterable
|
||||
{
|
||||
foreach (scandir($this->secretsFolder) as $fileName) {
|
||||
if ('.' === $fileName || '..' === $fileName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = basename($fileName, '.bin');
|
||||
yield $key => $this->getSecret($key);
|
||||
}
|
||||
}
|
||||
|
||||
private function decryptFile(string $filePath): string
|
||||
{
|
||||
$encrypted = file_get_contents($filePath);
|
||||
|
||||
$message = EncryptedMessage::createFromString($encrypted);
|
||||
|
||||
return sodium_crypto_stream_xor($message->getCiphertext(), $message->getNonce(), $this->encryptionKey);
|
||||
}
|
||||
|
||||
private function getFilePath(string $key): string
|
||||
{
|
||||
return $this->secretsFolder.$key.'.bin';
|
||||
}
|
||||
}
|
@ -11,8 +11,14 @@
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Secret;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Exception\SecretNotFoundException;
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Storage\SecretStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
|
||||
use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException;
|
||||
|
||||
/**
|
||||
* @author Tobias Schultze <http://tobion.de>
|
||||
*/
|
||||
class SecretEnvVarProcessor implements EnvVarProcessorInterface
|
||||
{
|
||||
private $secretStorage;
|
||||
@ -37,6 +43,10 @@ class SecretEnvVarProcessor implements EnvVarProcessorInterface
|
||||
*/
|
||||
public function getEnv($prefix, $name, \Closure $getEnv)
|
||||
{
|
||||
return $this->secretStorage->getSecret($name);
|
||||
try {
|
||||
return $this->secretStorage->getSecret($name);
|
||||
} catch (SecretNotFoundException $e) {
|
||||
throw new EnvNotFoundException($e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Secret;
|
||||
|
||||
interface SecretStorageInterface
|
||||
{
|
||||
public function getSecret(string $key): string;
|
||||
|
||||
public function putSecret(string $key, string $secret): void;
|
||||
|
||||
public function deleteSecret(string $key): void;
|
||||
|
||||
public function listSecrets(): iterable;
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Secret\Storage;
|
||||
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
/**
|
||||
* @author Tobias Schultze <http://tobion.de>
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CachedSecretStorage implements SecretStorageInterface
|
||||
{
|
||||
private $decoratedStorage;
|
||||
private $cache;
|
||||
|
||||
public function __construct(SecretStorageInterface $decoratedStorage, CacheInterface $cache)
|
||||
{
|
||||
$this->decoratedStorage = $decoratedStorage;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSecret(string $name): string
|
||||
{
|
||||
return $this->cache->get(md5(__CLASS__.$name), function () use ($name): string {
|
||||
return $this->decoratedStorage->getSecret($name);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function listSecrets(bool $reveal = false): iterable
|
||||
{
|
||||
return $this->decoratedStorage->listSecrets($reveal);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Secret\Storage;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Exception\SecretNotFoundException;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class ChainSecretStorage implements SecretStorageInterface
|
||||
{
|
||||
private $secretStorages;
|
||||
|
||||
/**
|
||||
* @param SecretStorageInterface[] $secretStorages
|
||||
*/
|
||||
public function __construct(iterable $secretStorages = [])
|
||||
{
|
||||
$this->secretStorages = $secretStorages;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSecret(string $name): string
|
||||
{
|
||||
foreach ($this->secretStorages as $secretStorage) {
|
||||
try {
|
||||
return $secretStorage->getSecret($name);
|
||||
} catch (SecretNotFoundException $e) {
|
||||
// ignore exception, to try the next storage
|
||||
}
|
||||
}
|
||||
|
||||
throw new SecretNotFoundException($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function listSecrets(bool $reveal = false): iterable
|
||||
{
|
||||
foreach ($this->secretStorages as $secretStorage) {
|
||||
yield from $secretStorage->listSecrets($reveal);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Secret\Storage;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Exception\SecretNotFoundException;
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Encoder\EncoderInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
/**
|
||||
* @author Tobias Schultze <http://tobion.de>
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class FilesSecretStorage implements MutableSecretStorageInterface
|
||||
{
|
||||
private const FILE_SUFFIX = '.bin';
|
||||
|
||||
private $secretsFolder;
|
||||
private $encoder;
|
||||
private $filesystem;
|
||||
|
||||
public function __construct(string $secretsFolder, EncoderInterface $encoder)
|
||||
{
|
||||
$this->secretsFolder = rtrim($secretsFolder, '\\/');
|
||||
$this->encoder = $encoder;
|
||||
$this->filesystem = new Filesystem();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function listSecrets(bool $reveal = false): iterable
|
||||
{
|
||||
if (!$this->filesystem->exists($this->secretsFolder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ((new Finder())->in($this->secretsFolder)->depth(0)->name('*'.self::FILE_SUFFIX)->files() as $file) {
|
||||
$name = $file->getBasename(self::FILE_SUFFIX);
|
||||
yield $name => $reveal ? $this->getSecret($name) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSecret(string $name): string
|
||||
{
|
||||
$filePath = $this->getFilePath($name);
|
||||
|
||||
if (!is_file($filePath) || false === $content = file_get_contents($filePath)) {
|
||||
throw new SecretNotFoundException($name);
|
||||
}
|
||||
|
||||
return $this->encoder->decrypt($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSecret(string $name, string $secret): void
|
||||
{
|
||||
$this->filesystem->dumpFile($this->getFilePath($name), $this->encoder->encrypt($secret));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeSecret(string $name): void
|
||||
{
|
||||
$filePath = $this->getFilePath($name);
|
||||
|
||||
if (!is_file($filePath)) {
|
||||
throw new SecretNotFoundException($name);
|
||||
}
|
||||
|
||||
$this->filesystem->remove($this->getFilePath($name));
|
||||
}
|
||||
|
||||
private function getFilePath(string $name): string
|
||||
{
|
||||
if (!preg_match('/^[\w\-]++$/', $name)) {
|
||||
throw new \InvalidArgumentException(sprintf('The secret name "%s" is not valid.', $name));
|
||||
}
|
||||
|
||||
return $this->secretsFolder.\DIRECTORY_SEPARATOR.$name.self::FILE_SUFFIX;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Secret\Storage;
|
||||
|
||||
/**
|
||||
* MutableSecretStorageInterface defines an interface to add and update a secrets in a storage.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface MutableSecretStorageInterface extends SecretStorageInterface
|
||||
{
|
||||
/**
|
||||
* Adds or replaces a secret in the store.
|
||||
*/
|
||||
public function setSecret(string $name, string $secret): void;
|
||||
|
||||
/**
|
||||
* Removes a secret from the store.
|
||||
*/
|
||||
public function removeSecret(string $name): void;
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Secret\Storage;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Exception\SecretNotFoundException;
|
||||
|
||||
/**
|
||||
* SecretStorageInterface defines an interface to retrieve secrets.
|
||||
*
|
||||
* @author Tobias Schultze <http://tobion.de>
|
||||
*/
|
||||
interface SecretStorageInterface
|
||||
{
|
||||
/**
|
||||
* Retrieves a decrypted secret from the storage.
|
||||
*
|
||||
* @throws SecretNotFoundException
|
||||
*/
|
||||
public function getSecret(string $name): string;
|
||||
|
||||
/**
|
||||
* Returns a list of all secrets indexed by their name.
|
||||
*
|
||||
* @param bool $reveal when true, returns the decrypted secret, null otherwise
|
||||
*
|
||||
* @return iterable a list of key => value pairs
|
||||
*/
|
||||
public function listSecrets(bool $reveal = false): iterable;
|
||||
}
|
@ -437,6 +437,11 @@ class ConfigurationTest extends TestCase
|
||||
'enabled' => !class_exists(FullStack::class) && class_exists(Mailer::class),
|
||||
],
|
||||
'error_controller' => 'error_controller',
|
||||
'secrets' => [
|
||||
'enabled' => false,
|
||||
'encrypted_secrets_dir' => '%kernel.project_dir%/config/secrets/%kernel.environment%',
|
||||
'encryption_key' => '%kernel.project_dir%/config/secrets/encryption_%kernel.environment%.key',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Tests\Secret\Encoder;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Encoder\SodiumEncoder;
|
||||
|
||||
/**
|
||||
* @requires extension sodium
|
||||
*/
|
||||
class SodiumEncoderTest extends TestCase
|
||||
{
|
||||
private $keyPath;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
$this->keyPath = tempnam(sys_get_temp_dir(), 'secret');
|
||||
unlink($this->keyPath);
|
||||
}
|
||||
|
||||
protected function tearDown()
|
||||
{
|
||||
@unlink($this->keyPath);
|
||||
}
|
||||
|
||||
public function testGenerateKey()
|
||||
{
|
||||
$encoder = new SodiumEncoder($this->keyPath);
|
||||
$resources = $encoder->generateKeys();
|
||||
|
||||
$this->assertCount(1, $resources);
|
||||
$this->assertEquals($this->keyPath, $resources[0]);
|
||||
$this->assertEquals(32, \strlen(file_get_contents($this->keyPath)));
|
||||
}
|
||||
|
||||
public function testGenerateCheckOtherKey()
|
||||
{
|
||||
$this->expectException(\LogicException::class);
|
||||
$this->expectExceptionMessageRegExp('/^A key already exists in/');
|
||||
|
||||
$encoder = new SodiumEncoder($this->keyPath);
|
||||
|
||||
$encoder->generateKeys();
|
||||
$encoder->generateKeys();
|
||||
}
|
||||
|
||||
public function testGenerateOverride()
|
||||
{
|
||||
$encoder = new SodiumEncoder($this->keyPath);
|
||||
|
||||
$encoder->generateKeys();
|
||||
$firstKey = file_get_contents($this->keyPath);
|
||||
$encoder->generateKeys(true);
|
||||
$secondKey = file_get_contents($this->keyPath);
|
||||
|
||||
$this->assertNotEquals($firstKey, $secondKey);
|
||||
}
|
||||
|
||||
public function testEncryptAndDecrypt()
|
||||
{
|
||||
$encoder = new SodiumEncoder($this->keyPath);
|
||||
$encoder->generateKeys();
|
||||
|
||||
$plain = 'plain';
|
||||
|
||||
$encrypted = $encoder->encrypt($plain);
|
||||
$this->assertNotEquals($plain, $encrypted);
|
||||
$decrypted = $encoder->decrypt($encrypted);
|
||||
$this->assertEquals($plain, $decrypted);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Tests\Secret\Storage;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\FrameworkBundle\Exception\SecretNotFoundException;
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Storage\ChainSecretStorage;
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Storage\SecretStorageInterface;
|
||||
|
||||
class ChainSecretStorageTest extends TestCase
|
||||
{
|
||||
public function testGetSecret()
|
||||
{
|
||||
$storage1 = $this->getMockBuilder(SecretStorageInterface::class)->getMock();
|
||||
$storage1
|
||||
->expects($this->once())
|
||||
->method('getSecret')
|
||||
->with('foo')
|
||||
->willThrowException(new SecretNotFoundException('foo'));
|
||||
$storage2 = $this->getMockBuilder(SecretStorageInterface::class)->getMock();
|
||||
$storage2
|
||||
->expects($this->once())
|
||||
->method('getSecret')
|
||||
->with('foo')
|
||||
->willReturn('bar');
|
||||
|
||||
$chainStorage = new ChainSecretStorage([$storage1, $storage2]);
|
||||
|
||||
$this->assertEquals('bar', $chainStorage->getSecret('foo'));
|
||||
}
|
||||
|
||||
public function testListSecrets()
|
||||
{
|
||||
$storage1 = $this->getMockBuilder(SecretStorageInterface::class)->getMock();
|
||||
$storage1
|
||||
->expects($this->once())
|
||||
->method('listSecrets')
|
||||
->with(true)
|
||||
->willReturn(['foo' => 'bar']);
|
||||
$storage2 = $this->getMockBuilder(SecretStorageInterface::class)->getMock();
|
||||
$storage2
|
||||
->expects($this->once())
|
||||
->method('listSecrets')
|
||||
->with(true)
|
||||
->willReturn(['baz' => 'qux']);
|
||||
|
||||
$chainStorage = new ChainSecretStorage([$storage1, $storage2]);
|
||||
|
||||
$this->assertEquals(['foo' => 'bar', 'baz' => 'qux'], iterator_to_array($chainStorage->listSecrets(true)));
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Tests\Secret\Storage;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Encoder\SodiumEncoder;
|
||||
use Symfony\Bundle\FrameworkBundle\Secret\Storage\FilesSecretStorage;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
/**
|
||||
* @requires extension sodium
|
||||
*/
|
||||
class FilesSecretStorageTest extends TestCase
|
||||
{
|
||||
private $workDir;
|
||||
private $encoder;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
$this->workDir = tempnam(sys_get_temp_dir(), 'secret');
|
||||
$fs = new Filesystem();
|
||||
$fs->remove($this->workDir);
|
||||
$fs->mkdir($this->workDir);
|
||||
$this->encoder = new SodiumEncoder($this->workDir.'/key');
|
||||
$this->encoder->generateKeys();
|
||||
}
|
||||
|
||||
protected function tearDown()
|
||||
{
|
||||
(new Filesystem())->remove($this->workDir);
|
||||
unset($this->encoder);
|
||||
}
|
||||
|
||||
public function testPutAndGetSecrets()
|
||||
{
|
||||
$storage = new FilesSecretStorage($this->workDir, $this->encoder);
|
||||
|
||||
$secrets = iterator_to_array($storage->listSecrets());
|
||||
$this->assertEmpty($secrets);
|
||||
|
||||
$storage->setSecret('foo', 'bar');
|
||||
|
||||
$this->assertEquals('bar', $storage->getSecret('foo'));
|
||||
}
|
||||
|
||||
public function testGetThrowsNotFound()
|
||||
{
|
||||
$this->expectException(\Symfony\Bundle\FrameworkBundle\Exception\SecretNotFoundException::class);
|
||||
|
||||
$storage = new FilesSecretStorage($this->workDir, $this->encoder);
|
||||
|
||||
$storage->getSecret('not-found');
|
||||
}
|
||||
|
||||
public function testListSecrets()
|
||||
{
|
||||
$storage = new FilesSecretStorage($this->workDir, $this->encoder);
|
||||
|
||||
$secrets = iterator_to_array($storage->listSecrets());
|
||||
$this->assertEmpty($secrets);
|
||||
|
||||
$storage->setSecret('foo', 'bar');
|
||||
|
||||
$secrets = iterator_to_array($storage->listSecrets());
|
||||
$this->assertCount(1, $secrets);
|
||||
$this->assertEquals(['foo'], array_keys($secrets));
|
||||
$this->assertEquals([null], array_values($secrets));
|
||||
|
||||
$secrets = iterator_to_array($storage->listSecrets(true));
|
||||
$this->assertCount(1, $secrets);
|
||||
$this->assertEquals(['foo'], array_keys($secrets));
|
||||
$this->assertEquals(['bar'], array_values($secrets));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user