Skip to content

Commit

Permalink
Implement lazy methods for File
Browse files Browse the repository at this point in the history
  • Loading branch information
paragonie-scott committed Oct 26, 2015
1 parent fe87a33 commit af1fd3d
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 13 deletions.
28 changes: 20 additions & 8 deletions doc/Features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand All @@ -73,22 +85,22 @@ 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,
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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
);
Expand All @@ -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
Expand Down
72 changes: 71 additions & 1 deletion src/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
*
Expand Down
1 change: 1 addition & 0 deletions src/SignatureKeyPair.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace ParagonIE\Halite;

use ParagonIE\Halite\Asymmetric\SignatureSecretKey;
use ParagonIE\Halite\Asymmetric\SignaturePublicKey;
use ParagonIE\Halite\Alerts as CryptoException;

/**
Expand Down
191 changes: 191 additions & 0 deletions test/unit/FileLazyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php
use \ParagonIE\Halite\File;
use \ParagonIE\Halite\EncryptionKeyPair;
use \ParagonIE\Halite\SignatureKeyPair;
use \ParagonIE\Halite\Symmetric\EncryptionKey;
use \ParagonIE\Halite\Alerts as CryptoException;

/**
* @backupGlobals disabled
* @backupStaticAttributes disabled
*/
class FileLazyTest extends PHPUnit_Framework_TestCase
{
public function testEncrypt()
{
\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',
__DIR__.'/tmp/paragon_avatar.encrypted.png',
$key
);

File::decrypt(
__DIR__.'/tmp/paragon_avatar.encrypted.png',
__DIR__.'/tmp/paragon_avatar.decrypted.png',
$key
);

$this->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);
}
}
}
Loading

0 comments on commit af1fd3d

Please sign in to comment.