Skip to content

Commit 00ea746

Browse files
Merge pull request #62779 from nextcloud/backport/60070/stable33
[stable33] fix(encryption): Correctly report size for zero-byte encrypted files
2 parents e72d176 + f3ebb6f commit 00ea746

9 files changed

Lines changed: 88 additions & 11 deletions

File tree

lib/private/Files/Cache/Cache.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,7 @@ protected function calculateFolderSizeInner(string $path, $entry = null, bool $i
10021002
$id = $entry['fileid'];
10031003

10041004
$query = $this->getQueryBuilder();
1005-
$query->select('size', 'unencrypted_size')
1005+
$query->select('size', 'unencrypted_size', 'encrypted')
10061006
->from('filecache')
10071007
->whereStorageId($this->getNumericStorageId())
10081008
->whereParent($id);
@@ -1022,7 +1022,7 @@ protected function calculateFolderSizeInner(string $path, $entry = null, bool $i
10221022
return Util::numericToNumber($row['unencrypted_size']);
10231023
}, $rows);
10241024
$unencryptedSizes = array_map(function (array $row) {
1025-
return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
1025+
return Util::numericToNumber($row['encrypted'] ? $row['unencrypted_size'] : $row['size']);
10261026
}, $rows);
10271027

10281028
$sum = array_sum($sizes);

lib/private/Files/Cache/CacheEntry.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public function __clone() {
119119
}
120120

121121
public function getUnencryptedSize(): int {
122-
if ($this->data['encrypted'] && isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) {
122+
if ($this->data['encrypted'] && isset($this->data['unencrypted_size'])) {
123123
return $this->data['unencrypted_size'];
124124
} else {
125125
return $this->data['size'] ?? 0;

lib/private/Files/Cache/Scanner.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,10 @@ public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData =
197197
}
198198
}
199199

200-
// we only updated unencrypted_size if it's already set
201-
if (isset($cacheData['unencrypted_size']) && $cacheData['unencrypted_size'] === 0) {
200+
// Skip updating unencrypted_size only when both cached and new values are 0
201+
if (isset($cacheData['unencrypted_size'])
202+
&& $cacheData['unencrypted_size'] === 0
203+
&& isset($data['unencrypted_size']) && $data['unencrypted_size'] === 0) {
202204
unset($data['unencrypted_size']);
203205
}
204206

@@ -216,7 +218,10 @@ public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData =
216218
$data['etag_changed'] = true;
217219
}
218220
} else {
219-
unset($data['unencrypted_size']);
221+
// For new files, only preserve unencrypted_size when the file is encrypted
222+
if (!isset($data['encrypted']) || !$data['encrypted']) {
223+
unset($data['unencrypted_size']);
224+
}
220225
$newData = $data;
221226
$fileId = -1;
222227
}

lib/private/Files/FileInfo.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public function __construct($path, $storage, $internalPath, $data, $mount, $owne
7373
$this->data = $data;
7474
$this->mount = $mount;
7575
$this->owner = $owner;
76-
if (isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] !== 0) {
76+
if (($this->data['encrypted'] ?? false) && isset($this->data['unencrypted_size'])) {
7777
$this->rawSize = $this->data['unencrypted_size'];
7878
} else {
7979
$this->rawSize = $this->data['size'] ?? 0;
@@ -174,7 +174,7 @@ public function getSize($includeMounts = true) {
174174
if ($includeMounts) {
175175
$this->updateEntryFromSubMounts();
176176

177-
if ($this->isEncrypted() && isset($this->data['unencrypted_size']) && $this->data['unencrypted_size'] > 0) {
177+
if ($this->isEncrypted() && isset($this->data['unencrypted_size'])) {
178178
return $this->data['unencrypted_size'];
179179
} else {
180180
return isset($this->data['size']) ? 0 + $this->data['size'] : 0;
@@ -342,7 +342,7 @@ public function addSubEntry($data, $entryPath) {
342342
if (!$data) {
343343
return;
344344
}
345-
$hasUnencryptedSize = isset($data['unencrypted_size']) && $data['unencrypted_size'] > 0;
345+
$hasUnencryptedSize = !empty($data['encrypted']) && isset($data['unencrypted_size']);
346346
if ($hasUnencryptedSize) {
347347
$subSize = $data['unencrypted_size'];
348348
} else {

lib/private/Files/Storage/Wrapper/Encryption.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ protected function verifyUnencryptedSize(string $path, int $unencryptedSize): in
388388
if ($unencryptedSize < 0
389389
|| ($size > 0 && $unencryptedSize === $size)
390390
|| $unencryptedSize > $size
391+
|| ($unencryptedSize === 0 && $size > $this->util->getHeaderSize())
391392
) {
392393
// check if we already calculate the unencrypted size for the
393394
// given path to avoid recursions

lib/private/Files/Stream/Encryption.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class Encryption extends Wrapper {
2828
protected string $cache;
2929
protected ?int $size = null;
3030
protected int $position;
31-
protected ?int $unencryptedSize = null;
31+
protected int|float|null $unencryptedSize = null;
3232
protected int $headerSize;
3333
protected int $unencryptedBlockSize;
3434
protected array $header;

tests/lib/Files/Cache/CacheTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@ public function testCacheEntryGetters(): void {
150150
$this->assertEquals($entry->getUnencryptedSize(), 100);
151151
}
152152

153+
public function testGetUnencryptedSizeEncryptedZeroByte(): void {
154+
$file1 = 'encrypted_zero';
155+
$this->cache->put($file1, ['size' => 8192, 'mtime' => 50, 'mimetype' => 'application/octet-stream', 'encrypted' => 1, 'unencrypted_size' => 0]);
156+
$entry = $this->cache->get($file1);
157+
158+
// getUnencryptedSize() must return 0 (the true plaintext size), not 8192 (the encrypted on-disk size)
159+
$this->assertEquals(0, $entry->getUnencryptedSize());
160+
$this->assertTrue($entry->isEncrypted());
161+
}
162+
153163
public function testPartial(): void {
154164
$file1 = 'foo';
155165

@@ -289,6 +299,39 @@ public function testEncryptedFolder(): void {
289299
$this->assertFalse($this->cache->inCache('folder/bar'));
290300
}
291301

302+
public function testCalculateFolderSizeWithEncryptedZeroByte(): void {
303+
$folder = 'enc_folder';
304+
$this->cache->put($folder, ['size' => -1, 'mtime' => 20, 'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE]);
305+
306+
// Child 1: zero-byte encrypted file — on-disk 8192 (header only), plaintext 0
307+
$child1 = $folder . '/empty.enc';
308+
$this->cache->put($child1, [
309+
'size' => 8192,
310+
'mtime' => 20,
311+
'mimetype' => 'application/octet-stream',
312+
'encrypted' => 1,
313+
'unencrypted_size' => 0,
314+
]);
315+
316+
// Child 2: non-zero encrypted file — opens the write-back gate ($unencryptedMax > 0)
317+
$child2 = $folder . '/small.enc';
318+
$this->cache->put($child2, [
319+
'size' => 8292,
320+
'mtime' => 20,
321+
'mimetype' => 'application/octet-stream',
322+
'encrypted' => 1,
323+
'unencrypted_size' => 100,
324+
]);
325+
326+
$this->cache->calculateFolderSize($folder);
327+
328+
$entry = $this->cache->get($folder);
329+
// Must sum plaintext sizes (0 + 100 = 100), not fall back to on-disk size for
330+
// the zero-byte child (8192 + 100 = 8292 with the old buggy code)
331+
$this->assertEquals(100, $entry['unencrypted_size'], 'Folder unencrypted_size should sum plaintext sizes');
332+
$this->assertEquals(16484, $entry['size'], 'Folder size should sum on-disk sizes (8192 + 8292)');
333+
}
334+
292335
public function testRootFolderSizeForNonHomeStorage(): void {
293336
$dir1 = 'knownsize';
294337
$dir2 = 'unknownsize';

tests/lib/Files/FileInfoTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,30 @@ protected function setUp(): void {
2828
$this->config = $this->getMockBuilder(IConfig::class)->getMock();
2929
}
3030

31+
private function makeFileInfo(array $data): FileInfo {
32+
$storage = new Temporary();
33+
return new FileInfo('', $storage, '', $data, new MountPoint($storage, '/foo/files'));
34+
}
35+
36+
public function testGetSizeEncryptedZeroByte(): void {
37+
$info = $this->makeFileInfo(['encrypted' => true, 'size' => 8192, 'unencrypted_size' => 0]);
38+
// Both paths must report the true plaintext size (0), not the on-disk encrypted size (8192)
39+
$this->assertSame(0, $info->getSize(true));
40+
$this->assertSame(0, $info->getSize(false));
41+
}
42+
43+
public function testGetSizeEncryptedNonZero(): void {
44+
$info = $this->makeFileInfo(['encrypted' => true, 'size' => 16384, 'unencrypted_size' => 5000]);
45+
$this->assertSame(5000, $info->getSize(true));
46+
$this->assertSame(5000, $info->getSize(false));
47+
}
48+
49+
public function testGetSizeNonEncrypted(): void {
50+
$info = $this->makeFileInfo(['encrypted' => false, 'size' => 100]);
51+
$this->assertSame(100, $info->getSize(true));
52+
$this->assertSame(100, $info->getSize(false));
53+
}
54+
3155
public function testIsMountedHomeStorage(): void {
3256
$user = $this->createMock(IUser::class);
3357
$user->method('getUID')

tests/lib/Files/Storage/Wrapper/EncryptionTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,11 @@ public static function dataTestVerifyUnencryptedSize(): array {
386386
[120, 80, false, 80],
387387
[120, 120, false, 80],
388388
[120, -1, false, 80],
389-
[120, -1, true, -1]
389+
[120, -1, true, -1],
390+
// Zero-byte encrypted file: on-disk size equals header only (8192) — should NOT recalculate
391+
[8192, 0, false, 0],
392+
// Encrypted file with content beyond header but unencrypted_size=0 — SHOULD recalculate
393+
[16384, 0, false, 80],
390394
];
391395
}
392396

0 commit comments

Comments
 (0)