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

Commit db6310d

Browse files
authored
Merge pull request #3 from Slamdunk/tag_final
Mark stream end to detect file truncation
2 parents 6b134c4 + 2cf6030 commit db6310d

File tree

5 files changed

+84
-17
lines changed

5 files changed

+84
-17
lines changed

infection.json.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
},
3535
"UnwrapSubstr": {
3636
"ignoreSourceCodeByRegex": [
37-
".+ = substr\\(\\$this->buffer,( 0,)? self::(EN|DE)CRYPT_READ_BYTES\\);"
37+
".+ = substr\\(\\$this->buffer,( 0,)? \\$(read|write)ChunkSize\\);"
3838
]
3939
}
4040
},

psalm-baseline.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,8 @@
2323
<code>EncryptorStreamFilterTest</code>
2424
<code>EncryptorStreamFilterTest</code>
2525
</PropertyNotSetInConstructor>
26+
<UnusedFunctionCall occurrences="1">
27+
<code>stream_get_contents</code>
28+
</UnusedFunctionCall>
2629
</file>
2730
</files>

src/EncryptorStreamFilter.php

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace SlamCompressAndEncryptProxy;
66

77
use php_user_filter;
8+
use RuntimeException;
89

910
/**
1011
* @internal
@@ -14,8 +15,7 @@ final class EncryptorStreamFilter extends php_user_filter
1415
private const FILTERNAME_PREFIX = 'slamflysystemencryptor';
1516
private const MODE_ENCRYPT = '.encrypt';
1617
private const MODE_DECRYPT = '.decrypt';
17-
private const ENCRYPT_READ_BYTES = 8175;
18-
private const DECRYPT_READ_BYTES = 8192;
18+
private const CHUNK_SIZE = 8192;
1919

2020
private ?string $key;
2121
private string $mode;
@@ -117,11 +117,21 @@ private function encryptFilter($out, &$consumed, $closing): int
117117
\assert(\is_string($header));
118118
}
119119

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

124-
$newBucketData = $header.sodium_crypto_secretstream_xchacha20poly1305_push($this->state, $data);
125+
$tag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE;
126+
if ($closing && '' === $this->buffer) {
127+
$tag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL;
128+
}
129+
$newBucketData = $header.sodium_crypto_secretstream_xchacha20poly1305_push(
130+
$this->state,
131+
$data,
132+
'',
133+
$tag
134+
);
125135

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

161-
while (self::DECRYPT_READ_BYTES <= \strlen($this->buffer) || $closing) {
162-
$data = substr($this->buffer, 0, self::DECRYPT_READ_BYTES);
163-
$this->buffer = substr($this->buffer, self::DECRYPT_READ_BYTES);
171+
$writeChunkSize = self::CHUNK_SIZE;
172+
while ($writeChunkSize <= \strlen($this->buffer) || $closing) {
173+
$data = substr($this->buffer, 0, $writeChunkSize);
174+
$this->buffer = substr($this->buffer, $writeChunkSize);
164175

165-
[$newBucketData] = sodium_crypto_secretstream_xchacha20poly1305_pull($this->state, $data);
176+
$expectedTag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE;
177+
if ($closing && '' === $this->buffer) {
178+
$expectedTag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL;
179+
}
180+
[$newBucketData, $tag] = sodium_crypto_secretstream_xchacha20poly1305_pull($this->state, $data);
166181
\assert(\is_string($newBucketData));
167182

183+
if ($expectedTag !== $tag) {
184+
throw new RuntimeException('Encrypted stream corrupted');
185+
}
168186
\assert(\is_resource($this->stream));
169187
$newBucket = stream_bucket_new(
170188
$this->stream,

test/CompressAndEncryptAdapterTest.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,22 @@ public function writing_a_file_writes_a_compressed_and_encrypted_file(): void
198198
public function regression(): void
199199
{
200200
$key = 'RjWFkMrJS4Jd5TDdhYJNAWdfSEL5nptu4KQHgkeKGI0=';
201-
$content = base64_decode('IZS+uLwIi3vfk+/txrq+7V7vQ0GGN9cwWetC8p/IRMstNUFfxB363Dt1jwxM7LbK3M4EX4earQ==', true);
202-
201+
$originalPlain = 'foobar';
203202
$adapter = new CompressAndEncryptAdapter(
204203
new LocalFilesystemAdapter($this->remoteMock),
205204
$key
206205
);
206+
207+
// To recreate assets, uncomment following lines
208+
// $adapter->write('/file.txt', $originalPlain, new Config());
209+
// var_dump(
210+
// base64_encode(file_get_contents($this->remoteMock.'/file.txt'.CompressAndEncryptAdapter::REMOTE_FILE_EXTENSION))
211+
// ); exit;
212+
213+
$content = base64_decode('Zp1CKRNAdEebRInjHnuJwuG1gI2owWedBVboddwd+sW4AKv/3a112UjHnlpJntUUZgPBStuSFw==', true);
207214
file_put_contents($this->remoteMock.'/file.txt'.CompressAndEncryptAdapter::REMOTE_FILE_EXTENSION, $content);
208215

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

212219
/**

test/EncryptorStreamFilterTest.php

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace SlamCompressAndEncryptProxy\Test;
66

77
use PHPUnit\Framework\TestCase;
8+
use RuntimeException;
89
use SlamCompressAndEncryptProxy\EncryptorStreamFilter;
910

1011
/**
@@ -62,6 +63,36 @@ public function provideCases(): array
6263
];
6364
}
6465

66+
/**
67+
* @test
68+
*/
69+
public function detect_file_corruption(): void
70+
{
71+
$key = sodium_crypto_secretstream_xchacha20poly1305_keygen();
72+
EncryptorStreamFilter::register();
73+
74+
$chunkSize = 8192;
75+
$originalPlain = random_bytes(10 * ($chunkSize - SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES));
76+
77+
$cipherStream = $this->streamFromContents($originalPlain);
78+
EncryptorStreamFilter::appendEncryption($cipherStream, $key);
79+
80+
$cipher = stream_get_contents($cipherStream);
81+
fclose($cipherStream);
82+
static::assertNotSame($originalPlain, $cipher);
83+
static::assertSame(
84+
(10 * $chunkSize) + SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES,
85+
\strlen($cipher)
86+
);
87+
88+
$truncatedCipher = substr($cipher, 0, (9 * $chunkSize) + SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES);
89+
$plainStream = $this->streamFromContents($truncatedCipher);
90+
EncryptorStreamFilter::appendDecryption($plainStream, $key);
91+
92+
$this->expectException(RuntimeException::class);
93+
stream_get_contents($plainStream);
94+
}
95+
6596
/**
6697
* @test
6798
*/
@@ -104,18 +135,26 @@ public function consecutive_filtering(): void
104135
public function regression(): void
105136
{
106137
$key = base64_decode('Z+Ry4nDufKcJ19pU2pEMgGiac9GBWFjEV18Cpb9jxRM=', true);
107-
$cipher = base64_decode('PMRzbW/xSj1WPnXp0DknCZvmM1Lv1XCYNbQH5wHozLpULVaGnoq7kVOuhg5Guew=', true);
108-
138+
$originalPlain = 'foobar';
109139
EncryptorStreamFilter::register();
110140

141+
// To recreate assets, uncomment following lines
142+
// $cipherStream = $this->streamFromContents($content);
143+
// EncryptorStreamFilter::appendEncryption($cipherStream, $key);
144+
// $cipher = stream_get_contents($cipherStream);
145+
// fclose($cipherStream);
146+
// var_dump(base64_encode($cipher)); exit;
147+
148+
$cipher = base64_decode('UbQpWpd03RyW8a2YiVQSlkmfeEN76IgkN67yPRb7UoXcxUeL7LmUGizXL7zwbtc=', true);
149+
111150
$plainStream = $this->streamFromContents($cipher);
112151
EncryptorStreamFilter::appendDecryption($plainStream, $key);
113152

114153
$plain = stream_get_contents($plainStream);
115154

116155
fclose($plainStream);
117156

118-
static::assertSame('foobar', $plain);
157+
static::assertSame($originalPlain, $plain);
119158
}
120159

121160
/**

0 commit comments

Comments
 (0)