diff --git a/Command/GenerateKeyPairCommand.php b/Command/GenerateKeyPairCommand.php index 350ab29c..6c3b320f 100644 --- a/Command/GenerateKeyPairCommand.php +++ b/Command/GenerateKeyPairCommand.php @@ -48,7 +48,9 @@ public function __construct(Filesystem $filesystem, string $secretKey, string $p protected function configure() { $this->setDescription('Generate public/private keys for use in your application.'); - $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do not update config files.'); + $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Do not update key files.'); + $this->addOption('skip-if-exists', null, InputOption::VALUE_NONE, 'Do not update key files if they already exist.'); + $this->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrite key files if they already exist.'); } protected function execute(InputInterface $input, OutputInterface $output) @@ -57,6 +59,24 @@ protected function execute(InputInterface $input, OutputInterface $output) list($privateKey, $publicKey) = self::generateKeyPair($this->passphrase); + $alreadyExists = $this->filesystem->exists($this->secretKey) || $this->filesystem->exists($this->publicKey); + + if (true === $alreadyExists) { + try { + $this->handleExistingKeys($input); + } catch (\RuntimeException $e) { + if (0 === $e->getCode()) { + $io->comment($e->getMessage()); + + return 0; + } + + $io->error($e->getMessage()); + + return 1; + } + } + if (true === $input->getOption('dry-run')) { $io->success('Your keys have been generated!'); $io->newLine(); @@ -83,6 +103,21 @@ protected function execute(InputInterface $input, OutputInterface $output) return 0; } + private function handleExistingKeys(InputInterface $input): void + { + if (true === $input->getOption('skip-if-exists') && true === $input->getOption('overwrite')) { + throw new \RuntimeException('Both options `--skip-if-exists` and `--overwrite` cannot be combined.', 1); + } + + if (true === $input->getOption('skip-if-exists')) { + throw new \RuntimeException('Your key files already exist, they won\'t be overriden.', 0); + } + + if (false === $input->getOption('overwrite')) { + throw new \RuntimeException('Your keys already exist. Use the `--overwrite` option to force regeneration.', 1); + } + } + private static function generateKeyPair($passphrase) { $config = [ diff --git a/Resources/doc/index.md b/Resources/doc/index.md index 2b6f46a3..a47d0944 100644 --- a/Resources/doc/index.md +++ b/Resources/doc/index.md @@ -45,6 +45,14 @@ return [ $ php bin/console lexik:jwt:generate-keypair ``` +Your keys will land in `config/jwt/private.pem` and `config/jwt/public.pem` (unless you configured a different path). + +Available options: +- `--skip-if-exists` will silently do nothing if keys already exist. +- `--overwrite` will overwrite your keys if they already exist. + +Otherwise, an error will be raised to prevent you from overwriting your keys accidentally. + Configuration ------------- diff --git a/Tests/Functional/Command/GenerateKeyPairCommandTest.php b/Tests/Functional/Command/GenerateKeyPairCommandTest.php index 1a85827f..22ace09e 100644 --- a/Tests/Functional/Command/GenerateKeyPairCommandTest.php +++ b/Tests/Functional/Command/GenerateKeyPairCommandTest.php @@ -12,10 +12,15 @@ class GenerateKeyPairCommandTest extends TestCase /** * @dataProvider providePassphrase */ - public function testRun($passphrase) + public function testItGeneratesKeyPair($passphrase) { $privateKeyFile = \tempnam(\sys_get_temp_dir(), 'private_'); $publicKeyFile = \tempnam(\sys_get_temp_dir(), 'public_'); + + // tempnam() actually create the files, but we have to simulate they don't exist + \unlink($privateKeyFile); + \unlink($publicKeyFile); + $tester = new CommandTester( new GenerateKeyPairCommand( new Filesystem(), @@ -25,11 +30,23 @@ public function testRun($passphrase) ) ); - $this->assertSame(0, $tester->execute([], ['interactive' => false])); - $this->assertNotFalse($privateKey = \file_get_contents($privateKeyFile)); + $returnCode = $tester->execute([], ['interactive' => false]); + $this->assertSame(0, $returnCode); + + $privateKey = \file_get_contents($privateKeyFile); + $publicKey = \file_get_contents($publicKeyFile); + $this->assertStringContainsString('Done!', $tester->getDisplay(true)); + $this->assertNotFalse($privateKey); + $this->assertNotFalse($publicKey); $this->assertStringContainsString('PRIVATE KEY', $privateKey); - $this->assertNotFalse($publicKey = \file_get_contents($publicKeyFile)); $this->assertStringContainsString('PUBLIC KEY', $publicKey); + + // Encryption / decryption test + $payload = 'Despite the constant negative press covfefe'; + \openssl_public_encrypt($payload, $encryptedData, \openssl_pkey_get_public($publicKey)); + \openssl_private_decrypt($encryptedData, $decryptedData, \openssl_pkey_get_private($privateKey, $passphrase)); + $this->assertSame($payload, $decryptedData); + } public function providePassphrase() @@ -37,4 +54,146 @@ public function providePassphrase() yield [null]; yield ['dummy']; } + + public function testOverwriteAndSkipCannotBeCombined() + { + $privateKeyFile = \tempnam(\sys_get_temp_dir(), 'private_'); + $publicKeyFile = \tempnam(\sys_get_temp_dir(), 'public_'); + + \file_put_contents($privateKeyFile, 'foobar'); + \file_put_contents($publicKeyFile, 'foobar'); + + $tester = new CommandTester( + new GenerateKeyPairCommand( + new Filesystem(), + $privateKeyFile, + $publicKeyFile, + null + ) + ); + $input = ['--overwrite' => true, '--skip-if-exists' => true]; + $returnCode = $tester->execute($input, ['interactive' => false]); + $this->assertSame(1, $returnCode); + $this->assertStringContainsString( + 'Both options `--skip-if-exists` and `--overwrite` cannot be combined.', + $tester->getDisplay(true) + ); + + $privateKey = \file_get_contents($privateKeyFile); + $publicKey = \file_get_contents($publicKeyFile); + $this->assertStringContainsString('foobar', $privateKey); + $this->assertStringContainsString('foobar', $publicKey); + } + + public function testNoOverwriteDoesNotOverwrite() + { + $privateKeyFile = \tempnam(\sys_get_temp_dir(), 'private_'); + $publicKeyFile = \tempnam(\sys_get_temp_dir(), 'public_'); + + \file_put_contents($privateKeyFile, 'foobar'); + \file_put_contents($publicKeyFile, 'foobar'); + + $tester = new CommandTester( + new GenerateKeyPairCommand( + new Filesystem(), + $privateKeyFile, + $publicKeyFile, + null + ) + ); + + $returnCode = $tester->execute([], ['interactive' => false]); + $this->assertSame(1, $returnCode); + $this->assertStringContainsString( + 'Your keys already exist. Use the `--overwrite` option to force regeneration.', + \strtr($tester->getDisplay(true), ["\n" => '']) + ); + + $privateKey = \file_get_contents($privateKeyFile); + $publicKey = \file_get_contents($publicKeyFile); + $this->assertStringContainsString('foobar', $privateKey); + $this->assertStringContainsString('foobar', $publicKey); + } + + public function testOverwriteActuallyOverwrites() + { + $privateKeyFile = \tempnam(\sys_get_temp_dir(), 'private_'); + $publicKeyFile = \tempnam(\sys_get_temp_dir(), 'public_'); + + \file_put_contents($privateKeyFile, 'foobar'); + \file_put_contents($publicKeyFile, 'foobar'); + + $tester = new CommandTester( + new GenerateKeyPairCommand( + new Filesystem(), + $privateKeyFile, + $publicKeyFile, + null + ) + ); + + $returnCode = $tester->execute(['--overwrite' => true], ['interactive' => false]); + $privateKey = \file_get_contents($privateKeyFile); + $publicKey = \file_get_contents($publicKeyFile); + + $this->assertSame(0, $returnCode); + $this->assertStringContainsString('PRIVATE KEY', $privateKey); + $this->assertStringContainsString('PUBLIC KEY', $publicKey); + } + + public function testSkipIfExistsWritesIfNotExists() + { + $privateKeyFile = \tempnam(\sys_get_temp_dir(), 'private_'); + $publicKeyFile = \tempnam(\sys_get_temp_dir(), 'public_'); + + // tempnam() actually create the files, but we have to simulate they don't exist + \unlink($privateKeyFile); + \unlink($publicKeyFile); + + $tester = new CommandTester( + new GenerateKeyPairCommand( + new Filesystem(), + $privateKeyFile, + $publicKeyFile, + null + ) + ); + + $this->assertSame(0, $tester->execute(['--skip-if-exists' => true], ['interactive' => false])); + $this->assertStringContainsString('Done!', $tester->getDisplay(true)); + $privateKey = \file_get_contents($privateKeyFile); + $publicKey = \file_get_contents($publicKeyFile); + $this->assertStringContainsString('PRIVATE KEY', $privateKey); + $this->assertStringContainsString('PUBLIC KEY', $publicKey); + } + + public function testSkipIfExistsDoesNothingIfExists() + { + $privateKeyFile = \tempnam(\sys_get_temp_dir(), 'private_'); + $publicKeyFile = \tempnam(\sys_get_temp_dir(), 'public_'); + + \file_put_contents($privateKeyFile, 'foobar'); + \file_put_contents($publicKeyFile, 'foobar'); + + $tester = new CommandTester( + new GenerateKeyPairCommand( + new Filesystem(), + $privateKeyFile, + $publicKeyFile, + null + ) + ); + + $this->assertSame(0, $tester->execute(['--skip-if-exists' => true], ['interactive' => false])); + $this->assertStringContainsString( + 'Your key files already exist, they won\'t be overriden.', + $tester->getDisplay(true) + ); + + $privateKey = \file_get_contents($privateKeyFile); + $publicKey = \file_get_contents($publicKeyFile); + $this->assertStringContainsString('foobar', $privateKey); + $this->assertStringContainsString('foobar', $publicKey); + } + }