diff --git a/README.md b/README.md index 0e9e90d..8e6fc66 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,149 @@ This is a class for doing symmetric encryption in PHP. **Requires PHP 5.4 or new Implementation -------------- -Messages are encrypted with AES-128 in CTR mode and are authenticated with +Messages are encrypted with AES-256 in CTR mode and are authenticated with HMAC-SHA256 (Encrypt-then-Mac). HKDF is used to split the user-provided key into two keys: one for encryption, and the other for authentication. It is implemented using the `openssl_` and `hash_hmac` functions. +## Installing this Library + +### Using Composer + +```sh +composer require defuse/php-encryption +``` + +### Direct Installation (Phar) + +Download the PHP Archive and public key. Extract + +### Direct Installation (Manual) + +Download the [latest release](https://github.com/defuse/php-encryption/releases). Extract all of the files into a directory on your webserver (e.g. `/var/www/lib/defuse/php-encryption`). + +Then add this to your PHP scripts: + +```php +require '/var/www/lib/defuse/php-encryption/autoload.php'; +``` + +## Using this Library + +1. Generate and store an encryption key. +2. Encrypt plaintext strings with your key to obtain ciphertext, using `Crypto`. +3. Decrypt ciphertext strings with your key to obtain plaintext, using `Crypto`. +4. Encrypt/decrypt files with your key, using `File`. + +### Generate and Store an Encryption Key + +Generate a new key: + +```php +$key = \Defuse\Crypto\Key::createNewRandomKey(); +```` + +The above command will generate a random encryption key, using a +cryptographically secure pseudorandom number generator. This will generally only +need to be done *once* if you need to reuse this key for multiple messages. + +```php +$encryptionKeyDataForStorage = $key->saveToAsciiSafeString() +``` + +This returns an encoded string that you can use to persist a key across multiple +runs of the application. You might decide to copy it to a configuration file +not tracked by Git, for example. To load it again on the next script execution, +just do this: + +```php +$key = \Defuse\Crypto\Key::LoadFromAsciiSafeString($storedKeyData); +``` + +### Encrypting Strings + +Once you have a `Key` object, you're ready to encrypt data. All you have to do +is pass your desired string and the `Key` object to `Crypto::encrypt()`. + +```php +try { + $ciphertext = \Defuse\Crypto\Crypto::encrypt("Test message", $key); +} catch (\Defuse\Crypto\Exception\CryptoTestFailedException $ex) { + die("Our platform is not secure enough to use this cryptography library."); +} +``` + +### Decrypting Strings + +If encryption made sense, then the decryption API should be intuitive and +precisely what you expect it to be: + + +### Interlude: A Complete Example + +First, generate a key and store it: + +```php +saveToAsciiSafeString()); +``` + +The two scripts below, `encrypt_msg.php` and `decrypt_msg.php` are command-line +PHP scripts meant to encrypt/decrypt messages using a pre-shared-key. + +Sender: + +```php + Some PHP encryption libraries, like libsodium-php [1], are not - > straightforward to install and cannot packaged with "just download and - > extract" applications. This library will always be just a handful of PHP - > files that you can copy to your source tree and require(). - -References: - - [1] https://github.com/jedisct1/libsodium-php + > Some PHP encryption libraries, like [libsodium-php](https://github.com/jedisct1/libsodium-php), + > are not straightforward to install and cannot packaged with "just download + > and extract" applications. This library will always be just a handful of + > PHP files that you can copy to your source tree and require(). Authors --------- -This library is authored by Taylor Hornby and Scott Arciszewski. +This library is authored by [Taylor Hornby](https://bqp.io) and [Scott Arciszewski](https://paragonie.com/blog/author/scott-arciszewski). \ No newline at end of file diff --git a/autoload.php b/autoload.php index 309cc98..2a08989 100644 --- a/autoload.php +++ b/autoload.php @@ -4,7 +4,7 @@ */ \spl_autoload_register(function ($class) { // Project-specific namespace prefix - $prefix = 'Defuse\\Crypto'; + $prefix = 'Defuse\\Crypto\\'; // Base directory for the namespace prefix $base_dir = __DIR__.'/src/'; @@ -18,19 +18,46 @@ // Get the relative class name $relative_class = \substr($class, $len); - - // Replace the namespace prefix with the base directory, replace namespace - // separators with directory separators in the relative class name, append - // with .php - $file = $base_dir. - \str_replace( - ['\\', '_'], - '/', - $relative_class - ).'.php'; - - // If the file exists, require it - if (\file_exists($file)) { - require $file; + + /** + * unserialize() -> autoloader -> LFI hardening + */ + $classmap = array( + 'Config' => + 'Config.php', + 'Core' => + 'Core.php', + 'Crypto' => + 'Crypto.php', + 'Encoding' => + 'Encoding.php', + 'ExceptionHandler' => + 'ExceptionHandler.php', + 'File' => + 'File.php', + 'FileConfig' => + 'FileConfig.php', + 'Key' => + 'Key.php', + 'KeyConfig' => + 'KeyConfig.php', + 'RuntimeTests' => + 'RuntimeTests.php', + 'StreamInterface' => + 'StreamInterface.php', + // Exceptions: + 'Exception\\CannotPerformOperationException' => + 'Exception/CannotPerformOperationException.php', + 'Exception\\CryptoException' => + 'Exception/CryptoException.php', + 'Exception\\CryptoTestFailedException' => + 'Exception/CryptoTestFailedException.php', + 'Exception\\InvalidCiphertextException' => + 'Exception/InvalidCiphertextException.php', + ); + foreach ($classmap as $classname => $file) { + if ($classname === $relative_class) { + require $base_dir.$file; + } } }); diff --git a/src/Core.php b/src/Core.php index 0c2822f..c2172ab 100644 --- a/src/Core.php +++ b/src/Core.php @@ -2,7 +2,6 @@ namespace Defuse\Crypto; use \Defuse\Crypto\Exception as Ex; -use \Defuse\Crypto\Crypto; final class Core { @@ -27,12 +26,7 @@ public static function incrementCounter($ctr, $inc, &$config) { static $ivsize = null; if ($ivsize === null) { - $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); - if ($ivsize === false) { - throw new Ex\CannotPerformOperationException( - "Problem obtaining the correct nonce length." - ); - } + $ivsize = self::cipherIvLength($config->cipherMethod()); } if (self::ourStrlen($ctr) !== $ivsize) { @@ -73,6 +67,27 @@ public static function incrementCounter($ctr, $inc, &$config) return $ctr; } + /** + * Returns the cipher initialization vector (iv) length. + * + * @param string $method + * @return int + * @throws Ex\CannotPerformOperationException + */ + public static function cipherIvLength($method) + { + self::ensureFunctionExists('openssl_cipher_iv_length'); + $ivsize = \openssl_cipher_iv_length($method); + + if ($ivsize === false || $ivsize <= 0) { + throw new Ex\CannotPerformOperationException( + 'Could not get the IV length from OpenSSL' + ); + } + + return $ivsize; + } + /** * Returns a random binary string of length $octets bytes. * diff --git a/src/Crypto.php b/src/Crypto.php index 6a38bd4..207090f 100755 --- a/src/Crypto.php +++ b/src/Crypto.php @@ -105,13 +105,7 @@ public static function encrypt($plaintext, $key, $raw_binary = false) ); // Generate a random initialization vector. - Core::ensureFunctionExists("openssl_cipher_iv_length"); - $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); - if ($ivsize === false || $ivsize <= 0) { - throw new Ex\CannotPerformOperationException( - "Could not get the IV length from OpenSSL" - ); - } + $ivsize = Core::cipherIvLength($config->cipherMethod()); $iv = Core::secureRandom($ivsize); $ciphertext = $salt . $iv . self::plainEncrypt($plaintext, $ekey, $iv, $config); @@ -210,13 +204,7 @@ public static function decrypt($ciphertext, $key, $raw_binary = false) $ekey = Core::HKDF($config->hashFunctionName(), $key, $config->keyByteSize(), $config->encryptionInfoString(), $salt, $config); // Extract the initialization vector from the ciphertext. - Core::EnsureFunctionExists("openssl_cipher_iv_length"); - $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); - if ($ivsize === false || $ivsize <= 0) { - throw new Ex\CannotPerformOperationException( - "Could not get the IV length from OpenSSL" - ); - } + $ivsize = Core::cipherIvLength($config->cipherMethod()); if (Core::ourStrlen($ciphertext) <= $ivsize) { throw new Ex\InvalidCiphertextException( "Ciphertext is too short." @@ -302,13 +290,7 @@ public static function legacyDecrypt($ciphertext, $key) ); // Extract the initialization vector from the ciphertext. - Core::EnsureFunctionExists("openssl_cipher_iv_length"); - $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); - if ($ivsize === false || $ivsize <= 0) { - throw new Ex\CannotPerformOperationException( - "Could not get the IV length from OpenSSL" - ); - } + $ivsize = Core::cipherIvLength($config->cipherMethod()); if (Core::ourStrlen($ciphertext) <= $ivsize) { throw new Ex\InvalidCiphertextException( "Ciphertext is too short." @@ -467,7 +449,7 @@ protected static function getVersionConfigFromMajorMinor($major, $minor) 'cipher_method' => 'aes-256-ctr', 'block_byte_size' => 16, 'key_byte_size' => 32, - 'salt_byte_size' => 16, + 'salt_byte_size' => 32, 'hash_function_name' => 'sha256', 'mac_byte_size' => 32, 'encryption_info_string' => 'DefusePHP|V2|KeyForEncryption', diff --git a/src/File.php b/src/File.php index 3db66bb..5475422 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, @@ -278,27 +273,17 @@ public static function encryptResource($inputHandle, $outputHandle, $key) /** * Generate a random initialization vector. */ - Core::ensureFunctionExists("openssl_cipher_iv_length"); - $ivsize = \openssl_cipher_iv_length($config->cipherMethod()); - if ($ivsize === false || $ivsize <= 0) { - throw new Ex\CannotPerformOperationException( - 'Improper IV size' - ); - } + $ivsize = Core::cipherIvLength($config->cipherMethod()); $iv = Core::secureRandom($ivsize); /** * 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 +318,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 +358,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 +382,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 +400,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 +412,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 +429,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ $ekey = Core::HKDF( $config->hashFunctionName(), - $key, + $key->getRawBytes(), $config->keyByteSize(), $config->encryptionInfoString(), $file_salt, @@ -470,7 +441,7 @@ public static function decryptResource($inputHandle, $outputHandle, $key) */ $akey = Core::HKDF( $config->hashFunctionName(), - $key, + $key->getRawBytes(), $config->keyByteSize(), $config->authenticationInfoString(), $file_salt, @@ -482,13 +453,8 @@ 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' - ); - } + $ivsize = Core::cipherIvLength($config->cipherMethod()); + $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 +482,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 +540,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 +603,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 +620,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 +680,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; } /** @@ -781,7 +738,7 @@ private static function getFileVersionConfigFromMajorMinor($major, $minor) 'cipher_method' => 'aes-256-ctr', 'block_byte_size' => 16, 'key_byte_size' => 32, - 'salt_byte_size' => 16, + 'salt_byte_size' => 32, 'hash_function_name' => 'sha256', 'mac_byte_size' => 32, 'encryption_info_string' => 'DefusePHP|V2File|KeyForEncryption', @@ -799,4 +756,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/Key.php b/src/Key.php index 227ff04..25e7964 100644 --- a/src/Key.php +++ b/src/Key.php @@ -56,6 +56,11 @@ final class Key private $key_bytes = null; private $config = null; + /** + * Creates a new random Key object for use with this library. + * + * @return \Defuse\Crypto\Key + */ public static function CreateNewRandomKey() { $config = self::GetKeyVersionConfigFromKeyHeader(self::KEY_CURRENT_VERSION); @@ -63,6 +68,13 @@ public static function CreateNewRandomKey() return new Key(self::KEY_CURRENT_VERSION, $bytes); } + /** + * Loads a Key object from an ASCII-safe string + * + * @param string $savedKeyString + * @return \Defuse\Crypto\Key + * @throws Ex\CannotPerformOperationException + */ public static function LoadFromAsciiSafeString($savedKeyString) { try { @@ -124,6 +136,14 @@ public static function LoadFromAsciiSafeString($savedKeyString) return new Key($version_header, $key_bytes); } + /** + * Private constructor -> cannot be instantiated directly: + * + * $key = new Key("\xDE\xF0\x02\x00", "some_key_string"); // errors + * + * @param string $version_header + * @param string $bytes + */ private function __construct($version_header, $bytes) { $this->key_version_header = $version_header; @@ -131,6 +151,11 @@ private function __construct($version_header, $bytes) $this->config = self::GetKeyVersionConfigFromKeyHeader($this->key_version_header); } + /** + * Encodes the key as an ASCII string, with a checksum, for storing. + * + * @return string + */ public function saveToAsciiSafeString() { return Encoding::binToHex( @@ -150,6 +175,12 @@ public function isSafeForCipherTextVersion($major, $minor) return $major == 2 && $minor == 0; } + /** + * Get the raw bytes of the encryption key + * + * @return string + * @throws CannotPerformOperationException + */ public function getRawBytes() { if (is_null($this->key_bytes) || Core::ourStrlen($this->key_bytes) < self::MIN_SAFE_KEY_BYTE_SIZE) { @@ -160,6 +191,13 @@ public function getRawBytes() return $this->key_bytes; } + /** + * Parse a key header, get the configuration + * + * @param string $key_header + * @return \Defuse\Crypto\KeyConfig + * @throws Ex\CannotPerformOperationException + */ private static function GetKeyVersionConfigFromKeyHeader($key_header) { if ($key_header === self::KEY_CURRENT_VERSION) { return new KeyConfig([ @@ -173,8 +211,11 @@ private static function GetKeyVersionConfigFromKeyHeader($key_header) { ); } - /* - * NEVER use this, exept for testing. + /** + * NEVER use this, except for testing. + * + * @param string $bytes + * @return \Defuse\Crypto\Key */ public static function LoadFromRawBytesForTestingPurposesOnlyInsecure($bytes) { diff --git a/src/RuntimeTests.php b/src/RuntimeTests.php index 770c6a5..b660710 100644 --- a/src/RuntimeTests.php +++ b/src/RuntimeTests.php @@ -14,7 +14,7 @@ class RuntimeTests extends Crypto { - /* + /** * Runs tests. * Raises Ex\CannotPerformOperationException or Ex\CryptoTestFailedException if * one of the tests fail. If any tests fails, your system is not capable of @@ -74,6 +74,13 @@ public static function runtimeTest() $test_state = 1; } + /** + * Run-time test: string encryption and decryption + * + * @param \Defuse\Crypto\Config $config + * + * @throws Ex\CryptoTestFailedException + */ private static function testEncryptDecrypt($config) { $key = Crypto::createNewRandomKey(); @@ -129,7 +136,9 @@ private static function testEncryptDecrypt($config) } /** - * Run-time testing + * Run-time testing: HKDF + * + * @param \Defuse\Crypto\Config $config * * @throws Ex\CryptoTestFailedException */ @@ -171,7 +180,7 @@ private static function HKDFTestVector($config) } /** - * Run-Time tests + * Run-Time testing: HMAC * * @throws Ex\CryptoTestFailedException */ @@ -190,7 +199,9 @@ private static function HMACTestVector($config) } /** - * Run-time tests + * Run-time testing: AES-256-CTR + * + * @param \Defuse\Crypto\Config $config * * @throws Ex\CryptoTestFailedException */ @@ -219,12 +230,14 @@ private static function AESTestVector($config) $computed_ciphertext = Crypto::plainEncrypt($plaintext, $key, $iv, $config); if ($computed_ciphertext !== $ciphertext) { + /* echo str_repeat("\n", 30); var_dump($config); echo \bin2hex($computed_ciphertext); echo "\n---\n"; echo \bin2hex($ciphertext); echo str_repeat("\n", 30); + */ throw new Ex\CryptoTestFailedException(); } @@ -233,5 +246,4 @@ private static function AESTestVector($config) throw new Ex\CryptoTestFailedException(); } } - } 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.sh b/test.sh index 1a55aa7..84ca6d7 100755 --- a/test.sh +++ b/test.sh @@ -2,9 +2,3 @@ set -e ./test/phpunit.sh echo "" -ORIGDIR=`pwd` -cd test/stream -php keygen.php -php encrypt.php -php decrypt.php -cd $ORIGDIR diff --git a/test/phpunit.sh b/test/phpunit.sh index 87e5b68..224f8b6 100755 --- a/test/phpunit.sh +++ b/test/phpunit.sh @@ -7,6 +7,7 @@ origdir=`pwd` cdir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) cd $origdir parentdir="$(dirname $cdir)" +PHP_VERSION=$(php -r "echo PHP_VERSION_ID;"); clean=0 # Clean up? gpg --fingerprint D8406D0D82947747293778314AA394086372C20A diff --git a/test/stream/decrypt.php b/test/stream/decrypt.php deleted file mode 100644 index b9c6a4e..0000000 --- a/test/stream/decrypt.php +++ /dev/null @@ -1,62 +0,0 @@ -getRawBytes())); diff --git a/test/stream/get_large.sh b/test/unit/File/get_large.sh old mode 100644 new mode 100755 similarity index 100% rename from test/stream/get_large.sh rename to test/unit/File/get_large.sh diff --git a/test/stream/large.jpg b/test/unit/File/large.jpg similarity index 100% rename from test/stream/large.jpg rename to test/unit/File/large.jpg diff --git a/test/stream/wat-gigantic-duck.jpg b/test/unit/File/wat-gigantic-duck.jpg similarity index 100% rename from test/stream/wat-gigantic-duck.jpg rename to test/unit/File/wat-gigantic-duck.jpg diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php new file mode 100644 index 0000000..885b961 --- /dev/null +++ b/test/unit/FileTest.php @@ -0,0 +1,154 @@ +key = Key::CreateNewRandomKey(); + } + + public function tearDown() + { + array_map('unlink', glob(self::$TEMP_DIR . '/*')); + rmdir(self::$TEMP_DIR); + } + + /** + * Test encryption from one file name to a destination file name + * @dataProvider fileToFileProvider + * @param string $srcName source file name + */ + public function testFileToFile($srcName) + { + $src = self::$FILE_DIR . '/' . $srcName; + + $dest1 = self::$TEMP_DIR . '/ff1'; + $result = File::encryptFile($src, $dest1, $this->key); + $this->assertTrue($result, + sprintf('File "%s" did not encrypt successfully.', $src)); + $this->assertFileExists($dest1, 'destination file not created.'); + + $reverse1 = self::$TEMP_DIR . '/rv1'; + $result = File::decryptFile($dest1, $reverse1, $this->key); + $this->assertTrue($result, + sprintf('File "%s" did not decrypt successfully.', $dest1)); + $this->assertFileExists($reverse1); + $this->assertEquals(md5_file($src), md5_file($reverse1), + 'File and encrypted-decrypted file do not match.'); + + $dest2 = self::$TEMP_DIR . '/ff2'; + $result = File::encryptFile($reverse1, $dest2, $this->key); + $this->assertFileExists($dest2); + $this->assertTrue($result, + sprintf('File "%s" did not re-encrypt successfully.', $reverse1)); + + $this->assertNotEquals(md5_file($dest1), md5_file($dest2), + 'First and second encryption produced identical files.'); + + $reverse2 = self::$TEMP_DIR . '/rv2'; + $result = File::decryptFile($dest2, $reverse2, $this->key); + $this->assertTrue($result, + sprintf('File "%s" did not re-decrypt successfully.', $dest1)); + $this->assertEquals(md5_file($src), md5_file($reverse2), + 'File and encrypted-decrypted file do not match.'); + + } + + /** + * @dataProvider fileToFileProvider + * @param string $src source handle + */ + public function testResourceToResource($srcFile) + { + $srcName = self::$FILE_DIR . '/' . $srcFile; + $destName = self::$TEMP_DIR . "/$srcFile.dest"; + $src = fopen($srcName, 'r'); + $dest = fopen($destName, 'w'); + + $success = File::encryptResource($src, $dest, $this->key); + $this->assertTrue($success, "File did not encrypt successfully."); + + fclose($src); + fclose($dest); + + $src2 = fopen($destName, 'r'); + $dest2 = fopen(self::$TEMP_DIR . '/dest2', 'w'); + + $success = File::decryptResource($src2, $dest2, $this->key); + $this->assertTrue($success, "File did not decrypt successfully."); + fclose($src2); + fclose($dest2); + + $this->assertEquals(md5_file($srcName), md5_file(self::$TEMP_DIR . '/dest2'), + 'Original file mismatches the result of encrypt and decrypt'); + + } + + /** + * @expectedException \Defuse\Crypto\Exception\InvalidCiphertextException + * @excpectedExceptionMessage Ciphertext file has a bad magic number. + */ + public function testGarbage() + { + $junk = self::$TEMP_DIR . '/junk'; + file_put_contents($junk, + str_repeat("this is not anything that can be decrypted.", 100)); + + $success = File::decryptFile($junk, self::$TEMP_DIR . '/unjunked', $this->key); + } + + /** + * @expectedException \Defuse\Crypto\Exception\InvalidCiphertextException + * @excpectedExceptionMessage Message Authentication failure; tampering detected. + */ + public function testExtraData() + { + $src = self::$FILE_DIR . '/wat-gigantic-duck.jpg'; + $dest = self::$TEMP_DIR . '/err'; + + File::encryptFile($src, $dest, $this->key); + + file_put_contents($dest, str_repeat('A', 2048), FILE_APPEND); + + File::decryptFile($dest, $dest . '.jpg', $this->key); + } + + public function testFileCreateRandomKey() + { + $result = File::createNewRandomKey(); + $this->assertInstanceOf('\Defuse\Crypto\Key', $result); + } + + public function fileToFileProvider() + { + $data = []; + + $data['wat-giagantic-duck'] = ['wat-gigantic-duck.jpg']; + $data['large'] = ['large.jpg']; + + if (file_exists(__DIR__ . '/File/In_the_Conservatory.jpg')){ + // see File/get_large.sh + $data['extra-large'] = ['In_the_Conservatory.jpg']; + } + + return $data; + } +}