Proof of concept for encrypted secrets
This commit is contained in:
parent
594e7aef87
commit
8c8f62390a
@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symfony\Bundle\FrameworkBundle\Command;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Secret\SecretStorageInterface;
|
||||||
|
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;
|
||||||
|
|
||||||
|
final class SecretsAddCommand extends Command
|
||||||
|
{
|
||||||
|
protected static $defaultName = 'secrets:add';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var SecretStorageInterface
|
||||||
|
*/
|
||||||
|
private $secretStorage;
|
||||||
|
|
||||||
|
public function __construct(SecretStorageInterface $secretStorage)
|
||||||
|
{
|
||||||
|
$this->secretStorage = $secretStorage;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setDescription('Adds a secret with the key.')
|
||||||
|
->addArgument(
|
||||||
|
'key',
|
||||||
|
InputArgument::REQUIRED
|
||||||
|
)
|
||||||
|
->addArgument(
|
||||||
|
'secret',
|
||||||
|
InputArgument::REQUIRED
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
$key = $input->getArgument('key');
|
||||||
|
$secret = $input->getArgument('secret');
|
||||||
|
|
||||||
|
$this->secretStorage->putSecret($key, $secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function interact(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
/** @var QuestionHelper $helper */
|
||||||
|
$helper = $this->getHelper('question');
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symfony\Bundle\FrameworkBundle\Command;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
final class SecretsGenerateKeyCommand extends Command
|
||||||
|
{
|
||||||
|
protected static $defaultName = 'secrets:generate-key';
|
||||||
|
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setDescription('Prints a randomly generated encryption key.')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
$encryptionKey = sodium_crypto_stream_keygen();
|
||||||
|
|
||||||
|
$output->write($encryptionKey, false, OutputInterface::OUTPUT_RAW);
|
||||||
|
|
||||||
|
sodium_memzero($encryptionKey);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symfony\Bundle\FrameworkBundle\Command;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Secret\SecretStorageInterface;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Helper\Table;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
final class SecretsListCommand extends Command
|
||||||
|
{
|
||||||
|
protected static $defaultName = 'secrets:list';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var SecretStorageInterface
|
||||||
|
*/
|
||||||
|
private $secretStorage;
|
||||||
|
|
||||||
|
public function __construct(SecretStorageInterface $secretStorage)
|
||||||
|
{
|
||||||
|
$this->secretStorage = $secretStorage;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setDescription('Lists all secrets.')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
$table = new Table($output);
|
||||||
|
$table->setHeaders(['key', 'plaintext secret']);
|
||||||
|
|
||||||
|
foreach ($this->secretStorage->listSecrets() as $key => $secret) {
|
||||||
|
$table->addRow([$key, $secret]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->render();
|
||||||
|
}
|
||||||
|
}
|
@ -115,10 +115,29 @@ class Configuration implements ConfigurationInterface
|
|||||||
$this->addRobotsIndexSection($rootNode);
|
$this->addRobotsIndexSection($rootNode);
|
||||||
$this->addHttpClientSection($rootNode);
|
$this->addHttpClientSection($rootNode);
|
||||||
$this->addMailerSection($rootNode);
|
$this->addMailerSection($rootNode);
|
||||||
|
$this->addSecretsSection($rootNode);
|
||||||
|
|
||||||
return $treeBuilder;
|
return $treeBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function addSecretsSection(ArrayNodeDefinition $rootNode)
|
||||||
|
{
|
||||||
|
$rootNode
|
||||||
|
->children()
|
||||||
|
->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()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
private function addCsrfSection(ArrayNodeDefinition $rootNode)
|
private function addCsrfSection(ArrayNodeDefinition $rootNode)
|
||||||
{
|
{
|
||||||
$rootNode
|
$rootNode
|
||||||
|
@ -334,6 +334,7 @@ class FrameworkExtension extends Extension
|
|||||||
$this->registerRouterConfiguration($config['router'], $container, $loader);
|
$this->registerRouterConfiguration($config['router'], $container, $loader);
|
||||||
$this->registerAnnotationsConfiguration($config['annotations'], $container, $loader);
|
$this->registerAnnotationsConfiguration($config['annotations'], $container, $loader);
|
||||||
$this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader);
|
$this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader);
|
||||||
|
$this->registerSecretsConfiguration($config['secrets'], $container, $loader);
|
||||||
|
|
||||||
if ($this->isConfigEnabled($container, $config['serializer'])) {
|
if ($this->isConfigEnabled($container, $config['serializer'])) {
|
||||||
if (!class_exists('Symfony\Component\Serializer\Serializer')) {
|
if (!class_exists('Symfony\Component\Serializer\Serializer')) {
|
||||||
@ -1441,6 +1442,20 @@ class FrameworkExtension extends Extension
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function registerSecretsConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
|
||||||
|
{
|
||||||
|
if (!$this->isConfigEnabled($container, $config)) {
|
||||||
|
$container->removeDefinition('console.command.secrets_add');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$loader->load('secrets.xml');
|
||||||
|
|
||||||
|
$container->getDefinition('secrets.storage.files')->replaceArgument(0, $config['encrypted_secrets_dir']);
|
||||||
|
$container->getDefinition('secrets.storage.files')->replaceArgument(1, $config['encryption_key']);
|
||||||
|
}
|
||||||
|
|
||||||
private function registerSecurityCsrfConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
|
private function registerSecurityCsrfConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
|
||||||
{
|
{
|
||||||
if (!$this->isConfigEnabled($container, $config)) {
|
if (!$this->isConfigEnabled($container, $config)) {
|
||||||
|
@ -200,5 +200,19 @@
|
|||||||
<argument type="service" id="debug.file_link_formatter" on-invalid="null" />
|
<argument type="service" id="debug.file_link_formatter" on-invalid="null" />
|
||||||
<tag name="console.command" command="debug:error-renderer" />
|
<tag name="console.command" command="debug:error-renderer" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service id="console.command.secrets_add" class="Symfony\Bundle\FrameworkBundle\Command\SecretsAddCommand">
|
||||||
|
<argument type="service" id="secrets.storage.cache" />
|
||||||
|
<tag name="console.command" command="secrets:add" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service id="console.command.secrets_generate_key" class="Symfony\Bundle\FrameworkBundle\Command\SecretsGenerateKeyCommand">
|
||||||
|
<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" />
|
||||||
|
</service>
|
||||||
</services>
|
</services>
|
||||||
</container>
|
</container>
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
|
||||||
|
<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 https://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||||
|
|
||||||
|
<services>
|
||||||
|
<service id="secrets.storage.files" class="Symfony\Bundle\FrameworkBundle\Secret\FilesSecretStorage">
|
||||||
|
<argument />
|
||||||
|
<argument />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service id="secrets.storage.cache" class="Symfony\Bundle\FrameworkBundle\Secret\CachedSecretStorage">
|
||||||
|
<argument type="service" id="secrets.storage.files" />
|
||||||
|
<argument type="service" id="cache.system" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service id="secrets.env_processor" class="Symfony\Bundle\FrameworkBundle\Secret\SecretEnvVarProcessor">
|
||||||
|
<argument type="service" id="secrets.storage.cache" />
|
||||||
|
<tag name="container.env_var_processor" />
|
||||||
|
</service>
|
||||||
|
</services>
|
||||||
|
</container>
|
@ -0,0 +1,78 @@
|
|||||||
|
<?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,49 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
<?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';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
|
||||||
|
|
||||||
|
class SecretEnvVarProcessor implements EnvVarProcessorInterface
|
||||||
|
{
|
||||||
|
private $secretStorage;
|
||||||
|
|
||||||
|
public function __construct(SecretStorageInterface $secretStorage)
|
||||||
|
{
|
||||||
|
$this->secretStorage = $secretStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public static function getProvidedTypes()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'secret' => 'string',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getEnv($prefix, $name, \Closure $getEnv)
|
||||||
|
{
|
||||||
|
return $this->secretStorage->getSecret($name);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
Reference in New Issue
Block a user