Skip to content

Commit 7c47ab9

Browse files
committed
feat: add command to create signing key
1 parent b1bf382 commit 7c47ab9

File tree

6 files changed

+144
-9
lines changed

6 files changed

+144
-9
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography;
4+
5+
use Tempest\Console\Console;
6+
use Tempest\Console\ConsoleCommand;
7+
use Tempest\Console\ExitCode;
8+
use Tempest\Cryptography\Encryption\EncryptionConfig;
9+
use Tempest\Cryptography\Encryption\EncryptionKey;
10+
use Tempest\Support\Filesystem;
11+
use Tempest\Support\Regex;
12+
use Tempest\Support\Str;
13+
14+
use function Tempest\root_path;
15+
16+
final readonly class CreateSigningKeyCommand
17+
{
18+
public function __construct(
19+
private EncryptionConfig $encryptionConfig,
20+
private Console $console,
21+
) {}
22+
23+
#[ConsoleCommand('key:generate', description: 'Generates the signing key required to sign and verify data.')]
24+
public function __invoke(): ExitCode
25+
{
26+
$key = EncryptionKey::generate($this->encryptionConfig->algorithm);
27+
28+
$this->console->writeln();
29+
$this->console->success('Signing key generated successfully.');
30+
31+
$this->createDotEnvIfNotExists();
32+
$this->addToDotEnv($key->toString());
33+
34+
return ExitCode::SUCCESS;
35+
}
36+
37+
private function getDotEnvPath(): string
38+
{
39+
return root_path('.env');
40+
}
41+
42+
private function addToDotEnv(string $key): void
43+
{
44+
$file = Filesystem\read_file($this->getDotEnvPath());
45+
46+
if (! Str\contains($file, 'SIGNING_KEY=')) {
47+
$file .= "\nSIGNING_KEY={$key}\n";
48+
} else {
49+
$file = Regex\replace($file, '/^SIGNING_KEY=.*$/m', "SIGNING_KEY={$key}");
50+
}
51+
52+
Filesystem\write_file($this->getDotEnvPath(), $file);
53+
}
54+
55+
private function createDotEnvIfNotExists(): void
56+
{
57+
if (Filesystem\exists($this->getDotEnvPath())) {
58+
return;
59+
}
60+
61+
Filesystem\create_file($this->getDotEnvPath());
62+
}
63+
}

packages/cryptography/src/Encryption/EncryptedData.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,16 @@ public function serialize(): string
3232

3333
public static function unserialize(string $data): self
3434
{
35-
$decoded = Json\decode(base64_decode($data));
35+
$decoded = Json\decode(base64_decode($data, strict: true));
3636

3737
if (! is_array($decoded) || ! isset($decoded['payload'], $decoded['iv'], $decoded['tag'], $decoded['signature'], $decoded['algorithm'])) {
3838
throw EncryptedDataWasInvalid::dueToInvalidFormat();
3939
}
4040

4141
return new self(
42-
payload: base64_decode($decoded['payload']),
43-
iv: base64_decode($decoded['iv']),
44-
tag: base64_decode($decoded['tag']),
42+
payload: base64_decode($decoded['payload'], strict: true),
43+
iv: base64_decode($decoded['iv'], strict: true),
44+
tag: base64_decode($decoded['tag'], strict: true),
4545
signature: new Signature($decoded['signature']),
4646
algorithm: EncryptionAlgorithm::from($decoded['algorithm']),
4747
);

packages/cryptography/src/Encryption/EncryptionKey.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,16 @@ public static function generate(EncryptionAlgorithm $algorithm): self
3333
*/
3434
public static function fromString(string $key, EncryptionAlgorithm $algorithm): self
3535
{
36-
return new self($key, $algorithm);
36+
return new self(base64_decode($key, strict: true), $algorithm);
37+
}
38+
39+
public function toString(): string
40+
{
41+
return base64_encode($this->value);
3742
}
3843

3944
public function __toString(): string
4045
{
41-
return $this->value;
46+
return $this->toString();
4247
}
4348
}

packages/cryptography/tests/Encryption/EncryptionTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ final class EncryptionTest extends TestCase
2323

2424
private function createEncrypter(?string $key = null, false|Duration $minimumExecutionDuration = false): GenericEncrypter
2525
{
26-
$key ??= EncryptionKey::generate(EncryptionAlgorithm::AES_256_GCM)->value;
26+
$key ??= EncryptionKey::generate(EncryptionAlgorithm::AES_256_GCM)->toString();
2727

2828
return new GenericEncrypter(
2929
signer: $this->createSigner(new SigningConfig(
@@ -48,7 +48,7 @@ public function test_encrypt(string $data): void
4848

4949
$serialized = $encrypted->serialize();
5050

51-
$this->assertTrue(json_validate(base64_decode($serialized)));
51+
$this->assertTrue(json_validate(base64_decode($serialized, strict: true)));
5252
$this->assertSame($data, $encrypter->decrypt($serialized));
5353
}
5454

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Integration\Cryptography;
4+
5+
use Dotenv\Dotenv;
6+
use Tempest\Core\FrameworkKernel;
7+
use Tempest\Cryptography\CreateSigningKeyCommand;
8+
use Tempest\Support\Filesystem;
9+
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
10+
11+
use function Tempest\root_path;
12+
13+
final class CreateSigningKeyCommandTest extends FrameworkIntegrationTestCase
14+
{
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
$this->container->get(FrameworkKernel::class)->root = __DIR__;
20+
}
21+
22+
protected function tearDown(): void
23+
{
24+
parent::tearDown();
25+
26+
Filesystem\delete_file(root_path('.env'));
27+
}
28+
29+
public function test_creates_dot_env(): void
30+
{
31+
$this->assertFalse(Filesystem\is_file(root_path('.env')));
32+
$this->console->call(CreateSigningKeyCommand::class)->assertSuccess();
33+
$this->assertTrue(Filesystem\is_file(root_path('.env')));
34+
35+
$file = Filesystem\read_file(root_path('.env'));
36+
$env = Dotenv::createImmutable(__DIR__)->parse($file);
37+
38+
$this->assertArrayHasKey('SIGNING_KEY', $env);
39+
$this->assertIsString($env['SIGNING_KEY']);
40+
}
41+
42+
public function test_updates_existing(): void
43+
{
44+
Filesystem\write_file(root_path('.env'), 'SIGNING_KEY=abc');
45+
$this->console->call(CreateSigningKeyCommand::class)->assertSuccess();
46+
$this->assertTrue(Filesystem\is_file(root_path('.env')));
47+
48+
$file = Filesystem\read_file(root_path('.env'));
49+
$env = Dotenv::createImmutable(__DIR__)->parse($file);
50+
51+
$this->assertArrayHasKey('SIGNING_KEY', $env);
52+
$this->assertNotSame('abc', $env['SIGNING_KEY']);
53+
}
54+
55+
public function test_add_if_missing(): void
56+
{
57+
Filesystem\create_file(root_path('.env'));
58+
$this->console->call(CreateSigningKeyCommand::class)->assertSuccess();
59+
$this->assertTrue(Filesystem\is_file(root_path('.env')));
60+
61+
$file = Filesystem\read_file(root_path('.env'));
62+
$env = Dotenv::createImmutable(__DIR__)->parse($file);
63+
64+
$this->assertArrayHasKey('SIGNING_KEY', $env);
65+
$this->assertIsString($env['SIGNING_KEY']);
66+
}
67+
}

tests/Integration/Cryptography/EncrypterTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function test_using_config(): void
2626
{
2727
$this->container->config(new EncryptionConfig(
2828
algorithm: EncryptionAlgorithm::AES_256_GCM,
29-
key: $key = EncryptionKey::generate(EncryptionAlgorithm::AES_256_GCM),
29+
key: $key = EncryptionKey::generate(EncryptionAlgorithm::AES_256_GCM)->toString(),
3030
));
3131

3232
$this->container->config(new SigningConfig(

0 commit comments

Comments
 (0)