deriveKeys($file_salt); $ekey = $keys->getEncryptionKey(); $akey = $keys->getAuthenticationKey(); $ivsize = Core::BLOCK_BYTE_SIZE; $iv = Core::secureRandom($ivsize); /* Initialize a streaming HMAC state. */ /** @var resource $hmac */ $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); if (!\is_resource($hmac)) { throw new Ex\EnvironmentIsBrokenException( 'Cannot initialize a hash context' ); } /* Write the header, salt, and IV. */ self::writeBytes( $outputHandle, Core::CURRENT_VERSION . $file_salt . $iv, Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + $ivsize ); /* Add the header, salt, and IV to the HMAC. */ \hash_update($hmac, Core::CURRENT_VERSION); \hash_update($hmac, $file_salt); \hash_update($hmac, $iv); /* $thisIv will be incremented after each call to the encryption. */ $thisIv = $iv; /* How many blocks do we encrypt at a time? We increment by this value. */ $inc = (int) (Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE); /* Loop until we reach the end of the input file. */ $at_file_end = false; while (! (\feof($inputHandle) || $at_file_end)) { /* Find out if we can read a full buffer, or only a partial one. */ /** @var int */ $pos = \ftell($inputHandle); if (!\is_int($pos)) { throw new Ex\IOException( 'Could not get current position in input file during encryption' ); } if ($pos + Core::BUFFER_BYTE_SIZE >= $inputSize) { /* We're at the end of the file, so we need to break out of the loop. */ $at_file_end = true; $read = self::readBytes( $inputHandle, $inputSize - $pos ); } else { $read = self::readBytes( $inputHandle, Core::BUFFER_BYTE_SIZE ); } /* Encrypt this buffer. */ /** @var string */ $encrypted = \openssl_encrypt( $read, Core::CIPHER_METHOD, $ekey, OPENSSL_RAW_DATA, $thisIv ); if (!\is_string($encrypted)) { throw new Ex\EnvironmentIsBrokenException( 'OpenSSL encryption error' ); } /* Write this buffer's ciphertext. */ self::writeBytes($outputHandle, $encrypted, Core::ourStrlen($encrypted)); /* Add this buffer's ciphertext to the HMAC. */ \hash_update($hmac, $encrypted); /* Increment the counter by the number 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. */ } /* Get the HMAC and append it to the ciphertext. */ $final_mac = \hash_final($hmac, true); self::writeBytes($outputHandle, $final_mac, Core::MAC_BYTE_SIZE); } /** * Decrypts a file-backed resource with either a key or a password. * * @param resource $inputHandle * @param resource $outputHandle * @param KeyOrPassword $secret * @return void * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\IOException * @throws Ex\WrongKeyOrModifiedCiphertextException */ 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 = (int) (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. */ /** @var int $cipher_end */ $cipher_end = \ftell($inputHandle); if (!\is_int($cipher_end)) { 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. */ /** @var string $stored_mac */ $stored_mac = self::readBytes($inputHandle, Core::MAC_BYTE_SIZE); /* Initialize a streaming HMAC state. */ /** @var resource $hmac */ $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); if (!\is_resource($hmac)) { 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); /** @var resource $hmac2 */ $hmac2 = \hash_copy($hmac); $break = false; while (! $break) { /** @var int $pos */ $pos = \ftell($inputHandle); if (!\is_int($pos)) { 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. */ /** @var resource $chunk_mac */ $chunk_mac = \hash_copy($hmac); if (!\is_resource($chunk_mac)) { throw new Ex\EnvironmentIsBrokenException( 'Cannot duplicate a hash context' ); } $macs []= \hash_final($chunk_mac); } /* Get the final HMAC, which should match the stored one. */ /** @var string $final_mac */ $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) { /** @var int $pos */ $pos = \ftell($inputHandle); if (!\is_int($pos)) { 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); /** @var resource $calc_mac */ $calc_mac = \hash_copy($hmac2); if (!\is_resource($calc_mac)) { 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. */ /** @var string $decrypted */ $decrypted = \openssl_decrypt( $read, Core::CIPHER_METHOD, $ekey, OPENSSL_RAW_DATA, $thisIv ); if (!\is_string($decrypted)) { 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. */ /** @var string $thisIv */ $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. */ } } /** * Read from a stream; prevent partial reads. * * @param resource $stream * @param int $num_bytes * @return string * * @throws Ex\IOException * @throws Ex\EnvironmentIsBrokenException * * @return string */ public static function readBytes($stream, $num_bytes) { if ($num_bytes < 0) { throw new Ex\EnvironmentIsBrokenException( 'Tried to read less than 0 bytes' ); } elseif ($num_bytes === 0) { return ''; } $buf = ''; $remaining = $num_bytes; while ($remaining > 0 && ! \feof($stream)) { /** @var string $read */ $read = \fread($stream, $remaining); if (!\is_string($read)) { throw new Ex\IOException( 'Could not read from the file' ); } $buf .= $read; $remaining -= Core::ourStrlen($read); } if (Core::ourStrlen($buf) !== $num_bytes) { throw new Ex\IOException( 'Tried to read past the end of the file' ); } return $buf; } /** * Write to a stream; prevents partial writes. * * @param resource $stream * @param string $buf * @param int $num_bytes * @return int * * @throws Ex\IOException * * @return string */ public static function writeBytes($stream, $buf, $num_bytes = null) { $bufSize = Core::ourStrlen($buf); if ($num_bytes === null) { $num_bytes = $bufSize; } if ($num_bytes > $bufSize) { throw new Ex\IOException( 'Trying to write more bytes than the buffer contains.' ); } if ($num_bytes < 0) { throw new Ex\IOException( 'Tried to write less than 0 bytes' ); } $remaining = $num_bytes; while ($remaining > 0) { /** @var int $written */ $written = \fwrite($stream, $buf, $remaining); if (!\is_int($written)) { throw new Ex\IOException( 'Could not write to the file' ); } $buf = (string) Core::ourSubstr($buf, $written, null); $remaining -= $written; } return $num_bytes; } /** * Returns the last PHP error's or warning's message string. * * @return string */ private static function getLastErrorMessage() { $error = error_get_last(); if ($error === null) { return '[no PHP error]'; } else { return $error['message']; } } }