Test Failed
Push — develop ( 266ee2...ce70fb )
by J.D.
05:51
created

ParagonIE_Sodium_File   D

Complexity

Total Complexity 130

Size/Duplication

Total Lines 1072
Duplicated Lines 23.6 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 253
loc 1072
rs 4.4192
c 0
b 0
f 0
wmc 130
lcom 1
cbo 7

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ParagonIE_Sodium_File often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ParagonIE_Sodium_File, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
if (class_exists('ParagonIE_Sodium_File', false)) {
4
    return;
5
}
6
/**
7
 * Class ParagonIE_Sodium_File
8
 */
9
class ParagonIE_Sodium_File extends ParagonIE_Sodium_Core_Util
10
{
11
    /* PHP's default buffer size is 8192 for fread()/fwrite(). */
12
    const BUFFER_SIZE = 8192;
13
14
    /**
15
     * Box a file (rather than a string). Uses less memory than
16
     * ParagonIE_Sodium_Compat::crypto_box(), but produces
17
     * the same result.
18
     *
19
     * @param string $inputFile  Absolute path to a file on the filesystem
20
     * @param string $outputFile Absolute path to a file on the filesystem
21
     * @param string $nonce      Number to be used only once
22
     * @param string $keyPair    ECDH secret key and ECDH public key concatenated
23
     *
24
     * @return bool
25
     * @throws Error
26
     * @throws TypeError
27
     */
28
    public static function box($inputFile, $outputFile, $nonce, $keyPair)
29
    {
30
        /* Type checks: */
31
        if (!is_string($inputFile)) {
32
            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
33
        }
34
        if (!is_string($outputFile)) {
35
            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
36
        }
37
        if (!is_string($nonce)) {
38
            throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
39
        }
40
41
        /* Input validation: */
42
        if (!is_string($keyPair)) {
43
            throw new TypeError('Argument 4 must be a string, ' . gettype($keyPair) . ' given.');
44
        }
45
        if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_NONCEBYTES) {
46
            throw new TypeError('Argument 3 must be CRYPTO_BOX_NONCEBYTES bytes');
47
        }
48
        if (self::strlen($keyPair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) {
49
            throw new TypeError('Argument 4 must be CRYPTO_BOX_KEYPAIRBYTES bytes');
50
        }
51
52
        /** @var int $size */
53
        $size = filesize($inputFile);
54
        if (!is_int($size)) {
55
            throw new Error('Could not obtain the file size');
56
        }
57
58
        /** @var resource $ifp */
59
        $ifp = fopen($inputFile, 'rb');
60
        if (!is_resource($ifp)) {
61
            throw new Error('Could not open input file for reading');
62
        }
63
64
        /** @var resource $ofp */
65
        $ofp = fopen($outputFile, 'wb');
66
        if (!is_resource($ofp)) {
67
            fclose($ifp);
68
            throw new Error('Could not open output file for writing');
69
        }
70
71
        $res = self::box_encrypt($ifp, $ofp, $size, $nonce, $keyPair);
72
        fclose($ifp);
73
        fclose($ofp);
74
        return $res;
75
    }
76
77
    /**
78
     * Open a boxed file (rather than a string). Uses less memory than
79
     * ParagonIE_Sodium_Compat::crypto_box_open(), but produces
80
     * the same result.
81
     *
82
     * Warning: Does not protect against TOCTOU attacks. You should
83
     * just load the file into memory and use crypto_box_open() if
84
     * you are worried about those.
85
     *
86
     * @param string $inputFile
87
     * @param string $outputFile
88
     * @param string $nonce
89
     * @param string $keypair
90
     * @return bool
91
     * @throws Error
92
     * @throws TypeError
93
     */
94
    public static function box_open($inputFile, $outputFile, $nonce, $keypair)
95
    {
96
        /* Type checks: */
97
        if (!is_string($inputFile)) {
98
            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
99
        }
100
        if (!is_string($outputFile)) {
101
            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
102
        }
103
        if (!is_string($nonce)) {
104
            throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
105
        }
106
        if (!is_string($keypair)) {
107
            throw new TypeError('Argument 4 must be a string, ' . gettype($keypair) . ' given.');
108
        }
109
110
        /* Input validation: */
111
        if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_NONCEBYTES) {
112
            throw new TypeError('Argument 4 must be CRYPTO_BOX_NONCEBYTES bytes');
113
        }
114
        if (self::strlen($keypair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) {
115
            throw new TypeError('Argument 4 must be CRYPTO_BOX_KEYPAIRBYTES bytes');
116
        }
117
118
        /** @var int $size */
119
        $size = filesize($inputFile);
120
        if (!is_int($size)) {
121
            throw new Error('Could not obtain the file size');
122
        }
123
124
        /** @var resource $ifp */
125
        $ifp = fopen($inputFile, 'rb');
126
        if (!is_resource($ifp)) {
127
            throw new Error('Could not open input file for reading');
128
        }
129
130
        /** @var resource $ofp */
131
        $ofp = fopen($outputFile, 'wb');
132
        if (!is_resource($ofp)) {
133
            fclose($ifp);
134
            throw new Error('Could not open output file for writing');
135
        }
136
137
        $res = self::box_decrypt($ifp, $ofp, $size, $nonce, $keypair);
138
        fclose($ifp);
139
        fclose($ofp);
140
        try {
141
            ParagonIE_Sodium_Compat::memzero($nonce);
142
            ParagonIE_Sodium_Compat::memzero($ephKeypair);
143
        } catch (Error $ex) {
144
            unset($ephKeypair);
145
        }
146
        return $res;
147
    }
148
149
    /**
150
     * Seal a file (rather than a string). Uses less memory than
151
     * ParagonIE_Sodium_Compat::crypto_box_seal(), but produces
152
     * the same result.
153
     *
154
     * @param string $inputFile  Absolute path to a file on the filesystem
155
     * @param string $outputFile Absolute path to a file on the filesystem
156
     * @param string $publicKey  ECDH public key
157
     *
158
     * @return bool
159
     * @throws Error
160
     * @throws TypeError
161
     */
162
    public static function box_seal($inputFile, $outputFile, $publicKey)
163
    {
164
        /* Type checks: */
165
        if (!is_string($inputFile)) {
166
            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
167
        }
168
        if (!is_string($outputFile)) {
169
            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
170
        }
171
        if (!is_string($publicKey)) {
172
            throw new TypeError('Argument 3 must be a string, ' . gettype($publicKey) . ' given.');
173
        }
174
175
        /* Input validation: */
176
        if (self::strlen($publicKey) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) {
177
            throw new TypeError('Argument 3 must be CRYPTO_BOX_PUBLICKEYBYTES bytes');
178
        }
179
180
        /** @var int $size */
181
        $size = filesize($inputFile);
182
        if (!is_int($size)) {
183
            throw new Error('Could not obtain the file size');
184
        }
185
186
        /** @var resource $ifp */
187
        $ifp = fopen($inputFile, 'rb');
188
        if (!is_resource($ifp)) {
189
            throw new Error('Could not open input file for reading');
190
        }
191
192
        /** @var resource $ofp */
193
        $ofp = fopen($outputFile, 'wb');
194
        if (!is_resource($ofp)) {
195
            fclose($ifp);
196
            throw new Error('Could not open output file for writing');
197
        }
198
199
        /** @var string $ephKeypair */
200
        $ephKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair();
201
202
        /** @var string $msgKeypair */
203
        $msgKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair_from_secretkey_and_publickey(
204
            ParagonIE_Sodium_Compat::crypto_box_secretkey($ephKeypair),
205
            $publicKey
206
        );
207
208
        /** @var string $ephemeralPK */
209
        $ephemeralPK = ParagonIE_Sodium_Compat::crypto_box_publickey($ephKeypair);
210
211
        /** @var string $nonce */
212
        $nonce = ParagonIE_Sodium_Compat::crypto_generichash(
213
            $ephemeralPK . $publicKey,
214
            '',
215
            24
216
        );
217
218
        /** @var int $firstWrite */
219
        $firstWrite = fwrite(
220
            $ofp,
221
            $ephemeralPK,
222
            ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES
223
        );
224
        if (!is_int($firstWrite)) {
225
            fclose($ifp);
226
            fclose($ofp);
227
            ParagonIE_Sodium_Compat::memzero($ephKeypair);
228
            throw new Error('Could not write to output file');
229
        }
230
        if ($firstWrite !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) {
231
            ParagonIE_Sodium_Compat::memzero($ephKeypair);
232
            fclose($ifp);
233
            fclose($ofp);
234
            throw new Error('Error writing public key to output file');
235
        }
236
237
        $res = self::box_encrypt($ifp, $ofp, $size, $nonce, $msgKeypair);
238
        fclose($ifp);
239
        fclose($ofp);
240
        try {
241
            ParagonIE_Sodium_Compat::memzero($nonce);
242
            ParagonIE_Sodium_Compat::memzero($ephKeypair);
243
        } catch (Error $ex) {
244
            unset($ephKeypair);
245
        }
246
        return $res;
247
    }
248
249
    /**
250
     * Open a sealed file (rather than a string). Uses less memory than
251
     * ParagonIE_Sodium_Compat::crypto_box_seal_open(), but produces
252
     * the same result.
253
     *
254
     * Warning: Does not protect against TOCTOU attacks. You should
255
     * just load the file into memory and use crypto_box_seal_open() if
256
     * you are worried about those.
257
     *
258
     * @param string $inputFile
259
     * @param string $outputFile
260
     * @param string $ecdhKeypair
261
     * @return bool
262
     * @throws Error
263
     * @throws TypeError
264
     */
265
    public static function box_seal_open($inputFile, $outputFile, $ecdhKeypair)
266
    {
267
        /* Type checks: */
268
        if (!is_string($inputFile)) {
269
            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
270
        }
271
        if (!is_string($outputFile)) {
272
            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
273
        }
274
        if (!is_string($ecdhKeypair)) {
275
            throw new TypeError('Argument 3 must be a string, ' . gettype($ecdhKeypair) . ' given.');
276
        }
277
278
        /* Input validation: */
279
        if (self::strlen($ecdhKeypair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) {
280
            throw new TypeError('Argument 3 must be CRYPTO_BOX_KEYPAIRBYTES bytes');
281
        }
282
283
        $publicKey = ParagonIE_Sodium_Compat::crypto_box_publickey($ecdhKeypair);
284
285
        /** @var int $size */
286
        $size = filesize($inputFile);
287
        if (!is_int($size)) {
288
            throw new Error('Could not obtain the file size');
289
        }
290
291
        /** @var resource $ifp */
292
        $ifp = fopen($inputFile, 'rb');
293
        if (!is_resource($ifp)) {
294
            throw new Error('Could not open input file for reading');
295
        }
296
297
        /** @var resource $ofp */
298
        $ofp = fopen($outputFile, 'wb');
299
        if (!is_resource($ofp)) {
300
            fclose($ifp);
301
            throw new Error('Could not open output file for writing');
302
        }
303
304
        $ephemeralPK = fread($ifp, ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES);
305
        if (!is_string($ephemeralPK)) {
306
            throw new Error('Could not read input file');
307
        }
308
        if (self::strlen($ephemeralPK) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) {
309
            fclose($ifp);
310
            fclose($ofp);
311
            throw new Error('Could not read public key from sealed file');
312
        }
313
314
        $nonce = ParagonIE_Sodium_Compat::crypto_generichash(
315
            $ephemeralPK . $publicKey,
316
            '',
317
            24
318
        );
319
        $msgKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair_from_secretkey_and_publickey(
320
            ParagonIE_Sodium_Compat::crypto_box_secretkey($ecdhKeypair),
321
            $ephemeralPK
322
        );
323
324
        $res = self::box_decrypt($ifp, $ofp, $size, $nonce, $msgKeypair);
325
        fclose($ifp);
326
        fclose($ofp);
327
        try {
328
            ParagonIE_Sodium_Compat::memzero($nonce);
329
            ParagonIE_Sodium_Compat::memzero($ephKeypair);
330
        } catch (Error $ex) {
331
            unset($ephKeypair);
332
        }
333
        return $res;
334
    }
335
336
    /**
337
     * Calculate the BLAKE2b hash of a file.
338
     *
339
     * @param string      $filePath     Absolute path to a file on the filesystem
340
     * @param string|null $key          BLAKE2b key
341
     * @param int         $outputLength Length of hash output
342
     *
343
     * @return string                   BLAKE2b hash
344
     * @throws Error
345
     * @throws TypeError
346
     * @psalm-suppress FailedTypeResolution
347
     */
348
    public static function generichash($filePath, $key = '', $outputLength = 32)
349
    {
350
        /* Type checks: */
351
        if (!is_string($filePath)) {
352
            throw new TypeError('Argument 1 must be a string, ' . gettype($filePath) . ' given.');
353
        }
354
        if (!is_string($key)) {
355
            if (is_null($key)) {
356
                $key = '';
357
            } else {
358
                throw new TypeError('Argument 2 must be a string, ' . gettype($key) . ' given.');
359
            }
360
        }
361
        if (!is_int($outputLength)) {
362
            if (!is_numeric($outputLength)) {
363
                throw new TypeError('Argument 3 must be an integer, ' . gettype($outputLength) . ' given.');
364
            }
365
            $outputLength = (int) $outputLength;
366
        }
367
368
        /* Input validation: */
369
        if (!empty($key)) {
370
            if (self::strlen($key) < ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_KEYBYTES_MIN) {
371
                throw new TypeError('Argument 2 must be at least CRYPTO_GENERICHASH_KEYBYTES_MIN bytes');
372
            }
373
            if (self::strlen($key) > ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_KEYBYTES_MAX) {
374
                throw new TypeError('Argument 2 must be at most CRYPTO_GENERICHASH_KEYBYTES_MAX bytes');
375
            }
376
        }
377
        if ($outputLength < ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_BYTES_MIN) {
378
            throw new Error('Argument 3 must be at least CRYPTO_GENERICHASH_BYTES_MIN');
379
        }
380
        if ($outputLength > ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_BYTES_MAX) {
381
            throw new Error('Argument 3 must be at least CRYPTO_GENERICHASH_BYTES_MAX');
382
        }
383
384
        /** @var int $size */
385
        $size = filesize($filePath);
386
        if (!is_int($size)) {
387
            throw new Error('Could not obtain the file size');
388
        }
389
390
        /** @var resource $fp */
391
        $fp = fopen($filePath, 'rb');
392
        if (!is_resource($fp)) {
393
            throw new Error('Could not open input file for reading');
394
        }
395
        $ctx = ParagonIE_Sodium_Compat::crypto_generichash_init($key, $outputLength);
396
        while ($size > 0) {
397
            $blockSize = $size > 64
398
                ? 64
399
                : $size;
400
            $read = fread($fp, $blockSize);
401
            if (!is_string($read)) {
402
                throw new Error('Could not read input file');
403
            }
404
            ParagonIE_Sodium_Compat::crypto_generichash_update($ctx, $read);
405
            $size -= $blockSize;
406
        }
407
408
        fclose($fp);
409
        return ParagonIE_Sodium_Compat::crypto_generichash_final($ctx, $outputLength);
410
    }
411
412
    /**
413
     * Encrypt a file (rather than a string). Uses less memory than
414
     * ParagonIE_Sodium_Compat::crypto_secretbox(), but produces
415
     * the same result.
416
     *
417
     * @param string $inputFile  Absolute path to a file on the filesystem
418
     * @param string $outputFile Absolute path to a file on the filesystem
419
     * @param string $nonce      Number to be used only once
420
     * @param string $key        Encryption key
421
     *
422
     * @return bool
423
     * @throws Error
424
     * @throws TypeError
425
     */
426
    public static function secretbox($inputFile, $outputFile, $nonce, $key)
427
    {
428
        /* Type checks: */
429
        if (!is_string($inputFile)) {
430
            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given..');
431
        }
432
        if (!is_string($outputFile)) {
433
            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
434
        }
435
        if (!is_string($nonce)) {
436
            throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
437
        }
438
439
        /* Input validation: */
440
        if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_NONCEBYTES) {
441
            throw new TypeError('Argument 3 must be CRYPTO_SECRETBOX_NONCEBYTES bytes');
442
        }
443
        if (!is_string($key)) {
444
            throw new TypeError('Argument 4 must be a string, ' . gettype($key) . ' given.');
445
        }
446
        if (self::strlen($key) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_KEYBYTES) {
447
            throw new TypeError('Argument 4 must be CRYPTO_SECRETBOX_KEYBYTES bytes');
448
        }
449
450
        /** @var int $size */
451
        $size = filesize($inputFile);
452
        if (!is_int($size)) {
453
            throw new Error('Could not obtain the file size');
454
        }
455
456
        /** @var resource $ifp */
457
        $ifp = fopen($inputFile, 'rb');
458
        if (!is_resource($ifp)) {
459
            throw new Error('Could not open input file for reading');
460
        }
461
462
        /** @var resource $ofp */
463
        $ofp = fopen($outputFile, 'wb');
464
        if (!is_resource($ofp)) {
465
            fclose($ifp);
466
            throw new Error('Could not open output file for writing');
467
        }
468
469
        $res = self::secretbox_encrypt($ifp, $ofp, $size, $nonce, $key);
470
        fclose($ifp);
471
        fclose($ofp);
472
        return $res;
473
    }
474
    /**
475
     * Seal a file (rather than a string). Uses less memory than
476
     * ParagonIE_Sodium_Compat::crypto_secretbox_open(), but produces
477
     * the same result.
478
     *
479
     * Warning: Does not protect against TOCTOU attacks. You should
480
     * just load the file into memory and use crypto_secretbox_open() if
481
     * you are worried about those.
482
     *
483
     * @param string $inputFile
484
     * @param string $outputFile
485
     * @param string $nonce
486
     * @param string $key
487
     * @return bool
488
     * @throws Error
489
     * @throws TypeError
490
     */
491
    public static function secretbox_open($inputFile, $outputFile, $nonce, $key)
492
    {
493
        /* Type checks: */
494
        if (!is_string($inputFile)) {
495
            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
496
        }
497
        if (!is_string($outputFile)) {
498
            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
499
        }
500
        if (!is_string($nonce)) {
501
            throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
502
        }
503
        if (!is_string($key)) {
504
            throw new TypeError('Argument 4 must be a string, ' . gettype($key) . ' given.');
505
        }
506
507
        /* Input validation: */
508
        if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_NONCEBYTES) {
509
            throw new TypeError('Argument 4 must be CRYPTO_SECRETBOX_NONCEBYTES bytes');
510
        }
511
        if (self::strlen($key) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_KEYBYTES) {
512
            throw new TypeError('Argument 4 must be CRYPTO_SECRETBOXBOX_KEYBYTES bytes');
513
        }
514
515
        /** @var int $size */
516
        $size = filesize($inputFile);
517
        if (!is_int($size)) {
518
            throw new Error('Could not obtain the file size');
519
        }
520
521
        /** @var resource $ifp */
522
        $ifp = fopen($inputFile, 'rb');
523
        if (!is_resource($ifp)) {
524
            throw new Error('Could not open input file for reading');
525
        }
526
527
        /** @var resource $ofp */
528
        $ofp = fopen($outputFile, 'wb');
529
        if (!is_resource($ofp)) {
530
            fclose($ifp);
531
            throw new Error('Could not open output file for writing');
532
        }
533
534
        $res = self::secretbox_decrypt($ifp, $ofp, $size, $nonce, $key);
535
        fclose($ifp);
536
        fclose($ofp);
537
        try {
538
            ParagonIE_Sodium_Compat::memzero($key);
539
        } catch (Error $ex) {
540
            unset($key);
541
        }
542
        return $res;
543
    }
544
545
    /**
546
     * Sign a file (rather than a string). Uses less memory than
547
     * ParagonIE_Sodium_Compat::crypto_sign_detached(), but produces
548
     * the same result.
549
     *
550
     * @param string $filePath  Absolute path to a file on the filesystem
551
     * @param string $secretKey Secret signing key
552
     *
553
     * @return string           Ed25519 signature
554
     * @throws Error
555
     * @throws TypeError
556
     */
557
    public static function sign($filePath, $secretKey)
558
    {
559
        /* Type checks: */
560
        if (!is_string($filePath)) {
561
            throw new TypeError('Argument 1 must be a string, ' . gettype($filePath) . ' given.');
562
        }
563
        if (!is_string($secretKey)) {
564
            throw new TypeError('Argument 2 must be a string, ' . gettype($secretKey) . ' given.');
565
        }
566
567
        /* Input validation: */
568
        if (self::strlen($secretKey) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_SECRETKEYBYTES) {
569
            throw new TypeError('Argument 2 must be CRYPTO_SIGN_SECRETKEYBYTES bytes');
570
        }
571
572
        /** @var int $size */
573
        $size = filesize($filePath);
574
        if (!is_int($size)) {
575
            throw new Error('Could not obtain the file size');
576
        }
577
578
        /** @var resource $fp */
579
        $fp = fopen($filePath, 'rb');
580
        if (!is_resource($fp)) {
581
            throw new Error('Could not open input file for reading');
582
        }
583
584
        /** @var string $az */
585
        $az = hash('sha512', self::substr($secretKey, 0, 32), true);
586
587
        $az[0] = self::intToChr(self::chrToInt($az[0]) & 248);
588
        $az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64);
589
590
        /** @var resource $hs */
591
        $hs = hash_init('sha512');
592
        hash_update($hs, self::substr($az, 32, 32));
593
        $hs = self::updateHashWithFile($hs, $fp, $size);
594
595
        /** @var string $nonceHash */
596
        $nonceHash = hash_final($hs, true);
597
598
        /** @var string $pk */
599
        $pk = self::substr($secretKey, 32, 32);
600
601
        /** @var string $nonce */
602
        $nonce = ParagonIE_Sodium_Core_Ed25519::sc_reduce($nonceHash) . self::substr($nonceHash, 32);
603
604
        /** @var string $sig */
605
        $sig = ParagonIE_Sodium_Core_Ed25519::ge_p3_tobytes(
606
            ParagonIE_Sodium_Core_Ed25519::ge_scalarmult_base($nonce)
607
        );
608
609
        /** @var resource $hs */
610
        $hs = hash_init('sha512');
611
        hash_update($hs, self::substr($sig, 0, 32));
612
        hash_update($hs, self::substr($pk, 0, 32));
613
        $hs = self::updateHashWithFile($hs, $fp, $size);
614
615
        /** @var string $hramHash */
616
        $hramHash = hash_final($hs, true);
617
618
        /** @var string $hram */
619
        $hram = ParagonIE_Sodium_Core_Ed25519::sc_reduce($hramHash);
620
621
        /** @var string $sigAfter */
622
        $sigAfter = ParagonIE_Sodium_Core_Ed25519::sc_muladd($hram, $az, $nonce);
623
624
        /** @var string $sig */
625
        $sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32);
626
627
        try {
628
            ParagonIE_Sodium_Compat::memzero($az);
629
        } catch (Error $ex) {
630
            $az = null;
631
        }
632
        fclose($fp);
633
        return $sig;
634
    }
635
636
    /**
637
     * Verify a file (rather than a string). Uses less memory than
638
     * ParagonIE_Sodium_Compat::crypto_sign_verify_detached(), but
639
     * produces the same result.
640
     *
641
     * @param string $sig       Ed25519 signature
642
     * @param string $filePath  Absolute path to a file on the filesystem
643
     * @param string $publicKey Signing public key
644
     *
645
     * @return bool
646
     * @throws Error
647
     * @throws Exception
648
     */
649
    public static function verify($sig, $filePath, $publicKey)
650
    {
651
        /* Type checks: */
652
        if (!is_string($sig)) {
653
            throw new TypeError('Argument 1 must be a string, ' . gettype($sig) . ' given.');
654
        }
655
        if (!is_string($filePath)) {
656
            throw new TypeError('Argument 2 must be a string, ' . gettype($filePath) . ' given.');
657
        }
658
        if (!is_string($publicKey)) {
659
            throw new TypeError('Argument 3 must be a string, ' . gettype($publicKey) . ' given.');
660
        }
661
662
        /* Input validation: */
663
        if (self::strlen($sig) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_BYTES) {
664
            throw new TypeError('Argument 1 must be CRYPTO_SIGN_BYTES bytes');
665
        }
666
        if (self::strlen($publicKey) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_PUBLICKEYBYTES) {
667
            throw new TypeError('Argument 3 must be CRYPTO_SIGN_PUBLICKEYBYTES bytes');
668
        }
669
        if (self::strlen($sig) < 64) {
670
            throw new Exception('Signature is too short');
671
        }
672
673
        /* Security checks */
674
        if (ParagonIE_Sodium_Core_Ed25519::check_S_lt_L(self::substr($sig, 32, 32))) {
675
            throw new Exception('S < L - Invalid signature');
676
        }
677
        if (ParagonIE_Sodium_Core_Ed25519::small_order($sig)) {
678
            throw new Exception('Signature is on too small of an order');
679
        }
680
        if ((self::chrToInt($sig[63]) & 224) !== 0) {
681
            throw new Exception('Invalid signature');
682
        }
683
        $d = 0;
684
        for ($i = 0; $i < 32; ++$i) {
685
            $d |= self::chrToInt($publicKey[$i]);
686
        }
687
        if ($d === 0) {
688
            throw new Exception('All zero public key');
689
        }
690
691
        /** @var int $size */
692
        $size = filesize($filePath);
693
        if (!is_int($size)) {
694
            throw new Error('Could not obtain the file size');
695
        }
696
697
        /** @var resource $fp */
698
        $fp = fopen($filePath, 'rb');
699
        if (!is_resource($fp)) {
700
            throw new Error('Could not open input file for reading');
701
        }
702
703
        /** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */
704
        $orig = ParagonIE_Sodium_Compat::$fastMult;
705
706
        // Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification.
707
        ParagonIE_Sodium_Compat::$fastMult = true;
708
709
        /** @var ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A */
710
        $A = ParagonIE_Sodium_Core_Ed25519::ge_frombytes_negate_vartime($publicKey);
711
712
        /** @var resource $hs */
713
        $hs = hash_init('sha512');
714
        hash_update($hs, self::substr($sig, 0, 32));
715
        hash_update($hs, self::substr($publicKey, 0, 32));
716
        $hs = self::updateHashWithFile($hs, $fp, $size);
717
        /** @var string $hDigest */
718
        $hDigest = hash_final($hs, true);
719
720
        /** @var string $h */
721
        $h = ParagonIE_Sodium_Core_Ed25519::sc_reduce($hDigest) . self::substr($hDigest, 32);
722
723
        /** @var ParagonIE_Sodium_Core_Curve25519_Ge_P2 $R */
724
        $R = ParagonIE_Sodium_Core_Ed25519::ge_double_scalarmult_vartime(
725
            $h,
726
            $A,
727
            self::substr($sig, 32)
728
        );
729
730
        /** @var string $rcheck */
731
        $rcheck = ParagonIE_Sodium_Core_Ed25519::ge_tobytes($R);
732
733
        // Close the file handle
734
        fclose($fp);
735
736
        // Reset ParagonIE_Sodium_Compat::$fastMult to what it was before.
737
        ParagonIE_Sodium_Compat::$fastMult = $orig;
738
        return self::verify_32($rcheck, self::substr($sig, 0, 32));
739
    }
740
741
    /**
742
     * @param resource $ifp
743
     * @param resource $ofp
744
     * @param int      $mlen
745
     * @param string   $nonce
746
     * @param string   $boxKeypair
747
     * @return bool
748
     */
749
    protected static function box_encrypt($ifp, $ofp, $mlen, $nonce, $boxKeypair)
750
    {
751
        return self::secretbox_encrypt(
752
            $ifp,
753
            $ofp,
754
            $mlen,
755
            $nonce,
756
            ParagonIE_Sodium_Crypto::box_beforenm(
757
                ParagonIE_Sodium_Crypto::box_secretkey($boxKeypair),
758
                ParagonIE_Sodium_Crypto::box_publickey($boxKeypair)
759
            )
760
        );
761
    }
762
763
764
    /**
765
     * @param resource $ifp
766
     * @param resource $ofp
767
     * @param int      $mlen
768
     * @param string   $nonce
769
     * @param string   $boxKeypair
770
     * @return bool
771
     */
772
    protected static function box_decrypt($ifp, $ofp, $mlen, $nonce, $boxKeypair)
773
    {
774
        return self::secretbox_decrypt(
775
            $ifp,
776
            $ofp,
777
            $mlen,
778
            $nonce,
779
            ParagonIE_Sodium_Crypto::box_beforenm(
780
                ParagonIE_Sodium_Crypto::box_secretkey($boxKeypair),
781
                ParagonIE_Sodium_Crypto::box_publickey($boxKeypair)
782
            )
783
        );
784
    }
785
786
    /**
787
     * Encrypt a file
788
     *
789
     * @param resource $ifp
790
     * @param resource $ofp
791
     * @param int $mlen
792
     * @param string $nonce
793
     * @param string $key
794
     * @return bool
795
     * @throws Error
796
     */
797
    protected static function secretbox_encrypt($ifp, $ofp, $mlen, $nonce, $key)
798
    {
799
        $plaintext = fread($ifp, 32);
800
        if (!is_string($plaintext)) {
801
            throw new Error('Could not read input file');
802
        }
803
        $first32 = ftell($ifp);
804
805
        /** @var string $subkey */
806
        $subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key);
807
808
        /** @var string $realNonce */
809
        $realNonce = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);
810
811
        /** @var string $block0 */
812
        $block0 = str_repeat("\x00", 32);
813
814
        /** @var int $mlen - Length of the plaintext message */
815
        $mlen0 = $mlen;
816
        if ($mlen0 > 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES) {
817
            $mlen0 = 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES;
818
        }
819
        $block0 .= ParagonIE_Sodium_Core_Util::substr($plaintext, 0, $mlen0);
820
821
        /** @var string $block0 */
822
        $block0 = ParagonIE_Sodium_Core_Salsa20::salsa20_xor(
823
            $block0,
824
            $realNonce,
825
            $subkey
826
        );
827
828
        $state = new ParagonIE_Sodium_Core_Poly1305_State(
829
            ParagonIE_Sodium_Core_Util::substr(
830
                $block0,
831
                0,
832
                ParagonIE_Sodium_Crypto::onetimeauth_poly1305_KEYBYTES
833
            )
834
        );
835
836
        // Pre-write 16 blank bytes for the Poly1305 tag
837
        $start = ftell($ofp);
838
        fwrite($ofp, str_repeat("\x00", 16));
839
840
        /** @var string $c */
841
        $cBlock = ParagonIE_Sodium_Core_Util::substr(
842
            $block0,
843
            ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES
844
        );
845
        $state->update($cBlock);
846
        fwrite($ofp, $cBlock);
847
        $mlen -= 32;
848
849
        /** @var int $iter */
850
        $iter = 1;
851
852
        /** @var int $incr */
853
        $incr = self::BUFFER_SIZE >> 6;
854
855
        /*
856
         * Set the cursor to the end of the first half-block. All future bytes will
857
         * generated from salsa20_xor_ic, starting from 1 (second block).
858
         */
859
        fseek($ifp, $first32, SEEK_SET);
860
861
        while ($mlen > 0) {
862
            $blockSize = $mlen > self::BUFFER_SIZE
863
                ? self::BUFFER_SIZE
864
                : $mlen;
865
            $plaintext = fread($ifp, $blockSize);
866
            if (!is_string($plaintext)) {
867
                throw new Error('Could not read input file');
868
            }
869
            $cBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
870
                $plaintext,
871
                $realNonce,
872
                $iter,
873
                $subkey
874
            );
875
            fwrite($ofp, $cBlock, $blockSize);
876
            $state->update($cBlock);
877
878
            $mlen -= $blockSize;
879
            $iter += $incr;
880
        }
881
        try {
882
            ParagonIE_Sodium_Compat::memzero($block0);
883
            ParagonIE_Sodium_Compat::memzero($subkey);
884
        } catch (Error $ex) {
885
            $block0 = null;
886
            $subkey = null;
887
        }
888
        $end = ftell($ofp);
889
890
        /*
891
         * Write the Poly1305 authentication tag that provides integrity
892
         * over the ciphertext (encrypt-then-MAC)
893
         */
894
        fseek($ofp, $start, SEEK_SET);
895
        fwrite($ofp, $state->finish(), ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_MACBYTES);
896
        fseek($ofp, $end, SEEK_SET);
897
        unset($state);
898
899
        return true;
900
    }
901
902
    /**
903
     * Decrypt a file
904
     *
905
     * @param resource $ifp
906
     * @param resource $ofp
907
     * @param int $mlen
908
     * @param string $nonce
909
     * @param string $key
910
     * @return bool
911
     * @throws Error
912
     * @throws Exception
913
     */
914
    protected static function secretbox_decrypt($ifp, $ofp, $mlen, $nonce, $key)
915
    {
916
        $tag = fread($ifp, 16);
917
        if (!is_string($tag)) {
918
            throw new Error('Could not read input file');
919
        }
920
921
        /** @var string $subkey */
922
        $subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key);
923
924
        /** @var string $realNonce */
925
        $realNonce = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);
926
927
        /** @var string $block0 */
928
        $block0 = ParagonIE_Sodium_Core_Salsa20::salsa20(
929
            64,
930
            ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8),
931
            $subkey
932
        );
933
934
        /* Verify the Poly1305 MAC -before- attempting to decrypt! */
935
        $state = new ParagonIE_Sodium_Core_Poly1305_State(self::substr($block0, 0, 32));
936
        if (!self::onetimeauth_verify($state, $ifp, $tag, $mlen)) {
937
            throw new Exception('Invalid MAC');
938
        }
939
940
        /*
941
         * Set the cursor to the end of the first half-block. All future bytes will
942
         * generated from salsa20_xor_ic, starting from 1 (second block).
943
         */
944
        $first32 = fread($ifp, 32);
945
        if (!is_string($first32)) {
946
            throw new Error('Could not read input file');
947
        }
948
        $first32len = self::strlen($first32);
949
        fwrite(
950
            $ofp,
951
            self::xorStrings(
952
                self::substr($block0, 32, $first32len),
953
                self::substr($first32, 0, $first32len)
954
            )
955
        );
956
        $mlen -= 32;
957
958
        /** @var int $iter */
959
        $iter = 1;
960
961
        /** @var int $incr */
962
        $incr = self::BUFFER_SIZE >> 6;
963
964
        /* Decrypts ciphertext, writes to output file. */
965
        while ($mlen > 0) {
966
            $blockSize = $mlen > self::BUFFER_SIZE
967
                ? self::BUFFER_SIZE
968
                : $mlen;
969
            $ciphertext = fread($ifp, $blockSize);
970
            if (!is_string($ciphertext)) {
971
                throw new Error('Could not read input file');
972
            }
973
            $pBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
974
                $ciphertext,
975
                $realNonce,
976
                $iter,
977
                $subkey
978
            );
979
            fwrite($ofp, $pBlock, $blockSize);
980
            $mlen -= $blockSize;
981
            $iter += $incr;
982
        }
983
        return true;
984
    }
985
986
    /**
987
     * @param ParagonIE_Sodium_Core_Poly1305_State $state
988
     * @param resource $ifp
989
     * @param string $tag
990
     * @param int $mlen
991
     * @return bool
992
     * @throws Error
993
     */
994
    protected static function onetimeauth_verify(ParagonIE_Sodium_Core_Poly1305_State $state, $ifp, $tag = '', $mlen = 0)
995
    {
996
        /** @var int $pos */
997
        $pos = ftell($ifp);
998
999
        /** @var int $iter */
1000
        $iter = 1;
1001
1002
        /** @var int $incr */
1003
        $incr = self::BUFFER_SIZE >> 6;
1004
1005
        while ($mlen > 0) {
1006
            $blockSize = $mlen > self::BUFFER_SIZE
1007
                ? self::BUFFER_SIZE
1008
                : $mlen;
1009
            $ciphertext = fread($ifp, $blockSize);
1010
            if (!is_string($ciphertext)) {
1011
                throw new Error('Could not read input file');
1012
            }
1013
            $state->update($ciphertext);
1014
            $mlen -= $blockSize;
1015
            $iter += $incr;
1016
        }
1017
        $res = ParagonIE_Sodium_Core_Util::verify_16($tag, $state->finish());
1018
1019
        fseek($ifp, $pos, SEEK_SET);
1020
        return $res;
1021
    }
1022
1023
    /**
1024
     * Update a hash context with the contents of a file, without
1025
     * loading the entire file into memory.
1026
     *
1027
     * @param resource|object $hash
1028
     * @param resource $fp
1029
     * @param int $size
1030
     * @return mixed (resource on PHP < 7.2, object on PHP >= 7.2)
1031
     * @throws Error
1032
     * @throws TypeError
1033
     * @psalm-suppress PossiblyInvalidArgument
1034
     *                 PHP 7.2 changes from a resource to an object,
1035
     *                 which causes Psalm to complain about an error.
1036
     */
1037
    public static function updateHashWithFile($hash, $fp, $size = 0)
1038
    {
1039
        /* Type checks: */
1040
        if (PHP_VERSION_ID < 70200) {
1041
            if (!is_resource($hash)) {
1042
                throw new TypeError('Argument 1 must be a resource, ' . gettype($hash) . ' given.');
1043
            }
1044
1045
        } else {
1046
            if (!is_object($hash)) {
1047
                throw new TypeError('Argument 1 must be an object (PHP 7.2+), ' . gettype($hash) . ' given.');
1048
            }
1049
        }
1050
        if (!is_resource($fp)) {
1051
            throw new TypeError('Argument 2 must be a resource, ' . gettype($fp) . ' given.');
1052
        }
1053
        if (!is_int($size)) {
1054
            throw new TypeError('Argument 3 must be an integer, ' . gettype($size) . ' given.');
1055
        }
1056
1057
        /** @var int $originalPosition */
1058
        $originalPosition = ftell($fp);
1059
1060
        // Move file pointer to beginning of file
1061
        fseek($fp, 0, SEEK_SET);
1062
        for ($i = 0; $i < $size; $i += self::BUFFER_SIZE) {
1063
            /** @var string $message */
1064
            $message = fread(
1065
                $fp,
1066
                ($size - $i) > self::BUFFER_SIZE
1067
                    ? $size - $i
1068
                    : self::BUFFER_SIZE
1069
            );
1070
            if (!is_string($message)) {
1071
                throw new Error('Unexpected error reading from file.');
1072
            }
1073
            /** @psalm-suppress InvalidArgument */
1074
            hash_update($hash, $message);
1075
        }
1076
        // Reset file pointer's position
1077
        fseek($fp, $originalPosition, SEEK_SET);
1078
        return $hash;
1079
    }
1080
}
1081