Skip to content

Commit e3dfc0c

Browse files
committed
feat(http)!: automatically encrypt cookies
1 parent 20a2858 commit e3dfc0c

File tree

19 files changed

+170
-43
lines changed

19 files changed

+170
-43
lines changed

packages/core/src/Kernel/LoadConfig.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public function find(): array
4545
$suffixes = [
4646
'production' => ['.production.config.php', '.prod.config.php', '.prd.config.php'],
4747
'staging' => ['.staging.config.php', '.stg.config.php'],
48-
'testing' => ['.test.config.php'],
48+
'testing' => ['.test.config.php', '.testing.config.php'],
4949
'development' => ['.dev.config.php', '.local.config.php'],
5050
];
5151

packages/cryptography/src/CreateSigningKeyCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ private function addToDotEnv(string $key): void
4444
$file = Filesystem\read_file($this->getDotEnvPath());
4545

4646
if (! Str\contains($file, 'SIGNING_KEY=')) {
47-
$file .= "\nSIGNING_KEY={$key}\n";
47+
$file = "SIGNING_KEY={$key}\n" . $file;
4848
} else {
4949
$file = Regex\replace($file, '/^SIGNING_KEY=.*$/m', "SIGNING_KEY={$key}");
5050
}

packages/cryptography/src/Encryption/EncryptedData.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ public function serialize(): string
2727
'algorithm' => $this->algorithm->value,
2828
];
2929

30-
return base64_encode(Json\encode($data));
30+
return Json\encode($data, base64: true);
3131
}
3232

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

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

packages/cryptography/src/Encryption/EncryptionConfig.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ final class EncryptionConfig
66
{
77
/**
88
* @param EncryptionAlgorithm $algorithm The algorithm used for encrypting and decrypting values.
9-
* @param non-empty-string $key A private, secure encryption key.
9+
* @param null|non-empty-string $key A private, secure encryption key.
1010
*/
1111
public function __construct(
1212
public EncryptionAlgorithm $algorithm,

packages/cryptography/src/Signing/SigningConfig.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ final class SigningConfig
88
{
99
/**
1010
* @param SigningAlgorithm $algorithm The algorithm used for signing and verifying signatures.
11-
* @param non-empty-string $key The key used for signing and verifying signatures.
11+
* @param null|non-empty-string $key The key used for signing and verifying signatures.
1212
* @param Duration|false $minimumExecutionDuration The minimum execution duration for signing operations, to prevent timing attacks. Set `false` to disable timing attack protection.
1313
*/
1414
public function __construct(
1515
public SigningAlgorithm $algorithm,
1616
#[\SensitiveParameter]
17-
public string $key,
17+
public readonly ?string $key,
1818
public false|Duration $minimumExecutionDuration,
1919
) {}
2020
}

packages/cryptography/src/Signing/SigningKey.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ public function __construct(
1818
/**
1919
* Creates a signing key from a string.
2020
*/
21-
public static function fromString(string $key): self
21+
public static function fromString(?string $key): self
2222
{
23-
return new self($key);
23+
return new self($key ?: '');
2424
}
2525

2626
public function __toString(): string

packages/cryptography/src/Signing/signing.config.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55

66
return new SigningConfig(
77
algorithm: SigningAlgorithm::SHA256,
8-
key: Tempest\env('SIGNING_KEY', default: ''),
8+
key: Tempest\env('SIGNING_KEY'),
99
minimumExecutionDuration: false,
1010
);

packages/http/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"tempest/console": "dev-main",
1111
"tempest/mapper": "dev-main",
1212
"tempest/container": "dev-main",
13+
"tempest/cryptography": "dev-main",
1314
"laminas/laminas-diactoros": "^3.3",
1415
"psr/http-factory": "^1.0",
1516
"psr/http-message": "^1.0|^2.0",

packages/http/src/Cookie/Cookie.php

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

55
namespace Tempest\Http\Cookie;
66

7+
use InvalidArgumentException;
78
use Stringable;
89
use Tempest\DateTime\DateTimeInterface;
910

@@ -32,6 +33,14 @@ public function __construct(
3233
public ?SameSite $sameSite = null,
3334
) {}
3435

36+
public function withValue(string $value): self
37+
{
38+
$clone = clone $this;
39+
$clone->value = $value;
40+
41+
return $clone;
42+
}
43+
3544
public function __toString(): string
3645
{
3746
$parts = [
@@ -78,4 +87,49 @@ public function getExpiresAtTime(): ?int
7887

7988
return null;
8089
}
90+
91+
/**
92+
* Creates acookie from the `Set-Cookie` header.
93+
*/
94+
public static function createFromString(string $string): self
95+
{
96+
if (! ($attributes = preg_split('/\s*;\s*/', $string, -1, PREG_SPLIT_NO_EMPTY))) {
97+
throw new InvalidArgumentException(sprintf('The raw value of the `Set Cookie` header `%s` could not be parsed.', $string));
98+
}
99+
100+
$nameAndValue = explode('=', array_shift($attributes), 2);
101+
$cookie = ['name' => $nameAndValue[0], 'value' => isset($nameAndValue[1]) ? urldecode($nameAndValue[1]) : ''];
102+
103+
while ($attribute = array_shift($attributes)) {
104+
$attribute = explode('=', $attribute, 2);
105+
$attributeName = strtolower($attribute[0]);
106+
$attributeValue = $attribute[1] ?? null;
107+
108+
if (in_array($attributeName, ['expires', 'domain', 'path', 'samesite'], true)) {
109+
$cookie[$attributeName] = $attributeValue;
110+
continue;
111+
}
112+
113+
if (in_array($attributeName, ['secure', 'httponly'], true)) {
114+
$cookie[$attributeName] = true;
115+
continue;
116+
}
117+
118+
if ($attributeName === 'max-age') {
119+
$cookie['expires'] = time() + ((int) $attributeValue);
120+
}
121+
}
122+
123+
return new Cookie(
124+
key: $cookie['name'],
125+
value: $cookie['value'] ?? null,
126+
expiresAt: isset($cookie['expires']) ? ((int) $cookie['expires']) : null,
127+
maxAge: isset($cookie['max-age']) ? ((int) $cookie['max-age']) : null,
128+
domain: $cookie['domain'] ?? null,
129+
path: $cookie['path'] ?? '/',
130+
secure: isset($cookie['secure']) && $cookie['secure'] === true,
131+
httpOnly: isset($cookie['httponly']) && $cookie['httponly'] === true,
132+
sameSite: isset($cookie['samesite']) ? SameSite::from($cookie['samesite']) : null,
133+
);
134+
}
81135
}

packages/http/src/Mappers/PsrRequestToGenericRequestMapper.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
88
use Psr\Http\Message\UploadedFileInterface;
9+
use Tempest\Cryptography\Encryption\Encrypter;
910
use Tempest\Http\Cookie\Cookie;
1011
use Tempest\Http\GenericRequest;
1112
use Tempest\Http\Method;
@@ -19,6 +20,10 @@
1920

2021
final readonly class PsrRequestToGenericRequestMapper implements Mapper
2122
{
23+
public function __construct(
24+
private readonly Encrypter $encrypter,
25+
) {}
26+
2227
public function canMap(mixed $from, mixed $to): bool
2328
{
2429
return false;
@@ -55,7 +60,13 @@ public function map(mixed $from, mixed $to): GenericRequest
5560
'path' => $from->getUri()->getPath(),
5661
'query' => $query,
5762
'files' => $uploads,
58-
'cookies' => Arr\map_iterable($_COOKIE, static fn (string $value, string $key) => new Cookie($key, $value)),
63+
'cookies' => Arr\map_iterable(
64+
array: $_COOKIE,
65+
map: fn (string $value, string $key) => new Cookie(
66+
key: $key,
67+
value: $this->encrypter->decrypt($value),
68+
),
69+
),
5970
...$data,
6071
...$uploads,
6172
])

0 commit comments

Comments
 (0)