Spaces:
Sleeping
Sleeping
if (class_exists('ParagonIE_Sodium_File', false)) { | |
return; | |
} | |
/** | |
* Class ParagonIE_Sodium_File | |
*/ | |
class ParagonIE_Sodium_File extends ParagonIE_Sodium_Core_Util | |
{ | |
/* PHP's default buffer size is 8192 for fread()/fwrite(). */ | |
const BUFFER_SIZE = 8192; | |
/** | |
* Box a file (rather than a string). Uses less memory than | |
* ParagonIE_Sodium_Compat::crypto_box(), but produces | |
* the same result. | |
* | |
* @param string $inputFile Absolute path to a file on the filesystem | |
* @param string $outputFile Absolute path to a file on the filesystem | |
* @param string $nonce Number to be used only once | |
* @param string $keyPair ECDH secret key and ECDH public key concatenated | |
* | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
public static function box($inputFile, $outputFile, $nonce, $keyPair) | |
{ | |
/* Type checks: */ | |
if (!is_string($inputFile)) { | |
throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.'); | |
} | |
if (!is_string($outputFile)) { | |
throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.'); | |
} | |
if (!is_string($nonce)) { | |
throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.'); | |
} | |
/* Input validation: */ | |
if (!is_string($keyPair)) { | |
throw new TypeError('Argument 4 must be a string, ' . gettype($keyPair) . ' given.'); | |
} | |
if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_NONCEBYTES) { | |
throw new TypeError('Argument 3 must be CRYPTO_BOX_NONCEBYTES bytes'); | |
} | |
if (self::strlen($keyPair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) { | |
throw new TypeError('Argument 4 must be CRYPTO_BOX_KEYPAIRBYTES bytes'); | |
} | |
/** @var int $size */ | |
$size = filesize($inputFile); | |
if (!is_int($size)) { | |
throw new SodiumException('Could not obtain the file size'); | |
} | |
/** @var resource $ifp */ | |
$ifp = fopen($inputFile, 'rb'); | |
if (!is_resource($ifp)) { | |
throw new SodiumException('Could not open input file for reading'); | |
} | |
/** @var resource $ofp */ | |
$ofp = fopen($outputFile, 'wb'); | |
if (!is_resource($ofp)) { | |
fclose($ifp); | |
throw new SodiumException('Could not open output file for writing'); | |
} | |
$res = self::box_encrypt($ifp, $ofp, $size, $nonce, $keyPair); | |
fclose($ifp); | |
fclose($ofp); | |
return $res; | |
} | |
/** | |
* Open a boxed file (rather than a string). Uses less memory than | |
* ParagonIE_Sodium_Compat::crypto_box_open(), but produces | |
* the same result. | |
* | |
* Warning: Does not protect against TOCTOU attacks. You should | |
* just load the file into memory and use crypto_box_open() if | |
* you are worried about those. | |
* | |
* @param string $inputFile | |
* @param string $outputFile | |
* @param string $nonce | |
* @param string $keypair | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
public static function box_open($inputFile, $outputFile, $nonce, $keypair) | |
{ | |
/* Type checks: */ | |
if (!is_string($inputFile)) { | |
throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.'); | |
} | |
if (!is_string($outputFile)) { | |
throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.'); | |
} | |
if (!is_string($nonce)) { | |
throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.'); | |
} | |
if (!is_string($keypair)) { | |
throw new TypeError('Argument 4 must be a string, ' . gettype($keypair) . ' given.'); | |
} | |
/* Input validation: */ | |
if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_NONCEBYTES) { | |
throw new TypeError('Argument 4 must be CRYPTO_BOX_NONCEBYTES bytes'); | |
} | |
if (self::strlen($keypair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) { | |
throw new TypeError('Argument 4 must be CRYPTO_BOX_KEYPAIRBYTES bytes'); | |
} | |
/** @var int $size */ | |
$size = filesize($inputFile); | |
if (!is_int($size)) { | |
throw new SodiumException('Could not obtain the file size'); | |
} | |
/** @var resource $ifp */ | |
$ifp = fopen($inputFile, 'rb'); | |
if (!is_resource($ifp)) { | |
throw new SodiumException('Could not open input file for reading'); | |
} | |
/** @var resource $ofp */ | |
$ofp = fopen($outputFile, 'wb'); | |
if (!is_resource($ofp)) { | |
fclose($ifp); | |
throw new SodiumException('Could not open output file for writing'); | |
} | |
$res = self::box_decrypt($ifp, $ofp, $size, $nonce, $keypair); | |
fclose($ifp); | |
fclose($ofp); | |
try { | |
ParagonIE_Sodium_Compat::memzero($nonce); | |
ParagonIE_Sodium_Compat::memzero($ephKeypair); | |
} catch (SodiumException $ex) { | |
if (isset($ephKeypair)) { | |
unset($ephKeypair); | |
} | |
} | |
return $res; | |
} | |
/** | |
* Seal a file (rather than a string). Uses less memory than | |
* ParagonIE_Sodium_Compat::crypto_box_seal(), but produces | |
* the same result. | |
* | |
* @param string $inputFile Absolute path to a file on the filesystem | |
* @param string $outputFile Absolute path to a file on the filesystem | |
* @param string $publicKey ECDH public key | |
* | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
public static function box_seal($inputFile, $outputFile, $publicKey) | |
{ | |
/* Type checks: */ | |
if (!is_string($inputFile)) { | |
throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.'); | |
} | |
if (!is_string($outputFile)) { | |
throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.'); | |
} | |
if (!is_string($publicKey)) { | |
throw new TypeError('Argument 3 must be a string, ' . gettype($publicKey) . ' given.'); | |
} | |
/* Input validation: */ | |
if (self::strlen($publicKey) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) { | |
throw new TypeError('Argument 3 must be CRYPTO_BOX_PUBLICKEYBYTES bytes'); | |
} | |
/** @var int $size */ | |
$size = filesize($inputFile); | |
if (!is_int($size)) { | |
throw new SodiumException('Could not obtain the file size'); | |
} | |
/** @var resource $ifp */ | |
$ifp = fopen($inputFile, 'rb'); | |
if (!is_resource($ifp)) { | |
throw new SodiumException('Could not open input file for reading'); | |
} | |
/** @var resource $ofp */ | |
$ofp = fopen($outputFile, 'wb'); | |
if (!is_resource($ofp)) { | |
fclose($ifp); | |
throw new SodiumException('Could not open output file for writing'); | |
} | |
/** @var string $ephKeypair */ | |
$ephKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair(); | |
/** @var string $msgKeypair */ | |
$msgKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair_from_secretkey_and_publickey( | |
ParagonIE_Sodium_Compat::crypto_box_secretkey($ephKeypair), | |
$publicKey | |
); | |
/** @var string $ephemeralPK */ | |
$ephemeralPK = ParagonIE_Sodium_Compat::crypto_box_publickey($ephKeypair); | |
/** @var string $nonce */ | |
$nonce = ParagonIE_Sodium_Compat::crypto_generichash( | |
$ephemeralPK . $publicKey, | |
'', | |
24 | |
); | |
/** @var int $firstWrite */ | |
$firstWrite = fwrite( | |
$ofp, | |
$ephemeralPK, | |
ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES | |
); | |
if (!is_int($firstWrite)) { | |
fclose($ifp); | |
fclose($ofp); | |
ParagonIE_Sodium_Compat::memzero($ephKeypair); | |
throw new SodiumException('Could not write to output file'); | |
} | |
if ($firstWrite !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) { | |
ParagonIE_Sodium_Compat::memzero($ephKeypair); | |
fclose($ifp); | |
fclose($ofp); | |
throw new SodiumException('Error writing public key to output file'); | |
} | |
$res = self::box_encrypt($ifp, $ofp, $size, $nonce, $msgKeypair); | |
fclose($ifp); | |
fclose($ofp); | |
try { | |
ParagonIE_Sodium_Compat::memzero($nonce); | |
ParagonIE_Sodium_Compat::memzero($ephKeypair); | |
} catch (SodiumException $ex) { | |
/** @psalm-suppress PossiblyUndefinedVariable */ | |
unset($ephKeypair); | |
} | |
return $res; | |
} | |
/** | |
* Open a sealed file (rather than a string). Uses less memory than | |
* ParagonIE_Sodium_Compat::crypto_box_seal_open(), but produces | |
* the same result. | |
* | |
* Warning: Does not protect against TOCTOU attacks. You should | |
* just load the file into memory and use crypto_box_seal_open() if | |
* you are worried about those. | |
* | |
* @param string $inputFile | |
* @param string $outputFile | |
* @param string $ecdhKeypair | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
public static function box_seal_open($inputFile, $outputFile, $ecdhKeypair) | |
{ | |
/* Type checks: */ | |
if (!is_string($inputFile)) { | |
throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.'); | |
} | |
if (!is_string($outputFile)) { | |
throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.'); | |
} | |
if (!is_string($ecdhKeypair)) { | |
throw new TypeError('Argument 3 must be a string, ' . gettype($ecdhKeypair) . ' given.'); | |
} | |
/* Input validation: */ | |
if (self::strlen($ecdhKeypair) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_KEYPAIRBYTES) { | |
throw new TypeError('Argument 3 must be CRYPTO_BOX_KEYPAIRBYTES bytes'); | |
} | |
$publicKey = ParagonIE_Sodium_Compat::crypto_box_publickey($ecdhKeypair); | |
/** @var int $size */ | |
$size = filesize($inputFile); | |
if (!is_int($size)) { | |
throw new SodiumException('Could not obtain the file size'); | |
} | |
/** @var resource $ifp */ | |
$ifp = fopen($inputFile, 'rb'); | |
if (!is_resource($ifp)) { | |
throw new SodiumException('Could not open input file for reading'); | |
} | |
/** @var resource $ofp */ | |
$ofp = fopen($outputFile, 'wb'); | |
if (!is_resource($ofp)) { | |
fclose($ifp); | |
throw new SodiumException('Could not open output file for writing'); | |
} | |
$ephemeralPK = fread($ifp, ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES); | |
if (!is_string($ephemeralPK)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
if (self::strlen($ephemeralPK) !== ParagonIE_Sodium_Compat::CRYPTO_BOX_PUBLICKEYBYTES) { | |
fclose($ifp); | |
fclose($ofp); | |
throw new SodiumException('Could not read public key from sealed file'); | |
} | |
$nonce = ParagonIE_Sodium_Compat::crypto_generichash( | |
$ephemeralPK . $publicKey, | |
'', | |
24 | |
); | |
$msgKeypair = ParagonIE_Sodium_Compat::crypto_box_keypair_from_secretkey_and_publickey( | |
ParagonIE_Sodium_Compat::crypto_box_secretkey($ecdhKeypair), | |
$ephemeralPK | |
); | |
$res = self::box_decrypt($ifp, $ofp, $size, $nonce, $msgKeypair); | |
fclose($ifp); | |
fclose($ofp); | |
try { | |
ParagonIE_Sodium_Compat::memzero($nonce); | |
ParagonIE_Sodium_Compat::memzero($ephKeypair); | |
} catch (SodiumException $ex) { | |
if (isset($ephKeypair)) { | |
unset($ephKeypair); | |
} | |
} | |
return $res; | |
} | |
/** | |
* Calculate the BLAKE2b hash of a file. | |
* | |
* @param string $filePath Absolute path to a file on the filesystem | |
* @param string|null $key BLAKE2b key | |
* @param int $outputLength Length of hash output | |
* | |
* @return string BLAKE2b hash | |
* @throws SodiumException | |
* @throws TypeError | |
* @psalm-suppress FailedTypeResolution | |
*/ | |
public static function generichash($filePath, $key = '', $outputLength = 32) | |
{ | |
/* Type checks: */ | |
if (!is_string($filePath)) { | |
throw new TypeError('Argument 1 must be a string, ' . gettype($filePath) . ' given.'); | |
} | |
if (!is_string($key)) { | |
if (is_null($key)) { | |
$key = ''; | |
} else { | |
throw new TypeError('Argument 2 must be a string, ' . gettype($key) . ' given.'); | |
} | |
} | |
if (!is_int($outputLength)) { | |
if (!is_numeric($outputLength)) { | |
throw new TypeError('Argument 3 must be an integer, ' . gettype($outputLength) . ' given.'); | |
} | |
$outputLength = (int) $outputLength; | |
} | |
/* Input validation: */ | |
if (!empty($key)) { | |
if (self::strlen($key) < ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_KEYBYTES_MIN) { | |
throw new TypeError('Argument 2 must be at least CRYPTO_GENERICHASH_KEYBYTES_MIN bytes'); | |
} | |
if (self::strlen($key) > ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_KEYBYTES_MAX) { | |
throw new TypeError('Argument 2 must be at most CRYPTO_GENERICHASH_KEYBYTES_MAX bytes'); | |
} | |
} | |
if ($outputLength < ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_BYTES_MIN) { | |
throw new SodiumException('Argument 3 must be at least CRYPTO_GENERICHASH_BYTES_MIN'); | |
} | |
if ($outputLength > ParagonIE_Sodium_Compat::CRYPTO_GENERICHASH_BYTES_MAX) { | |
throw new SodiumException('Argument 3 must be at least CRYPTO_GENERICHASH_BYTES_MAX'); | |
} | |
/** @var int $size */ | |
$size = filesize($filePath); | |
if (!is_int($size)) { | |
throw new SodiumException('Could not obtain the file size'); | |
} | |
/** @var resource $fp */ | |
$fp = fopen($filePath, 'rb'); | |
if (!is_resource($fp)) { | |
throw new SodiumException('Could not open input file for reading'); | |
} | |
$ctx = ParagonIE_Sodium_Compat::crypto_generichash_init($key, $outputLength); | |
while ($size > 0) { | |
$blockSize = $size > 64 | |
? 64 | |
: $size; | |
$read = fread($fp, $blockSize); | |
if (!is_string($read)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
ParagonIE_Sodium_Compat::crypto_generichash_update($ctx, $read); | |
$size -= $blockSize; | |
} | |
fclose($fp); | |
return ParagonIE_Sodium_Compat::crypto_generichash_final($ctx, $outputLength); | |
} | |
/** | |
* Encrypt a file (rather than a string). Uses less memory than | |
* ParagonIE_Sodium_Compat::crypto_secretbox(), but produces | |
* the same result. | |
* | |
* @param string $inputFile Absolute path to a file on the filesystem | |
* @param string $outputFile Absolute path to a file on the filesystem | |
* @param string $nonce Number to be used only once | |
* @param string $key Encryption key | |
* | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
public static function secretbox($inputFile, $outputFile, $nonce, $key) | |
{ | |
/* Type checks: */ | |
if (!is_string($inputFile)) { | |
throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given..'); | |
} | |
if (!is_string($outputFile)) { | |
throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.'); | |
} | |
if (!is_string($nonce)) { | |
throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.'); | |
} | |
/* Input validation: */ | |
if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_NONCEBYTES) { | |
throw new TypeError('Argument 3 must be CRYPTO_SECRETBOX_NONCEBYTES bytes'); | |
} | |
if (!is_string($key)) { | |
throw new TypeError('Argument 4 must be a string, ' . gettype($key) . ' given.'); | |
} | |
if (self::strlen($key) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_KEYBYTES) { | |
throw new TypeError('Argument 4 must be CRYPTO_SECRETBOX_KEYBYTES bytes'); | |
} | |
/** @var int $size */ | |
$size = filesize($inputFile); | |
if (!is_int($size)) { | |
throw new SodiumException('Could not obtain the file size'); | |
} | |
/** @var resource $ifp */ | |
$ifp = fopen($inputFile, 'rb'); | |
if (!is_resource($ifp)) { | |
throw new SodiumException('Could not open input file for reading'); | |
} | |
/** @var resource $ofp */ | |
$ofp = fopen($outputFile, 'wb'); | |
if (!is_resource($ofp)) { | |
fclose($ifp); | |
throw new SodiumException('Could not open output file for writing'); | |
} | |
$res = self::secretbox_encrypt($ifp, $ofp, $size, $nonce, $key); | |
fclose($ifp); | |
fclose($ofp); | |
return $res; | |
} | |
/** | |
* Seal a file (rather than a string). Uses less memory than | |
* ParagonIE_Sodium_Compat::crypto_secretbox_open(), but produces | |
* the same result. | |
* | |
* Warning: Does not protect against TOCTOU attacks. You should | |
* just load the file into memory and use crypto_secretbox_open() if | |
* you are worried about those. | |
* | |
* @param string $inputFile | |
* @param string $outputFile | |
* @param string $nonce | |
* @param string $key | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
public static function secretbox_open($inputFile, $outputFile, $nonce, $key) | |
{ | |
/* Type checks: */ | |
if (!is_string($inputFile)) { | |
throw new TypeError('Argument 1 must be a string, ' . gettype($inputFile) . ' given.'); | |
} | |
if (!is_string($outputFile)) { | |
throw new TypeError('Argument 2 must be a string, ' . gettype($outputFile) . ' given.'); | |
} | |
if (!is_string($nonce)) { | |
throw new TypeError('Argument 3 must be a string, ' . gettype($nonce) . ' given.'); | |
} | |
if (!is_string($key)) { | |
throw new TypeError('Argument 4 must be a string, ' . gettype($key) . ' given.'); | |
} | |
/* Input validation: */ | |
if (self::strlen($nonce) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_NONCEBYTES) { | |
throw new TypeError('Argument 4 must be CRYPTO_SECRETBOX_NONCEBYTES bytes'); | |
} | |
if (self::strlen($key) !== ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_KEYBYTES) { | |
throw new TypeError('Argument 4 must be CRYPTO_SECRETBOXBOX_KEYBYTES bytes'); | |
} | |
/** @var int $size */ | |
$size = filesize($inputFile); | |
if (!is_int($size)) { | |
throw new SodiumException('Could not obtain the file size'); | |
} | |
/** @var resource $ifp */ | |
$ifp = fopen($inputFile, 'rb'); | |
if (!is_resource($ifp)) { | |
throw new SodiumException('Could not open input file for reading'); | |
} | |
/** @var resource $ofp */ | |
$ofp = fopen($outputFile, 'wb'); | |
if (!is_resource($ofp)) { | |
fclose($ifp); | |
throw new SodiumException('Could not open output file for writing'); | |
} | |
$res = self::secretbox_decrypt($ifp, $ofp, $size, $nonce, $key); | |
fclose($ifp); | |
fclose($ofp); | |
try { | |
ParagonIE_Sodium_Compat::memzero($key); | |
} catch (SodiumException $ex) { | |
/** @psalm-suppress PossiblyUndefinedVariable */ | |
unset($key); | |
} | |
return $res; | |
} | |
/** | |
* Sign a file (rather than a string). Uses less memory than | |
* ParagonIE_Sodium_Compat::crypto_sign_detached(), but produces | |
* the same result. | |
* | |
* @param string $filePath Absolute path to a file on the filesystem | |
* @param string $secretKey Secret signing key | |
* | |
* @return string Ed25519 signature | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
public static function sign($filePath, $secretKey) | |
{ | |
/* Type checks: */ | |
if (!is_string($filePath)) { | |
throw new TypeError('Argument 1 must be a string, ' . gettype($filePath) . ' given.'); | |
} | |
if (!is_string($secretKey)) { | |
throw new TypeError('Argument 2 must be a string, ' . gettype($secretKey) . ' given.'); | |
} | |
/* Input validation: */ | |
if (self::strlen($secretKey) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_SECRETKEYBYTES) { | |
throw new TypeError('Argument 2 must be CRYPTO_SIGN_SECRETKEYBYTES bytes'); | |
} | |
if (PHP_INT_SIZE === 4) { | |
return self::sign_core32($filePath, $secretKey); | |
} | |
/** @var int $size */ | |
$size = filesize($filePath); | |
if (!is_int($size)) { | |
throw new SodiumException('Could not obtain the file size'); | |
} | |
/** @var resource $fp */ | |
$fp = fopen($filePath, 'rb'); | |
if (!is_resource($fp)) { | |
throw new SodiumException('Could not open input file for reading'); | |
} | |
/** @var string $az */ | |
$az = hash('sha512', self::substr($secretKey, 0, 32), true); | |
$az[0] = self::intToChr(self::chrToInt($az[0]) & 248); | |
$az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64); | |
$hs = hash_init('sha512'); | |
self::hash_update($hs, self::substr($az, 32, 32)); | |
/** @var resource $hs */ | |
$hs = self::updateHashWithFile($hs, $fp, $size); | |
/** @var string $nonceHash */ | |
$nonceHash = hash_final($hs, true); | |
/** @var string $pk */ | |
$pk = self::substr($secretKey, 32, 32); | |
/** @var string $nonce */ | |
$nonce = ParagonIE_Sodium_Core_Ed25519::sc_reduce($nonceHash) . self::substr($nonceHash, 32); | |
/** @var string $sig */ | |
$sig = ParagonIE_Sodium_Core_Ed25519::ge_p3_tobytes( | |
ParagonIE_Sodium_Core_Ed25519::ge_scalarmult_base($nonce) | |
); | |
$hs = hash_init('sha512'); | |
self::hash_update($hs, self::substr($sig, 0, 32)); | |
self::hash_update($hs, self::substr($pk, 0, 32)); | |
/** @var resource $hs */ | |
$hs = self::updateHashWithFile($hs, $fp, $size); | |
/** @var string $hramHash */ | |
$hramHash = hash_final($hs, true); | |
/** @var string $hram */ | |
$hram = ParagonIE_Sodium_Core_Ed25519::sc_reduce($hramHash); | |
/** @var string $sigAfter */ | |
$sigAfter = ParagonIE_Sodium_Core_Ed25519::sc_muladd($hram, $az, $nonce); | |
/** @var string $sig */ | |
$sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32); | |
try { | |
ParagonIE_Sodium_Compat::memzero($az); | |
} catch (SodiumException $ex) { | |
$az = null; | |
} | |
fclose($fp); | |
return $sig; | |
} | |
/** | |
* Verify a file (rather than a string). Uses less memory than | |
* ParagonIE_Sodium_Compat::crypto_sign_verify_detached(), but | |
* produces the same result. | |
* | |
* @param string $sig Ed25519 signature | |
* @param string $filePath Absolute path to a file on the filesystem | |
* @param string $publicKey Signing public key | |
* | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
* @throws Exception | |
*/ | |
public static function verify($sig, $filePath, $publicKey) | |
{ | |
/* Type checks: */ | |
if (!is_string($sig)) { | |
throw new TypeError('Argument 1 must be a string, ' . gettype($sig) . ' given.'); | |
} | |
if (!is_string($filePath)) { | |
throw new TypeError('Argument 2 must be a string, ' . gettype($filePath) . ' given.'); | |
} | |
if (!is_string($publicKey)) { | |
throw new TypeError('Argument 3 must be a string, ' . gettype($publicKey) . ' given.'); | |
} | |
/* Input validation: */ | |
if (self::strlen($sig) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_BYTES) { | |
throw new TypeError('Argument 1 must be CRYPTO_SIGN_BYTES bytes'); | |
} | |
if (self::strlen($publicKey) !== ParagonIE_Sodium_Compat::CRYPTO_SIGN_PUBLICKEYBYTES) { | |
throw new TypeError('Argument 3 must be CRYPTO_SIGN_PUBLICKEYBYTES bytes'); | |
} | |
if (self::strlen($sig) < 64) { | |
throw new SodiumException('Signature is too short'); | |
} | |
if (PHP_INT_SIZE === 4) { | |
return self::verify_core32($sig, $filePath, $publicKey); | |
} | |
/* Security checks */ | |
if ( | |
(ParagonIE_Sodium_Core_Ed25519::chrToInt($sig[63]) & 240) | |
&& | |
ParagonIE_Sodium_Core_Ed25519::check_S_lt_L(self::substr($sig, 32, 32)) | |
) { | |
throw new SodiumException('S < L - Invalid signature'); | |
} | |
if (ParagonIE_Sodium_Core_Ed25519::small_order($sig)) { | |
throw new SodiumException('Signature is on too small of an order'); | |
} | |
if ((self::chrToInt($sig[63]) & 224) !== 0) { | |
throw new SodiumException('Invalid signature'); | |
} | |
$d = 0; | |
for ($i = 0; $i < 32; ++$i) { | |
$d |= self::chrToInt($publicKey[$i]); | |
} | |
if ($d === 0) { | |
throw new SodiumException('All zero public key'); | |
} | |
/** @var int $size */ | |
$size = filesize($filePath); | |
if (!is_int($size)) { | |
throw new SodiumException('Could not obtain the file size'); | |
} | |
/** @var resource $fp */ | |
$fp = fopen($filePath, 'rb'); | |
if (!is_resource($fp)) { | |
throw new SodiumException('Could not open input file for reading'); | |
} | |
/** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */ | |
$orig = ParagonIE_Sodium_Compat::$fastMult; | |
// Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification. | |
ParagonIE_Sodium_Compat::$fastMult = true; | |
/** @var ParagonIE_Sodium_Core_Curve25519_Ge_P3 $A */ | |
$A = ParagonIE_Sodium_Core_Ed25519::ge_frombytes_negate_vartime($publicKey); | |
$hs = hash_init('sha512'); | |
self::hash_update($hs, self::substr($sig, 0, 32)); | |
self::hash_update($hs, self::substr($publicKey, 0, 32)); | |
/** @var resource $hs */ | |
$hs = self::updateHashWithFile($hs, $fp, $size); | |
/** @var string $hDigest */ | |
$hDigest = hash_final($hs, true); | |
/** @var string $h */ | |
$h = ParagonIE_Sodium_Core_Ed25519::sc_reduce($hDigest) . self::substr($hDigest, 32); | |
/** @var ParagonIE_Sodium_Core_Curve25519_Ge_P2 $R */ | |
$R = ParagonIE_Sodium_Core_Ed25519::ge_double_scalarmult_vartime( | |
$h, | |
$A, | |
self::substr($sig, 32) | |
); | |
/** @var string $rcheck */ | |
$rcheck = ParagonIE_Sodium_Core_Ed25519::ge_tobytes($R); | |
// Close the file handle | |
fclose($fp); | |
// Reset ParagonIE_Sodium_Compat::$fastMult to what it was before. | |
ParagonIE_Sodium_Compat::$fastMult = $orig; | |
return self::verify_32($rcheck, self::substr($sig, 0, 32)); | |
} | |
/** | |
* @param resource $ifp | |
* @param resource $ofp | |
* @param int $mlen | |
* @param string $nonce | |
* @param string $boxKeypair | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
protected static function box_encrypt($ifp, $ofp, $mlen, $nonce, $boxKeypair) | |
{ | |
if (PHP_INT_SIZE === 4) { | |
return self::secretbox_encrypt( | |
$ifp, | |
$ofp, | |
$mlen, | |
$nonce, | |
ParagonIE_Sodium_Crypto32::box_beforenm( | |
ParagonIE_Sodium_Crypto32::box_secretkey($boxKeypair), | |
ParagonIE_Sodium_Crypto32::box_publickey($boxKeypair) | |
) | |
); | |
} | |
return self::secretbox_encrypt( | |
$ifp, | |
$ofp, | |
$mlen, | |
$nonce, | |
ParagonIE_Sodium_Crypto::box_beforenm( | |
ParagonIE_Sodium_Crypto::box_secretkey($boxKeypair), | |
ParagonIE_Sodium_Crypto::box_publickey($boxKeypair) | |
) | |
); | |
} | |
/** | |
* @param resource $ifp | |
* @param resource $ofp | |
* @param int $mlen | |
* @param string $nonce | |
* @param string $boxKeypair | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
protected static function box_decrypt($ifp, $ofp, $mlen, $nonce, $boxKeypair) | |
{ | |
if (PHP_INT_SIZE === 4) { | |
return self::secretbox_decrypt( | |
$ifp, | |
$ofp, | |
$mlen, | |
$nonce, | |
ParagonIE_Sodium_Crypto32::box_beforenm( | |
ParagonIE_Sodium_Crypto32::box_secretkey($boxKeypair), | |
ParagonIE_Sodium_Crypto32::box_publickey($boxKeypair) | |
) | |
); | |
} | |
return self::secretbox_decrypt( | |
$ifp, | |
$ofp, | |
$mlen, | |
$nonce, | |
ParagonIE_Sodium_Crypto::box_beforenm( | |
ParagonIE_Sodium_Crypto::box_secretkey($boxKeypair), | |
ParagonIE_Sodium_Crypto::box_publickey($boxKeypair) | |
) | |
); | |
} | |
/** | |
* Encrypt a file | |
* | |
* @param resource $ifp | |
* @param resource $ofp | |
* @param int $mlen | |
* @param string $nonce | |
* @param string $key | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
protected static function secretbox_encrypt($ifp, $ofp, $mlen, $nonce, $key) | |
{ | |
if (PHP_INT_SIZE === 4) { | |
return self::secretbox_encrypt_core32($ifp, $ofp, $mlen, $nonce, $key); | |
} | |
$plaintext = fread($ifp, 32); | |
if (!is_string($plaintext)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
$first32 = self::ftell($ifp); | |
/** @var string $subkey */ | |
$subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key); | |
/** @var string $realNonce */ | |
$realNonce = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8); | |
/** @var string $block0 */ | |
$block0 = str_repeat("\x00", 32); | |
/** @var int $mlen - Length of the plaintext message */ | |
$mlen0 = $mlen; | |
if ($mlen0 > 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES) { | |
$mlen0 = 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES; | |
} | |
$block0 .= ParagonIE_Sodium_Core_Util::substr($plaintext, 0, $mlen0); | |
/** @var string $block0 */ | |
$block0 = ParagonIE_Sodium_Core_Salsa20::salsa20_xor( | |
$block0, | |
$realNonce, | |
$subkey | |
); | |
$state = new ParagonIE_Sodium_Core_Poly1305_State( | |
ParagonIE_Sodium_Core_Util::substr( | |
$block0, | |
0, | |
ParagonIE_Sodium_Crypto::onetimeauth_poly1305_KEYBYTES | |
) | |
); | |
// Pre-write 16 blank bytes for the Poly1305 tag | |
$start = self::ftell($ofp); | |
fwrite($ofp, str_repeat("\x00", 16)); | |
/** @var string $c */ | |
$cBlock = ParagonIE_Sodium_Core_Util::substr( | |
$block0, | |
ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES | |
); | |
$state->update($cBlock); | |
fwrite($ofp, $cBlock); | |
$mlen -= 32; | |
/** @var int $iter */ | |
$iter = 1; | |
/** @var int $incr */ | |
$incr = self::BUFFER_SIZE >> 6; | |
/* | |
* Set the cursor to the end of the first half-block. All future bytes will | |
* generated from salsa20_xor_ic, starting from 1 (second block). | |
*/ | |
fseek($ifp, $first32, SEEK_SET); | |
while ($mlen > 0) { | |
$blockSize = $mlen > self::BUFFER_SIZE | |
? self::BUFFER_SIZE | |
: $mlen; | |
$plaintext = fread($ifp, $blockSize); | |
if (!is_string($plaintext)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
$cBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic( | |
$plaintext, | |
$realNonce, | |
$iter, | |
$subkey | |
); | |
fwrite($ofp, $cBlock, $blockSize); | |
$state->update($cBlock); | |
$mlen -= $blockSize; | |
$iter += $incr; | |
} | |
try { | |
ParagonIE_Sodium_Compat::memzero($block0); | |
ParagonIE_Sodium_Compat::memzero($subkey); | |
} catch (SodiumException $ex) { | |
$block0 = null; | |
$subkey = null; | |
} | |
$end = self::ftell($ofp); | |
/* | |
* Write the Poly1305 authentication tag that provides integrity | |
* over the ciphertext (encrypt-then-MAC) | |
*/ | |
fseek($ofp, $start, SEEK_SET); | |
fwrite($ofp, $state->finish(), ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_MACBYTES); | |
fseek($ofp, $end, SEEK_SET); | |
unset($state); | |
return true; | |
} | |
/** | |
* Decrypt a file | |
* | |
* @param resource $ifp | |
* @param resource $ofp | |
* @param int $mlen | |
* @param string $nonce | |
* @param string $key | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
protected static function secretbox_decrypt($ifp, $ofp, $mlen, $nonce, $key) | |
{ | |
if (PHP_INT_SIZE === 4) { | |
return self::secretbox_decrypt_core32($ifp, $ofp, $mlen, $nonce, $key); | |
} | |
$tag = fread($ifp, 16); | |
if (!is_string($tag)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
/** @var string $subkey */ | |
$subkey = ParagonIE_Sodium_Core_HSalsa20::hsalsa20($nonce, $key); | |
/** @var string $realNonce */ | |
$realNonce = ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8); | |
/** @var string $block0 */ | |
$block0 = ParagonIE_Sodium_Core_Salsa20::salsa20( | |
64, | |
ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8), | |
$subkey | |
); | |
/* Verify the Poly1305 MAC -before- attempting to decrypt! */ | |
$state = new ParagonIE_Sodium_Core_Poly1305_State(self::substr($block0, 0, 32)); | |
if (!self::onetimeauth_verify($state, $ifp, $tag, $mlen)) { | |
throw new SodiumException('Invalid MAC'); | |
} | |
/* | |
* Set the cursor to the end of the first half-block. All future bytes will | |
* generated from salsa20_xor_ic, starting from 1 (second block). | |
*/ | |
$first32 = fread($ifp, 32); | |
if (!is_string($first32)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
$first32len = self::strlen($first32); | |
fwrite( | |
$ofp, | |
self::xorStrings( | |
self::substr($block0, 32, $first32len), | |
self::substr($first32, 0, $first32len) | |
) | |
); | |
$mlen -= 32; | |
/** @var int $iter */ | |
$iter = 1; | |
/** @var int $incr */ | |
$incr = self::BUFFER_SIZE >> 6; | |
/* Decrypts ciphertext, writes to output file. */ | |
while ($mlen > 0) { | |
$blockSize = $mlen > self::BUFFER_SIZE | |
? self::BUFFER_SIZE | |
: $mlen; | |
$ciphertext = fread($ifp, $blockSize); | |
if (!is_string($ciphertext)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
$pBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic( | |
$ciphertext, | |
$realNonce, | |
$iter, | |
$subkey | |
); | |
fwrite($ofp, $pBlock, $blockSize); | |
$mlen -= $blockSize; | |
$iter += $incr; | |
} | |
return true; | |
} | |
/** | |
* @param ParagonIE_Sodium_Core_Poly1305_State $state | |
* @param resource $ifp | |
* @param string $tag | |
* @param int $mlen | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
protected static function onetimeauth_verify( | |
ParagonIE_Sodium_Core_Poly1305_State $state, | |
$ifp, | |
$tag = '', | |
$mlen = 0 | |
) { | |
/** @var int $pos */ | |
$pos = self::ftell($ifp); | |
/** @var int $iter */ | |
$iter = 1; | |
/** @var int $incr */ | |
$incr = self::BUFFER_SIZE >> 6; | |
while ($mlen > 0) { | |
$blockSize = $mlen > self::BUFFER_SIZE | |
? self::BUFFER_SIZE | |
: $mlen; | |
$ciphertext = fread($ifp, $blockSize); | |
if (!is_string($ciphertext)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
$state->update($ciphertext); | |
$mlen -= $blockSize; | |
$iter += $incr; | |
} | |
$res = ParagonIE_Sodium_Core_Util::verify_16($tag, $state->finish()); | |
fseek($ifp, $pos, SEEK_SET); | |
return $res; | |
} | |
/** | |
* Update a hash context with the contents of a file, without | |
* loading the entire file into memory. | |
* | |
* @param resource|HashContext $hash | |
* @param resource $fp | |
* @param int $size | |
* @return resource|object Resource on PHP < 7.2, HashContext object on PHP >= 7.2 | |
* @throws SodiumException | |
* @throws TypeError | |
* @psalm-suppress PossiblyInvalidArgument | |
* PHP 7.2 changes from a resource to an object, | |
* which causes Psalm to complain about an error. | |
* @psalm-suppress TypeCoercion | |
* Ditto. | |
*/ | |
public static function updateHashWithFile($hash, $fp, $size = 0) | |
{ | |
/* Type checks: */ | |
if (PHP_VERSION_ID < 70200) { | |
if (!is_resource($hash)) { | |
throw new TypeError('Argument 1 must be a resource, ' . gettype($hash) . ' given.'); | |
} | |
} else { | |
if (!is_object($hash)) { | |
throw new TypeError('Argument 1 must be an object (PHP 7.2+), ' . gettype($hash) . ' given.'); | |
} | |
} | |
if (!is_resource($fp)) { | |
throw new TypeError('Argument 2 must be a resource, ' . gettype($fp) . ' given.'); | |
} | |
if (!is_int($size)) { | |
throw new TypeError('Argument 3 must be an integer, ' . gettype($size) . ' given.'); | |
} | |
/** @var int $originalPosition */ | |
$originalPosition = self::ftell($fp); | |
// Move file pointer to beginning of file | |
fseek($fp, 0, SEEK_SET); | |
for ($i = 0; $i < $size; $i += self::BUFFER_SIZE) { | |
/** @var string|bool $message */ | |
$message = fread( | |
$fp, | |
($size - $i) > self::BUFFER_SIZE | |
? $size - $i | |
: self::BUFFER_SIZE | |
); | |
if (!is_string($message)) { | |
throw new SodiumException('Unexpected error reading from file.'); | |
} | |
/** @var string $message */ | |
/** @psalm-suppress InvalidArgument */ | |
self::hash_update($hash, $message); | |
} | |
// Reset file pointer's position | |
fseek($fp, $originalPosition, SEEK_SET); | |
return $hash; | |
} | |
/** | |
* Sign a file (rather than a string). Uses less memory than | |
* ParagonIE_Sodium_Compat::crypto_sign_detached(), but produces | |
* the same result. (32-bit) | |
* | |
* @param string $filePath Absolute path to a file on the filesystem | |
* @param string $secretKey Secret signing key | |
* | |
* @return string Ed25519 signature | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
private static function sign_core32($filePath, $secretKey) | |
{ | |
$size = filesize($filePath); | |
if (!is_int($size)) { | |
throw new SodiumException('Could not obtain the file size'); | |
} | |
$fp = fopen($filePath, 'rb'); | |
if (!is_resource($fp)) { | |
throw new SodiumException('Could not open input file for reading'); | |
} | |
/** @var string $az */ | |
$az = hash('sha512', self::substr($secretKey, 0, 32), true); | |
$az[0] = self::intToChr(self::chrToInt($az[0]) & 248); | |
$az[31] = self::intToChr((self::chrToInt($az[31]) & 63) | 64); | |
$hs = hash_init('sha512'); | |
self::hash_update($hs, self::substr($az, 32, 32)); | |
/** @var resource $hs */ | |
$hs = self::updateHashWithFile($hs, $fp, $size); | |
$nonceHash = hash_final($hs, true); | |
$pk = self::substr($secretKey, 32, 32); | |
$nonce = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($nonceHash) . self::substr($nonceHash, 32); | |
$sig = ParagonIE_Sodium_Core32_Ed25519::ge_p3_tobytes( | |
ParagonIE_Sodium_Core32_Ed25519::ge_scalarmult_base($nonce) | |
); | |
$hs = hash_init('sha512'); | |
self::hash_update($hs, self::substr($sig, 0, 32)); | |
self::hash_update($hs, self::substr($pk, 0, 32)); | |
/** @var resource $hs */ | |
$hs = self::updateHashWithFile($hs, $fp, $size); | |
$hramHash = hash_final($hs, true); | |
$hram = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($hramHash); | |
$sigAfter = ParagonIE_Sodium_Core32_Ed25519::sc_muladd($hram, $az, $nonce); | |
/** @var string $sig */ | |
$sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32); | |
try { | |
ParagonIE_Sodium_Compat::memzero($az); | |
} catch (SodiumException $ex) { | |
$az = null; | |
} | |
fclose($fp); | |
return $sig; | |
} | |
/** | |
* | |
* Verify a file (rather than a string). Uses less memory than | |
* ParagonIE_Sodium_Compat::crypto_sign_verify_detached(), but | |
* produces the same result. (32-bit) | |
* | |
* @param string $sig Ed25519 signature | |
* @param string $filePath Absolute path to a file on the filesystem | |
* @param string $publicKey Signing public key | |
* | |
* @return bool | |
* @throws SodiumException | |
* @throws Exception | |
*/ | |
public static function verify_core32($sig, $filePath, $publicKey) | |
{ | |
/* Security checks */ | |
if (ParagonIE_Sodium_Core32_Ed25519::check_S_lt_L(self::substr($sig, 32, 32))) { | |
throw new SodiumException('S < L - Invalid signature'); | |
} | |
if (ParagonIE_Sodium_Core32_Ed25519::small_order($sig)) { | |
throw new SodiumException('Signature is on too small of an order'); | |
} | |
if ((self::chrToInt($sig[63]) & 224) !== 0) { | |
throw new SodiumException('Invalid signature'); | |
} | |
$d = 0; | |
for ($i = 0; $i < 32; ++$i) { | |
$d |= self::chrToInt($publicKey[$i]); | |
} | |
if ($d === 0) { | |
throw new SodiumException('All zero public key'); | |
} | |
/** @var int|bool $size */ | |
$size = filesize($filePath); | |
if (!is_int($size)) { | |
throw new SodiumException('Could not obtain the file size'); | |
} | |
/** @var int $size */ | |
/** @var resource|bool $fp */ | |
$fp = fopen($filePath, 'rb'); | |
if (!is_resource($fp)) { | |
throw new SodiumException('Could not open input file for reading'); | |
} | |
/** @var resource $fp */ | |
/** @var bool The original value of ParagonIE_Sodium_Compat::$fastMult */ | |
$orig = ParagonIE_Sodium_Compat::$fastMult; | |
// Set ParagonIE_Sodium_Compat::$fastMult to true to speed up verification. | |
ParagonIE_Sodium_Compat::$fastMult = true; | |
/** @var ParagonIE_Sodium_Core32_Curve25519_Ge_P3 $A */ | |
$A = ParagonIE_Sodium_Core32_Ed25519::ge_frombytes_negate_vartime($publicKey); | |
$hs = hash_init('sha512'); | |
self::hash_update($hs, self::substr($sig, 0, 32)); | |
self::hash_update($hs, self::substr($publicKey, 0, 32)); | |
/** @var resource $hs */ | |
$hs = self::updateHashWithFile($hs, $fp, $size); | |
/** @var string $hDigest */ | |
$hDigest = hash_final($hs, true); | |
/** @var string $h */ | |
$h = ParagonIE_Sodium_Core32_Ed25519::sc_reduce($hDigest) . self::substr($hDigest, 32); | |
/** @var ParagonIE_Sodium_Core32_Curve25519_Ge_P2 $R */ | |
$R = ParagonIE_Sodium_Core32_Ed25519::ge_double_scalarmult_vartime( | |
$h, | |
$A, | |
self::substr($sig, 32) | |
); | |
/** @var string $rcheck */ | |
$rcheck = ParagonIE_Sodium_Core32_Ed25519::ge_tobytes($R); | |
// Close the file handle | |
fclose($fp); | |
// Reset ParagonIE_Sodium_Compat::$fastMult to what it was before. | |
ParagonIE_Sodium_Compat::$fastMult = $orig; | |
return self::verify_32($rcheck, self::substr($sig, 0, 32)); | |
} | |
/** | |
* Encrypt a file (32-bit) | |
* | |
* @param resource $ifp | |
* @param resource $ofp | |
* @param int $mlen | |
* @param string $nonce | |
* @param string $key | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
protected static function secretbox_encrypt_core32($ifp, $ofp, $mlen, $nonce, $key) | |
{ | |
$plaintext = fread($ifp, 32); | |
if (!is_string($plaintext)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
$first32 = self::ftell($ifp); | |
/** @var string $subkey */ | |
$subkey = ParagonIE_Sodium_Core32_HSalsa20::hsalsa20($nonce, $key); | |
/** @var string $realNonce */ | |
$realNonce = ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8); | |
/** @var string $block0 */ | |
$block0 = str_repeat("\x00", 32); | |
/** @var int $mlen - Length of the plaintext message */ | |
$mlen0 = $mlen; | |
if ($mlen0 > 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES) { | |
$mlen0 = 64 - ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES; | |
} | |
$block0 .= ParagonIE_Sodium_Core32_Util::substr($plaintext, 0, $mlen0); | |
/** @var string $block0 */ | |
$block0 = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor( | |
$block0, | |
$realNonce, | |
$subkey | |
); | |
$state = new ParagonIE_Sodium_Core32_Poly1305_State( | |
ParagonIE_Sodium_Core32_Util::substr( | |
$block0, | |
0, | |
ParagonIE_Sodium_Crypto::onetimeauth_poly1305_KEYBYTES | |
) | |
); | |
// Pre-write 16 blank bytes for the Poly1305 tag | |
$start = self::ftell($ofp); | |
fwrite($ofp, str_repeat("\x00", 16)); | |
/** @var string $c */ | |
$cBlock = ParagonIE_Sodium_Core32_Util::substr( | |
$block0, | |
ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES | |
); | |
$state->update($cBlock); | |
fwrite($ofp, $cBlock); | |
$mlen -= 32; | |
/** @var int $iter */ | |
$iter = 1; | |
/** @var int $incr */ | |
$incr = self::BUFFER_SIZE >> 6; | |
/* | |
* Set the cursor to the end of the first half-block. All future bytes will | |
* generated from salsa20_xor_ic, starting from 1 (second block). | |
*/ | |
fseek($ifp, $first32, SEEK_SET); | |
while ($mlen > 0) { | |
$blockSize = $mlen > self::BUFFER_SIZE | |
? self::BUFFER_SIZE | |
: $mlen; | |
$plaintext = fread($ifp, $blockSize); | |
if (!is_string($plaintext)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
$cBlock = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic( | |
$plaintext, | |
$realNonce, | |
$iter, | |
$subkey | |
); | |
fwrite($ofp, $cBlock, $blockSize); | |
$state->update($cBlock); | |
$mlen -= $blockSize; | |
$iter += $incr; | |
} | |
try { | |
ParagonIE_Sodium_Compat::memzero($block0); | |
ParagonIE_Sodium_Compat::memzero($subkey); | |
} catch (SodiumException $ex) { | |
$block0 = null; | |
$subkey = null; | |
} | |
$end = self::ftell($ofp); | |
/* | |
* Write the Poly1305 authentication tag that provides integrity | |
* over the ciphertext (encrypt-then-MAC) | |
*/ | |
fseek($ofp, $start, SEEK_SET); | |
fwrite($ofp, $state->finish(), ParagonIE_Sodium_Compat::CRYPTO_SECRETBOX_MACBYTES); | |
fseek($ofp, $end, SEEK_SET); | |
unset($state); | |
return true; | |
} | |
/** | |
* Decrypt a file (32-bit) | |
* | |
* @param resource $ifp | |
* @param resource $ofp | |
* @param int $mlen | |
* @param string $nonce | |
* @param string $key | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
protected static function secretbox_decrypt_core32($ifp, $ofp, $mlen, $nonce, $key) | |
{ | |
$tag = fread($ifp, 16); | |
if (!is_string($tag)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
/** @var string $subkey */ | |
$subkey = ParagonIE_Sodium_Core32_HSalsa20::hsalsa20($nonce, $key); | |
/** @var string $realNonce */ | |
$realNonce = ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8); | |
/** @var string $block0 */ | |
$block0 = ParagonIE_Sodium_Core32_Salsa20::salsa20( | |
64, | |
ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8), | |
$subkey | |
); | |
/* Verify the Poly1305 MAC -before- attempting to decrypt! */ | |
$state = new ParagonIE_Sodium_Core32_Poly1305_State(self::substr($block0, 0, 32)); | |
if (!self::onetimeauth_verify_core32($state, $ifp, $tag, $mlen)) { | |
throw new SodiumException('Invalid MAC'); | |
} | |
/* | |
* Set the cursor to the end of the first half-block. All future bytes will | |
* generated from salsa20_xor_ic, starting from 1 (second block). | |
*/ | |
$first32 = fread($ifp, 32); | |
if (!is_string($first32)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
$first32len = self::strlen($first32); | |
fwrite( | |
$ofp, | |
self::xorStrings( | |
self::substr($block0, 32, $first32len), | |
self::substr($first32, 0, $first32len) | |
) | |
); | |
$mlen -= 32; | |
/** @var int $iter */ | |
$iter = 1; | |
/** @var int $incr */ | |
$incr = self::BUFFER_SIZE >> 6; | |
/* Decrypts ciphertext, writes to output file. */ | |
while ($mlen > 0) { | |
$blockSize = $mlen > self::BUFFER_SIZE | |
? self::BUFFER_SIZE | |
: $mlen; | |
$ciphertext = fread($ifp, $blockSize); | |
if (!is_string($ciphertext)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
$pBlock = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic( | |
$ciphertext, | |
$realNonce, | |
$iter, | |
$subkey | |
); | |
fwrite($ofp, $pBlock, $blockSize); | |
$mlen -= $blockSize; | |
$iter += $incr; | |
} | |
return true; | |
} | |
/** | |
* One-time message authentication for 32-bit systems | |
* | |
* @param ParagonIE_Sodium_Core32_Poly1305_State $state | |
* @param resource $ifp | |
* @param string $tag | |
* @param int $mlen | |
* @return bool | |
* @throws SodiumException | |
* @throws TypeError | |
*/ | |
protected static function onetimeauth_verify_core32( | |
ParagonIE_Sodium_Core32_Poly1305_State $state, | |
$ifp, | |
$tag = '', | |
$mlen = 0 | |
) { | |
/** @var int $pos */ | |
$pos = self::ftell($ifp); | |
while ($mlen > 0) { | |
$blockSize = $mlen > self::BUFFER_SIZE | |
? self::BUFFER_SIZE | |
: $mlen; | |
$ciphertext = fread($ifp, $blockSize); | |
if (!is_string($ciphertext)) { | |
throw new SodiumException('Could not read input file'); | |
} | |
$state->update($ciphertext); | |
$mlen -= $blockSize; | |
} | |
$res = ParagonIE_Sodium_Core32_Util::verify_16($tag, $state->finish()); | |
fseek($ifp, $pos, SEEK_SET); | |
return $res; | |
} | |
/** | |
* @param resource $resource | |
* @return int | |
* @throws SodiumException | |
*/ | |
private static function ftell($resource) | |
{ | |
$return = ftell($resource); | |
if (!is_int($return)) { | |
throw new SodiumException('ftell() returned false'); | |
} | |
return (int) $return; | |
} | |
} | |