• 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

98.46
/src/Psl/Crypto/StreamCipher/Context.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Psl\Crypto\StreamCipher;
6

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

14
use function openssl_encrypt;
15
use function sodium_crypto_stream_xchacha20_xor_ic;
16

17
use const OPENSSL_RAW_DATA;
18
use const OPENSSL_ZERO_PADDING;
19

20
/**
21
 * Stream cipher context that maintains a keystream buffer across calls.
22
 *
23
 * This solves the partial-block problem by buffering unused keystream bytes
24
 * so that subsequent calls continue correctly from where the previous one left off.
25
 */
26
final class Context
27
{
28
    private string $iv;
29
    private string $keystreamBuffer = '';
30
    private int $chachaCounter = 0;
31
    /** @var positive-int */
32
    private readonly int $blockSize;
33

34
    /**
35
     * @throws Exception\InvalidArgumentException If the key length does not match the algorithm requirements.
36
     * @throws Exception\RuntimeException If the IV length is invalid.
37
     */
38
    public function __construct(
39
        #[SensitiveParameter]
40
        private readonly Key $key,
41
        #[SensitiveParameter]
42
        string $iv,
43
        private readonly Algorithm $algorithm,
44
    ) {
45
        $this->iv = $iv;
20✔
46

47
        $this->blockSize = match ($algorithm) {
20✔
48
            Algorithm::Aes256Ctr, Algorithm::Aes128Ctr => namespace\AES_CTR_BLOCK_BYTES,
20✔
49
            Algorithm::XChaCha20 => namespace\XCHACHA20_BLOCK_BYTES,
8✔
50
        };
20✔
51

52
        $expectedKeySize = match ($algorithm) {
20✔
53
            Algorithm::Aes256Ctr => namespace\AES_256_KEY_BYTES,
20✔
54
            Algorithm::Aes128Ctr => namespace\AES_128_KEY_BYTES,
10✔
55
            Algorithm::XChaCha20 => namespace\XCHACHA20_KEY_BYTES,
8✔
56
        };
20✔
57

58
        if (Byte\length($key->bytes) !== $expectedKeySize) {
20✔
59
            throw new Exception\InvalidArgumentException('Key size does not match algorithm requirements.');
2✔
60
        }
61

62
        $expectedIvSize = match ($algorithm) {
18✔
63
            Algorithm::Aes256Ctr, Algorithm::Aes128Ctr => namespace\AES_CTR_IV_BYTES,
18✔
64
            Algorithm::XChaCha20 => namespace\XCHACHA20_IV_BYTES,
8✔
65
        };
18✔
66

67
        if (Byte\length($iv) !== $expectedIvSize) {
18✔
68
            throw new Exception\RuntimeException('IV size does not match algorithm requirements.');
2✔
69
        }
70
    }
71

72
    /**
73
     * XOR data with the keystream, maintaining buffer continuity.
74
     *
75
     * @throws Exception\RuntimeException If keystream generation fails.
76
     */
77
    public function apply(#[SensitiveParameter] string $data): string
78
    {
79
        $needed = Byte\length($data);
16✔
80
        $result = '';
16✔
81
        $offset = 0;
16✔
82

83
        while ($needed > 0) {
16✔
84
            if ($this->keystreamBuffer === '') {
15✔
85
                $this->keystreamBuffer = $this->generateKeystreamBlock();
15✔
86
            }
87

88
            $available = Byte\length($this->keystreamBuffer);
15✔
89
            $use = Math\minva($needed, $available);
15✔
90

91
            $dataChunk = Byte\slice($data, $offset, $use);
15✔
92
            $keyChunk = Byte\slice($this->keystreamBuffer, 0, $use);
15✔
93

94
            /**
95
             * @mago-expect analysis:invalid-operand,invalid-operand - mago does not like string ^ string
96
             * @var string $xored
97
             */
98
            $xored = $dataChunk ^ $keyChunk;
15✔
99
            $result .= $xored;
15✔
100

101
            $this->keystreamBuffer = Byte\slice($this->keystreamBuffer, $use);
15✔
102
            $offset += $use;
15✔
103
            $needed -= $use;
15✔
104
        }
105

106
        return $result;
16✔
107
    }
108

109
    /**
110
     * @throws Exception\RuntimeException If keystream generation fails.
111
     */
112
    private function generateKeystreamBlock(): string
113
    {
114
        return match ($this->algorithm) {
15✔
115
            Algorithm::Aes256Ctr => $this->generateAesCtrBlock('aes-256-ctr'),
15✔
116
            Algorithm::Aes128Ctr => $this->generateAesCtrBlock('aes-128-ctr'),
8✔
117
            Algorithm::XChaCha20 => $this->generateXChaCha20Block(),
15✔
118
        };
15✔
119
    }
120

121
    /**
122
     * @throws Exception\RuntimeException If AES-CTR keystream generation fails.
123
     */
124
    private function generateAesCtrBlock(string $cipher): string
125
    {
126
        $zeros = Str\repeat("\x00", $this->blockSize);
8✔
127
        $keystream = openssl_encrypt(
8✔
128
            $zeros,
8✔
129
            $cipher,
8✔
130
            $this->key->bytes,
8✔
131
            OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
8✔
132
            $this->iv,
8✔
133
        );
8✔
134

135
        if ($keystream === false) {
8✔
NEW
136
            throw new Exception\RuntimeException('AES-CTR keystream generation failed.');
×
137
        }
138

139
        $this->advanceAesCtrIv();
8✔
140

141
        return $keystream;
8✔
142
    }
143

144
    /**
145
     * Increment the 128-bit IV (big-endian counter) for AES-CTR.
146
     */
147
    private function advanceAesCtrIv(): void
148
    {
149
        for ($i = namespace\AES_CTR_IV_BYTES - 1; $i >= 0; $i--) {
8✔
150
            $val = Byte\ord($this->iv[$i]) + 1;
8✔
151
            $this->iv[$i] = Byte\chr($val & 0xff);
8✔
152
            if ($val < 256) {
8✔
153
                break;
8✔
154
            }
155
        }
156
    }
157

158
    /**
159
     * @throws Exception\RuntimeException If XChaCha20 keystream generation fails.
160
     */
161
    private function generateXChaCha20Block(): string
162
    {
163
        $zeros = Str\repeat("\x00", $this->blockSize);
7✔
164
        $keystream = Internal\call_sodium(fn() => sodium_crypto_stream_xchacha20_xor_ic(
7✔
165
            $zeros,
7✔
166
            $this->iv,
7✔
167
            $this->chachaCounter,
7✔
168
            $this->key->bytes,
7✔
169
        ));
7✔
170

171
        $this->chachaCounter++;
7✔
172

173
        return $keystream;
7✔
174
    }
175
}
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