Skip to content

Commit b1bf382

Browse files
committed
feat: add encryption
1 parent 96ae9b9 commit b1bf382

19 files changed

+591
-16
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Encryption;
4+
5+
use Stringable;
6+
use Tempest\Cryptography\Encryption\Exceptions\EncryptedDataWasInvalid;
7+
use Tempest\Cryptography\Signing\Signature;
8+
use Tempest\Support\Json;
9+
10+
final readonly class EncryptedData implements Stringable
11+
{
12+
public function __construct(
13+
private(set) string $payload,
14+
private(set) string $iv,
15+
private(set) string $tag,
16+
private(set) Signature $signature,
17+
private(set) EncryptionAlgorithm $algorithm,
18+
) {}
19+
20+
public function serialize(): string
21+
{
22+
$data = [
23+
'payload' => base64_encode($this->payload),
24+
'iv' => base64_encode($this->iv),
25+
'tag' => base64_encode($this->tag),
26+
'signature' => $this->signature->value,
27+
'algorithm' => $this->algorithm->value,
28+
];
29+
30+
return base64_encode(Json\encode($data));
31+
}
32+
33+
public static function unserialize(string $data): self
34+
{
35+
$decoded = Json\decode(base64_decode($data));
36+
37+
if (! is_array($decoded) || ! isset($decoded['payload'], $decoded['iv'], $decoded['tag'], $decoded['signature'], $decoded['algorithm'])) {
38+
throw EncryptedDataWasInvalid::dueToInvalidFormat();
39+
}
40+
41+
return new self(
42+
payload: base64_decode($decoded['payload']),
43+
iv: base64_decode($decoded['iv']),
44+
tag: base64_decode($decoded['tag']),
45+
signature: new Signature($decoded['signature']),
46+
algorithm: EncryptionAlgorithm::from($decoded['algorithm']),
47+
);
48+
}
49+
50+
public function __toString(): string
51+
{
52+
return $this->serialize();
53+
}
54+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Encryption;
4+
5+
interface Encrypter
6+
{
7+
public EncryptionAlgorithm $algorithm {
8+
get;
9+
}
10+
11+
/**
12+
* Encrypts the specified data.
13+
*/
14+
public function encrypt(#[\SensitiveParameter] string $data): EncryptedData;
15+
16+
/**
17+
* Decrypts the specified data.
18+
*/
19+
public function decrypt(string|EncryptedData $data): string;
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Encryption;
4+
5+
use Tempest\Container\Container;
6+
use Tempest\Container\Initializer;
7+
use Tempest\Container\Singleton;
8+
use Tempest\Cryptography\Signing\Signer;
9+
10+
final class EncrypterInitializer implements Initializer
11+
{
12+
#[Singleton]
13+
public function initialize(Container $container): Encrypter
14+
{
15+
return new GenericEncrypter(
16+
signer: $container->get(Signer::class),
17+
config: $container->get(EncryptionConfig::class),
18+
);
19+
}
20+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Encryption;
4+
5+
enum EncryptionAlgorithm: string
6+
{
7+
case AES_256_GCM = 'aes-256-gcm';
8+
case AES_256_CBC = 'aes-256-cbc';
9+
case AES_128_GCM = 'aes-128-gcm';
10+
case AES_128_CBC = 'aes-128-cbc';
11+
case CHACHA20_POLY1305 = 'chacha20-poly1305';
12+
13+
public function getKeyLength(): int
14+
{
15+
return openssl_cipher_key_length($this->value);
16+
}
17+
18+
public function getIvLength(): int
19+
{
20+
return openssl_cipher_iv_length($this->value);
21+
}
22+
23+
public function isAead(): bool
24+
{
25+
return match ($this) {
26+
self::AES_256_GCM, self::AES_128_GCM, self::CHACHA20_POLY1305 => true,
27+
self::AES_256_CBC, self::AES_128_CBC => false,
28+
};
29+
}
30+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Encryption;
4+
5+
final class EncryptionConfig
6+
{
7+
/**
8+
* @param EncryptionAlgorithm $algorithm The algorithm used for encrypting and decrypting values.
9+
* @param non-empty-string $key A private, secure encryption key.
10+
*/
11+
public function __construct(
12+
public EncryptionAlgorithm $algorithm,
13+
#[\SensitiveParameter]
14+
public string $key,
15+
) {}
16+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Encryption;
4+
5+
use Stringable;
6+
use Tempest\Cryptography\Encryption\Exceptions\EncryptionKeyWasInvalid;
7+
8+
final readonly class EncryptionKey implements Stringable
9+
{
10+
public function __construct(
11+
private(set) string $value,
12+
private(set) EncryptionAlgorithm $algorithm,
13+
) {
14+
if (trim($value) === '') {
15+
throw EncryptionKeyWasInvalid::becauseItIsMissing($algorithm);
16+
}
17+
18+
if (strlen($value) !== $algorithm->getKeyLength()) {
19+
throw EncryptionKeyWasInvalid::becauseLengthMismatched($algorithm);
20+
}
21+
}
22+
23+
/**
24+
* Generates a new cryptographically secure key using the specified algorithm.
25+
*/
26+
public static function generate(EncryptionAlgorithm $algorithm): self
27+
{
28+
return new self(random_bytes($algorithm->getKeyLength()), $algorithm);
29+
}
30+
31+
/**
32+
* Creates an encryption key from a string.
33+
*/
34+
public static function fromString(string $key, EncryptionAlgorithm $algorithm): self
35+
{
36+
return new self($key, $algorithm);
37+
}
38+
39+
public function __toString(): string
40+
{
41+
return $this->value;
42+
}
43+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Encryption\Exceptions;
4+
5+
use Exception;
6+
7+
final class AlgorithmMismatched extends Exception implements EncryptionException
8+
{
9+
public static function betweenKeyAndData(): self
10+
{
11+
return new self('The encryption algorithm used for the key does not match the algorithm used for the data. Ensure that both are using the same algorithm.');
12+
}
13+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Encryption\Exceptions;
4+
5+
use Exception;
6+
use Tempest\Core\HasContext;
7+
8+
final class DecryptionFailed extends Exception implements EncryptionException, HasContext
9+
{
10+
public function __construct(
11+
string $message,
12+
private readonly array $context = [],
13+
) {
14+
parent::__construct($message);
15+
}
16+
17+
public static function becauseOpenSslFailed(string $error): self
18+
{
19+
return new self('OpenSSL encryption failed.', ['error' => $error]);
20+
}
21+
22+
public function context(): array
23+
{
24+
return $this->context;
25+
}
26+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Encryption\Exceptions;
4+
5+
use Exception;
6+
7+
final class EncryptedDataWasInvalid extends Exception implements EncryptionException
8+
{
9+
public static function dueToInvalidFormat(): self
10+
{
11+
return new self('The encrypted data is not in the expected format.');
12+
}
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Tempest\Cryptography\Encryption\Exceptions;
4+
5+
interface EncryptionException
6+
{
7+
}

0 commit comments

Comments
 (0)