diff --git a/doc/Features.md b/doc/Features.md index b764fd9..ee22a71 100644 --- a/doc/Features.md +++ b/doc/Features.md @@ -45,6 +45,14 @@ footprint. The `File` API looks like this: +* Lazy Mode + * `File::checksum`(`lazy`, [`AuthenticationKey?`](Classes/Symmetric/AuthenticationKey.md), `bool?`): `string` + * `File::encrypt`(`lazy`, `lazy`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md)) + * `File::decrypt`(`lazy`, `lazy`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md)) + * `File::seal`(`lazy`, `lazy`, [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)) + * `File::unseal`(`lazy`, `lazy`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md)) + * `File::sign`(`lazy`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md)): `string` + * `File::verify`(`lazy`, [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)): `bool` * Filenames * `File::checksumFile`(`string`, [`AuthenticationKey?`](Classes/Symmetric/AuthenticationKey.md), `bool?`): `string` * `File::encryptFile`(`string`, `string`, [`EncryptionKey`](Classes/Symmetric/EncryptionKey.md)) @@ -62,6 +70,10 @@ The `File` API looks like this: * `File::signResource`(`resource`, [`EncryptionSecretKey`](Classes/Asymmetric/EncryptionSecretKey.md)): `string` * `File::verifyResource`(`resource`, [`EncryptionPublicKey`](Classes/Asymmetric/EncryptionPublicKey.md)): `bool` +The `lazy` type indicates that the argument can be either a `string` containing +the file's path, or a `resource` (open file handle). Don't mix and match types. +If one is a `resource`, both must be. If the other is a `string`, both must be. + Each of feature is designed to work in a streaming fashion. > In each case, any call to `::*File` is just a friendly wrapper for @@ -73,7 +85,7 @@ Each of feature is designed to work in a streaming fashion. Basic usage: ```php -$checksum = \ParagonIE\Halite\File::checksumFile('/source/file/path'); +$checksum = \ParagonIE\Halite\File::checksum('/source/file/path'); ``` If for some reason you desire to use a keyed hash rather than just a plain one, @@ -81,14 +93,14 @@ you can pass an [`AuthenticationKey`](Classes/Symmetric/AuthenticationKey.md) to an optional second parameter, or `null`. ```php -$keyed = \ParagonIE\Halite\File::checksumFile('/source/file/path', $auth_key); +$keyed = \ParagonIE\Halite\File::checksum('/source/file/path', $auth_key); ``` Finally, you can pass `true` as the optional third argument if you would like a raw binary string rather than a hexadecimal string. ```php -$keyed_checksum = \ParagonIE\Halite\File::checksumFile('/source/file/path', null, true); +$keyed_checksum = \ParagonIE\Halite\File::checksum('/source/file/path', null, true); ``` ### Symmetric-Key File Encryption / Decryption @@ -100,7 +112,7 @@ API. To work around these limitations, use `File::encryptFile()` instead. For example: ```php -\ParagonIE\Halite\File::encryptFile( +\ParagonIE\Halite\File::encrypt( $inputFilename, $outputFilename, $enc_key @@ -134,7 +146,7 @@ $seal_public = $seal_keypair->getPublicKey(); **Sealing** the contents of a file using Halite: ```php -\ParagonIE\Halite\File::sealFile( +\ParagonIE\Halite\File::seal( $inputFilename, $outputFilename, $seal_public @@ -144,7 +156,7 @@ $seal_public = $seal_keypair->getPublicKey(); **Opening** the contents of a sealed file using Halite: ```php -\ParagonIE\Halite\File::unsealFile( +\ParagonIE\Halite\File::unseal( $inputFilename, $outputFilename, $seal_secret @@ -164,7 +176,7 @@ $sign_keypair = \ParagonIE\Halite\SignatureKeyPair::generate(); **Signing** the contents of a file using your secret key: ```php -$signature = \ParagonIE\Halite\File::signFile( +$signature = \ParagonIE\Halite\File::sign( $inputFilename, $sign_secret ); @@ -173,7 +185,7 @@ $signature = \ParagonIE\Halite\File::signFile( **Verifying** the contents of a file using a known public key: ```php -$valid = \ParagonIE\Halite\File::verifyFile( +$valid = \ParagonIE\Halite\File::verify( $inputFilename, $sign_public, $signature diff --git a/src/File.php b/src/File.php index c51819b..3628e01 100644 --- a/src/File.php +++ b/src/File.php @@ -82,7 +82,7 @@ public static function decrypt( } /** - * Encrypt a file with a public key + * Lazy fallthrough method for sealFile() and sealResource() * * @param string|resource $input * @param string|resource $output @@ -103,6 +103,76 @@ public static function seal( ); } + /** + * Lazy fallthrough method for sealFile() and sealResource() + * + * @param string|resource $input + * @param string|resource $output + * @param EncryptionSecretKey $secretkey + */ + public static function unseal( + $input, + $output, + EncryptionSecretKey $secretkey + ) { + if (\is_resource($input) && \is_resource($output)) { + return self::unsealResource($input, $output, $secretkey); + } elseif (\is_string($input) && \is_string($output)) { + return self::unsealFile($input, $output, $secretkey); + } + throw new \InvalidArgumentException( + 'Strings or file handles expected' + ); + } + + /** + * Lazy fallthrough method for signFile() and signResource() + * + * @param string|resource $filename + * @param SignatureSecretKey $secretkey + * @param bool $raw_binary + * + * @return string + */ + public static function sign( + $filename, + SignatureSecretKey $secretkey, + $raw_binary = false + ) { + if (\is_resource($filename)) { + return self::signResource($filename, $secretkey, $raw_binary); + } elseif (\is_string($filename)) { + return self::signFile($filename, $secretkey, $raw_binary); + } + throw new \InvalidArgumentException( + 'String or file handle expected' + ); + } + + /** + * Lazy fallthrough method for verifyFile() and verifyResource() + * + * @param string|resource $filename + * @param SignaturePublicKey $publickey + * @param bool $raw_binary + * + * @return string + */ + public static function verify( + $filename, + SignaturePublicKey $publickey, + $raw_binary = false + ) { + if (\is_resource($filename)) { + return self::verifyResource($filename, $publickey, $raw_binary); + } elseif (\is_string($filename)) { + return self::verifyFile($filename, $publickey, $raw_binary); + } + throw new \InvalidArgumentException( + 'String or file handle expected' + ); + } + /** * Calculate a checksum (derived from BLAKE2b) of a file * diff --git a/src/SignatureKeyPair.php b/src/SignatureKeyPair.php index 7e3e26f..c85d49d 100644 --- a/src/SignatureKeyPair.php +++ b/src/SignatureKeyPair.php @@ -2,6 +2,7 @@ namespace ParagonIE\Halite; use ParagonIE\Halite\Asymmetric\SignatureSecretKey; +use ParagonIE\Halite\Asymmetric\SignaturePublicKey; use ParagonIE\Halite\Alerts as CryptoException; /** diff --git a/test/unit/FileLazyTest.php b/test/unit/FileLazyTest.php new file mode 100644 index 0000000..de5120d --- /dev/null +++ b/test/unit/FileLazyTest.php @@ -0,0 +1,191 @@ +assertEquals( + \hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), + \hash_file('sha256', __DIR__.'/tmp/paragon_avatar.decrypted.png') + ); + } + + public function testEncryptFail() + { + \touch(__DIR__.'/tmp/paragon_avatar.encrypt_fail.png'); + \chmod(__DIR__.'/tmp/paragon_avatar.encrypt_fail.png', 0777); + \touch(__DIR__.'/tmp/paragon_avatar.decrypt_fail.png'); + \chmod(__DIR__.'/tmp/paragon_avatar.decrypt_fail.png', 0777); + + $key = new EncryptionKey(\str_repeat('B', 32)); + File::encrypt( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.encrypt_fail.png', + $key + ); + + $fp = \fopen(__DIR__.'/tmp/paragon_avatar.encrypt_fail.png', 'ab'); + \fwrite($fp, \Sodium\randombytes_buf(1)); + fclose($fp); + + try { + File::decrypt( + __DIR__.'/tmp/paragon_avatar.encrypt_fail.png', + __DIR__.'/tmp/paragon_avatar.decrypt_fail.png', + $key + ); + throw new \Exception('ERROR: THIS SHOULD ALWAYS FAIL'); + } catch (CryptoException\InvalidMessage $e) { + $this->assertTrue($e instanceof CryptoException\InvalidMessage); + } + } + + public function testSeal() + { + \touch(__DIR__.'/tmp/paragon_avatar.sealed.png'); + \chmod(__DIR__.'/tmp/paragon_avatar.sealed.png', 0777); + \touch(__DIR__.'/tmp/paragon_avatar.opened.png'); + \chmod(__DIR__.'/tmp/paragon_avatar.opened.png', 0777); + + $keypair = EncryptionKeyPair::generate(); + $secretkey = $keypair->getSecretKey(); + $publickey = $keypair->getPublicKey(); + + File::seal( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.sealed.png', + $publickey + ); + + File::unseal( + __DIR__.'/tmp/paragon_avatar.sealed.png', + __DIR__.'/tmp/paragon_avatar.opened.png', + $secretkey + ); + + $this->assertEquals( + \hash_file('sha256', __DIR__.'/tmp/paragon_avatar.png'), + \hash_file('sha256', __DIR__.'/tmp/paragon_avatar.opened.png') + ); + } + + public function testSealFail() + { + \touch(__DIR__.'/tmp/paragon_avatar.seal_fail.png'); + \chmod(__DIR__.'/tmp/paragon_avatar.seal_fail.png', 0777); + \touch(__DIR__.'/tmp/paragon_avatar.open_fail.png'); + \chmod(__DIR__.'/tmp/paragon_avatar.open_fail.png', 0777); + + $keypair = EncryptionKeyPair::generate(); + $secretkey = $keypair->getSecretKey(); + $publickey = $keypair->getPublicKey(); + + File::seal( + __DIR__.'/tmp/paragon_avatar.png', + __DIR__.'/tmp/paragon_avatar.seal_fail.png', + $publickey + ); + + $fp = \fopen(__DIR__.'/tmp/paragon_avatar.seal_fail.png', 'ab'); + \fwrite($fp, \Sodium\randombytes_buf(1)); + fclose($fp); + + try { + File::unseal( + __DIR__.'/tmp/paragon_avatar.seal_fail.png', + __DIR__.'/tmp/paragon_avatar.opened.png', + $secretkey + ); + throw new \Exception('ERROR: THIS SHOULD ALWAYS FAIL'); + } catch (CryptoException\InvalidMessage $e) { + $this->assertTrue($e instanceof CryptoException\InvalidMessage); + } + } + + public function testSign() + { + $keypair = SignatureKeyPair::generate(); + $secretkey = $keypair->getSecretKey(); + $publickey = $keypair->getPublicKey(); + + $signature = File::sign( + __DIR__.'/tmp/paragon_avatar.png', + $secretkey + ); + + $this->assertTrue( + File::verify( + __DIR__.'/tmp/paragon_avatar.png', + $publickey, + $signature + ) + ); + } + + public function testChecksum() + { + $csum = File::checksum(__DIR__.'/tmp/paragon_avatar.png'); + $this->assertEquals( + $csum, + "09f9f74a0e742d057ca08394db4c2e444be88c0c94fe9a914c3d3758c7eccafb". + "8dd286e3d6bc37f353e76c0c5aa2036d978ca28ffaccfa59f5dc1f076c5517a0" + ); + + $data = \Sodium\randombytes_buf(32); + \file_put_contents(__DIR__.'/tmp/garbage.dat', $data); + + $hash = \Sodium\crypto_generichash($data, null, 64); + $file = File::checksum(__DIR__.'/tmp/garbage.dat', null, true); + $this->assertEquals( + $hash, + $file + ); + } + + public function testArgFail() + { + try { + \touch(__DIR__.'/tmp/paragon_avatar.encrypted.png'); + \chmod(__DIR__.'/tmp/paragon_avatar.encrypted.png', 0777); + \touch(__DIR__.'/tmp/paragon_avatar.decrypted.png'); + \chmod(__DIR__.'/tmp/paragon_avatar.decrypted.png', 0777); + + $key = new EncryptionKey(\str_repeat('B', 32)); + File::encrypt( + __DIR__.'/tmp/paragon_avatar.png', + \fopen(__DIR__.'/tmp/paragon_avatar.encrypted.png', 'wb'), + $key + ); + $this->fail('We should be throwing an exception, not failing open.'); + } catch (\InvalidArgumentException $e) { + $this->assertTrue($e instanceof \InvalidArgumentException); + } + } +} \ No newline at end of file diff --git a/test/unit/FileTest.php b/test/unit/FileTest.php index eb80659..978ed26 100644 --- a/test/unit/FileTest.php +++ b/test/unit/FileTest.php @@ -1,6 +1,5 @@ getSecretKey(); + $publickey = $keypair->getPublicKey(); $signature = File::signFile( __DIR__.'/tmp/paragon_avatar.png', @@ -187,6 +188,5 @@ public function testStreamOperations() \fclose($fp); $this->assertEquals($written, $BYTES); - } - + } }