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);
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(
ParagonIE_Sodium_Core_Util::substr($nonce, 16, 8),
/* 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);
self::substr($block0, 32, $first32len),
self::substr($first32, 0, $first32len)
$incr = self::BUFFER_SIZE >> 6;
/* Decrypts ciphertext, writes to output file. */
$blockSize = $mlen > self::BUFFER_SIZE
$ciphertext = fread($ifp, $blockSize);
if (!is_string($ciphertext)) {
throw new SodiumException('Could not read input file');
$pBlock = ParagonIE_Sodium_Core_Salsa20::salsa20_xor_ic(
fwrite($ofp, $pBlock, $blockSize);
* @param ParagonIE_Sodium_Core_Poly1305_State $state
* @throws SodiumException
protected static function onetimeauth_verify(
ParagonIE_Sodium_Core_Poly1305_State $state,
$pos = self::ftell($ifp);
$incr = self::BUFFER_SIZE >> 6;
$blockSize = $mlen > self::BUFFER_SIZE
$ciphertext = fread($ifp, $blockSize);
if (!is_string($ciphertext)) {
throw new SodiumException('Could not read input file');
$state->update($ciphertext);
$res = ParagonIE_Sodium_Core_Util::verify_16($tag, $state->finish());
fseek($ifp, $pos, SEEK_SET);
* Update a hash context with the contents of a file, without
* loading the entire file into memory.
* @param resource|HashContext $hash
* @return resource|object Resource on PHP < 7.2, HashContext object on PHP >= 7.2
* @throws SodiumException
* @psalm-suppress PossiblyInvalidArgument
* PHP 7.2 changes from a resource to an object,
* which causes Psalm to complain about an error.
* @psalm-suppress TypeCoercion
public static function updateHashWithFile($hash, $fp, $size = 0)
if (PHP_VERSION_ID < 70200) {
if (!is_resource($hash)) {
throw new TypeError('Argument 1 must be a resource, ' . gettype($hash) . ' given.');
throw new TypeError('Argument 1 must be an object (PHP 7.2+), ' . gettype($hash) . ' given.');
throw new TypeError('Argument 2 must be a resource, ' . gettype($fp) . ' given.');
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
for ($i = 0; $i < $size; $i += self::BUFFER_SIZE) {
/** @var string|bool $message */
($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);
* 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
private static function sign_core32($filePath, $secretKey)
$size = filesize($filePath);
throw new SodiumException('Could not obtain the file size');
$fp = fopen($filePath, 'rb');
throw new SodiumException('Could not open input file for reading');
$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));
$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));
$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);
$sig = self::substr($sig, 0, 32) . self::substr($sigAfter, 0, 32);
ParagonIE_Sodium_Compat::memzero($az);
} catch (SodiumException $ex) {
* 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
* @throws SodiumException
public static function verify_core32($sig, $filePath, $publicKey)
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');
for ($i = 0; $i < 32; ++$i) {
$d |= self::chrToInt($publicKey[$i]);
throw new SodiumException('All zero public key');
/** @var int|bool $size */
$size = filesize($filePath);
throw new SodiumException('Could not obtain the file size');
/** @var resource|bool $fp */
$fp = fopen($filePath, 'rb');
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_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));
$hs = self::updateHashWithFile($hs, $fp, $size);
/** @var string $hDigest */
$hDigest = hash_final($hs, true);
$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(
/** @var string $rcheck */
$rcheck = ParagonIE_Sodium_Core32_Ed25519::ge_tobytes($R);
// 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)
* @throws SodiumException
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 */
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(
$state = new ParagonIE_Sodium_Core32_Poly1305_State(
ParagonIE_Sodium_Core32_Util::substr(
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));
$cBlock = ParagonIE_Sodium_Core32_Util::substr(
ParagonIE_Sodium_Crypto::secretbox_xsalsa20poly1305_ZEROBYTES
$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);
$blockSize = $mlen > self::BUFFER_SIZE
$plaintext = fread($ifp, $blockSize);
if (!is_string($plaintext)) {
throw new SodiumException('Could not read input file');
$cBlock = ParagonIE_Sodium_Core32_Salsa20::salsa20_xor_ic(
fwrite($ofp, $cBlock, $blockSize);
ParagonIE_Sodium_Compat::memzero($block0);
ParagonIE_Sodium_Compat::memzero($subkey);
} catch (SodiumException $ex) {
$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);
* Decrypt a file (32-bit)
* @throws SodiumException
protected static function secretbox_decrypt_core32($ifp, $ofp, $mlen, $nonce, $key)
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(
ParagonIE_Sodium_Core32_Util::substr($nonce, 16, 8),
/* 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);