diff --git a/apps/testing/appinfo/info.xml b/apps/testing/appinfo/info.xml index 9b833778a52ea..ee19663cfef4c 100644 --- a/apps/testing/appinfo/info.xml +++ b/apps/testing/appinfo/info.xml @@ -18,6 +18,7 @@ + OCA\Testing\Command\SetUnencryptedSize OCA\Testing\Command\StaticHunt diff --git a/apps/testing/composer/composer/autoload_classmap.php b/apps/testing/composer/composer/autoload_classmap.php index ee7424a0ea809..7f2e8e4d68001 100644 --- a/apps/testing/composer/composer/autoload_classmap.php +++ b/apps/testing/composer/composer/autoload_classmap.php @@ -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', diff --git a/apps/testing/composer/composer/autoload_static.php b/apps/testing/composer/composer/autoload_static.php index 9ea8e6dc0a8f0..4f842897e7fa2 100644 --- a/apps/testing/composer/composer/autoload_static.php +++ b/apps/testing/composer/composer/autoload_static.php @@ -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', diff --git a/apps/testing/lib/Command/SetUnencryptedSize.php b/apps/testing/lib/Command/SetUnencryptedSize.php new file mode 100644 index 0000000000000..d2bb1f10e7434 --- /dev/null +++ b/apps/testing/lib/Command/SetUnencryptedSize.php @@ -0,0 +1,51 @@ +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; + } +} diff --git a/build/integration/encryption_features/encryption.feature b/build/integration/encryption_features/encryption.feature index fed221e1a72ad..cb173726162d5 100644 --- a/build/integration/encryption_features/encryption.feature +++ b/build/integration/encryption_features/encryption.feature @@ -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 diff --git a/lib/private/Files/Storage/Wrapper/Encryption.php b/lib/private/Files/Storage/Wrapper/Encryption.php index 04ae28be70f97..748b81f482230 100644 --- a/lib/private/Files/Storage/Wrapper/Encryption.php +++ b/lib/private/Files/Storage/Wrapper/Encryption.php @@ -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); @@ -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'); diff --git a/tests/lib/Files/Storage/Wrapper/EncryptionTest.php b/tests/lib/Files/Storage/Wrapper/EncryptionTest.php index e6adab2ec119d..4775242fd88f3 100644 --- a/tests/lib/Files/Storage/Wrapper/EncryptionTest.php +++ b/tests/lib/Files/Storage/Wrapper/EncryptionTest.php @@ -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