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            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 SodiumException
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 SodiumException('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 SodiumException('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 SodiumException('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 SodiumException('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 SodiumException('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 (SodiumException $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 SodiumException
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 SodiumException('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 SodiumException('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 SodiumException('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 SodiumException('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 SodiumException('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 (SodiumException $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 SodiumException
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 SodiumException('Argument 3 must be at least CRYPTO_GENERICHASH_BYTES_MIN');
379        }
380        if ($outputLength > ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_BYTES_MAX) {
381            throw new SodiumException('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 SodiumException('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 SodiumException('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 SodiumException('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 SodiumException
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 SodiumException('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 SodiumException('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 SodiumException('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 SodiumException
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 SodiumException('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 SodiumException('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 SodiumException('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 (SodiumException $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 SodiumException
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        if (PHP_INT_SIZE === 4) {
572            return self::sign_core32($filePath, $secretKey);
573        }
574
575        /** @var int $size */
576        $size = filesize($filePath);
577        if (!is_int($size)) {
578            throw new SodiumException('Could not obtain the file size');
579        }
580
581        /** @var resource $fp */
582        $fp = fopen($filePath, 'rb');
583        if (!is_resource($fp)) {
584            throw new SodiumException('Could not open input file for reading');
585        }
586
587        /** @var string $az */
588        $az = hash('sha512', self::substr($secretKey, 0, 32), true);
589
590        $az[0] = self::intToChr(self::chrToInt($az[0]) & 248);
591        $az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64);
592
593        /** @var resource $hs */
594        $hs = hash_init('sha512');
595        hash_update($hs, self::substr($az, 32, 32));
596        /** @var resource $hs */
597        $hs = self::updateHashWithFile($hs, $fp, $size);
598
599        /** @var string $nonceHash */
600        $nonceHash = hash_final($hs, true);
601
602        /** @var string $pk */
603        $pk = self::substr($secretKey, 32, 32);
604
605        /** @var string $nonce */
606        $nonce = ParagonIE_Sodium_Core_Ed25519::sc_reduce($nonceHash) . self::substr($nonceHash, 32);
607
608        /** @var string $sig */
609        $sig = ParagonIE_Sodium_Core_Ed25519::ge_p3_tobytes(
610            ParagonIE_Sodium_Core_Ed25519::ge_scalarmult_base($nonce)
611        );
612
613        /** @var resource $hs */
614        $hs = hash_init('sha512');
615        hash_update($hs, self::substr($sig, 0, 32));
616        hash_update($hs, self::substr($pk, 0, 32));
617        /** @var resource $hs */
618        $hs = self::updateHashWithFile($hs, $fp, $size);
619
620        /** @var string $hramHash */
621        $hramHash = hash_final($hs, true);
622
623        /** @var string $hram */
624        $hram = ParagonIE_Sodium_Core_Ed25519::sc_reduce($hramHash);
625
626        /** @var string $sigAfter */
627        $sigAfter = ParagonIE_Sodium_Core_Ed25519::sc_muladd($hram, $az, $nonce);
628
629        /** @var string $sig */
630        $sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32);
631
632        try {
633            ParagonIE_Sodium_Compat::memzero($az);
634        } catch (SodiumException $ex) {
635            $az = null;
636        }
637        fclose($fp);
638        return $sig;
639    }
640
641    /**
642     * Verify a file (rather than a string). Uses less memory than
643     * ParagonIE_Sodium_Compat::crypto_sign_verify_detached(), but
644     * produces the same result.
645     *
646     * @param string $sig       Ed25519 signature
647     * @param string $filePath  Absolute path to a file on the filesystem
648     * @param string $publicKey Signing public key
649     *
650     * @return bool
651     * @throws SodiumException
652     * @throws TypeError
653     * @throws Exception
654     */
655    public static function verify($sig, $filePath, $publicKey)
656    {
657        /* Type checks: */
658        if (!is_string($sig)) {
659            throw new TypeError('Argument 1 must be a string, ' . gettype($sig) . ' given.');
660        }
661        if (!is_string($filePath)) {
662            throw new TypeError('Argument 2 must be a string, ' . gettype($filePath) . ' given.');
663        }
664        if (!is_string($publicKey)) {
665            throw new TypeError('Argument 3 must be a string, ' . gettype($publicKey) . ' given.');
666        }
667
668        /* Input validation: */
669        if (self::strlen($sig) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_BYTES) {
670            throw new TypeError('Argument 1 must be CRYPTO_SIGN_BYTES bytes');
671        }
672        if (self::strlen($publicKey) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_PUBLICKEYBYTES) {
673            throw new TypeError('Argument 3 must be CRYPTO_SIGN_PUBLICKEYBYTES bytes');
674        }
675        if (self::strlen($sig) < 64) {
676            throw new SodiumException('Signature is too short');
677        }
678
679        if (PHP_INT_SIZE === 4) {
680            return self::verify_core32($sig, $filePath, $publicKey);
681        }
682
683        /* Security checks */
684        if (ParagonIE_Sodium_Core_Ed25519::check_S_lt_L(self::substr($sig, 32, 32))) {
685            throw new SodiumException('S < L - Invalid signature');
686        }
687        if (ParagonIE_Sodium_Core_Ed25519::small_order($sig)) {
688            throw new SodiumException('Signature is on too small of an order');
689        }
690        if ((self::chrToInt($sig[63]) & 224) !== 0) {
691            throw new SodiumException('Invalid signature');
692        }
693        $d = 0;
694        for ($i = 0; $i < 32; ++$i) {
695            $d |= self::chrToInt($publicKey[$i]);
696        }
697        if ($d === 0) {
698            throw new SodiumException('All zero public key');
699        }
700
701        /** @var int $size */
702        $size = filesize($filePath);
703        if (!is_int($size)) {
704            throw new SodiumException('Could not obtain the file size');
705        }
706
707        /** @var resource $fp */
708        $fp = fopen($filePath, 'rb');
709        if (!is_resource($fp)) {
710            throw new SodiumException('Could not open input file for reading');
711        }
712
713        /** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */
714        $orig = ParagonIE_Sodium_Compat::$fastMult;
715
716        // Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification.
717        ParagonIE_Sodium_Compat::$fastMult = true;
718
719        /** @var ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A */
720        $A = ParagonIE_Sodium_Core_Ed25519::ge_frombytes_negate_vartime($publicKey);
721
722        /** @var resource $hs */
723        $hs = hash_init('sha512');
724        hash_update($hs, self::substr($sig, 0, 32));
725        hash_update($hs, self::substr($publicKey, 0, 32));
726        /** @var resource $hs */
727        $hs = self::updateHashWithFile($hs, $fp, $size);
728        /** @var string $hDigest */
729        $hDigest = hash_final($hs, true);
730
731        /** @var string $h */
732        $h = ParagonIE_Sodium_Core_Ed25519::sc_reduce($hDigest) . self::substr($hDigest, 32);
733
734        /** @var ParagonIE_Sodium_Core_Curve25519_Ge_P2 $R */
735        $R = ParagonIE_Sodium_Core_Ed25519::ge_double_scalarmult_vartime(
736            $h,
737            $A,
738            self::substr($sig, 32)
739        );
740
741        /** @var string $rcheck */
742        $rcheck = ParagonIE_Sodium_Core_Ed25519::ge_tobytes($R);
743
744        // Close the file handle
745        fclose($fp);
746
747        // Reset ParagonIE_Sodium_Compat::$fastMult to what it was before.
748        ParagonIE_Sodium_Compat::$fastMult = $orig;
749        return self::verify_32($rcheck, self::substr($sig, 0, 32));
750    }
751
752    /**
753     * @param resource $ifp
754     * @param resource $ofp
755     * @param int      $mlen
756     * @param string   $nonce
757     * @param string   $boxKeypair
758     * @return bool
759     * @throws SodiumException
760     * @throws TypeError
761     */
762    protected static function box_encrypt($ifp, $ofp, $mlen, $nonce, $boxKeypair)
763    {
764        if (PHP_INT_SIZE === 4) {
765            return self::secretbox_encrypt(
766                $ifp,
767                $ofp,
768                $mlen,
769                $nonce,
770                ParagonIE_Sodium_Crypto32::box_beforenm(
771                    ParagonIE_Sodium_Crypto32::box_secretkey($boxKeypair),
772                    ParagonIE_Sodium_Crypto32::box_publickey($boxKeypair)
773                )
774            );
775        }
776        return self::secretbox_encrypt(
777            $ifp,
778            $ofp,
779            $mlen,
780            $nonce,
781            ParagonIE_Sodium_Crypto::box_beforenm(
782                ParagonIE_Sodium_Crypto::box_secretkey($boxKeypair),
783                ParagonIE_Sodium_Crypto::box_publickey($boxKeypair)
784            )
785        );
786    }
787
788
789    /**
790     * @param resource $ifp
791     * @param resource $ofp
792     * @param int      $mlen
793     * @param string   $nonce
794     * @param string   $boxKeypair
795     * @return bool
796     * @throws SodiumException
797     * @throws TypeError
798     */
799    protected static function box_decrypt($ifp, $ofp, $mlen, $nonce, $boxKeypair)
800    {
801        if (PHP_INT_SIZE === 4) {
802            return self::secretbox_decrypt(
803                $ifp,
804                $ofp,
805                $mlen,
806                $nonce,
807                ParagonIE_Sodium_Crypto32::box_beforenm(
808                    ParagonIE_Sodium_Crypto32::box_secretkey($boxKeypair),
809                    ParagonIE_Sodium_Crypto32::box_publickey($boxKeypair)
810                )
811            );
812        }
813        return self::secretbox_decrypt(
814            $ifp,
815            $ofp,
816            $mlen,
817            $nonce,
818            ParagonIE_Sodium_Crypto::box_beforenm(
819                ParagonIE_Sodium_Crypto::box_secretkey($boxKeypair),
820                ParagonIE_Sodium_Crypto::box_publickey($boxKeypair)
821            )
822        );
823    }
824
825    /**
826     * Encrypt a file
827     *
828     * @param resource $ifp
829     * @param resource $ofp
830     * @param int $mlen
831     * @param string $nonce
832     * @param string $key
833     * @return bool
834     * @throws SodiumException
835     * @throws TypeError
836     */
837    protected static function secretbox_encrypt($ifp, $ofp, $mlen, $nonce, $key)
838    {
839        if (PHP_INT_SIZE === 4) {
840            return self::secretbox_encrypt_core32($ifp, $ofp, $mlen, $nonce, $key);
841        }
842
843        $plaintext = fread($ifp, 32);
844        if (!is_string($plaintext)) {
845            throw new SodiumException('Could not read input file');
846        }
847        $first32 = ftell($ifp);
848
849        /** @var string $subkey */
850        $subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key);
851
852        /** @var string $realNonce */
853        $realNonce = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);
854
855        /** @var string $block0 */
856        $block0 = str_repeat("\x00", 32);
857
858        /** @var int $mlen - Length of the plaintext message */
859        $mlen0 = $mlen;
860        if ($mlen0 > 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES) {
861            $mlen0 = 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES;
862        }
863        $block0 .= ParagonIE_Sodium_Core_Util::substr($plaintext, 0, $mlen0);
864
865        /** @var string $block0 */
866        $block0 = ParagonIE_Sodium_Core_Salsa20::salsa20_xor(
867            $block0,
868            $realNonce,
869            $subkey
870        );
871
872        $state = new ParagonIE_Sodium_Core_Poly1305_State(
873            ParagonIE_Sodium_Core_Util::substr(
874                $block0,
875                0,
876                ParagonIE_Sodium_Crypto::onetimeauth_poly1305_KEYBYTES
877            )
878        );
879
880        // Pre-write 16 blank bytes for the Poly1305 tag
881        $start = ftell($ofp);
882        fwrite($ofp, str_repeat("\x00", 16));
883
884        /** @var string $c */
885        $cBlock = ParagonIE_Sodium_Core_Util::substr(
886            $block0,
887            ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES
888        );
889        $state->update($cBlock);
890        fwrite($ofp, $cBlock);
891        $mlen -= 32;
892
893        /** @var int $iter */
894        $iter = 1;
895
896        /** @var int $incr */
897        $incr = self::BUFFER_SIZE >> 6;
898
899        /*
900         * Set the cursor to the end of the first half-block. All future bytes will
901         * generated from salsa20_xor_ic, starting from 1 (second block).
902         */
903        fseek($ifp, $first32, SEEK_SET);
904
905        while ($mlen > 0) {
906            $blockSize = $mlen > self::BUFFER_SIZE
907                ? self::BUFFER_SIZE
908                : $mlen;
909            $plaintext = fread($ifp, $blockSize);
910            if (!is_string($plaintext)) {
911                throw new SodiumException('Could not read input file');
912            }
913            $cBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
914                $plaintext,
915                $realNonce,
916                $iter,
917                $subkey
918            );
919            fwrite($ofp, $cBlock, $blockSize);
920            $state->update($cBlock);
921
922            $mlen -= $blockSize;
923            $iter += $incr;
924        }
925        try {
926            ParagonIE_Sodium_Compat::memzero($block0);
927            ParagonIE_Sodium_Compat::memzero($subkey);
928        } catch (SodiumException $ex) {
929            $block0 = null;
930            $subkey = null;
931        }
932        $end = ftell($ofp);
933
934        /*
935         * Write the Poly1305 authentication tag that provides integrity
936         * over the ciphertext (encrypt-then-MAC)
937         */
938        fseek($ofp, $start, SEEK_SET);
939        fwrite($ofp, $state->finish(), ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_MACBYTES);
940        fseek($ofp, $end, SEEK_SET);
941        unset($state);
942
943        return true;
944    }
945
946    /**
947     * Decrypt a file
948     *
949     * @param resource $ifp
950     * @param resource $ofp
951     * @param int $mlen
952     * @param string $nonce
953     * @param string $key
954     * @return bool
955     * @throws SodiumException
956     * @throws TypeError
957     */
958    protected static function secretbox_decrypt($ifp, $ofp, $mlen, $nonce, $key)
959    {
960        if (PHP_INT_SIZE === 4) {
961            return self::secretbox_decrypt_core32($ifp, $ofp, $mlen, $nonce, $key);
962        }
963        $tag = fread($ifp, 16);
964        if (!is_string($tag)) {
965            throw new SodiumException('Could not read input file');
966        }
967
968        /** @var string $subkey */
969        $subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key);
970
971        /** @var string $realNonce */
972        $realNonce = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8);
973
974        /** @var string $block0 */
975        $block0 = ParagonIE_Sodium_Core_Salsa20::salsa20(
976            64,
977            ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8),
978            $subkey
979        );
980
981        /* Verify the Poly1305 MAC -before- attempting to decrypt! */
982        $state = new ParagonIE_Sodium_Core_Poly1305_State(self::substr($block0, 0, 32));
983        if (!self::onetimeauth_verify($state, $ifp, $tag, $mlen)) {
984            throw new SodiumException('Invalid MAC');
985        }
986
987        /*
988         * Set the cursor to the end of the first half-block. All future bytes will
989         * generated from salsa20_xor_ic, starting from 1 (second block).
990         */
991        $first32 = fread($ifp, 32);
992        if (!is_string($first32)) {
993            throw new SodiumException('Could not read input file');
994        }
995        $first32len = self::strlen($first32);
996        fwrite(
997            $ofp,
998            self::xorStrings(
999                self::substr($block0, 32, $first32len),
1000                self::substr($first32, 0, $first32len)
1001            )
1002        );
1003        $mlen -= 32;
1004
1005        /** @var int $iter */
1006        $iter = 1;
1007
1008        /** @var int $incr */
1009        $incr = self::BUFFER_SIZE >> 6;
1010
1011        /* Decrypts ciphertext, writes to output file. */
1012        while ($mlen > 0) {
1013            $blockSize = $mlen > self::BUFFER_SIZE
1014                ? self::BUFFER_SIZE
1015                : $mlen;
1016            $ciphertext = fread($ifp, $blockSize);
1017            if (!is_string($ciphertext)) {
1018                throw new SodiumException('Could not read input file');
1019            }
1020            $pBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
1021                $ciphertext,
1022                $realNonce,
1023                $iter,
1024                $subkey
1025            );
1026            fwrite($ofp, $pBlock, $blockSize);
1027            $mlen -= $blockSize;
1028            $iter += $incr;
1029        }
1030        return true;
1031    }
1032
1033    /**
1034     * @param ParagonIE_Sodium_Core_Poly1305_State $state
1035     * @param resource $ifp
1036     * @param string $tag
1037     * @param int $mlen
1038     * @return bool
1039     * @throws SodiumException
1040     * @throws TypeError
1041     */
1042    protected static function onetimeauth_verify(
1043        ParagonIE_Sodium_Core_Poly1305_State $state,
1044        $ifp,
1045        $tag = '',
1046        $mlen = 0
1047    ) {
1048        /** @var int $pos */
1049        $pos = ftell($ifp);
1050
1051        /** @var int $iter */
1052        $iter = 1;
1053
1054        /** @var int $incr */
1055        $incr = self::BUFFER_SIZE >> 6;
1056
1057        while ($mlen > 0) {
1058            $blockSize = $mlen > self::BUFFER_SIZE
1059                ? self::BUFFER_SIZE
1060                : $mlen;
1061            $ciphertext = fread($ifp, $blockSize);
1062            if (!is_string($ciphertext)) {
1063                throw new SodiumException('Could not read input file');
1064            }
1065            $state->update($ciphertext);
1066            $mlen -= $blockSize;
1067            $iter += $incr;
1068        }
1069        $res = ParagonIE_Sodium_Core_Util::verify_16($tag, $state->finish());
1070
1071        fseek($ifp, $pos, SEEK_SET);
1072        return $res;
1073    }
1074
1075    /**
1076     * Update a hash context with the contents of a file, without
1077     * loading the entire file into memory.
1078     *
1079     * @param resource|object $hash
1080     * @param resource $fp
1081     * @param int $size
1082     * @return mixed (resource on PHP < 7.2, object on PHP >= 7.2)
1083     * @throws SodiumException
1084     * @throws TypeError
1085     * @psalm-suppress PossiblyInvalidArgument
1086     *                 PHP 7.2 changes from a resource to an object,
1087     *                 which causes Psalm to complain about an error.
1088     * @psalm-suppress TypeCoercion
1089     *                 Ditto.
1090     */
1091    public static function updateHashWithFile($hash, $fp, $size = 0)
1092    {
1093        /* Type checks: */
1094        if (PHP_VERSION_ID < 70200) {
1095            if (!is_resource($hash)) {
1096                throw new TypeError('Argument 1 must be a resource, ' . gettype($hash) . ' given.');
1097            }
1098
1099        } else {
1100            if (!is_object($hash)) {
1101                throw new TypeError('Argument 1 must be an object (PHP 7.2+), ' . gettype($hash) . ' given.');
1102            }
1103        }
1104        if (!is_resource($fp)) {
1105            throw new TypeError('Argument 2 must be a resource, ' . gettype($fp) . ' given.');
1106        }
1107        if (!is_int($size)) {
1108            throw new TypeError('Argument 3 must be an integer, ' . gettype($size) . ' given.');
1109        }
1110
1111        /** @var int $originalPosition */
1112        $originalPosition = ftell($fp);
1113
1114        // Move file pointer to beginning of file
1115        fseek($fp, 0, SEEK_SET);
1116        for ($i = 0; $i < $size; $i += self::BUFFER_SIZE) {
1117            /** @var string|bool $message */
1118            $message = fread(
1119                $fp,
1120                ($size - $i) > self::BUFFER_SIZE
1121                    ? $size - $i
1122                    : self::BUFFER_SIZE
1123            );
1124            if (!is_string($message)) {
1125                throw new SodiumException('Unexpected error reading from file.');
1126            }
1127            /** @var string $message */
1128            /** @psalm-suppress InvalidArgument */
1129            hash_update($hash, $message);
1130        }
1131        // Reset file pointer's position
1132        fseek($fp, $originalPosition, SEEK_SET);
1133        return $hash;
1134    }
1135
1136    /**
1137     * Sign a file (rather than a string). Uses less memory than
1138     * ParagonIE_Sodium_Compat::crypto_sign_detached(), but produces
1139     * the same result. (32-bit)
1140     *
1141     * @param string $filePath  Absolute path to a file on the filesystem
1142     * @param string $secretKey Secret signing key
1143     *
1144     * @return string           Ed25519 signature
1145     * @throws SodiumException
1146     * @throws TypeError
1147     */
1148    private static function sign_core32($filePath, $secretKey)
1149    {
1150        /** @var int|bool $size */
1151        $size = filesize($filePath);
1152        if (!is_int($size)) {
1153            throw new SodiumException('Could not obtain the file size');
1154        }
1155        /** @var int $size */
1156
1157        /** @var resource|bool $fp */
1158        $fp = fopen($filePath, 'rb');
1159        if (!is_resource($fp)) {
1160            throw new SodiumException('Could not open input file for reading');
1161        }
1162        /** @var resource $fp */
1163
1164        /** @var string $az */
1165        $az = hash('sha512', self::substr($secretKey, 0, 32), true);
1166
1167        $az[0] = self::intToChr(self::chrToInt($az[0]) & 248);
1168        $az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64);
1169
1170        /** @var resource $hs */
1171        $hs = hash_init('sha512');
1172        hash_update($hs, self::substr($az, 32, 32));
1173        /** @var resource $hs */
1174        $hs = self::updateHashWithFile($hs, $fp, $size);
1175
1176        /** @var string $nonceHash */
1177        $nonceHash = hash_final($hs, true);
1178
1179        /** @var string $pk */
1180        $pk = self::substr($secretKey, 32, 32);
1181
1182        /** @var string $nonce */
1183        $nonce = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($nonceHash) . self::substr($nonceHash, 32);
1184
1185        /** @var string $sig */
1186        $sig = ParagonIE_Sodium_Core32_Ed25519::ge_p3_tobytes(
1187            ParagonIE_Sodium_Core32_Ed25519::ge_scalarmult_base($nonce)
1188        );
1189
1190        /** @var resource $hs */
1191        $hs = hash_init('sha512');
1192        hash_update($hs, self::substr($sig, 0, 32));
1193        hash_update($hs, self::substr($pk, 0, 32));
1194        /** @var resource $hs */
1195        $hs = self::updateHashWithFile($hs, $fp, $size);
1196
1197        /** @var string $hramHash */
1198        $hramHash = hash_final($hs, true);
1199
1200        /** @var string $hram */
1201        $hram = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($hramHash);
1202
1203        /** @var string $sigAfter */
1204        $sigAfter = ParagonIE_Sodium_Core32_Ed25519::sc_muladd($hram, $az, $nonce);
1205
1206        /** @var string $sig */
1207        $sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32);
1208
1209        try {
1210            ParagonIE_Sodium_Compat::memzero($az);
1211        } catch (SodiumException $ex) {
1212            $az = null;
1213        }
1214        fclose($fp);
1215        return $sig;
1216    }
1217
1218    /**
1219     *
1220     * Verify a file (rather than a string). Uses less memory than
1221     * ParagonIE_Sodium_Compat::crypto_sign_verify_detached(), but
1222     * produces the same result. (32-bit)
1223     *
1224     * @param string $sig       Ed25519 signature
1225     * @param string $filePath  Absolute path to a file on the filesystem
1226     * @param string $publicKey Signing public key
1227     *
1228     * @return bool
1229     * @throws SodiumException
1230     * @throws Exception
1231     */
1232    public static function verify_core32($sig, $filePath, $publicKey)
1233    {
1234        /* Security checks */
1235        if (ParagonIE_Sodium_Core32_Ed25519::check_S_lt_L(self::substr($sig, 32, 32))) {
1236            throw new SodiumException('S < L - Invalid signature');
1237        }
1238        if (ParagonIE_Sodium_Core32_Ed25519::small_order($sig)) {
1239            throw new SodiumException('Signature is on too small of an order');
1240        }
1241        if ((self::chrToInt($sig[63]) & 224) !== 0) {
1242            throw new SodiumException('Invalid signature');
1243        }
1244        $d = 0;
1245        for ($i = 0; $i < 32; ++$i) {
1246            $d |= self::chrToInt($publicKey[$i]);
1247        }
1248        if ($d === 0) {
1249            throw new SodiumException('All zero public key');
1250        }
1251
1252        /** @var int|bool $size */
1253        $size = filesize($filePath);
1254        if (!is_int($size)) {
1255            throw new SodiumException('Could not obtain the file size');
1256        }
1257        /** @var int $size */
1258
1259        /** @var resource|bool $fp */
1260        $fp = fopen($filePath, 'rb');
1261        if (!is_resource($fp)) {
1262            throw new SodiumException('Could not open input file for reading');
1263        }
1264        /** @var resource $fp */
1265
1266        /** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */
1267        $orig = ParagonIE_Sodium_Compat::$fastMult;
1268
1269        // Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification.
1270        ParagonIE_Sodium_Compat::$fastMult = true;
1271
1272        /** @var ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $A */
1273        $A = ParagonIE_Sodium_Core32_Ed25519::ge_frombytes_negate_vartime($publicKey);
1274
1275        /** @var resource $hs */
1276        $hs = hash_init('sha512');
1277        hash_update($hs, self::substr($sig, 0, 32));
1278        hash_update($hs, self::substr($publicKey, 0, 32));
1279        /** @var resource $hs */
1280        $hs = self::updateHashWithFile($hs, $fp, $size);
1281        /** @var string $hDigest */
1282        $hDigest = hash_final($hs, true);
1283
1284        /** @var string $h */
1285        $h = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($hDigest) . self::substr($hDigest, 32);
1286
1287        /** @var ParagonIE_Sodium_Core32_Curve25519_Ge_P2 $R */
1288        $R = ParagonIE_Sodium_Core32_Ed25519::ge_double_scalarmult_vartime(
1289            $h,
1290            $A,
1291            self::substr($sig, 32)
1292        );
1293
1294        /** @var string $rcheck */
1295        $rcheck = ParagonIE_Sodium_Core32_Ed25519::ge_tobytes($R);
1296
1297        // Close the file handle
1298        fclose($fp);
1299
1300        // Reset ParagonIE_Sodium_Compat::$fastMult to what it was before.
1301        ParagonIE_Sodium_Compat::$fastMult = $orig;
1302        return self::verify_32($rcheck, self::substr($sig, 0, 32));
1303    }
1304
1305    /**
1306     * Encrypt a file (32-bit)
1307     *
1308     * @param resource $ifp
1309     * @param resource $ofp
1310     * @param int $mlen
1311     * @param string $nonce
1312     * @param string $key
1313     * @return bool
1314     * @throws SodiumException
1315     * @throws TypeError
1316     */
1317    protected static function secretbox_encrypt_core32($ifp, $ofp, $mlen, $nonce, $key)
1318    {
1319        $plaintext = fread($ifp, 32);
1320        if (!is_string($plaintext)) {
1321            throw new SodiumException('Could not read input file');
1322        }
1323        $first32 = ftell($ifp);
1324
1325        /** @var string $subkey */
1326        $subkey = ParagonIE_Sodium_Core32_HSalsa20::hsalsa20($nonce, $key);
1327
1328        /** @var string $realNonce */
1329        $realNonce = ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8);
1330
1331        /** @var string $block0 */
1332        $block0 = str_repeat("\x00", 32);
1333
1334        /** @var int $mlen - Length of the plaintext message */
1335        $mlen0 = $mlen;
1336        if ($mlen0 > 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES) {
1337            $mlen0 = 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES;
1338        }
1339        $block0 .= ParagonIE_Sodium_Core32_Util::substr($plaintext, 0, $mlen0);
1340
1341        /** @var string $block0 */
1342        $block0 = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor(
1343            $block0,
1344            $realNonce,
1345            $subkey
1346        );
1347
1348        $state = new ParagonIE_Sodium_Core32_Poly1305_State(
1349            ParagonIE_Sodium_Core32_Util::substr(
1350                $block0,
1351                0,
1352                ParagonIE_Sodium_Crypto::onetimeauth_poly1305_KEYBYTES
1353            )
1354        );
1355
1356        // Pre-write 16 blank bytes for the Poly1305 tag
1357        $start = ftell($ofp);
1358        fwrite($ofp, str_repeat("\x00", 16));
1359
1360        /** @var string $c */
1361        $cBlock = ParagonIE_Sodium_Core32_Util::substr(
1362            $block0,
1363            ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES
1364        );
1365        $state->update($cBlock);
1366        fwrite($ofp, $cBlock);
1367        $mlen -= 32;
1368
1369        /** @var int $iter */
1370        $iter = 1;
1371
1372        /** @var int $incr */
1373        $incr = self::BUFFER_SIZE >> 6;
1374
1375        /*
1376         * Set the cursor to the end of the first half-block. All future bytes will
1377         * generated from salsa20_xor_ic, starting from 1 (second block).
1378         */
1379        fseek($ifp, $first32, SEEK_SET);
1380
1381        while ($mlen > 0) {
1382            $blockSize = $mlen > self::BUFFER_SIZE
1383                ? self::BUFFER_SIZE
1384                : $mlen;
1385            $plaintext = fread($ifp, $blockSize);
1386            if (!is_string($plaintext)) {
1387                throw new SodiumException('Could not read input file');
1388            }
1389            $cBlock = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic(
1390                $plaintext,
1391                $realNonce,
1392                $iter,
1393                $subkey
1394            );
1395            fwrite($ofp, $cBlock, $blockSize);
1396            $state->update($cBlock);
1397
1398            $mlen -= $blockSize;
1399            $iter += $incr;
1400        }
1401        try {
1402            ParagonIE_Sodium_Compat::memzero($block0);
1403            ParagonIE_Sodium_Compat::memzero($subkey);
1404        } catch (SodiumException $ex) {
1405            $block0 = null;
1406            $subkey = null;
1407        }
1408        $end = ftell($ofp);
1409
1410        /*
1411         * Write the Poly1305 authentication tag that provides integrity
1412         * over the ciphertext (encrypt-then-MAC)
1413         */
1414        fseek($ofp, $start, SEEK_SET);
1415        fwrite($ofp, $state->finish(), ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_MACBYTES);
1416        fseek($ofp, $end, SEEK_SET);
1417        unset($state);
1418
1419        return true;
1420    }
1421
1422    /**
1423     * Decrypt a file (32-bit)
1424     *
1425     * @param resource $ifp
1426     * @param resource $ofp
1427     * @param int $mlen
1428     * @param string $nonce
1429     * @param string $key
1430     * @return bool
1431     * @throws SodiumException
1432     * @throws TypeError
1433     */
1434    protected static function secretbox_decrypt_core32($ifp, $ofp, $mlen, $nonce, $key)
1435    {
1436        $tag = fread($ifp, 16);
1437        if (!is_string($tag)) {
1438            throw new SodiumException('Could not read input file');
1439        }
1440
1441        /** @var string $subkey */
1442        $subkey = ParagonIE_Sodium_Core32_HSalsa20::hsalsa20($nonce, $key);
1443
1444        /** @var string $realNonce */
1445        $realNonce = ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8);
1446
1447        /** @var string $block0 */
1448        $block0 = ParagonIE_Sodium_Core32_Salsa20::salsa20(
1449            64,
1450            ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8),
1451            $subkey
1452        );
1453
1454        /* Verify the Poly1305 MAC -before- attempting to decrypt! */
1455        $state = new ParagonIE_Sodium_Core32_Poly1305_State(self::substr($block0, 0, 32));
1456        if (!self::onetimeauth_verify_core32($state, $ifp, $tag, $mlen)) {
1457            throw new SodiumException('Invalid MAC');
1458        }
1459
1460        /*
1461         * Set the cursor to the end of the first half-block. All future bytes will
1462         * generated from salsa20_xor_ic, starting from 1 (second block).
1463         */
1464        $first32 = fread($ifp, 32);
1465        if (!is_string($first32)) {
1466            throw new SodiumException('Could not read input file');
1467        }
1468        $first32len = self::strlen($first32);
1469        fwrite(
1470            $ofp,
1471            self::xorStrings(
1472                self::substr($block0, 32, $first32len),
1473                self::substr($first32, 0, $first32len)
1474            )
1475        );
1476        $mlen -= 32;
1477
1478        /** @var int $iter */
1479        $iter = 1;
1480
1481        /** @var int $incr */
1482        $incr = self::BUFFER_SIZE >> 6;
1483
1484        /* Decrypts ciphertext, writes to output file. */
1485        while ($mlen > 0) {
1486            $blockSize = $mlen > self::BUFFER_SIZE
1487                ? self::BUFFER_SIZE
1488                : $mlen;
1489            $ciphertext = fread($ifp, $blockSize);
1490            if (!is_string($ciphertext)) {
1491                throw new SodiumException('Could not read input file');
1492            }
1493            $pBlock = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic(
1494                $ciphertext,
1495                $realNonce,
1496                $iter,
1497                $subkey
1498            );
1499            fwrite($ofp, $pBlock, $blockSize);
1500            $mlen -= $blockSize;
1501            $iter += $incr;
1502        }
1503        return true;
1504    }
1505
1506    /**
1507     * One-time message authentication for 32-bit systems
1508     *
1509     * @param ParagonIE_Sodium_Core32_Poly1305_State $state
1510     * @param resource $ifp
1511     * @param string $tag
1512     * @param int $mlen
1513     * @return bool
1514     * @throws SodiumException
1515     * @throws TypeError
1516     */
1517    protected static function onetimeauth_verify_core32(
1518        ParagonIE_Sodium_Core32_Poly1305_State $state,
1519        $ifp,
1520        $tag = '',
1521        $mlen = 0
1522    ) {
1523        /** @var int $pos */
1524        $pos = ftell($ifp);
1525
1526        /** @var int $iter */
1527        $iter = 1;
1528
1529        /** @var int $incr */
1530        $incr = self::BUFFER_SIZE >> 6;
1531
1532        while ($mlen > 0) {
1533            $blockSize = $mlen > self::BUFFER_SIZE
1534                ? self::BUFFER_SIZE
1535                : $mlen;
1536            $ciphertext = fread($ifp, $blockSize);
1537            if (!is_string($ciphertext)) {
1538                throw new SodiumException('Could not read input file');
1539            }
1540            $state->update($ciphertext);
1541            $mlen -= $blockSize;
1542            $iter += $incr;
1543        }
1544        $res = ParagonIE_Sodium_Core32_Util::verify_16($tag, $state->finish());
1545
1546        fseek($ifp, $pos, SEEK_SET);
1547        return $res;
1548    }
1549}
1550