Skip to content
This repository has been archived by the owner on May 27, 2022. It is now read-only.

Commit

Permalink
Merge pull request #3 from Slamdunk/tag_final
Browse files Browse the repository at this point in the history
Mark stream end to detect file truncation
  • Loading branch information
Slamdunk authored Nov 7, 2021
2 parents 6b134c4 + 2cf6030 commit db6310d
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 17 deletions.
2 changes: 1 addition & 1 deletion infection.json.dist
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
},
"UnwrapSubstr": {
"ignoreSourceCodeByRegex": [
".+ = substr\\(\\$this->buffer,( 0,)? self::(EN|DE)CRYPT_READ_BYTES\\);"
".+ = substr\\(\\$this->buffer,( 0,)? \\$(read|write)ChunkSize\\);"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,8 @@
<code>EncryptorStreamFilterTest</code>
<code>EncryptorStreamFilterTest</code>
</PropertyNotSetInConstructor>
<UnusedFunctionCall occurrences="1">
<code>stream_get_contents</code>
</UnusedFunctionCall>
</file>
</files>
38 changes: 28 additions & 10 deletions src/EncryptorStreamFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace SlamCompressAndEncryptProxy;

use php_user_filter;
use RuntimeException;

/**
* @internal
Expand All @@ -14,8 +15,7 @@ final class EncryptorStreamFilter extends php_user_filter
private const FILTERNAME_PREFIX = 'slamflysystemencryptor';
private const MODE_ENCRYPT = '.encrypt';
private const MODE_DECRYPT = '.decrypt';
private const ENCRYPT_READ_BYTES = 8175;
private const DECRYPT_READ_BYTES = 8192;
private const CHUNK_SIZE = 8192;

private ?string $key;
private string $mode;
Expand Down Expand Up @@ -117,11 +117,21 @@ private function encryptFilter($out, &$consumed, $closing): int
\assert(\is_string($header));
}

while (self::ENCRYPT_READ_BYTES <= \strlen($this->buffer) || $closing) {
$data = substr($this->buffer, 0, self::ENCRYPT_READ_BYTES);
$this->buffer = substr($this->buffer, self::ENCRYPT_READ_BYTES);
$readChunkSize = self::CHUNK_SIZE - SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES;
while ($readChunkSize <= \strlen($this->buffer) || $closing) {
$data = substr($this->buffer, 0, $readChunkSize);
$this->buffer = substr($this->buffer, $readChunkSize);

$newBucketData = $header.sodium_crypto_secretstream_xchacha20poly1305_push($this->state, $data);
$tag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE;
if ($closing && '' === $this->buffer) {
$tag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL;
}
$newBucketData = $header.sodium_crypto_secretstream_xchacha20poly1305_push(
$this->state,
$data,
'',
$tag
);

\assert(\is_resource($this->stream));
$newBucket = stream_bucket_new(
Expand Down Expand Up @@ -158,13 +168,21 @@ private function decryptFilter($out, &$consumed, $closing): int
sodium_memzero($this->key);
}

while (self::DECRYPT_READ_BYTES <= \strlen($this->buffer) || $closing) {
$data = substr($this->buffer, 0, self::DECRYPT_READ_BYTES);
$this->buffer = substr($this->buffer, self::DECRYPT_READ_BYTES);
$writeChunkSize = self::CHUNK_SIZE;
while ($writeChunkSize <= \strlen($this->buffer) || $closing) {
$data = substr($this->buffer, 0, $writeChunkSize);
$this->buffer = substr($this->buffer, $writeChunkSize);

[$newBucketData] = sodium_crypto_secretstream_xchacha20poly1305_pull($this->state, $data);
$expectedTag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE;
if ($closing && '' === $this->buffer) {
$expectedTag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL;
}
[$newBucketData, $tag] = sodium_crypto_secretstream_xchacha20poly1305_pull($this->state, $data);
\assert(\is_string($newBucketData));

if ($expectedTag !== $tag) {
throw new RuntimeException('Encrypted stream corrupted');
}
\assert(\is_resource($this->stream));
$newBucket = stream_bucket_new(
$this->stream,
Expand Down
13 changes: 10 additions & 3 deletions test/CompressAndEncryptAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,15 +198,22 @@ public function writing_a_file_writes_a_compressed_and_encrypted_file(): void
public function regression(): void
{
$key = 'RjWFkMrJS4Jd5TDdhYJNAWdfSEL5nptu4KQHgkeKGI0=';
$content = base64_decode('IZS+uLwIi3vfk+/txrq+7V7vQ0GGN9cwWetC8p/IRMstNUFfxB363Dt1jwxM7LbK3M4EX4earQ==', true);

$originalPlain = 'foobar';
$adapter = new CompressAndEncryptAdapter(
new LocalFilesystemAdapter($this->remoteMock),
$key
);

// To recreate assets, uncomment following lines
// $adapter->write('/file.txt', $originalPlain, new Config());
// var_dump(
// base64_encode(file_get_contents($this->remoteMock.'/file.txt'.CompressAndEncryptAdapter::REMOTE_FILE_EXTENSION))
// ); exit;

$content = base64_decode('Zp1CKRNAdEebRInjHnuJwuG1gI2owWedBVboddwd+sW4AKv/3a112UjHnlpJntUUZgPBStuSFw==', true);
file_put_contents($this->remoteMock.'/file.txt'.CompressAndEncryptAdapter::REMOTE_FILE_EXTENSION, $content);

static::assertSame('foobar', $adapter->read('/file.txt'));
static::assertSame($originalPlain, $adapter->read('/file.txt'));
}

/**
Expand Down
45 changes: 42 additions & 3 deletions test/EncryptorStreamFilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace SlamCompressAndEncryptProxy\Test;

use PHPUnit\Framework\TestCase;
use RuntimeException;
use SlamCompressAndEncryptProxy\EncryptorStreamFilter;

/**
Expand Down Expand Up @@ -62,6 +63,36 @@ public function provideCases(): array
];
}

/**
* @test
*/
public function detect_file_corruption(): void
{
$key = sodium_crypto_secretstream_xchacha20poly1305_keygen();
EncryptorStreamFilter::register();

$chunkSize = 8192;
$originalPlain = random_bytes(10 * ($chunkSize - SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES));

$cipherStream = $this->streamFromContents($originalPlain);
EncryptorStreamFilter::appendEncryption($cipherStream, $key);

$cipher = stream_get_contents($cipherStream);
fclose($cipherStream);
static::assertNotSame($originalPlain, $cipher);
static::assertSame(
(10 * $chunkSize) + SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES,
\strlen($cipher)
);

$truncatedCipher = substr($cipher, 0, (9 * $chunkSize) + SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES);
$plainStream = $this->streamFromContents($truncatedCipher);
EncryptorStreamFilter::appendDecryption($plainStream, $key);

$this->expectException(RuntimeException::class);
stream_get_contents($plainStream);
}

/**
* @test
*/
Expand Down Expand Up @@ -104,18 +135,26 @@ public function consecutive_filtering(): void
public function regression(): void
{
$key = base64_decode('Z+Ry4nDufKcJ19pU2pEMgGiac9GBWFjEV18Cpb9jxRM=', true);
$cipher = base64_decode('PMRzbW/xSj1WPnXp0DknCZvmM1Lv1XCYNbQH5wHozLpULVaGnoq7kVOuhg5Guew=', true);

$originalPlain = 'foobar';
EncryptorStreamFilter::register();

// To recreate assets, uncomment following lines
// $cipherStream = $this->streamFromContents($content);
// EncryptorStreamFilter::appendEncryption($cipherStream, $key);
// $cipher = stream_get_contents($cipherStream);
// fclose($cipherStream);
// var_dump(base64_encode($cipher)); exit;

$cipher = base64_decode('UbQpWpd03RyW8a2YiVQSlkmfeEN76IgkN67yPRb7UoXcxUeL7LmUGizXL7zwbtc=', true);

$plainStream = $this->streamFromContents($cipher);
EncryptorStreamFilter::appendDecryption($plainStream, $key);

$plain = stream_get_contents($plainStream);

fclose($plainStream);

static::assertSame('foobar', $plain);
static::assertSame($originalPlain, $plain);
}

/**
Expand Down

0 comments on commit db6310d

Please sign in to comment.