Skip to content

Commit 1381f01

Browse files
Tobionnicolas-grekas
authored andcommitted
Proof of concept for encrypted secrets
1 parent 9f2aa3a commit 1381f01

12 files changed

+464
-0
lines changed

Command/SecretsAddCommand.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace Symfony\Bundle\FrameworkBundle\Command;
4+
5+
use Symfony\Bundle\FrameworkBundle\Secret\SecretStorageInterface;
6+
use Symfony\Component\Console\Command\Command;
7+
use Symfony\Component\Console\Helper\QuestionHelper;
8+
use Symfony\Component\Console\Input\InputArgument;
9+
use Symfony\Component\Console\Input\InputInterface;
10+
use Symfony\Component\Console\Output\OutputInterface;
11+
use Symfony\Component\Console\Question\Question;
12+
13+
final class SecretsAddCommand extends Command
14+
{
15+
protected static $defaultName = 'secrets:add';
16+
17+
/**
18+
* @var SecretStorageInterface
19+
*/
20+
private $secretStorage;
21+
22+
public function __construct(SecretStorageInterface $secretStorage)
23+
{
24+
$this->secretStorage = $secretStorage;
25+
26+
parent::__construct();
27+
}
28+
29+
protected function configure()
30+
{
31+
$this
32+
->setDescription('Adds a secret with the key.')
33+
->addArgument(
34+
'key',
35+
InputArgument::REQUIRED
36+
)
37+
->addArgument(
38+
'secret',
39+
InputArgument::REQUIRED
40+
)
41+
;
42+
}
43+
44+
protected function execute(InputInterface $input, OutputInterface $output)
45+
{
46+
$key = $input->getArgument('key');
47+
$secret = $input->getArgument('secret');
48+
49+
$this->secretStorage->putSecret($key, $secret);
50+
}
51+
52+
protected function interact(InputInterface $input, OutputInterface $output)
53+
{
54+
/** @var QuestionHelper $helper */
55+
$helper = $this->getHelper('question');
56+
57+
$question = new Question('Key of the secret: ', $input->getArgument('key'));
58+
59+
$key = $helper->ask($input, $output, $question);
60+
$input->setArgument('key', $key);
61+
62+
$question = new Question('Plaintext secret value: ', $input->getArgument('secret'));
63+
$question->setHidden(true);
64+
65+
$secret = $helper->ask($input, $output, $question);
66+
$input->setArgument('secret', $secret);
67+
}
68+
}

Command/SecretsGenerateKeyCommand.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Symfony\Bundle\FrameworkBundle\Command;
4+
5+
use Symfony\Component\Console\Command\Command;
6+
use Symfony\Component\Console\Input\InputInterface;
7+
use Symfony\Component\Console\Output\OutputInterface;
8+
9+
final class SecretsGenerateKeyCommand extends Command
10+
{
11+
protected static $defaultName = 'secrets:generate-key';
12+
13+
protected function configure()
14+
{
15+
$this
16+
->setDescription('Prints a randomly generated encryption key.')
17+
;
18+
}
19+
20+
protected function execute(InputInterface $input, OutputInterface $output)
21+
{
22+
$encryptionKey = sodium_crypto_stream_keygen();
23+
24+
$output->write($encryptionKey, false, OutputInterface::OUTPUT_RAW);
25+
26+
sodium_memzero($encryptionKey);
27+
}
28+
}

Command/SecretsListCommand.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Symfony\Bundle\FrameworkBundle\Command;
4+
5+
use Symfony\Bundle\FrameworkBundle\Secret\SecretStorageInterface;
6+
use Symfony\Component\Console\Command\Command;
7+
use Symfony\Component\Console\Helper\Table;
8+
use Symfony\Component\Console\Input\InputInterface;
9+
use Symfony\Component\Console\Output\OutputInterface;
10+
11+
final class SecretsListCommand extends Command
12+
{
13+
protected static $defaultName = 'secrets:list';
14+
15+
/**
16+
* @var SecretStorageInterface
17+
*/
18+
private $secretStorage;
19+
20+
public function __construct(SecretStorageInterface $secretStorage)
21+
{
22+
$this->secretStorage = $secretStorage;
23+
24+
parent::__construct();
25+
}
26+
27+
protected function configure()
28+
{
29+
$this
30+
->setDescription('Lists all secrets.')
31+
;
32+
}
33+
34+
protected function execute(InputInterface $input, OutputInterface $output)
35+
{
36+
$table = new Table($output);
37+
$table->setHeaders(['key', 'plaintext secret']);
38+
39+
foreach ($this->secretStorage->listSecrets() as $key => $secret) {
40+
$table->addRow([$key, $secret]);
41+
}
42+
43+
$table->render();
44+
}
45+
}

DependencyInjection/Configuration.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,29 @@ public function getConfigTreeBuilder()
115115
$this->addRobotsIndexSection($rootNode);
116116
$this->addHttpClientSection($rootNode);
117117
$this->addMailerSection($rootNode);
118+
$this->addSecretsSection($rootNode);
118119

119120
return $treeBuilder;
120121
}
121122

123+
private function addSecretsSection(ArrayNodeDefinition $rootNode)
124+
{
125+
$rootNode
126+
->children()
127+
->arrayNode('secrets')
128+
->canBeEnabled()
129+
->children()
130+
->scalarNode('encrypted_secrets_dir')->end()
131+
->scalarNode('encryption_key')->end()
132+
//->scalarNode('public_key')->end()
133+
//->scalarNode('private_key')->end()
134+
->scalarNode('decrypted_secrets_cache')->end()
135+
->end()
136+
->end()
137+
->end()
138+
;
139+
}
140+
122141
private function addCsrfSection(ArrayNodeDefinition $rootNode)
123142
{
124143
$rootNode

DependencyInjection/FrameworkExtension.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ public function load(array $configs, ContainerBuilder $container)
334334
$this->registerRouterConfiguration($config['router'], $container, $loader);
335335
$this->registerAnnotationsConfiguration($config['annotations'], $container, $loader);
336336
$this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader);
337+
$this->registerSecretsConfiguration($config['secrets'], $container, $loader);
337338

338339
if ($this->isConfigEnabled($container, $config['serializer'])) {
339340
if (!class_exists('Symfony\Component\Serializer\Serializer')) {
@@ -1441,6 +1442,20 @@ private function registerPropertyAccessConfiguration(array $config, ContainerBui
14411442
;
14421443
}
14431444

1445+
private function registerSecretsConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
1446+
{
1447+
if (!$this->isConfigEnabled($container, $config)) {
1448+
$container->removeDefinition('console.command.secrets_add');
1449+
1450+
return;
1451+
}
1452+
1453+
$loader->load('secrets.xml');
1454+
1455+
$container->getDefinition('secrets.storage.files')->replaceArgument(0, $config['encrypted_secrets_dir']);
1456+
$container->getDefinition('secrets.storage.files')->replaceArgument(1, $config['encryption_key']);
1457+
}
1458+
14441459
private function registerSecurityCsrfConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
14451460
{
14461461
if (!$this->isConfigEnabled($container, $config)) {

Resources/config/console.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,5 +200,19 @@
200200
<argument type="service" id="debug.file_link_formatter" on-invalid="null" />
201201
<tag name="console.command" command="debug:error-renderer" />
202202
</service>
203+
204+
<service id="console.command.secrets_add" class="Symfony\Bundle\FrameworkBundle\Command\SecretsAddCommand">
205+
<argument type="service" id="secrets.storage.cache" />
206+
<tag name="console.command" command="secrets:add" />
207+
</service>
208+
209+
<service id="console.command.secrets_generate_key" class="Symfony\Bundle\FrameworkBundle\Command\SecretsGenerateKeyCommand">
210+
<tag name="console.command" command="secrets:generate-key" />
211+
</service>
212+
213+
<service id="console.command.secrets_list" class="Symfony\Bundle\FrameworkBundle\Command\SecretsListCommand">
214+
<argument type="service" id="secrets.storage.cache" />
215+
<tag name="console.command" command="secrets:list" />
216+
</service>
203217
</services>
204218
</container>

Resources/config/secrets.xml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<service id="secrets.storage.files" class="Symfony\Bundle\FrameworkBundle\Secret\FilesSecretStorage">
9+
<argument />
10+
<argument />
11+
</service>
12+
13+
<service id="secrets.storage.cache" class="Symfony\Bundle\FrameworkBundle\Secret\CachedSecretStorage">
14+
<argument type="service" id="secrets.storage.files" />
15+
<argument type="service" id="cache.system" />
16+
</service>
17+
18+
<service id="secrets.env_processor" class="Symfony\Bundle\FrameworkBundle\Secret\SecretEnvVarProcessor">
19+
<argument type="service" id="secrets.storage.cache" />
20+
<tag name="container.env_var_processor" />
21+
</service>
22+
</services>
23+
</container>

Secret/CachedSecretStorage.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
namespace Symfony\Bundle\FrameworkBundle\Secret;
4+
5+
use Psr\Cache\CacheItemInterface;
6+
use Psr\Cache\CacheItemPoolInterface;
7+
8+
class CachedSecretStorage implements SecretStorageInterface
9+
{
10+
/**
11+
* @var SecretStorageInterface
12+
*/
13+
private $decoratedStorage;
14+
/**
15+
* @var CacheItemPoolInterface
16+
*/
17+
private $cache;
18+
19+
public function __construct(SecretStorageInterface $decoratedStorage, CacheItemPoolInterface $cache)
20+
{
21+
$this->decoratedStorage = $decoratedStorage;
22+
$this->cache = $cache;
23+
}
24+
25+
public function getSecret(string $key): string
26+
{
27+
$cacheItem = $this->cache->getItem('secrets.php');
28+
29+
if ($cacheItem->isHit()) {
30+
$secrets = $cacheItem->get();
31+
if (isset($secrets[$key])) {
32+
return $secrets[$key];
33+
}
34+
}
35+
36+
$this->regenerateCache($cacheItem);
37+
38+
return $this->decoratedStorage->getSecret($key);
39+
}
40+
41+
public function putSecret(string $key, string $secret): void
42+
{
43+
$this->decoratedStorage->putSecret($key, $secret);
44+
$this->regenerateCache();
45+
}
46+
47+
public function deleteSecret(string $key): void
48+
{
49+
$this->decoratedStorage->deleteSecret($key);
50+
$this->regenerateCache();
51+
}
52+
53+
public function listSecrets(): iterable
54+
{
55+
$cacheItem = $this->cache->getItem('secrets.php');
56+
57+
if ($cacheItem->isHit()) {
58+
return $cacheItem->get();
59+
}
60+
61+
return $this->regenerateCache($cacheItem);
62+
}
63+
64+
private function regenerateCache(?CacheItemInterface $cacheItem = null): array
65+
{
66+
$cacheItem = $cacheItem ?? $this->cache->getItem('secrets.php');
67+
68+
$secrets = [];
69+
foreach ($this->decoratedStorage->listSecrets() as $key => $secret) {
70+
$secrets[$key] = $secret;
71+
}
72+
73+
$cacheItem->set($secrets);
74+
$this->cache->save($cacheItem);
75+
76+
return $secrets;
77+
}
78+
}

Secret/EncryptedMessage.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Symfony\Bundle\FrameworkBundle\Secret;
4+
5+
class EncryptedMessage
6+
{
7+
/**
8+
* @var string
9+
*/
10+
private $ciphertext;
11+
12+
/**
13+
* @var string
14+
*/
15+
private $nonce;
16+
17+
public function __construct(string $ciphertext, string $nonce)
18+
{
19+
$this->ciphertext = $ciphertext;
20+
$this->nonce = $nonce;
21+
}
22+
23+
public function __toString()
24+
{
25+
return $this->nonce.$this->ciphertext;
26+
}
27+
28+
public function getCiphertext(): string
29+
{
30+
return $this->ciphertext;
31+
}
32+
33+
public function getNonce(): string
34+
{
35+
return $this->nonce;
36+
}
37+
38+
public static function createFromString(string $message): self
39+
{
40+
if (\strlen($message) < SODIUM_CRYPTO_STREAM_NONCEBYTES) {
41+
throw new \RuntimeException('Invalid ciphertext. Message is too short.');
42+
}
43+
44+
$nonce = substr($message, 0, SODIUM_CRYPTO_STREAM_NONCEBYTES);
45+
$ciphertext = substr($message, SODIUM_CRYPTO_STREAM_NONCEBYTES);
46+
47+
return new self($ciphertext, $nonce);
48+
}
49+
}

0 commit comments

Comments
 (0)