Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/testing/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
</types>

<commands>
<command>OCA\Testing\Command\SetUnencryptedSize</command>
<command>OCA\Testing\Command\StaticHunt</command>
</commands>

Expand Down
1 change: 1 addition & 0 deletions apps/testing/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'OCA\\Testing\\AlternativeHomeUserBackend' => $baseDir . '/../lib/AlternativeHomeUserBackend.php',
'OCA\\Testing\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
'OCA\\Testing\\Command\\SetUnencryptedSize' => $baseDir . '/../lib/Command/SetUnencryptedSize.php',
'OCA\\Testing\\Command\\StaticHunt' => $baseDir . '/../lib/Command/StaticHunt.php',
'OCA\\Testing\\Controller\\ConfigController' => $baseDir . '/../lib/Controller/ConfigController.php',
'OCA\\Testing\\Controller\\LockingController' => $baseDir . '/../lib/Controller/LockingController.php',
Expand Down
1 change: 1 addition & 0 deletions apps/testing/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ComposerStaticInitTesting
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'OCA\\Testing\\AlternativeHomeUserBackend' => __DIR__ . '/..' . '/../lib/AlternativeHomeUserBackend.php',
'OCA\\Testing\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
'OCA\\Testing\\Command\\SetUnencryptedSize' => __DIR__ . '/..' . '/../lib/Command/SetUnencryptedSize.php',
'OCA\\Testing\\Command\\StaticHunt' => __DIR__ . '/..' . '/../lib/Command/StaticHunt.php',
'OCA\\Testing\\Controller\\ConfigController' => __DIR__ . '/..' . '/../lib/Controller/ConfigController.php',
'OCA\\Testing\\Controller\\LockingController' => __DIR__ . '/..' . '/../lib/Controller/LockingController.php',
Expand Down
51 changes: 51 additions & 0 deletions apps/testing/lib/Command/SetUnencryptedSize.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Testing\Command;

use OCP\Files\IRootFolder;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
* Overwrite the unencrypted size stored in the file cache, e.g. to simulate
* file cache entries written by older versions.
*/
class SetUnencryptedSize extends Command {
public function __construct(
private IRootFolder $rootFolder,
) {
parent::__construct();
}

#[\Override]
protected function configure(): void {
$this
->setName('testing:set-unencrypted-size')
->setDescription('Overwrite the unencrypted size of a file in the file cache')
->addArgument('user', InputArgument::REQUIRED, 'Owner of the file')
->addArgument('path', InputArgument::REQUIRED, 'Path of the file relative to the home of the user')
->addArgument('size', InputArgument::REQUIRED, 'Unencrypted size to store');
}

#[\Override]
protected function execute(InputInterface $input, OutputInterface $output): int {
$node = $this->rootFolder
->getUserFolder($input->getArgument('user'))
->get($input->getArgument('path'));

$node->getStorage()->getCache()->update($node->getId(), [
'unencrypted_size' => (int)$input->getArgument('size'),
]);

return self::SUCCESS;
}
}
19 changes: 19 additions & 0 deletions build/integration/encryption_features/encryption.feature
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,25 @@ Feature: encryption
Then the HTTP status code should be "200"
And Downloaded content should be "the second content"

# Files written by older versions use the base64 encoding and may have no
# unencrypted size in the file cache, so it has to be recalculated from the
# blocks of the file.
Scenario: Download a base64 encoded file with an unknown unencrypted size
Given invoking occ with "config:system:set encryption.use_legacy_base64_encoding --value true --type boolean"
And the command was successful
And user "user0" exists
And As an "user0"
And User "user0" adds a file of 20000 bytes to "/legacy.bin"
And invoking occ with "config:system:delete encryption.use_legacy_base64_encoding"
And the command was successful
And invoking occ with "app:enable testing"
And the command was successful
And invoking occ with "testing:set-unencrypted-size user0 /legacy.bin 0"
And the command was successful
When Downloading file "/legacy.bin"
Then the HTTP status code should be "200"
And File "/legacy.bin" should have prop "d:getcontentlength" equal to "20000"

# With "part_file_in_storage" disabled the part file is written to the user
# home while the target lives on another storage, so the upload has to read the
# part file back to move it over. A part file never has a file cache entry, so
Expand Down
3 changes: 2 additions & 1 deletion lib/private/Files/Storage/Wrapper/Encryption.php
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@ protected function fixUnencryptedSize(string $path, int $size, int $unencryptedS
return 0;
}

// the block size depends on the encoding the module reads from the header
$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
$signed = isset($header['signed']) && $header['signed'] === 'true';
$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);

Expand Down Expand Up @@ -476,7 +478,6 @@ protected function fixUnencryptedSize(string $path, int $size, int $unencryptedS
fclose($stream);

// we have to decrypt the last chunk to get it actual size
$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');

Expand Down
62 changes: 62 additions & 0 deletions tests/lib/Files/Storage/Wrapper/EncryptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,68 @@ public static function dataTestVerifyUnencryptedSize(): array {
];
}

/**
* The unencrypted block size depends on the encoding stored in the file header,
* which the encryption module only knows once begin() has read the header.
*/
public function testFixUnencryptedSizeUsesBlockSizeOfFileEncoding(): void {
$begun = false;
$module = $this->createMock(IEncryptionModule::class);
$module->method('begin')->willReturnCallback(function () use (&$begun) {
$begun = true;
return [];
});
$module->method('getUnencryptedBlockSize')->willReturnCallback(function (bool $signed) use (&$begun) {
return $begun ? 6072 : 8096;
});
$module->method('decrypt')->willReturn(str_repeat('a', 42));
$module->method('end')->willReturn('');

$header = ['cipher' => 'AES-256-CTR', 'signed' => 'true', 'oc_encryption_module' => 'UNIT_TEST_MODULE'];
$content = str_pad(Util::HEADER_START, $this->headerSize, '-') . str_repeat('x', 2 * 8192 + 100);
$stream = fopen('php://memory', 'r+');
fwrite($stream, $content);
rewind($stream);
$cache = $this->createMock(ICache::class);
$cache->method('get')->with('legacy.txt')->willReturn(['fileid' => 42]);
$sourceStorage = $this->createMock(\OC\Files\Storage\Storage::class);
$sourceStorage->method('fopen')->with('legacy.txt', 'r')->willReturn($stream);
$sourceStorage->method('getCache')->willReturn($cache);

$expected = 2 * 6072 + 42;
$cache->expects($this->once())->method('update')->with(42, ['unencrypted_size' => $expected]);

$instance = $this->getMockBuilder(Encryption::class)
->setConstructorArgs(
[
[
'storage' => $sourceStorage,
'root' => 'foo',
'mountPoint' => '/',
'mount' => $this->mount
],
$this->encryptionManager,
$this->util,
$this->logger,
$this->file,
null,
$this->keyStore,
$this->mountManager,
$this->arrayCache,
]
)
->onlyMethods(['getHeader', 'getHeaderSize', 'getEncryptionModule'])
->getMock();
$instance->method('getHeader')->willReturn($header);
$instance->method('getHeaderSize')->willReturn($this->headerSize);
$instance->method('getEncryptionModule')->willReturn($module);

$this->assertEquals(
$expected,
$this->invokePrivate($instance, 'fixUnencryptedSize', ['legacy.txt', strlen($content), 0])
);
}

/**
*
* @param string $source
Expand Down
Loading