• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

azjezz / psl / 22639242943

03 Mar 2026 07:30PM UTC coverage: 97.792% (-0.4%) from 98.201%
22639242943

Pull #607

github

azjezz
feat: introduce `Crypto` component

Signed-off-by: azjezz <azjezz@protonmail.com>
Pull Request #607: feat: introduce `Crypto` component

348 of 394 new or added lines in 49 files covered. (88.32%)

2 existing lines in 1 file now uncovered.

9302 of 9512 relevant lines covered (97.79%)

39.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.07
/src/Psl/Crypto/Symmetric/StreamEncryptor.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Psl\Crypto\Symmetric;
6

7
use Psl\Crypto\Exception;
8
use Psl\Crypto\Internal;
9
use Psl\IO;
10
use Psl\Str\Byte;
11
use SensitiveParameter;
12

13
use function pack;
14
use function sodium_crypto_secretstream_xchacha20poly1305_init_pull;
15
use function sodium_crypto_secretstream_xchacha20poly1305_init_push;
16
use function sodium_crypto_secretstream_xchacha20poly1305_pull;
17
use function sodium_crypto_secretstream_xchacha20poly1305_push;
18
use function unpack;
19

20
use const SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL;
21
use const SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE;
22

23
final readonly class StreamEncryptor implements StreamEncryptorInterface
24
{
25
    public function __construct(
26
        #[SensitiveParameter]
27
        private Key $key,
28
    ) {}
12✔
29

30
    /**
31
     * {@inheritDoc}
32
     */
33
    public function copySealed(
34
        IO\ReadHandleInterface $source,
35
        IO\WriteHandleInterface $destination,
36
        int $chunkSize = 8192,
37
    ): void {
38
        /** @var array{string, string} $init */
39
        $init = Internal\call_sodium(fn() => sodium_crypto_secretstream_xchacha20poly1305_init_push($this->key->bytes));
12✔
40
        [$state, $header] = $init;
12✔
41

42
        $destination->writeAll($header);
12✔
43
        $destination->writeAll(pack('V', $chunkSize));
12✔
44

45
        while (!$source->reachedEndOfDataSource()) {
12✔
46
            $chunk = $source->read($chunkSize);
12✔
47
            if ($chunk === '') {
12✔
48
                break;
2✔
49
            }
50

51
            $isLast = $source->reachedEndOfDataSource() || Byte\length($chunk) < $chunkSize;
11✔
52
            $tag = $isLast
11✔
53
                ? SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL
11✔
54
                : SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE;
5✔
55

56
            $encrypted = Internal\call_sodium(static function () use (&$state, $chunk, $tag): string {
11✔
57
                return sodium_crypto_secretstream_xchacha20poly1305_push($state, $chunk, '', $tag);
11✔
58
            });
11✔
59
            $destination->writeAll(pack('V', Byte\length($encrypted)));
11✔
60
            $destination->writeAll($encrypted);
11✔
61

62
            if ($isLast) {
11✔
63
                return;
11✔
64
            }
65
        }
66

67
        $encrypted = Internal\call_sodium(static function () use (&$state): string {
2✔
68
            return sodium_crypto_secretstream_xchacha20poly1305_push(
2✔
69
                $state,
2✔
70
                '',
2✔
71
                '',
2✔
72
                SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL,
2✔
73
            );
2✔
74
        });
2✔
75
        $destination->writeAll(pack('V', Byte\length($encrypted)));
2✔
76
        $destination->writeAll($encrypted);
2✔
77
    }
78

79
    /**
80
     * {@inheritDoc}
81
     */
82
    public function copyOpened(IO\ReadHandleInterface $source, IO\WriteHandleInterface $destination): void
83
    {
84
        $header = $source->readFixedSize(namespace\STREAM_HEADER_BYTES);
11✔
85
        $chunkSizeBytes = $source->readFixedSize(4);
11✔
86
        /** @var int $chunkSize */
87
        $chunkSize = unpack('V', $chunkSizeBytes)[1];
11✔
88

89
        if ($chunkSize < 1 || $chunkSize > namespace\MAX_CHUNK_BYTES) {
11✔
NEW
90
            throw new Exception\DecryptionException('Invalid chunk size in stream header.');
×
91
        }
92

93
        $state = Internal\call_sodium(fn() => sodium_crypto_secretstream_xchacha20poly1305_init_pull(
11✔
94
            $header,
11✔
95
            $this->key->bytes,
11✔
96
        ));
11✔
97

98
        while (!$source->reachedEndOfDataSource()) {
11✔
99
            $lengthBytes = $source->read(4);
11✔
100
            if ($lengthBytes === '') {
11✔
NEW
101
                break;
×
102
            }
103

104
            if (Byte\length($lengthBytes) < 4) {
11✔
105
                /** @var positive-int $remaining */
NEW
106
                $remaining = 4 - Byte\length($lengthBytes);
×
107
                try {
NEW
108
                    $lengthBytes .= $source->readFixedSize($remaining);
×
NEW
109
                } catch (IO\Exception\RuntimeException $e) {
×
NEW
110
                    throw new Exception\DecryptionException(
×
NEW
111
                        'Stream decryption failed: truncated frame header.',
×
NEW
112
                        previous: $e,
×
NEW
113
                    );
×
114
                }
115
            }
116

117
            /** @var positive-int $frameSize */
118
            $frameSize = unpack('V', $lengthBytes)[1];
11✔
119

120
            if ($frameSize > ($chunkSize + namespace\STREAM_TAG_BYTES)) {
11✔
121
                throw new Exception\DecryptionException('Invalid frame size in stream.');
1✔
122
            }
123

124
            try {
125
                $chunk = $source->readFixedSize($frameSize);
10✔
126
            } catch (IO\Exception\RuntimeException $e) {
1✔
127
                throw new Exception\DecryptionException('Stream decryption failed: truncated frame.', previous: $e);
1✔
128
            }
129

130
            /** @var array{string, int}|false $result */
131
            $result = Internal\call_sodium(static function () use (&$state, $chunk): array|false {
9✔
132
                return sodium_crypto_secretstream_xchacha20poly1305_pull($state, $chunk);
9✔
133
            });
9✔
134
            if ($result === false) {
9✔
135
                throw new Exception\DecryptionException('Stream decryption failed.');
1✔
136
            }
137

138
            [$plaintext, $tag] = $result;
8✔
139
            $destination->writeAll($plaintext);
8✔
140

141
            if ($tag === SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL) {
8✔
142
                return;
8✔
143
            }
144
        }
145

NEW
146
        throw new Exception\DecryptionException('Stream ended without final tag.');
×
147
    }
148
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc