Viewing file: File.php (25.27 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php
namespace Defuse\Crypto;
use Defuse\Crypto\Exception as Ex;
final class File { /** * Encrypts the input file, saving the ciphertext to the output file. * * @param string $inputFilename * @param string $outputFilename * @param Key $key * @return void * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\IOException */ public static function encryptFile($inputFilename, $outputFilename, Key $key) { self::encryptFileInternal( $inputFilename, $outputFilename, KeyOrPassword::createFromKey($key) ); }
/** * Encrypts a file with a password, using a slow key derivation function to * make password cracking more expensive. * * @param string $inputFilename * @param string $outputFilename * @param string $password * @return void * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\IOException */ public static function encryptFileWithPassword($inputFilename, $outputFilename, $password) { self::encryptFileInternal( $inputFilename, $outputFilename, KeyOrPassword::createFromPassword($password) ); }
/** * Decrypts the input file, saving the plaintext to the output file. * * @param string $inputFilename * @param string $outputFilename * @param Key $key * @return void * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\IOException * @throws Ex\WrongKeyOrModifiedCiphertextException */ public static function decryptFile($inputFilename, $outputFilename, Key $key) { self::decryptFileInternal( $inputFilename, $outputFilename, KeyOrPassword::createFromKey($key) ); }
/** * Decrypts a file with a password, using a slow key derivation function to * make password cracking more expensive. * * @param string $inputFilename * @param string $outputFilename * @param string $password * @return void * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\IOException * @throws Ex\WrongKeyOrModifiedCiphertextException */ public static function decryptFileWithPassword($inputFilename, $outputFilename, $password) { self::decryptFileInternal( $inputFilename, $outputFilename, KeyOrPassword::createFromPassword($password) ); }
/** * Takes two resource handles and encrypts the contents of the first, * writing the ciphertext into the second. * * @param resource $inputHandle * @param resource $outputHandle * @param Key $key * @return void * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\WrongKeyOrModifiedCiphertextException */ public static function encryptResource($inputHandle, $outputHandle, Key $key) { self::encryptResourceInternal( $inputHandle, $outputHandle, KeyOrPassword::createFromKey($key) ); }
/** * Encrypts the contents of one resource handle into another with a * password, using a slow key derivation function to make password cracking * more expensive. * * @param resource $inputHandle * @param resource $outputHandle * @param string $password * @return void * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\IOException * @throws Ex\WrongKeyOrModifiedCiphertextException */ public static function encryptResourceWithPassword($inputHandle, $outputHandle, $password) { self::encryptResourceInternal( $inputHandle, $outputHandle, KeyOrPassword::createFromPassword($password) ); }
/** * Takes two resource handles and decrypts the contents of the first, * writing the plaintext into the second. * * @param resource $inputHandle * @param resource $outputHandle * @param Key $key * @return void * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\IOException * @throws Ex\WrongKeyOrModifiedCiphertextException */ public static function decryptResource($inputHandle, $outputHandle, Key $key) { self::decryptResourceInternal( $inputHandle, $outputHandle, KeyOrPassword::createFromKey($key) ); }
/** * Decrypts the contents of one resource into another with a password, using * a slow key derivation function to make password cracking more expensive. * * @param resource $inputHandle * @param resource $outputHandle * @param string $password * @return void * * @throws Ex\EnvironmentIsBrokenException * @throws Ex\IOException * @throws Ex\WrongKeyOrModifiedCiphertextException */ public static function decryptResourceWithPassword($inputHandle, $outputHandle, $password) { self::decryptResourceInternal( $inputHandle, $outputHandle, KeyOrPassword::createFromPassword($password) ); }
/** * Encrypts a file with either a key or a password. * * @param string $inputFilename * @param string $outputFilename * @param KeyOrPassword $secret * @return void * * @throws Ex\CryptoException * @throws Ex\IOException */ private static function encryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret) { if (file_exists($inputFilename) && file_exists($outputFilename) && realpath($inputFilename) === realpath($outputFilename)) { throw new Ex\IOException('Input and output filenames must be different.'); }
/* Open the input file. */ $if = @\fopen($inputFilename, 'rb'); if ($if === false) { throw new Ex\IOException( 'Cannot open input file for encrypting: ' . self::getLastErrorMessage() ); } if (\is_callable('\\stream_set_read_buffer')) { /* This call can fail, but the only consequence is performance. */ \stream_set_read_buffer($if, 0); }
/* Open the output file. */ $of = @\fopen($outputFilename, 'wb'); if ($of === false) { \fclose($if); throw new Ex\IOException( 'Cannot open output file for encrypting: ' . self::getLastErrorMessage() ); } if (\is_callable('\\stream_set_write_buffer')) { /* This call can fail, but the only consequence is performance. */ \stream_set_write_buffer($of, 0); }
/* Perform the encryption. */ try { self::encryptResourceInternal($if, $of, $secret); } catch (Ex\CryptoException $ex) { \fclose($if); \fclose($of); throw $ex; }
/* Close the input file. */ if (\fclose($if) === false) { \fclose($of); throw new Ex\IOException( 'Cannot close input file after encrypting' ); }
/* Close the output file. */ if (\fclose($of) === false) { throw new Ex\IOException( 'Cannot close output file after encrypting' ); } }
/** * Decrypts a file with either a key or a password. * * @param string $inputFilename * @param string $outputFilename * @param KeyOrPassword $secret * @return void * * @throws Ex\CryptoException * @throws Ex\IOException */ private static function decryptFileInternal($inputFilename, $outputFilename, KeyOrPassword $secret) { if (file_exists($inputFilename) && file_exists($outputFilename) && realpath($inputFilename) === realpath($outputFilename)) { throw new Ex\IOException('Input and output filenames must be different.'); }
/* Open the input file. */ $if = @\fopen($inputFilename, 'rb'); if ($if === false) { throw new Ex\IOException( 'Cannot open input file for decrypting: ' . self::getLastErrorMessage() ); }
if (\is_callable('\\stream_set_read_buffer')) { /* This call can fail, but the only consequence is performance. */ \stream_set_read_buffer($if, 0); }
/* Open the output file. */ $of = @\fopen($outputFilename, 'wb'); if ($of === false) { \fclose($if); throw new Ex\IOException( 'Cannot open output file for decrypting: ' . self::getLastErrorMessage() ); }
if (\is_callable('\\stream_set_write_buffer')) { /* This call can fail, but the only consequence is performance. */ \stream_set_write_buffer($of, 0); }
/* Perform the decryption. */ try { self::decryptResourceInternal($if, $of, $secret); } catch (Ex\CryptoException $ex) { \fclose($if); \fclose($of); throw $ex; }
/* Close the input file. */ if (\fclose($if) === false) { \fclose($of); throw new Ex\IOException( 'Cannot close input file after decrypting' ); }
/* Close the output file. */ if (\fclose($of) === false) { throw new Ex\IOException( 'Cannot close output file after decrypting' ); } }
/** * Encrypts a 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 * @psalm-suppress PossiblyInvalidArgument * Fixes erroneous errors caused by PHP 7.2 switching the return value * of hash_init from a resource to a HashContext. */ private static function encryptResourceInternal($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!' ); }
$inputStat = \fstat($inputHandle); $inputSize = $inputStat['size'];
$file_salt = Core::secureRandom(Core::SALT_BYTE_SIZE); $keys = $secret->deriveKeys($file_salt); $ekey = $keys->getEncryptionKey(); $akey = $keys->getAuthenticationKey();
$ivsize = Core::BLOCK_BYTE_SIZE; $iv = Core::secureRandom($ivsize);
/* Initialize a streaming HMAC state. */ /** @var mixed $hmac */ $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); Core::ensureTrue( \is_resource($hmac) || \is_object($hmac), '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. */ /** * @psalm-suppress RedundantCast */ $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 );
Core::ensureTrue(\is_string($encrypted), '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 * @psalm-suppress PossiblyInvalidArgument * Fixes erroneous errors caused by PHP 7.2 switching the return value * of hash_init from a resource to a HashContext. */ 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. */ /** * @psalm-suppress RedundantCast */ $inc = (int) (Core::BUFFER_BYTE_SIZE / Core::BLOCK_BYTE_SIZE);
/* Get the HMAC. */ if (\fseek($inputHandle, (-1 * Core::MAC_BYTE_SIZE), SEEK_END) === -1) { 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 mixed $hmac */ $hmac = \hash_init(Core::HASH_FUNCTION_NAME, HASH_HMAC, $akey); Core::ensureTrue(\is_resource($hmac) || \is_object($hmac), '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) === -1) { 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) === -1) { 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 mixed $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 mixed $chunk_mac */ $chunk_mac = \hash_copy($hmac); Core::ensureTrue(\is_resource($chunk_mac) || \is_object($chunk_mac), '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) === -1) { 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 mixed $calc_mac */ $calc_mac = \hash_copy($hmac2); Core::ensureTrue(\is_resource($calc_mac) || \is_object($calc_mac), '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 ); Core::ensureTrue(\is_string($decrypted), '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 */ public static function readBytes($stream, $num_bytes) { Core::ensureTrue($num_bytes >= 0, 'Tried to read less than 0 bytes');
if ($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 */ 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']; } } }
|