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