Passed
Push — master ( 35232d...8d0b59 )
by Melech
06:16 queued 02:04
created

SodiumCrypt   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 271
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 24
eloc 46
dl 0
loc 271
rs 10
c 1
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A validatePlainDecoded() 0 4 2
A decrypt() 0 8 1
A isValidDecodedType() 0 3 1
A validateDecodedType() 0 4 2
A isValidDecodedStrLen() 0 3 1
A encryptObject() 0 3 1
A getDecoded() 0 9 1
A __construct() 0 3 1
A validateDecodedStrLen() 0 4 2
A getKeyAsBytes() 0 11 2
A validateDecoded() 0 5 1
A isValidPlainDecoded() 0 3 1
A decryptObject() 0 3 1
A encrypt() 0 10 1
A getDecodedPlain() 0 17 2
A encryptArray() 0 3 1
A isValidEncryptedMessage() 0 11 2
A decryptArray() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Crypt;
15
16
use Exception;
17
use JsonException;
18
use SodiumException;
19
use Valkyrja\Crypt\Contract\Crypt;
20
use Valkyrja\Crypt\Exception\CryptException;
21
use Valkyrja\Type\BuiltIn\Support\Arr;
22
use Valkyrja\Type\BuiltIn\Support\Obj;
23
24
use function bin2hex;
25
use function hex2bin;
26
use function random_bytes;
27
use function sodium_crypto_secretbox;
28
use function sodium_crypto_secretbox_open;
29
use function sodium_memzero;
30
31
use const SODIUM_CRYPTO_SECRETBOX_MACBYTES;
32
use const SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
33
34
/**
35
 * Class SodiumCrypt.
36
 *
37
 * @author Melech Mizrachi
38
 */
39
class SodiumCrypt implements Crypt
40
{
41
    /**
42
     * @param non-empty-string $key The key
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
43
     */
44
    public function __construct(
45
        protected string $key,
46
    ) {
47
    }
48
49
    /**
50
     * @inheritDoc
51
     */
52
    public function isValidEncryptedMessage(string $encrypted): bool
53
    {
54
        try {
55
            $this->getDecoded($encrypted);
56
57
            return true;
58
        } catch (CryptException) {
59
            // Left empty to default to false
60
        }
61
62
        return false;
63
    }
64
65
    /**
66
     * @inheritDoc
67
     *
68
     * @throws Exception
69
     * @throws SodiumException
70
     */
71
    public function encrypt(string $message, string|null $key = null): string
72
    {
73
        $key    = $this->getKeyAsBytes($key);
74
        $nonce  = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
75
        $cipher = bin2hex($nonce . sodium_crypto_secretbox($message, $nonce, $key));
76
77
        sodium_memzero($message);
78
        sodium_memzero($key);
79
80
        return $cipher;
81
    }
82
83
    /**
84
     * @inheritDoc
85
     *
86
     * @throws JsonException
87
     * @throws SodiumException
88
     */
89
    public function encryptArray(array $array, string|null $key = null): string
90
    {
91
        return $this->encrypt(Arr::toString($array), $key);
92
    }
93
94
    /**
95
     * @inheritDoc
96
     *
97
     * @throws JsonException
98
     * @throws SodiumException
99
     */
100
    public function encryptObject(object $object, string|null $key = null): string
101
    {
102
        return $this->encrypt(Obj::toString($object), $key);
103
    }
104
105
    /**
106
     * @inheritDoc
107
     *
108
     * @throws SodiumException
109
     */
110
    public function decrypt(string $encrypted, string|null $key = null): string
111
    {
112
        $key   = $this->getKeyAsBytes($key);
113
        $plain = $this->getDecodedPlain($this->getDecoded($encrypted), $key);
114
115
        sodium_memzero($key);
116
117
        return $plain;
118
    }
119
120
    /**
121
     * @inheritDoc
122
     *
123
     * @throws JsonException
124
     * @throws SodiumException
125
     */
126
    public function decryptArray(string $encrypted, string|null $key = null): array
127
    {
128
        return Arr::fromString($this->decrypt($encrypted, $key));
129
    }
130
131
    /**
132
     * @inheritDoc
133
     *
134
     * @throws JsonException
135
     * @throws SodiumException
136
     */
137
    public function decryptObject(string $encrypted, string|null $key = null): object
138
    {
139
        return Obj::fromString($this->decrypt($encrypted, $key));
140
    }
141
142
    /**
143
     * Get a decoded encrypted message.
144
     *
145
     * @param string $encrypted The encrypted message
146
     *
147
     * @throws CryptException
148
     *
149
     * @return string
150
     */
151
    protected function getDecoded(string $encrypted): string
152
    {
153
        $decoded = hex2bin($encrypted);
154
155
        $this->validateDecoded($decoded);
156
157
        /** @var string $decoded Checked in validateDecoded */
158
159
        return $decoded;
160
    }
161
162
    /**
163
     * Validate a decoded encrypted message.
164
     *
165
     * @param string|false $decoded
166
     *
167
     * @throws CryptException
168
     *
169
     * @return void
170
     */
171
    protected function validateDecoded(string|false $decoded): void
172
    {
173
        $this->validateDecodedType($decoded);
174
        /** @var string $decoded */
175
        $this->validateDecodedStrLen($decoded);
176
    }
177
178
    /**
179
     * Validate a decoded encrypted message type.
180
     *
181
     * @param string|false $decoded
182
     *
183
     * @throws CryptException
184
     *
185
     * @return void
186
     */
187
    protected function validateDecodedType(string|false $decoded): void
188
    {
189
        if (! $this->isValidDecodedType($decoded)) {
190
            throw new CryptException('The encoding failed');
191
        }
192
    }
193
194
    /**
195
     * Check if a decoded encrypted message is a valid type.
196
     *
197
     * @param string|false $decoded
198
     *
199
     * @return bool
200
     */
201
    protected function isValidDecodedType(string|false $decoded): bool
202
    {
203
        return $decoded !== false;
204
    }
205
206
    /**
207
     * Validate a decoded encrypted message string length.
208
     *
209
     * @param string $decoded
210
     *
211
     * @throws CryptException
212
     *
213
     * @return void
214
     */
215
    protected function validateDecodedStrLen(string $decoded): void
216
    {
217
        if (! $this->isValidDecodedStrLen($decoded)) {
218
            throw new CryptException('The message was truncated');
219
        }
220
    }
221
222
    /**
223
     * Validate a decoded encrypted message string length.
224
     *
225
     * @param string $decoded
226
     *
227
     * @return bool
228
     */
229
    protected function isValidDecodedStrLen(string $decoded): bool
230
    {
231
        return mb_strlen($decoded, '8bit') > (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES);
232
    }
233
234
    /**
235
     * Get plain text from decoded encrypted string.
236
     *
237
     * @param string      $decoded The decoded encrypted message
238
     * @param string|null $key     The encryption key
239
     *
240
     * @throws CryptException
241
     * @throws SodiumException
242
     *
243
     * @return string
244
     */
245
    protected function getDecodedPlain(string $decoded, string|null $key = null): string
246
    {
247
        if ($key === null) {
248
            throw new CryptException("Invalid ky `$key` provided");
249
        }
250
251
        $nonce      = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
252
        $cipherText = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
253
254
        $plain = sodium_crypto_secretbox_open($cipherText, $nonce, $key);
255
256
        $this->validatePlainDecoded($plain);
257
258
        /** @var string $plain */
259
        sodium_memzero($cipherText);
260
261
        return $plain;
262
    }
263
264
    /**
265
     * Validate a plain text encrypted message.
266
     *
267
     * @param bool|string $plain
268
     *
269
     * @throws CryptException
270
     *
271
     * @return void
272
     */
273
    protected function validatePlainDecoded(bool|string $plain): void
274
    {
275
        if (! $this->isValidPlainDecoded($plain)) {
276
            throw new CryptException('The message was tampered with in transit');
277
        }
278
    }
279
280
    /**
281
     * Validate a plain text encrypted message.
282
     *
283
     * @param bool|string $plain
284
     *
285
     * @return bool
286
     */
287
    protected function isValidPlainDecoded(bool|string $plain): bool
288
    {
289
        return $plain !== false;
290
    }
291
292
    /**
293
     * Get a key as bytes.
294
     *
295
     * @param string|null $key [optional] The key
296
     *
297
     * @return string
298
     */
299
    protected function getKeyAsBytes(string|null $key = null): string
300
    {
301
        $key ??= $this->key;
302
303
        $keyAsBytes = hex2bin($key);
304
305
        if ($keyAsBytes === false) {
306
            throw new CryptException("$key could not be converted to bytes");
307
        }
308
309
        return $keyAsBytes;
310
    }
311
}
312