public static function decryptResourceInternal($inputHandle, $outputHandle, KeyOrPassword $secret)
{
if (!\is_resource($inputHandle)) {
throw new Ex\IOException('Input handle must be a resource!');
}
if (!\is_resource($outputHandle)) {
throw new Ex\IOException('Output handle must be a resource!');
}
/* Make sure the file is big enough for all the reads we need to do. */
$stat = \fstat($inputHandle);
if ($stat['size'] < Core::MINIMUM_CIPHERTEXT_SIZE) {
throw new Ex\WrongKeyOrModifiedCiphertextException('Input file is too small to have been created by this library.');
}
/* Check the version header. */
$header = self::readBytes($inputHandle, Core::HEADER_VERSION_SIZE);
if ($header !== Core::CURRENT_VERSION) {
throw new Ex\WrongKeyOrModifiedCiphertextException('Bad version header.');
}
/* Get the salt. */
$file_salt = self::readBytes($inputHandle, Core::SALT_BYTE_SIZE);
/* Get the IV. */
$ivsize = Core::BLOCK_BYTE_SIZE;
$iv = self::readBytes($inputHandle, $ivsize);
/* Derive the authentication and encryption keys. */
$keys = $secret->deriveKeys($file_salt);
$ekey = $keys->getEncryptionKey();
$akey = $keys->getAuthenticationKey();
/* We'll store the MAC of each buffer-sized chunk as we verify the
* actual MAC, so that we can check them again when decrypting. */
$macs = [];
/* $thisIv will be incremented after each call to the decryption. */
$thisIv = $iv;
/* How many blocks do we encrypt at a time? We increment by this value. */
$inc = Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE;
/* Get the HMAC. */
if (\fseek($inputHandle, -1 * Core::MAC_BYTE_SIZE, SEEK_END) === false) {
throw new Ex\IOException('Cannot seek to beginning of MAC within input file');
}
/* Get the position of the last byte in the actual ciphertext. */
$cipher_end = \ftell($inputHandle);
if ($cipher_end === false) {
throw new Ex\IOException('Cannot read input file');
}
/* We have the position of the first byte of the HMAC. Go back by one. */
--$cipher_end;
/* Read the HMAC. */
$stored_mac = self::readBytes($inputHandle, Core::MAC_BYTE_SIZE);
/* Initialize a streaming HMAC state. */
$hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey);
if ($hmac === false) {
throw new Ex\EnvironmentIsBrokenException('Cannot initialize a hash context');
}
/* Reset file pointer to the beginning of the file after the header */
if (\fseek($inputHandle, Core::HEADER_VERSION_SIZE, SEEK_SET) === false) {
throw new Ex\IOException('Cannot read seek within input file');
}
/* Seek to the start of the actual ciphertext. */
if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize, SEEK_CUR) === false) {
throw new Ex\IOException('Cannot seek input file to beginning of ciphertext');
}
/* PASS #1: Calculating the HMAC. */
\hash_update($hmac, $header);
\hash_update($hmac, $file_salt);
\hash_update($hmac, $iv);
$hmac2 = \hash_copy($hmac);
$break = false;
while (!$break) {
$pos = \ftell($inputHandle);
if ($pos === false) {
throw new Ex\IOException('Could not get current position in input file during decryption');
}
/* Read the next buffer-sized chunk (or less). */
if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) {
$break = true;
$read = self::readBytes($inputHandle, $cipher_end - $pos + 1);
} else {
$read = self::readBytes($inputHandle, Core::BUFFER_BYTE_SIZE);
}
/* Update the HMAC. */
\hash_update($hmac, $read);
/* Remember this buffer-sized chunk's HMAC. */
$chunk_mac = \hash_copy($hmac);
if ($chunk_mac === false) {
throw new Ex\EnvironmentIsBrokenException('Cannot duplicate a hash context');
}
$macs[] = \hash_final($chunk_mac);
}
/* Get the final HMAC, which should match the stored one. */
$final_mac = \hash_final($hmac, true);
/* Verify the HMAC. */
if (!Core::hashEquals($final_mac, $stored_mac)) {
throw new Ex\WrongKeyOrModifiedCiphertextException('Integrity check failed.');
}
/* PASS #2: Decrypt and write output. */
/* Rewind to the start of the actual ciphertext. */
if (\fseek($inputHandle, Core::SALT_BYTE_SIZE + $ivsize + Core::HEADER_VERSION_SIZE, SEEK_SET) === false) {
throw new Ex\IOException('Could not move the input file pointer during decryption');
}
$at_file_end = false;
while (!$at_file_end) {
$pos = \ftell($inputHandle);
if ($pos === false) {
throw new Ex\IOException('Could not get current position in input file during decryption');
}
/* Read the next buffer-sized chunk (or less). */
if ($pos + Core::BUFFER_BYTE_SIZE >= $cipher_end) {
$at_file_end = true;
$read = self::readBytes($inputHandle, $cipher_end - $pos + 1);
} else {
$read = self::readBytes($inputHandle, Core::BUFFER_BYTE_SIZE);
}
/* Recalculate the MAC (so far) and compare it with the one we
* remembered from pass #1 to ensure attackers didn't change the
* ciphertext after MAC verification. */
\hash_update($hmac2, $read);
$calc_mac = \hash_copy($hmac2);
if ($calc_mac === false) {
throw new Ex\EnvironmentIsBrokenException('Cannot duplicate a hash context');
}
$calc = \hash_final($calc_mac);
if (empty($macs)) {
throw new Ex\WrongKeyOrModifiedCiphertextException('File was modified after MAC verification');
} elseif (!Core::hashEquals(\array_shift($macs), $calc)) {
throw new Ex\WrongKeyOrModifiedCiphertextException('File was modified after MAC verification');
}
/* Decrypt this buffer-sized chunk. */
$decrypted = \openssl_decrypt($read, Core::CIPHER_METHOD, $ekey, OPENSSL_RAW_DATA, $thisIv);
if ($decrypted === false) {
throw new Ex\EnvironmentIsBrokenException('OpenSSL decryption error');
}
/* Write the plaintext to the output file. */
self::writeBytes($outputHandle, $decrypted, Core::ourStrlen($decrypted));
/* Increment the IV by the amount of blocks in a buffer. */
$thisIv = Core::incrementCounter($thisIv, $inc);
/* WARNING: Usually, unless the file is a multiple of the buffer
* size, $thisIv will contain an incorrect value here on the last
* iteration of this loop. */
}
}