1<?php
2
3if (class_exists('ParagonIE_Sodium_File', false)) {
4    return;
5}
6/**
7 * Class ParagonIE_Sodium_File
8 */
9class 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 SodiumException
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 SodiumException('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 SodiumException('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 SodiumException('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 SodiumException
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 SodiumException('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 SodiumException('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 SodiumException('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 (SodiumException $ex) {
144            if (isset($ephKeypair)) {
145                unset($ephKeypair);
146            }
147        }
148        return $res;
149    }
150
151    /**
152     * Seal a file (rather than a string). Uses less memory than
153     * ParagonIE_Sodium_Compat::crypto_box_seal(), but produces
154     * the same result.
155     *
156     * @param string $inputFile  Absolute path to a file on the filesystem
157     * @param string $outputFile Absolute path to a file on the filesystem
158     * @param string $publicKey  ECDH public key
159     *
160     * @return bool
161     * @throws SodiumException
162     * @throws TypeError
163     */
164    public static function box_seal($inputFile, $outputFile, $publicKey)
165    {
166        /* Type checks: */
167        if (!is_string($inputFile)) {
168            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
169        }
170        if (!is_string($outputFile)) {
171            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
172        }
173        if (!is_string($publicKey)) {
174            throw new TypeError('Argument 3 must be a string, ' . gettype($publicKey) . ' given.');
175        }
176
177        /* Input validation: */
178        if (self::strlen($publicKey) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) {
179            throw new TypeError('Argument 3 must be CRYPTO_BOX_PUBLICKEYBYTES bytes');
180        }
181
182        /** @var int $size */
183        $size = filesize($inputFile);
184        if (!is_int($size)) {
185            throw new SodiumException('Could not obtain the file size');
186        }
187
188        /** @var resource $ifp */
189        $ifp = fopen($inputFile, 'rb');
190        if (!is_resource($ifp)) {
191            throw new SodiumException('Could not open input file for reading');
192        }
193
194        /** @var resource $ofp */
195        $ofp = fopen($outputFile, 'wb');
196        if (!is_resource($ofp)) {
197            fclose($ifp);
198            throw new SodiumException('Could not open output file for writing');
199        }
200
201        /** @var string $ephKeypair */
202        $ephKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair();
203
204        /** @var string $msgKeypair */
205        $msgKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair_from_secretkey_and_publickey(
206            ParagonIE_Sodium_Compat::crypto_box_secretkey($ephKeypair),
207            $publicKey
208        );
209
210        /** @var string $ephemeralPK */
211        $ephemeralPK = ParagonIE_Sodium_Compat::crypto_box_publickey($ephKeypair);
212
213        /** @var string $nonce */
214        $nonce = ParagonIE_Sodium_Compat::crypto_generichash(
215            $ephemeralPK . $publicKey,
216            '',
217            24
218        );
219
220        /** @var int $firstWrite */
221        $firstWrite = fwrite(
222            $ofp,
223            $ephemeralPK,
224            ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES
225        );
226        if (!is_int($firstWrite)) {
227            fclose($ifp);
228            fclose($ofp);
229            ParagonIE_Sodium_Compat::memzero($ephKeypair);
230            throw new SodiumException('Could not write to output file');
231        }
232        if ($firstWrite !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) {
233            ParagonIE_Sodium_Compat::memzero($ephKeypair);
234            fclose($ifp);
235            fclose($ofp);
236            throw new SodiumException('Error writing public key to output file');
237        }
238
239        $res = self::box_encrypt($ifp, $ofp, $size, $nonce, $msgKeypair);
240        fclose($ifp);
241        fclose($ofp);
242        try {
243            ParagonIE_Sodium_Compat::memzero($nonce);
244            ParagonIE_Sodium_Compat::memzero($ephKeypair);
245        } catch (SodiumException $ex) {
246            /** @psalm-suppress PossiblyUndefinedVariable */
247            unset($ephKeypair);
248        }
249        return $res;
250    }
251
252    /**
253     * Open a sealed file (rather than a string). Uses less memory than
254     * ParagonIE_Sodium_Compat::crypto_box_seal_open(), but produces
255     * the same result.
256     *
257     * Warning: Does not protect against TOCTOU attacks. You should
258     * just load the file into memory and use crypto_box_seal_open() if
259     * you are worried about those.
260     *
261     * @param string $inputFile
262     * @param string $outputFile
263     * @param string $ecdhKeypair
264     * @return bool
265     * @throws SodiumException
266     * @throws TypeError
267     */
268    public static function box_seal_open($inputFile, $outputFile, $ecdhKeypair)
269    {
270        /* Type checks: */
271        if (!is_string($inputFile)) {
272            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
273        }
274        if (!is_string($outputFile)) {
275            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
276        }
277        if (!is_string($ecdhKeypair)) {
278            throw new TypeError('Argument 3 must be a string, ' . gettype($ecdhKeypair) . ' given.');
279        }
280
281        /* Input validation: */
282        if (self::strlen($ecdhKeypair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) {
283            throw new TypeError('Argument 3 must be CRYPTO_BOX_KEYPAIRBYTES bytes');
284        }
285
286        $publicKey = ParagonIE_Sodium_Compat::crypto_box_publickey($ecdhKeypair);
287
288        /** @var int $size */
289        $size = filesize($inputFile);
290        if (!is_int($size)) {
291            throw new SodiumException('Could not obtain the file size');
292        }
293
294        /** @var resource $ifp */
295        $ifp = fopen($inputFile, 'rb');
296        if (!is_resource($ifp)) {
297            throw new SodiumException('Could not open input file for reading');
298        }
299
300        /** @var resource $ofp */
301        $ofp = fopen($outputFile, 'wb');
302        if (!is_resource($ofp)) {
303            fclose($ifp);
304            throw new SodiumException('Could not open output file for writing');
305        }
306
307        $ephemeralPK = fread($ifp, ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES);
308        if (!is_string($ephemeralPK)) {
309            throw new SodiumException('Could not read input file');
310        }
311        if (self::strlen($ephemeralPK) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) {
312            fclose($ifp);
313            fclose($ofp);
314            throw new SodiumException('Could not read public key from sealed file');
315        }
316
317        $nonce = ParagonIE_Sodium_Compat::crypto_generichash(
318            $ephemeralPK . $publicKey,
319            '',
320            24
321        );
322        $msgKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair_from_secretkey_and_publickey(
323            ParagonIE_Sodium_Compat::crypto_box_secretkey($ecdhKeypair),
324            $ephemeralPK
325        );
326
327        $res = self::box_decrypt($ifp, $ofp, $size, $nonce, $msgKeypair);
328        fclose($ifp);
329        fclose($ofp);
330        try {
331            ParagonIE_Sodium_Compat::memzero($nonce);
332            ParagonIE_Sodium_Compat::memzero($ephKeypair);
333        } catch (SodiumException $ex) {
334            if (isset($ephKeypair)) {
335                unset($ephKeypair);
336            }
337        }
338        return $res;
339    }
340
341    /**
342     * Calculate the BLAKE2b hash of a file.
343     *
344     * @param string      $filePath     Absolute path to a file on the filesystem
345     * @param string|null $key          BLAKE2b key
346     * @param int         $outputLength Length of hash output
347     *
348     * @return string                   BLAKE2b hash
349     * @throws SodiumException
350     * @throws TypeError
351     * @psalm-suppress FailedTypeResolution
352     */
353    public static function generichash($filePath, $key = '', $outputLength = 32)
354    {
355        /* Type checks: */
356        if (!is_string($filePath)) {
357            throw new TypeError('Argument 1 must be a string, ' . gettype($filePath) . ' given.');
358        }
359        if (!is_string($key)) {
360            if (is_null($key)) {
361                $key = '';
362            } else {
363                throw new TypeError('Argument 2 must be a string, ' . gettype($key) . ' given.');
364            }
365        }
366        if (!is_int($outputLength)) {
367            if (!is_numeric($outputLength)) {
368                throw new TypeError('Argument 3 must be an integer, ' . gettype($outputLength) . ' given.');
369            }
370            $outputLength = (int) $outputLength;
371        }
372
373        /* Input validation: */
374        if (!empty($key)) {
375            if (self::strlen($key) < ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_KEYBYTES_MIN) {
376                throw new TypeError('Argument 2 must be at least CRYPTO_GENERICHASH_KEYBYTES_MIN bytes');
377            }
378            if (self::strlen($key) > ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_KEYBYTES_MAX) {
379                throw new TypeError('Argument 2 must be at most CRYPTO_GENERICHASH_KEYBYTES_MAX bytes');
380            }
381        }
382        if ($outputLength < ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_BYTES_MIN) {
383            throw new SodiumException('Argument 3 must be at least CRYPTO_GENERICHASH_BYTES_MIN');
384        }
385        if ($outputLength > ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_BYTES_MAX) {
386            throw new SodiumException('Argument 3 must be at least CRYPTO_GENERICHASH_BYTES_MAX');
387        }
388
389        /** @var int $size */
390        $size = filesize($filePath);
391        if (!is_int($size)) {
392            throw new SodiumException('Could not obtain the file size');
393        }
394
395        /** @var resource $fp */
396        $fp = fopen($filePath, 'rb');
397        if (!is_resource($fp)) {
398            throw new SodiumException('Could not open input file for reading');
399        }
400        $ctx = ParagonIE_Sodium_Compat::crypto_generichash_init($key, $outputLength);
401        while ($size > 0) {
402            $blockSize = $size > 64
403                ? 64
404                : $size;
405            $read = fread($fp, $blockSize);
406            if (!is_string($read)) {
407                throw new SodiumException('Could not read input file');
408            }
409            ParagonIE_Sodium_Compat::crypto_generichash_update($ctx, $read);
410            $size -= $blockSize;
411        }
412
413        fclose($fp);
414        return ParagonIE_Sodium_Compat::crypto_generichash_final($ctx, $outputLength);
415    }
416
417    /**
418     * Encrypt a file (rather than a string). Uses less memory than
419     * ParagonIE_Sodium_Compat::crypto_secretbox(), but produces
420     * the same result.
421     *
422     * @param string $inputFile  Absolute path to a file on the filesystem
423     * @param string $outputFile Absolute path to a file on the filesystem
424     * @param string $nonce      Number to be used only once
425     * @param string $key        Encryption key
426     *
427     * @return bool
428     * @throws SodiumException
429     * @throws TypeError
430     */
431    public static function secretbox($inputFile, $outputFile, $nonce, $key)
432    {
433        /* Type checks: */
434        if (!is_string($inputFile)) {
435            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given..');
436        }
437        if (!is_string($outputFile)) {
438            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
439        }
440        if (!is_string($nonce)) {
441            throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
442        }
443
444        /* Input validation: */
445        if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_NONCEBYTES) {
446            throw new TypeError('Argument 3 must be CRYPTO_SECRETBOX_NONCEBYTES bytes');
447        }
448        if (!is_string($key)) {
449            throw new TypeError('Argument 4 must be a string, ' . gettype($key) . ' given.');
450        }
451        if (self::strlen($key) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_KEYBYTES) {
452            throw new TypeError('Argument 4 must be CRYPTO_SECRETBOX_KEYBYTES bytes');
453        }
454
455        /** @var int $size */
456        $size = filesize($inputFile);
457        if (!is_int($size)) {
458            throw new SodiumException('Could not obtain the file size');
459        }
460
461        /** @var resource $ifp */
462        $ifp = fopen($inputFile, 'rb');
463        if (!is_resource($ifp)) {
464            throw new SodiumException('Could not open input file for reading');
465        }
466
467        /** @var resource $ofp */
468        $ofp = fopen($outputFile, 'wb');
469        if (!is_resource($ofp)) {
470            fclose($ifp);
471            throw new SodiumException('Could not open output file for writing');
472        }
473
474        $res = self::secretbox_encrypt($ifp, $ofp, $size, $nonce, $key);
475        fclose($ifp);
476        fclose($ofp);
477        return $res;
478    }
479    /**
480     * Seal a file (rather than a string). Uses less memory than
481     * ParagonIE_Sodium_Compat::crypto_secretbox_open(), but produces
482     * the same result.
483     *
484     * Warning: Does not protect against TOCTOU attacks. You should
485     * just load the file into memory and use crypto_secretbox_open() if
486     * you are worried about those.
487     *
488     * @param string $inputFile
489     * @param string $outputFile
490     * @param string $nonce
491     * @param string $key
492     * @return bool
493     * @throws SodiumException
494     * @throws TypeError
495     */
496    public static function secretbox_open($inputFile, $outputFile, $nonce, $key)
497    {
498        /* Type checks: */
499        if (!is_string($inputFile)) {
500            throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.');
501        }
502        if (!is_string($outputFile)) {
503            throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.');
504        }
505        if (!is_string($nonce)) {
506            throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.');
507        }
508        if (!is_string($key)) {
509            throw new TypeError('Argument 4 must be a string, ' . gettype($key) . ' given.');
510        }
511
512        /* Input validation: */
513        if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_NONCEBYTES) {
514            throw new TypeError('Argument 4 must be CRYPTO_SECRETBOX_NONCEBYTES bytes');
515        }
516        if (self::strlen($key) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_KEYBYTES) {
517            throw new TypeError('Argument 4 must be CRYPTO_SECRETBOXBOX_KEYBYTES bytes');
518        }
519
520        /** @var int $size */
521        $size = filesize($inputFile);
522        if (!is_int($size)) {
523            throw new SodiumException('Could not obtain the file size');
524        }
525
526        /** @var resource $ifp */
527        $ifp = fopen($inputFile, 'rb');
528        if (!is_resource($ifp)) {
529            throw new SodiumException('Could not open input file for reading');
530        }
531
532        /** @var resource $ofp */
533        $ofp = fopen($outputFile, 'wb');
534        if (!is_resource($ofp)) {
535            fclose($ifp);
536            throw new SodiumException('Could not open output file for writing');
537        }
538
539        $res = self::secretbox_decrypt($ifp, $ofp, $size, $nonce, $key);
540        fclose($ifp);
541        fclose($ofp);
542        try {
543            ParagonIE_Sodium_Compat::memzero($key);
544        } catch (SodiumException $ex) {
545            /** @psalm-suppress PossiblyUndefinedVariable */
546            unset($key);
547        }
548        return $res;
549    }
550
551    /**
552     * Sign a file (rather than a string). Uses less memory than
553     * ParagonIE_Sodium_Compat::crypto_sign_detached(), but produces
554     * the same result.
555     *
556     * @param string $filePath  Absolute path to a file on the filesystem
557     * @param string $secretKey Secret signing key
558     *
559     * @return string           Ed25519 signature
560     * @throws SodiumException
561     * @throws TypeError
562     */
563    public static function sign($filePath, $secretKey)
564    {
565        /* Type checks: */
566        if (!is_string($filePath)) {
567            throw new TypeError('Argument 1 must be a string, ' . gettype($filePath) . ' given.');
568        }
569        if (!is_string($secretKey)) {
570            throw new TypeError('Argument 2 must be a string, ' . gettype($secretKey) . ' given.');
571        }
572
573        /* Input validation: */
574        if (self::strlen($secretKey) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_SECRETKEYBYTES) {
575            throw new TypeError('Argument 2 must be CRYPTO_SIGN_SECRETKEYBYTES bytes');
576        }
577        if (PHP_INT_SIZE === 4) {
578            return self::sign_core32($filePath, $secretKey);
579        }
580
581        /** @var int $size */
582        $size = filesize($filePath);
583        if (!is_int($size)) {
584            throw new SodiumException('Could not obtain the file size');
585        }
586
587        /** @var resource $fp */
588        $fp = fopen($filePath, 'rb');
589        if (!is_resource($fp)) {
590            throw new SodiumException('Could not open input file for reading');
591        }
592
593        /** @var string $az */
594        $az = hash('sha512', self::substr($secretKey, 0, 32), true);
595
596        $az[0] = self::intToChr(self::chrToInt($az[0]) & 248);
597        $az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64);
598
599        $hs = hash_init('sha512');
600        self::hash_update($hs, self::substr($az, 32, 32));
601        /** @var resource $hs */
602        $hs = self::updateHashWithFile($hs, $fp, $size);
603
604        /** @var string $nonceHash */
605        $nonceHash = hash_final($hs, true);
606
607        /** @var string $pk */
608        $pk = self::substr($secretKey, 32, 32);
609
610        /** @var string $nonce */
611        $nonce = ParagonIE_Sodium_Core_Ed25519::sc_reduce($nonceHash) . self::substr($nonceHash, 32);
612
613        /** @var string $sig */
614        $sig = ParagonIE_Sodium_Core_Ed25519::ge_p3_tobytes(
615            ParagonIE_Sodium_Core_Ed25519::ge_scalarmult_base($nonce)
616        );
617
618        $hs = hash_init('sha512');
619        self::hash_update($hs, self::substr($sig, 0, 32));
620        self::hash_update($hs, self::substr($pk, 0, 32));
621        /** @var resource $hs */
622        $hs = self::updateHashWithFile($hs, $fp, $size);
623
624        /** @var string $hramHash */
625        $hramHash = hash_final($hs, true);
626
627        /** @var string $hram */
628        $hram = ParagonIE_Sodium_Core_Ed25519::sc_reduce($hramHash);
629
630        /** @var string $sigAfter */
631        $sigAfter = ParagonIE_Sodium_Core_Ed25519::sc_muladd($hram, $az, $nonce);
632
633        /** @var string $sig */
634        $sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32);
635
636        try {
637            ParagonIE_Sodium_Compat::memzero($az);
638        } catch (SodiumException $ex) {
639            $az = null;
640        }
641        fclose($fp);
642        return $sig;
643    }
644
645    /**
646     * Verify a file (rather than a string). Uses less memory than
647     * ParagonIE_Sodium_Compat::crypto_sign_verify_detached(), but
648     * produces the same result.
649     *
650     * @param string $sig       Ed25519 signature
651     * @param string $filePath  Absolute path to a file on the filesystem
652     * @param string $publicKey Signing public key
653     *
654     * @return bool
655     * @throws SodiumException
656     * @throws TypeError
657     * @throws Exception
658     */
659    public static function verify($sig, $filePath, $publicKey)
660    {
661        /* Type checks: */
662        if (!is_string($sig)) {
663            throw new TypeError('Argument 1 must be a string, ' . gettype($sig) . ' given.');
664        }
665        if (!is_string($filePath)) {
666            throw new TypeError('Argument 2 must be a string, ' . gettype($filePath) . ' given.');
667        }
668        if (!is_string($publicKey)) {
669            throw new TypeError('Argument 3 must be a string, ' . gettype($publicKey) . ' given.');
670        }
671
672        /* Input validation: */
673        if (self::strlen($sig) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_BYTES) {
674            throw new TypeError('Argument 1 must be CRYPTO_SIGN_BYTES bytes');
675        }
676        if (self::strlen($publicKey) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_PUBLICKEYBYTES) {
677            throw new TypeError('Argument 3 must be CRYPTO_SIGN_PUBLICKEYBYTES bytes');
678        }
679        if (self::strlen($sig) < 64) {
680            throw new SodiumException('Signature is too short');
681        }
682
683        if (PHP_INT_SIZE === 4) {
684            return self::verify_core32($sig, $filePath, $publicKey);
685        }
686
687        /* Security checks */
688        if (
689            (ParagonIE_Sodium_Core_Ed25519::chrToInt($sig[63]) & 240)
690                &&
691            ParagonIE_Sodium_Core_Ed25519::check_S_lt_L(self::substr($sig, 32, 32))
692        ) {
693            throw new SodiumException('S < L - Invalid signature');
694        }
695        if (ParagonIE_Sodium_Core_Ed25519::small_order($sig)) {
696            throw new SodiumException('Signature is on too small of an order');
697        }
698        if ((self::chrToInt($sig[63]) & 224) !== 0) {
699            throw new SodiumException('Invalid signature');
700        }
701        $d = 0;
702        for ($i = 0; $i < 32; ++$i) {
703            $d |= self::chrToInt($publicKey[$i]);
704        }
705        if ($d === 0) {
706            throw new SodiumException('All zero public key');
707        }
708
709        /** @var int $size */
710        $size = filesize($filePath);
711        if (!is_int($size)) {
712            throw new SodiumException('Could not obtain the file size');
713        }
714
715        /** @var resource $fp */
716        $fp = fopen($filePath, 'rb');
717        if (!is_resource($fp)) {
718            throw new SodiumException('Could not open input file for reading');
719        }
720
721        /** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */
722        $orig = ParagonIE_Sodium_Compat::$fastMult;
723
724        // Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification.
725        ParagonIE_Sodium_Compat::$fastMult = true;
726
727        /** @var ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A */
728        $A = ParagonIE_Sodium_Core_Ed25519::ge_frombytes_negate_vartime($publicKey);
729
730        $hs = hash_init('sha512');
731        self::hash_update($hs, self::substr($sig, 0, 32));
732        self::hash_update($hs, self::substr($publicKey, 0, 32));
733        /** @var resource $hs */
734        $hs = self::updateHashWithFile($hs, $fp, $size);
735        /** @var string $hDigest */
736        $hDigest = hash_final($hs, true);
737
738        /** @var string $h */
739        $h = ParagonIE_Sodium_Core_Ed25519::sc_reduce($hDigest) . self::substr($hDigest, 32);
740
741        /** @var ParagonIE_Sodium_Core_Curve25519_Ge_P2 $R */
742        $R = ParagonIE_Sodium_Core_Ed25519::ge_double_scalarmult_vartime(
743            $h,
744            $A,
745            self::substr($sig, 32)
746        );
747
748        /** @var string $rcheck */
749        $rcheck = ParagonIE_Sodium_Core_Ed25519::ge_tobytes($R);
750
751        // Close the file handle
752        fclose($fp);
753
754        // Reset ParagonIE_Sodium_Compat::$fastMult to what it was before.
755        ParagonIE_Sodium_Compat::$fastMult = $orig;
756        return self::verify_32($rcheck, self::substr($sig, 0, 32));
757    }
758
759    /**
760     * @param resource $ifp
761     * @param resource $ofp
762     * @param int      $mlen
763     * @param string   $nonce
764     * @param string   $boxKeypair
765     * @return bool
766     * @throws SodiumException
767     * @throws TypeError
768     */
769    protected static function box_encrypt($ifp, $ofp, $mlen, $nonce, $boxKeypair)
770    {
771        if (PHP_INT_SIZE === 4) {
772            return self::secretbox_encrypt(
773                $ifp,
774                $ofp,
775                $mlen,
776                $nonce,
777                ParagonIE_Sodium_Crypto32::box_beforenm(
778                    ParagonIE_Sodium_Crypto32::box_secretkey($boxKeypair),
779                    ParagonIE_Sodium_Crypto32::box_publickey($boxKeypair)
780                )
781            );
782        }
783        return self::secretbox_encrypt(
784            $ifp,
785            $ofp,
786            $mlen,
787            $nonce,
788            ParagonIE_Sodium_Crypto::box_beforenm(
789                ParagonIE_Sodium_Crypto::box_secretkey($boxKeypair),
790                ParagonIE_Sodium_Crypto::box_publickey($boxKeypair)
791            )
792        );
793    }
794
795
796    /**
797     * @param resource $ifp
798     * @param resource $ofp
799     * @param int      $mlen
800     * @param string   $nonce
801     * @param string   $boxKeypair
802     * @return bool
803     * @throws SodiumException
804     * @throws TypeError
805     */
806    protected static function box_decrypt($ifp, $ofp, $mlen, $nonce, $boxKeypair)
807    {
808        if (PHP_INT_SIZE === 4) {
809            return self::secretbox_decrypt(
810                $ifp,
811                $ofp,
812                $mlen,
813                $nonce,
814                ParagonIE_Sodium_Crypto32::box_beforenm(
815                    ParagonIE_Sodium_Crypto32::box_secretkey($boxKeypair),
816                    ParagonIE_Sodium_Crypto32::box_publickey($boxKeypair)
817                )
818            );
819        }
820        return self::secretbox_decrypt(
821            $ifp,
822            $ofp,
823            $mlen,
824            $nonce,
825            ParagonIE_Sodium_Crypto::box_beforenm(
826                ParagonIE_Sodium_Crypto::box_secretkey($boxKeypair),
827                ParagonIE_Sodium_Crypto::box_publickey($boxKeypair)
828            )
829        );
830    }
831
832    /**
833     * Encrypt a file
834     *
835     * @param resource $ifp
836     * @param resource $ofp
837     * @param int $mlen
838     * @param string $nonce
839     * @param string $key
840     * @return bool
841     * @throws SodiumException
842     * @throws TypeError
843     */
844    protected static function secretbox_encrypt($ifp, $ofp, $mlen, $nonce, $key)
845    {
846        if (PHP_INT_SIZE === 4) {
847            return self::secretbox_encrypt_core32($ifp, $ofp, $mlen, $nonce, $key);
848        }
849
850        $plaintext = fread($ifp, 32);
851        if (!is_string($plaintext)) {
852            throw new SodiumException('Could not read input file');
853        }
854        $first32 = self::ftell($ifp);
855
856        /** @var string $subkey */
857        $subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key);
858
859        /** @var string $realNonce */
860        $realNonce = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);
861
862        /** @var string $block0 */
863        $block0 = str_repeat("\x00", 32);
864
865        /** @var int $mlen - Length of the plaintext message */
866        $mlen0 = $mlen;
867        if ($mlen0 > 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES) {
868            $mlen0 = 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES;
869        }
870        $block0 .= ParagonIE_Sodium_Core_Util::substr($plaintext, 0, $mlen0);
871
872        /** @var string $block0 */
873        $block0 = ParagonIE_Sodium_Core_Salsa20::salsa20_xor(
874            $block0,
875            $realNonce,
876            $subkey
877        );
878
879        $state = new ParagonIE_Sodium_Core_Poly1305_State(
880            ParagonIE_Sodium_Core_Util::substr(
881                $block0,
882                0,
883                ParagonIE_Sodium_Crypto::onetimeauth_poly1305_KEYBYTES
884            )
885        );
886
887        // Pre-write 16 blank bytes for the Poly1305 tag
888        $start = self::ftell($ofp);
889        fwrite($ofp, str_repeat("\x00", 16));
890
891        /** @var string $c */
892        $cBlock = ParagonIE_Sodium_Core_Util::substr(
893            $block0,
894            ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES
895        );
896        $state->update($cBlock);
897        fwrite($ofp, $cBlock);
898        $mlen -= 32;
899
900        /** @var int $iter */
901        $iter = 1;
902
903        /** @var int $incr */
904        $incr = self::BUFFER_SIZE >> 6;
905
906        /*
907         * Set the cursor to the end of the first half-block. All future bytes will
908         * generated from salsa20_xor_ic, starting from 1 (second block).
909         */
910        fseek($ifp, $first32, SEEK_SET);
911
912        while ($mlen > 0) {
913            $blockSize = $mlen > self::BUFFER_SIZE
914                ? self::BUFFER_SIZE
915                : $mlen;
916            $plaintext = fread($ifp, $blockSize);
917            if (!is_string($plaintext)) {
918                throw new SodiumException('Could not read input file');
919            }
920            $cBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
921                $plaintext,
922                $realNonce,
923                $iter,
924                $subkey
925            );
926            fwrite($ofp, $cBlock, $blockSize);
927            $state->update($cBlock);
928
929            $mlen -= $blockSize;
930            $iter += $incr;
931        }
932        try {
933            ParagonIE_Sodium_Compat::memzero($block0);
934            ParagonIE_Sodium_Compat::memzero($subkey);
935        } catch (SodiumException $ex) {
936            $block0 = null;
937            $subkey = null;
938        }
939        $end = self::ftell($ofp);
940
941        /*
942         * Write the Poly1305 authentication tag that provides integrity
943         * over the ciphertext (encrypt-then-MAC)
944         */
945        fseek($ofp, $start, SEEK_SET);
946        fwrite($ofp, $state->finish(), ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_MACBYTES);
947        fseek($ofp, $end, SEEK_SET);
948        unset($state);
949
950        return true;
951    }
952
953    /**
954     * Decrypt a file
955     *
956     * @param resource $ifp
957     * @param resource $ofp
958     * @param int $mlen
959     * @param string $nonce
960     * @param string $key
961     * @return bool
962     * @throws SodiumException
963     * @throws TypeError
964     */
965    protected static function secretbox_decrypt($ifp, $ofp, $mlen, $nonce, $key)
966    {
967        if (PHP_INT_SIZE === 4) {
968            return self::secretbox_decrypt_core32($ifp, $ofp, $mlen, $nonce, $key);
969        }
970        $tag = fread($ifp, 16);
971        if (!is_string($tag)) {
972            throw new SodiumException('Could not read input file');
973        }
974
975        /** @var string $subkey */
976        $subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key);
977
978        /** @var string $realNonce */
979        $realNonce = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);
980
981        /** @var string $block0 */
982        $block0 = ParagonIE_Sodium_Core_Salsa20::salsa20(
983            64,
984            ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8),
985            $subkey
986        );
987
988        /* Verify the Poly1305 MAC -before- attempting to decrypt! */
989        $state = new ParagonIE_Sodium_Core_Poly1305_State(self::substr($block0, 0, 32));
990        if (!self::onetimeauth_verify($state, $ifp, $tag, $mlen)) {
991            throw new SodiumException('Invalid MAC');
992        }
993
994        /*
995         * Set the cursor to the end of the first half-block. All future bytes will
996         * generated from salsa20_xor_ic, starting from 1 (second block).
997         */
998        $first32 = fread($ifp, 32);
999        if (!is_string($first32)) {
1000            throw new SodiumException('Could not read input file');
1001        }
1002        $first32len = self::strlen($first32);
1003        fwrite(
1004            $ofp,
1005            self::xorStrings(
1006                self::substr($block0, 32, $first32len),
1007                self::substr($first32, 0, $first32len)
1008            )
1009        );
1010        $mlen -= 32;
1011
1012        /** @var int $iter */
1013        $iter = 1;
1014
1015        /** @var int $incr */
1016        $incr = self::BUFFER_SIZE >> 6;
1017
1018        /* Decrypts ciphertext, writes to output file. */
1019        while ($mlen > 0) {
1020            $blockSize = $mlen > self::BUFFER_SIZE
1021                ? self::BUFFER_SIZE
1022                : $mlen;
1023            $ciphertext = fread($ifp, $blockSize);
1024            if (!is_string($ciphertext)) {
1025                throw new SodiumException('Could not read input file');
1026            }
1027            $pBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
1028                $ciphertext,
1029                $realNonce,
1030                $iter,
1031                $subkey
1032            );
1033            fwrite($ofp, $pBlock, $blockSize);
1034            $mlen -= $blockSize;
1035            $iter += $incr;
1036        }
1037        return true;
1038    }
1039
1040    /**
1041     * @param ParagonIE_Sodium_Core_Poly1305_State $state
1042     * @param resource $ifp
1043     * @param string $tag
1044     * @param int $mlen
1045     * @return bool
1046     * @throws SodiumException
1047     * @throws TypeError
1048     */
1049    protected static function onetimeauth_verify(
1050        ParagonIE_Sodium_Core_Poly1305_State $state,
1051        $ifp,
1052        $tag = '',
1053        $mlen = 0
1054    ) {
1055        /** @var int $pos */
1056        $pos = self::ftell($ifp);
1057
1058        /** @var int $iter */
1059        $iter = 1;
1060
1061        /** @var int $incr */
1062        $incr = self::BUFFER_SIZE >> 6;
1063
1064        while ($mlen > 0) {
1065            $blockSize = $mlen > self::BUFFER_SIZE
1066                ? self::BUFFER_SIZE
1067                : $mlen;
1068            $ciphertext = fread($ifp, $blockSize);
1069            if (!is_string($ciphertext)) {
1070                throw new SodiumException('Could not read input file');
1071            }
1072            $state->update($ciphertext);
1073            $mlen -= $blockSize;
1074            $iter += $incr;
1075        }
1076        $res = ParagonIE_Sodium_Core_Util::verify_16($tag, $state->finish());
1077
1078        fseek($ifp, $pos, SEEK_SET);
1079        return $res;
1080    }
1081
1082    /**
1083     * Update a hash context with the contents of a file, without
1084     * loading the entire file into memory.
1085     *
1086     * @param resource|HashContext $hash
1087     * @param resource $fp
1088     * @param int $size
1089     * @return resource|object Resource on PHP < 7.2, HashContext object on PHP >= 7.2
1090     * @throws SodiumException
1091     * @throws TypeError
1092     * @psalm-suppress PossiblyInvalidArgument
1093     *                 PHP 7.2 changes from a resource to an object,
1094     *                 which causes Psalm to complain about an error.
1095     * @psalm-suppress TypeCoercion
1096     *                 Ditto.
1097     */
1098    public static function updateHashWithFile($hash, $fp, $size = 0)
1099    {
1100        /* Type checks: */
1101        if (PHP_VERSION_ID < 70200) {
1102            if (!is_resource($hash)) {
1103                throw new TypeError('Argument 1 must be a resource, ' . gettype($hash) . ' given.');
1104            }
1105        } else {
1106            if (!is_object($hash)) {
1107                throw new TypeError('Argument 1 must be an object (PHP 7.2+), ' . gettype($hash) . ' given.');
1108            }
1109        }
1110
1111        if (!is_resource($fp)) {
1112            throw new TypeError('Argument 2 must be a resource, ' . gettype($fp) . ' given.');
1113        }
1114        if (!is_int($size)) {
1115            throw new TypeError('Argument 3 must be an integer, ' . gettype($size) . ' given.');
1116        }
1117
1118        /** @var int $originalPosition */
1119        $originalPosition = self::ftell($fp);
1120
1121        // Move file pointer to beginning of file
1122        fseek($fp, 0, SEEK_SET);
1123        for ($i = 0; $i < $size; $i += self::BUFFER_SIZE) {
1124            /** @var string|bool $message */
1125            $message = fread(
1126                $fp,
1127                ($size - $i) > self::BUFFER_SIZE
1128                    ? $size - $i
1129                    : self::BUFFER_SIZE
1130            );
1131            if (!is_string($message)) {
1132                throw new SodiumException('Unexpected error reading from file.');
1133            }
1134            /** @var string $message */
1135            /** @psalm-suppress InvalidArgument */
1136            self::hash_update($hash, $message);
1137        }
1138        // Reset file pointer's position
1139        fseek($fp, $originalPosition, SEEK_SET);
1140        return $hash;
1141    }
1142
1143    /**
1144     * Sign a file (rather than a string). Uses less memory than
1145     * ParagonIE_Sodium_Compat::crypto_sign_detached(), but produces
1146     * the same result. (32-bit)
1147     *
1148     * @param string $filePath  Absolute path to a file on the filesystem
1149     * @param string $secretKey Secret signing key
1150     *
1151     * @return string           Ed25519 signature
1152     * @throws SodiumException
1153     * @throws TypeError
1154     */
1155    private static function sign_core32($filePath, $secretKey)
1156    {
1157        /** @var int|bool $size */
1158        $size = filesize($filePath);
1159        if (!is_int($size)) {
1160            throw new SodiumException('Could not obtain the file size');
1161        }
1162        /** @var int $size */
1163
1164        /** @var resource|bool $fp */
1165        $fp = fopen($filePath, 'rb');
1166        if (!is_resource($fp)) {
1167            throw new SodiumException('Could not open input file for reading');
1168        }
1169        /** @var resource $fp */
1170
1171        /** @var string $az */
1172        $az = hash('sha512', self::substr($secretKey, 0, 32), true);
1173
1174        $az[0] = self::intToChr(self::chrToInt($az[0]) & 248);
1175        $az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64);
1176
1177        $hs = hash_init('sha512');
1178        self::hash_update($hs, self::substr($az, 32, 32));
1179        /** @var resource $hs */
1180        $hs = self::updateHashWithFile($hs, $fp, $size);
1181
1182        /** @var string $nonceHash */
1183        $nonceHash = hash_final($hs, true);
1184
1185        /** @var string $pk */
1186        $pk = self::substr($secretKey, 32, 32);
1187
1188        /** @var string $nonce */
1189        $nonce = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($nonceHash) . self::substr($nonceHash, 32);
1190
1191        /** @var string $sig */
1192        $sig = ParagonIE_Sodium_Core32_Ed25519::ge_p3_tobytes(
1193            ParagonIE_Sodium_Core32_Ed25519::ge_scalarmult_base($nonce)
1194        );
1195
1196        $hs = hash_init('sha512');
1197        self::hash_update($hs, self::substr($sig, 0, 32));
1198        self::hash_update($hs, self::substr($pk, 0, 32));
1199        /** @var resource $hs */
1200        $hs = self::updateHashWithFile($hs, $fp, $size);
1201
1202        /** @var string $hramHash */
1203        $hramHash = hash_final($hs, true);
1204
1205        /** @var string $hram */
1206        $hram = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($hramHash);
1207
1208        /** @var string $sigAfter */
1209        $sigAfter = ParagonIE_Sodium_Core32_Ed25519::sc_muladd($hram, $az, $nonce);
1210
1211        /** @var string $sig */
1212        $sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32);
1213
1214        try {
1215            ParagonIE_Sodium_Compat::memzero($az);
1216        } catch (SodiumException $ex) {
1217            $az = null;
1218        }
1219        fclose($fp);
1220        return $sig;
1221    }
1222
1223    /**
1224     *
1225     * Verify a file (rather than a string). Uses less memory than
1226     * ParagonIE_Sodium_Compat::crypto_sign_verify_detached(), but
1227     * produces the same result. (32-bit)
1228     *
1229     * @param string $sig       Ed25519 signature
1230     * @param string $filePath  Absolute path to a file on the filesystem
1231     * @param string $publicKey Signing public key
1232     *
1233     * @return bool
1234     * @throws SodiumException
1235     * @throws Exception
1236     */
1237    public static function verify_core32($sig, $filePath, $publicKey)
1238    {
1239        /* Security checks */
1240        if (ParagonIE_Sodium_Core32_Ed25519::check_S_lt_L(self::substr($sig, 32, 32))) {
1241            throw new SodiumException('S < L - Invalid signature');
1242        }
1243        if (ParagonIE_Sodium_Core32_Ed25519::small_order($sig)) {
1244            throw new SodiumException('Signature is on too small of an order');
1245        }
1246        if ((self::chrToInt($sig[63]) & 224) !== 0) {
1247            throw new SodiumException('Invalid signature');
1248        }
1249        $d = 0;
1250        for ($i = 0; $i < 32; ++$i) {
1251            $d |= self::chrToInt($publicKey[$i]);
1252        }
1253        if ($d === 0) {
1254            throw new SodiumException('All zero public key');
1255        }
1256
1257        /** @var int|bool $size */
1258        $size = filesize($filePath);
1259        if (!is_int($size)) {
1260            throw new SodiumException('Could not obtain the file size');
1261        }
1262        /** @var int $size */
1263
1264        /** @var resource|bool $fp */
1265        $fp = fopen($filePath, 'rb');
1266        if (!is_resource($fp)) {
1267            throw new SodiumException('Could not open input file for reading');
1268        }
1269        /** @var resource $fp */
1270
1271        /** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */
1272        $orig = ParagonIE_Sodium_Compat::$fastMult;
1273
1274        // Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification.
1275        ParagonIE_Sodium_Compat::$fastMult = true;
1276
1277        /** @var ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $A */
1278        $A = ParagonIE_Sodium_Core32_Ed25519::ge_frombytes_negate_vartime($publicKey);
1279
1280        $hs = hash_init('sha512');
1281        self::hash_update($hs, self::substr($sig, 0, 32));
1282        self::hash_update($hs, self::substr($publicKey, 0, 32));
1283        /** @var resource $hs */
1284        $hs = self::updateHashWithFile($hs, $fp, $size);
1285        /** @var string $hDigest */
1286        $hDigest = hash_final($hs, true);
1287
1288        /** @var string $h */
1289        $h = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($hDigest) . self::substr($hDigest, 32);
1290
1291        /** @var ParagonIE_Sodium_Core32_Curve25519_Ge_P2 $R */
1292        $R = ParagonIE_Sodium_Core32_Ed25519::ge_double_scalarmult_vartime(
1293            $h,
1294            $A,
1295            self::substr($sig, 32)
1296        );
1297
1298        /** @var string $rcheck */
1299        $rcheck = ParagonIE_Sodium_Core32_Ed25519::ge_tobytes($R);
1300
1301        // Close the file handle
1302        fclose($fp);
1303
1304        // Reset ParagonIE_Sodium_Compat::$fastMult to what it was before.
1305        ParagonIE_Sodium_Compat::$fastMult = $orig;
1306        return self::verify_32($rcheck, self::substr($sig, 0, 32));
1307    }
1308
1309    /**
1310     * Encrypt a file (32-bit)
1311     *
1312     * @param resource $ifp
1313     * @param resource $ofp
1314     * @param int $mlen
1315     * @param string $nonce
1316     * @param string $key
1317     * @return bool
1318     * @throws SodiumException
1319     * @throws TypeError
1320     */
1321    protected static function secretbox_encrypt_core32($ifp, $ofp, $mlen, $nonce, $key)
1322    {
1323        $plaintext = fread($ifp, 32);
1324        if (!is_string($plaintext)) {
1325            throw new SodiumException('Could not read input file');
1326        }
1327        $first32 = self::ftell($ifp);
1328
1329        /** @var string $subkey */
1330        $subkey = ParagonIE_Sodium_Core32_HSalsa20::hsalsa20($nonce, $key);
1331
1332        /** @var string $realNonce */
1333        $realNonce = ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8);
1334
1335        /** @var string $block0 */
1336        $block0 = str_repeat("\x00", 32);
1337
1338        /** @var int $mlen - Length of the plaintext message */
1339        $mlen0 = $mlen;
1340        if ($mlen0 > 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES) {
1341            $mlen0 = 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES;
1342        }
1343        $block0 .= ParagonIE_Sodium_Core32_Util::substr($plaintext, 0, $mlen0);
1344
1345        /** @var string $block0 */
1346        $block0 = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor(
1347            $block0,
1348            $realNonce,
1349            $subkey
1350        );
1351
1352        $state = new ParagonIE_Sodium_Core32_Poly1305_State(
1353            ParagonIE_Sodium_Core32_Util::substr(
1354                $block0,
1355                0,
1356                ParagonIE_Sodium_Crypto::onetimeauth_poly1305_KEYBYTES
1357            )
1358        );
1359
1360        // Pre-write 16 blank bytes for the Poly1305 tag
1361        $start = self::ftell($ofp);
1362        fwrite($ofp, str_repeat("\x00", 16));
1363
1364        /** @var string $c */
1365        $cBlock = ParagonIE_Sodium_Core32_Util::substr(
1366            $block0,
1367            ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES
1368        );
1369        $state->update($cBlock);
1370        fwrite($ofp, $cBlock);
1371        $mlen -= 32;
1372
1373        /** @var int $iter */
1374        $iter = 1;
1375
1376        /** @var int $incr */
1377        $incr = self::BUFFER_SIZE >> 6;
1378
1379        /*
1380         * Set the cursor to the end of the first half-block. All future bytes will
1381         * generated from salsa20_xor_ic, starting from 1 (second block).
1382         */
1383        fseek($ifp, $first32, SEEK_SET);
1384
1385        while ($mlen > 0) {
1386            $blockSize = $mlen > self::BUFFER_SIZE
1387                ? self::BUFFER_SIZE
1388                : $mlen;
1389            $plaintext = fread($ifp, $blockSize);
1390            if (!is_string($plaintext)) {
1391                throw new SodiumException('Could not read input file');
1392            }
1393            $cBlock = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic(
1394                $plaintext,
1395                $realNonce,
1396                $iter,
1397                $subkey
1398            );
1399            fwrite($ofp, $cBlock, $blockSize);
1400            $state->update($cBlock);
1401
1402            $mlen -= $blockSize;
1403            $iter += $incr;
1404        }
1405        try {
1406            ParagonIE_Sodium_Compat::memzero($block0);
1407            ParagonIE_Sodium_Compat::memzero($subkey);
1408        } catch (SodiumException $ex) {
1409            $block0 = null;
1410            $subkey = null;
1411        }
1412        $end = self::ftell($ofp);
1413
1414        /*
1415         * Write the Poly1305 authentication tag that provides integrity
1416         * over the ciphertext (encrypt-then-MAC)
1417         */
1418        fseek($ofp, $start, SEEK_SET);
1419        fwrite($ofp, $state->finish(), ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_MACBYTES);
1420        fseek($ofp, $end, SEEK_SET);
1421        unset($state);
1422
1423        return true;
1424    }
1425
1426    /**
1427     * Decrypt a file (32-bit)
1428     *
1429     * @param resource $ifp
1430     * @param resource $ofp
1431     * @param int $mlen
1432     * @param string $nonce
1433     * @param string $key
1434     * @return bool
1435     * @throws SodiumException
1436     * @throws TypeError
1437     */
1438    protected static function secretbox_decrypt_core32($ifp, $ofp, $mlen, $nonce, $key)
1439    {
1440        $tag = fread($ifp, 16);
1441        if (!is_string($tag)) {
1442            throw new SodiumException('Could not read input file');
1443        }
1444
1445        /** @var string $subkey */
1446        $subkey = ParagonIE_Sodium_Core32_HSalsa20::hsalsa20($nonce, $key);
1447
1448        /** @var string $realNonce */
1449        $realNonce = ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8);
1450
1451        /** @var string $block0 */
1452        $block0 = ParagonIE_Sodium_Core32_Salsa20::salsa20(
1453            64,
1454            ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8),
1455            $subkey
1456        );
1457
1458        /* Verify the Poly1305 MAC -before- attempting to decrypt! */
1459        $state = new ParagonIE_Sodium_Core32_Poly1305_State(self::substr($block0, 0, 32));
1460        if (!self::onetimeauth_verify_core32($state, $ifp, $tag, $mlen)) {
1461            throw new SodiumException('Invalid MAC');
1462        }
1463
1464        /*
1465         * Set the cursor to the end of the first half-block. All future bytes will
1466         * generated from salsa20_xor_ic, starting from 1 (second block).
1467         */
1468        $first32 = fread($ifp, 32);
1469        if (!is_string($first32)) {
1470            throw new SodiumException('Could not read input file');
1471        }
1472        $first32len = self::strlen($first32);
1473        fwrite(
1474            $ofp,
1475            self::xorStrings(
1476                self::substr($block0, 32, $first32len),
1477                self::substr($first32, 0, $first32len)
1478            )
1479        );
1480        $mlen -= 32;
1481
1482        /** @var int $iter */
1483        $iter = 1;
1484
1485        /** @var int $incr */
1486        $incr = self::BUFFER_SIZE >> 6;
1487
1488        /* Decrypts ciphertext, writes to output file. */
1489        while ($mlen > 0) {
1490            $blockSize = $mlen > self::BUFFER_SIZE
1491                ? self::BUFFER_SIZE
1492                : $mlen;
1493            $ciphertext = fread($ifp, $blockSize);
1494            if (!is_string($ciphertext)) {
1495                throw new SodiumException('Could not read input file');
1496            }
1497            $pBlock = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic(
1498                $ciphertext,
1499                $realNonce,
1500                $iter,
1501                $subkey
1502            );
1503            fwrite($ofp, $pBlock, $blockSize);
1504            $mlen -= $blockSize;
1505            $iter += $incr;
1506        }
1507        return true;
1508    }
1509
1510    /**
1511     * One-time message authentication for 32-bit systems
1512     *
1513     * @param ParagonIE_Sodium_Core32_Poly1305_State $state
1514     * @param resource $ifp
1515     * @param string $tag
1516     * @param int $mlen
1517     * @return bool
1518     * @throws SodiumException
1519     * @throws TypeError
1520     */
1521    protected static function onetimeauth_verify_core32(
1522        ParagonIE_Sodium_Core32_Poly1305_State $state,
1523        $ifp,
1524        $tag = '',
1525        $mlen = 0
1526    ) {
1527        /** @var int $pos */
1528        $pos = self::ftell($ifp);
1529
1530        while ($mlen > 0) {
1531            $blockSize = $mlen > self::BUFFER_SIZE
1532                ? self::BUFFER_SIZE
1533                : $mlen;
1534            $ciphertext = fread($ifp, $blockSize);
1535            if (!is_string($ciphertext)) {
1536                throw new SodiumException('Could not read input file');
1537            }
1538            $state->update($ciphertext);
1539            $mlen -= $blockSize;
1540        }
1541        $res = ParagonIE_Sodium_Core32_Util::verify_16($tag, $state->finish());
1542
1543        fseek($ifp, $pos, SEEK_SET);
1544        return $res;
1545    }
1546
1547    /**
1548     * @param resource $resource
1549     * @return int
1550     * @throws SodiumException
1551     */
1552    private static function ftell($resource)
1553    {
1554        $return = ftell($resource);
1555        if (!is_int($return)) {
1556            throw new SodiumException('ftell() returned false');
1557        }
1558        return (int) $return;
1559    }
1560}
1561