diff --git a/src/File.php b/src/File.php index 3db66bb..6b44fbb 100644 --- a/src/File.php +++ b/src/File.php @@ -59,10 +59,10 @@ public static function createNewRandomKey() * * @param string $inputFilename * @param string $outputFilename - * @param string $key + * @param Key $key * @return boolean */ - public static function encryptFile($inputFilename, $outputFilename, $key) + public static function encryptFile($inputFilename, $outputFilename, Key $key) { if (!\is_string($inputFilename)) { throw new Ex\InvalidInput( @@ -137,10 +137,10 @@ public static function encryptFile($inputFilename, $outputFilename, $key) * * @param string $inputFilename * @param string $outputFilename - * @param string $key + * @param Key $key * @return boolean */ - public static function decryptFile($inputFilename, $outputFilename, $key) + public static function decryptFile($inputFilename, $outputFilename, Key $key) { if (!\is_string($inputFilename)) { throw new Ex\InvalidInput( @@ -215,10 +215,10 @@ public static function decryptFile($inputFilename, $outputFilename, $key) * * @param resource $inputHandle * @param resource $outputHandle - * @param string $key + * @param Key $key * @return boolean */ - public static function encryptResource($inputHandle, $outputHandle, $key) + public static function encryptResource($inputHandle, $outputHandle, Key $key) { // Because we don't have strict typing in PHP 5 if (!\is_resource($inputHandle)) { @@ -235,6 +235,8 @@ public static function encryptResource($inputHandle, $outputHandle, $key) Core::CURRENT_FILE_VERSION, Core::CURRENT_FILE_VERSION ); + $inputStat = \fstat($inputHandle); + $inputSize = $inputStat['size']; // Let's add this check before anything if (!\in_array($config->hashFunctionName(), \hash_algos())) { @@ -243,13 +245,6 @@ public static function encryptResource($inputHandle, $outputHandle, $key) ); } - // Sanity check; key must be the appropriate length! - if (Core::ourStrlen($key) !== $config->keyByteSize()) { - throw new Ex\InvalidInput( - 'Invalid key length. Keys should be '.$config->keyByteSize().' bytes long.' - ); - } - /** * Let's split our keys */ @@ -258,7 +253,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) // $ekey -- Encryption Key -- used for AES $ekey = Core::HKDF( $config->hashFunctionName(), - $key, + $key->getRawBytes(), $config->keyByteSize(), $config->encryptionInfoString(), $file_salt, @@ -268,7 +263,7 @@ public static function encryptResource($inputHandle, $outputHandle, $key) // $akey -- Authentication Key -- used for HMAC $akey = Core::HKDF( $config->hashFunctionName(), - $key, + $key->getRawBytes(), $config->keyByteSize(), $config->authenticationInfoString(), $file_salt, @@ -290,15 +285,11 @@ public static function encryptResource($inputHandle, $outputHandle, $key) /** * First let's write our header, file salt, and IV to the first N blocks of the output file */ - if (\fwrite( + self::writeBytes( $outputHandle, Core::CURRENT_FILE_VERSION . $file_salt . $iv, Core::HEADER_VERSION_SIZE + $config->saltByteSize() + $ivsize - ) === false) { - throw new Ex\CannotPerformOperationException( - 'Cannot write to output file' - ); - } + ); /** * We're going to initialize a HMAC-SHA256 with the given $akey @@ -333,11 +324,20 @@ public static function encryptResource($inputHandle, $outputHandle, $key) /** * Iterate until we reach the end of the input file */ + $breakR = false; while (!\feof($inputHandle)) { - $read = \fread($inputHandle, $config->bufferByteSize()); - if ($read === false) { - throw new Ex\CannotPerformOperationException( - 'Cannot read input file' + $pos = \ftell($inputHandle); + if ($pos + $config->bufferByteSize() >= $inputSize) { + $breakR = true; + // We need to break after this loop iteration + $read = self::readBytes( + $inputHandle, + $inputSize - $pos + ); + } else { + $read = self::readBytes( + $inputHandle, + $config->bufferByteSize() ); } $thisIv = Core::incrementCounter($thisIv, $inc, $config); @@ -364,27 +364,21 @@ public static function encryptResource($inputHandle, $outputHandle, $key) /** * Write the ciphertext to the output file */ - if (\fwrite($outputHandle, $encrypted, Core::ourStrlen($encrypted)) === false) { - throw new Ex\CannotPerformOperationException( - 'Cannot write to output file during encryption' - ); - } + self::writeBytes($outputHandle, $encrypted, Core::ourStrlen($encrypted)); /** * Update the HMAC for the entire file with the data from this block */ \hash_update($hmac, $encrypted); + if ($breakR) { + break; + } } // Now let's get our HMAC and append it $finalHMAC = \hash_final($hmac, true); - $appended = \fwrite($outputHandle, $finalHMAC, $config->macByteSize()); - if ($appended === false) { - throw new Ex\CannotPerformOperationException( - 'Cannot write to output file' - ); - } + self::writeBytes($outputHandle, $finalHMAC, $config->macByteSize()); return true; } @@ -394,10 +388,10 @@ public static function encryptResource($inputHandle, $outputHandle, $key) * * @param resource $inputHandle * @param resource $outputHandle - * @param string $key + * @param Key $key * @return boolean */ - public static function decryptResource($inputHandle, $outputHandle, $key) + public static function decryptResource($inputHandle, $outputHandle, Key $key) { // Because we don't have strict typing in PHP 5 if (!\is_resource($inputHandle)) { @@ -412,13 +406,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) } // Parse the header. - $header = ''; - $remaining = Core::HEADER_VERSION_SIZE; - do { - $header .= \fread($inputHandle, $remaining); - $remaining = Core::HEADER_VERSION_SIZE - Core::ourStrlen($header); - } while ($remaining > 0); - + $header = self::readBytes($inputHandle, Core::HEADER_VERSION_SIZE); $config = self::getFileVersionConfigFromHeader( $header, Core::CURRENT_FILE_VERSION @@ -430,20 +418,9 @@ public static function decryptResource($inputHandle, $outputHandle, $key) 'The specified hash function does not exist' ); } - - // Sanity check; key must be the appropriate length! - if (Core::ourStrlen($key) !== $config->keyByteSize()) { - throw new Ex\InvalidInput( - 'Invalid key length. Keys should be '.$config->keyByteSize().' bytes long.' - ); - } + // Let's grab the file salt. - $file_salt = \fread($inputHandle, $config->saltByteSize()); - if ($file_salt === false ) { - throw new Ex\CannotPerformOperationException( - 'Cannot read input file' - ); - } + $file_salt = self::readBytes($inputHandle, $config->saltByteSize()); // For storing MACs of each buffer chunk $macs = []; @@ -458,7 +435,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ $ekey = Core::HKDF( $config->hashFunctionName(), - $key, + $key->getRawBytes(), $config->keyByteSize(), $config->encryptionInfoString(), $file_salt, @@ -470,7 +447,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ $akey = Core::HKDF( $config->hashFunctionName(), - $key, + $key->getRawBytes(), $config->keyByteSize(), $config->authenticationInfoString(), $file_salt, @@ -483,12 +460,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) * It should be the first N blocks of the file (N = 16) */ $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); - $iv = \fread($inputHandle, $ivsize); - if ($iv === false ) { - throw new Ex\CannotPerformOperationException( - 'Cannot read input file' - ); - } + $iv = self::readBytes($inputHandle, $ivsize); // How much do we increase the counter after each buffered encryption to prevent nonce reuse $inc = $config->bufferByteSize() / $config->blockByteSize(); @@ -516,12 +488,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) --$cipher_end; // We need to subtract one // We keep our MAC stored in this variable - $stored_mac = \fread($inputHandle, $config->macByteSize()); - if ($stored_mac === false) { - throw new Ex\CannotPerformOperationException( - 'Cannot read input file' - ); - } + $stored_mac = self::readBytes($inputHandle, $config->macByteSize()); /** * We begin recalculating the HMAC for the entire file... @@ -579,9 +546,15 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ if ($pos + $config->bufferByteSize() >= $cipher_end) { $break = true; - $read = \fread($inputHandle, $cipher_end - $pos + 1); + $read = self::readBytes( + $inputHandle, + $cipher_end - $pos + 1 + ); } else { - $read = \fread($inputHandle, $config->bufferByteSize()); + $read = self::readBytes( + $inputHandle, + $config->bufferByteSize() + ); } if ($read === false) { throw new Ex\CannotPerformOperationException( @@ -636,7 +609,6 @@ public static function decryptResource($inputHandle, $outputHandle, $key) /** * This loop writes plaintext to the destination file: */ - $result = null; while (!$breakW) { /** * Get the current position @@ -654,13 +626,14 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ if ($pos + $config->bufferByteSize() >= $cipher_end) { $breakW = true; - $read = \fread($inputHandle, $cipher_end - $pos + 1); + $read = self::readBytes( + $inputHandle, + $cipher_end - $pos + 1 + ); } else { - $read = \fread($inputHandle, $config->bufferByteSize()); - } - if ($read === false) { - throw new Ex\CannotPerformOperationException( - 'Could not read input file during decryption' + $read = self::readBytes( + $inputHandle, + $config->bufferByteSize() ); } @@ -713,23 +686,13 @@ public static function decryptResource($inputHandle, $outputHandle, $key) /** * Write the plaintext out to the output file */ - $result = \fwrite( - $outputHandle, - $decrypted, + self::writeBytes( + $outputHandle, + $decrypted, Core::ourStrlen($decrypted) ); - - /** - * Check result - */ - if ($result === false) { - throw new Ex\CannotPerformOperationException( - 'Could not write to output file during decryption.' - ); - } } - // This should be an integer - return $result; + return true; } /** @@ -799,4 +762,81 @@ private static function getFileVersionConfigFromMajorMinor($major, $minor) ); } } + + /** + * Read from a stream; prevent partial reads + * + * @param resource $stream + * @param int $num + * @return string + * + * @throws \RangeException + * @throws Ex\CannotPerformOperationException + */ + final public static function readBytes($stream, $num) + { + if ($num <= 0) { + throw new \RangeException( + 'Tried to read less than 0 bytes' + ); + } + $buf = ''; + $remaining = $num; + while ($remaining > 0 && !\feof($stream)) { + $read = \fread($stream, $remaining); + + if ($read === false) { + throw new Ex\CannotPerformOperationException( + 'Could not read from the file' + ); + } + $buf .= $read; + $remaining -= Core::ourStrlen($read); + } + if (Core::ourStrlen($buf) !== $num) { + throw new Ex\CannotPerformOperationException( + 'Tried to read past the end of the file' + ); + } + return $buf; + } + + /** + * Write to a stream; prevent partial writes + * + * @param resource $stream + * @param string $buf + * @param int $num (number of bytes) + * @return string + * @throws Ex\CannotPerformOperationException + */ + final public static function writeBytes($stream, $buf, $num = null) + { + $bufSize = Core::ourStrlen($buf); + if ($num === null) { + $num = $bufSize; + } + if ($num > $bufSize) { + throw new Ex\CannotPerformOperationException( + 'Trying to write more bytes than the buffer contains.' + ); + } + if ($num < 0) { + throw new Ex\CannotPerformOperationException( + 'Tried to write less than 0 bytes' + ); + } + $remaining = $num; + while ($remaining > 0) { + $written = \fwrite($stream, $buf, $remaining); + if ($written === false) { + throw new Ex\CannotPerformOperationException( + 'Could not write to the file' + ); + } + $buf = Core::ourSubstr($buf, $written, null); + $remaining -= $written; + } + return $num; + } } diff --git a/src/StreamInterface.php b/src/StreamInterface.php index e31a5e9..0b8a962 100644 --- a/src/StreamInterface.php +++ b/src/StreamInterface.php @@ -9,10 +9,10 @@ interface StreamInterface * * @param string $inputFilename * @param string $outputFilename - * @param string $key + * @param Key $key * @return boolean */ - public static function encryptFile($inputFilename, $outputFilename, $key); + public static function encryptFile($inputFilename, $outputFilename, Key $key); /** * Decrypt the contents at $inputFilename, storing the result in $outputFilename @@ -20,10 +20,10 @@ public static function encryptFile($inputFilename, $outputFilename, $key); * * @param string $inputFilename * @param string $outputFilename - * @param string $key + * @param Key $key * @return boolean */ - public static function decryptFile($inputFilename, $outputFilename, $key); + public static function decryptFile($inputFilename, $outputFilename, Key $key); /** * Encrypt the contents of a file handle $inputHandle and store the results @@ -31,10 +31,10 @@ public static function decryptFile($inputFilename, $outputFilename, $key); * * @param resource $inputHandle * @param resource $outputHandle - * @param string $key + * @param Key $key * @return boolean */ - public static function encryptResource($inputHandle, $outputHandle, $key); + public static function encryptResource($inputHandle, $outputHandle, Key $key); /** * Decrypt the contents of a file handle $inputHandle and store the results @@ -42,8 +42,8 @@ public static function encryptResource($inputHandle, $outputHandle, $key); * * @param resource $inputHandle * @param resource $outputHandle - * @param string $key + * @param Key $key * @return boolean */ - public static function decryptResource($inputHandle, $outputHandle, $key); + public static function decryptResource($inputHandle, $outputHandle, Key $key); } diff --git a/test/stream/decrypt.php b/test/stream/decrypt.php index b9c6a4e..e6710aa 100644 --- a/test/stream/decrypt.php +++ b/test/stream/decrypt.php @@ -8,52 +8,65 @@ $mem = 0; $start_time = $end_time = \microtime(true); -$key = \Defuse\Crypto\Encoding::hexToBin(\file_get_contents('key.txt')); +$key = \Defuse\Crypto\Key::LoadFromAsciiSafeString(\file_get_contents('key.txt')); +$end_time = \microtime(true); echo 'Decrypting', "\n", str_repeat('-', 50), "\n\n"; echo "Load Key:\n\t"; -echo \number_format($end_time - $start_time, 2), +echo \number_format($end_time - $start_time, 4), 's (Memory: ', \number_format(\memory_get_usage() / 1024, 2), ' KB)', "\n"; -$end_time = $start_time; -\Defuse\Crypto\File::decryptFile( +$start_time = \microtime(true); +$success = \Defuse\Crypto\File::decryptFile( 'wat-encrypted.data', 'wat-decrypted.jpg', $key ); - $end_time = \microtime(true); + +if (!$success) { + echo 'File did not encrypt successfully.', "\n"; + exit(1); +} echo "wat-encrypted.data:\n\t"; -echo \number_format($end_time - $start_time, 2), +echo \number_format($end_time - $start_time, 4), 's (Memory: ', \number_format(\memory_get_usage() / 1024, 2), ' KB)', "\n"; -$end_time = $start_time; -\Defuse\Crypto\File::decryptFile( +$start_time = \microtime(true); +$success = \Defuse\Crypto\File::decryptFile( 'large.data', 'large-decrypted.jpg', $key ); - $end_time = \microtime(true); + +if (!$success) { + echo 'File did not encrypt successfully.', "\n"; + exit(1); +} echo "large.data:\n\t"; -echo \number_format($end_time - $start_time, 2), +echo \number_format($end_time - $start_time, 4), 's (Memory: ', \number_format(\memory_get_usage() / 1024, 2), ' KB)', "\n"; -$end_time = $start_time; if (\file_exists('In_the_Conservatory.jpg')) { - \Defuse\Crypto\File::encryptFile( + $start_time = \microtime(true); + $success = \Defuse\Crypto\File::encryptFile( 'In_the_Conservatory.data', 'In_the_Conservatory_decrypted.jpg', $key ); - $end_time = \microtime(true); + + if (!$success) { + echo 'File did not encrypt successfully.', "\n"; + exit(1); + } echo "In_the_Conservatory.data:\n\t"; - echo \number_format($end_time - $start_time, 2), + echo \number_format($end_time - $start_time, 4), 's (Memory: ', \number_format(\memory_get_usage() / 1024, 2), ' KB)', "\n"; $end_time = $start_time; diff --git a/test/stream/encrypt.php b/test/stream/encrypt.php index 467b994..b07cfb2 100644 --- a/test/stream/encrypt.php +++ b/test/stream/encrypt.php @@ -8,54 +8,67 @@ $mem = 0; $start_time = $end_time = \microtime(true); -$key = \Defuse\Crypto\Encoding::hexToBin(\file_get_contents('key.txt')); +$key = \Defuse\Crypto\Key::LoadFromAsciiSafeString(\file_get_contents('key.txt')); +$end_time = \microtime(true); echo 'Encrypting', "\n", str_repeat('-', 50), "\n\n"; echo "Load Key:\n\t"; -echo \number_format($end_time - $start_time, 2), +echo \number_format($end_time - $start_time, 4), 's (Memory: ', \number_format(\memory_get_usage() / 1024, 2), ' KB)', "\n"; -$end_time = $start_time; -\Defuse\Crypto\File::encryptFile( +$start_time = \microtime(true); +$success = \Defuse\Crypto\File::encryptFile( 'wat-gigantic-duck.jpg', 'wat-encrypted.data', $key ); - $end_time = \microtime(true); + +if (!$success) { + echo 'File did not encrypt successfully.', "\n"; + exit(1); +} echo "wat-gigantic-duck.jpg:\n\t"; -echo \number_format($end_time - $start_time, 2), +echo \number_format($end_time - $start_time, 4), 's (Memory: ', \number_format(\memory_get_usage() / 1024, 2), ' KB)', "\n"; -$end_time = $start_time; -\Defuse\Crypto\File::encryptFile( +$start_time = \microtime(true); +$success = \Defuse\Crypto\File::encryptFile( 'large.jpg', 'large.data', $key ); $end_time = \microtime(true); + +if (!$success) { + echo 'File did not encrypt successfully.', "\n"; + exit(1); +} echo "large.jpg:\n\t"; -echo \number_format($end_time - $start_time, 2), +echo \number_format($end_time - $start_time, 4), 's (Memory: ', \number_format(\memory_get_usage() / 1024, 2), ' KB)', "\n"; -$end_time = $start_time; if (\file_exists('In_the_Conservatory.jpg')) { - \Defuse\Crypto\File::encryptFile( + $start_time = \microtime(true); + $success = \Defuse\Crypto\File::encryptFile( 'In_the_Conservatory.jpg', 'In_the_Conservatory.data', $key ); $end_time = \microtime(true); + if (!$success) { + echo 'File did not encrypt successfully.', "\n"; + exit(1); + } echo "In_the_Conservatory.jpg:\n\t"; - echo \number_format($end_time - $start_time, 2), + echo \number_format($end_time - $start_time, 4), 's (Memory: ', \number_format(\memory_get_usage() / 1024, 2), ' KB)', "\n"; - $end_time = $start_time; } echo 'Peak Memory: ', \number_format(\memory_get_peak_usage() / 1048576, 2), ' MB', "\n\n"; diff --git a/test/stream/error.php b/test/stream/error.php index 31bcc0b..74aa5b9 100644 --- a/test/stream/error.php +++ b/test/stream/error.php @@ -1,7 +1,7 @@ getRawBytes())); +\file_put_contents('key.txt', $key->saveToAsciiSafeString());