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