Skip to content

Commit a905938

Browse files
committed
Add encryption functionality
1 parent 94cc8d0 commit a905938

File tree

16 files changed

+470
-13
lines changed

16 files changed

+470
-13
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/.env
22
/compose.override.yaml
33
/config/dirigent.yaml
4+
/config/encryption/
45
/config/packages/dirigent.yaml
56
/storage/
67

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ RUN set -e; \
6161
php82-phar \
6262
php82-session \
6363
php82-simplexml \
64+
php82-sodium \
6465
php82-tokenizer \
6566
php82-xml \
6667
postgresql \

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"ext-ctype": "*",
1212
"ext-curl": "*",
1313
"ext-iconv": "*",
14+
"ext-sodium": "*",
1415
"cebe/markdown": "^1.2",
1516
"composer/composer": "^2.7",
1617
"doctrine/doctrine-bundle": "^2.11",

composer.lock

Lines changed: 14 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docker/config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
framework:
22
secret: '%env(file:KERNEL_SECRET_FILE)%'
3+
4+
dirigent:
5+
encryption:
6+
private_key: '%env(DECRYPTION_KEY_FILE)%'
7+
public_key: '%env(ENCRYPTION_KEY_FILE)%'

docker/env.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
return [
44
'APP_ENV' => 'prod',
55
'DATABASE_URL' => 'postgresql://dirigent@127.0.0.1:5432/dirigent?serverVersion=16&charset=utf8',
6+
'DECRYPTION_KEY_FILE' => '/srv/config/secrets/decryption_key',
67
'DIRIGENT_IMAGE' => '1',
8+
'ENCRYPTION_KEY_FILE' => '/srv/config/secrets/encryption_key',
79
'GITHUB_TOKEN' => '',
810
'KERNEL_SECRET_FILE' => '/srv/config/secrets/kernel_secret',
911
'MAILER_DSN' => 'null://null',
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env sh
2+
3+
set -e
4+
5+
bin/console encryption:generate-keys --no-ansi --no-interaction
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
namespace CodedMonkey\Dirigent\Command;
4+
5+
use CodedMonkey\Dirigent\Encryption\Encryption;
6+
use CodedMonkey\Dirigent\Encryption\EncryptionException;
7+
use Symfony\Component\Console\Attribute\AsCommand;
8+
use Symfony\Component\Console\Command\Command;
9+
use Symfony\Component\Console\Input\InputInterface;
10+
use Symfony\Component\Console\Output\OutputInterface;
11+
use Symfony\Component\Console\Style\StyleInterface;
12+
use Symfony\Component\Console\Style\SymfonyStyle;
13+
use Symfony\Component\Filesystem\Filesystem;
14+
15+
#[AsCommand(
16+
name: 'encryption:generate-keys',
17+
description: 'Generates an encryption key pair',
18+
)]
19+
class EncryptionGenerateKeysCommand extends Command
20+
{
21+
public function __construct(
22+
public readonly ?string $privateKeyPath,
23+
public readonly ?string $publicKeyPath,
24+
) {
25+
parent::__construct();
26+
}
27+
28+
protected function execute(InputInterface $input, OutputInterface $output): int
29+
{
30+
$io = new SymfonyStyle($input, $output);
31+
32+
if (!$this->privateKeyPath || !$this->publicKeyPath) {
33+
$io->info('Encryption key files are disabled.');
34+
35+
return Command::SUCCESS;
36+
}
37+
38+
$filesystem = new Filesystem();
39+
40+
$decryptionKeyExists = $filesystem->exists($this->privateKeyPath);
41+
$encryptionKeyExists = $filesystem->exists($this->publicKeyPath);
42+
43+
if (!$decryptionKeyExists && $encryptionKeyExists) {
44+
$io->error('Unable to generate (private) decryption key because a (public) encryption key exists.');
45+
46+
return Command::FAILURE;
47+
} elseif ($decryptionKeyExists && $encryptionKeyExists) {
48+
$io->info('Encryption keys already exist.');
49+
} elseif ($decryptionKeyExists && !$encryptionKeyExists) {
50+
$decryptionKey = sodium_hex2bin($filesystem->readFile($this->privateKeyPath));
51+
$encryptionKey = sodium_crypto_box_publickey($decryptionKey);
52+
53+
$filesystem->dumpFile($this->publicKeyPath, sodium_bin2hex($encryptionKey));
54+
55+
$io->success('Generated a new (public) encryption key.');
56+
} else {
57+
$decryptionKey = sodium_crypto_box_keypair();
58+
$encryptionKey = sodium_crypto_box_publickey($decryptionKey);
59+
60+
$filesystem->dumpFile($this->privateKeyPath, sodium_bin2hex($decryptionKey));
61+
$filesystem->dumpFile($this->publicKeyPath, sodium_bin2hex($encryptionKey));
62+
63+
$io->success('Generated encryption keys.');
64+
}
65+
66+
return $this->validateKeys($io);
67+
}
68+
69+
private function validateKeys(StyleInterface $output): int
70+
{
71+
$encryption = Encryption::createFromFiles($this->privateKeyPath, $this->publicKeyPath, []);
72+
73+
$value = 'thank you for the music';
74+
75+
try {
76+
$sealedValue = $encryption->seal($value);
77+
$revealedValue = $encryption->reveal($sealedValue);
78+
79+
if ($value !== $revealedValue) {
80+
throw new EncryptionException('Encryption failed');
81+
}
82+
83+
return Command::SUCCESS;
84+
} catch (EncryptionException $exception) {
85+
$output->error('Encryption key validation failed: ' . $exception->getMessage());
86+
87+
return Command::FAILURE;
88+
}
89+
}
90+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace CodedMonkey\Dirigent\DependencyInjection\Compiler;
4+
5+
use CodedMonkey\Dirigent\Command\EncryptionGenerateKeysCommand;
6+
use CodedMonkey\Dirigent\Encryption\Encryption;
7+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
8+
use Symfony\Component\DependencyInjection\ContainerBuilder;
9+
10+
class EncryptionPass implements CompilerPassInterface
11+
{
12+
public function process(ContainerBuilder $container): void
13+
{
14+
$parameterBag = $container->getParameterBag();
15+
16+
$privateKey = $parameterBag->get('dirigent.encryption.private_key');
17+
$publicKey = $parameterBag->get('dirigent.encryption.public_key');
18+
$rotatedKeys = $parameterBag->get('dirigent.encryption.rotated_keys');
19+
20+
$privateKeyPath = $parameterBag->get('dirigent.encryption.private_key_path');
21+
$publicKeyPath = $parameterBag->get('dirigent.encryption.public_key_path');
22+
$rotatedKeyPaths = $parameterBag->get('dirigent.encryption.rotated_key_paths');
23+
24+
$useFiles = !$privateKey && !$publicKey;
25+
26+
if ($useFiles) {
27+
$container->getDefinition(Encryption::class)
28+
->setLazy(true)
29+
->setFactory([Encryption::class, 'createFromFiles'])
30+
->setArguments([$privateKeyPath, $publicKeyPath, $rotatedKeyPaths]);
31+
32+
$container->getDefinition(EncryptionGenerateKeysCommand::class)
33+
->setArguments([$privateKeyPath, $publicKeyPath]);
34+
} else {
35+
$container->getDefinition(Encryption::class)
36+
->setLazy(true)
37+
->setFactory([Encryption::class, 'createFromHex'])
38+
->setArguments([$privateKey, $publicKey, $rotatedKeys]);
39+
40+
$container->getDefinition(EncryptionGenerateKeysCommand::class)
41+
->setArguments([null, null]);
42+
}
43+
44+
$parameterBag->remove('dirigent.encryption.private_key');
45+
$parameterBag->remove('dirigent.encryption.public_key');
46+
$parameterBag->remove('dirigent.encryption.rotated_keys');
47+
48+
$parameterBag->remove('dirigent.encryption.private_key_path');
49+
$parameterBag->remove('dirigent.encryption.public_key_path');
50+
$parameterBag->remove('dirigent.encryption.rotated_key_paths');
51+
}
52+
}

src/DependencyInjection/DirigentConfiguration.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
66
use Symfony\Component\Config\Definition\ConfigurationInterface;
7+
use function Symfony\Component\String\u;
78

89
class DirigentConfiguration implements ConfigurationInterface
910
{
@@ -22,6 +23,44 @@ public function getConfigTreeBuilder(): TreeBuilder
2223
->booleanNode('registration')->defaultFalse()->end()
2324
->end()
2425
->end()
26+
->arrayNode('encryption')
27+
->info('Dirigent uses a X25519 keypair to encrypt sensitive info stored in the database')
28+
->addDefaultsIfNotSet()
29+
->children()
30+
->scalarNode('private_key')
31+
->defaultNull()
32+
->info('The (private) decryption key, if empty, a file will be used to store the key instead')
33+
->end()
34+
->scalarNode('private_key_path')
35+
->defaultValue('%kernel.project_dir%/config/encryption/private.key')
36+
->info('Path to the (private) decryption key if private_key is empty')
37+
->end()
38+
->scalarNode('public_key')
39+
->defaultNull()
40+
->info('The (public) encryption key, if empty, a file will be used to store the key instead')
41+
->end()
42+
->scalarNode('public_key_path')
43+
->defaultValue('%kernel.project_dir%/config/encryption/public.key')
44+
->info('Path to the (public) encryption key if public_key is empty')
45+
->end()
46+
->arrayNode('rotated_keys')
47+
->info('Previously used (private) decryption keys')
48+
->beforeNormalization()
49+
->ifString()
50+
->then(fn (string $keys): array => u($$keys)->split(','))
51+
->end()
52+
->prototype('scalar')->end()
53+
->end()
54+
->arrayNode('rotated_key_paths')
55+
->info('Paths to previously used (private) decryption keys')
56+
->beforeNormalization()
57+
->ifString()
58+
->then(fn (string $paths): array => u($$paths)->split(','))
59+
->end()
60+
->prototype('scalar')->end()
61+
->end()
62+
->end()
63+
->end()
2564
->arrayNode('storage')
2665
->addDefaultsIfNotSet()
2766
->children()

0 commit comments

Comments
 (0)