From fd6c5a134ffe63c1f880723a6b85ab1c124e2eec Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Wed, 11 Aug 2021 09:18:16 +0200 Subject: [PATCH 1/2] Fix encrypted version to 0 when finding unencrypted file Whenever the command is run and a "legacy cipher" seems to be detected when the legacy option is disabled, it's highly likely that the file is actually unencrypted but the database contains a encrypted version higher than 0 for some reason. The command now detects this case and automatically sets the encrypted version to 0 so that the file can be read again. Signed-off-by: Vincent Petry --- .../lib/Command/FixEncryptedVersion.php | 29 +++++++++++++- .../tests/Command/FixEncryptedVersionTest.php | 40 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/apps/encryption/lib/Command/FixEncryptedVersion.php b/apps/encryption/lib/Command/FixEncryptedVersion.php index a85a96258fcf4..e12d733d2b927 100644 --- a/apps/encryption/lib/Command/FixEncryptedVersion.php +++ b/apps/encryption/lib/Command/FixEncryptedVersion.php @@ -24,6 +24,7 @@ use OC\Files\View; use OC\HintException; +use OC\ServerNotAvailableException; use OCA\Encryption\Util; use OCP\Files\IRootFolder; use OCP\IConfig; @@ -53,6 +54,9 @@ class FixEncryptedVersion extends Command { /** @var View */ private $view; + /** @var bool */ + private $supportLegacy; + public function __construct( IConfig $config, ILogger $logger, @@ -67,6 +71,8 @@ public function __construct( $this->userManager = $userManager; $this->util = $util; $this->view = $view; + $this->supportLegacy = false; + parent::__construct(); } @@ -95,6 +101,7 @@ protected function configure(): void { */ protected function execute(InputInterface $input, OutputInterface $output): int { $skipSignatureCheck = $this->config->getSystemValue('encryption_skip_signature_check', false); + $this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false); if ($skipSignatureCheck) { $output->writeln("Repairing is not possible when \"encryption_skip_signature_check\" is set. Please disable this flag in the configuration.\n"); @@ -187,6 +194,14 @@ private function verifyFileContent($path, OutputInterface $output, $ignoreCorrec \fclose($handle); return true; + } catch (ServerNotAvailableException $e) { + // not a "bad signature" error and likely "legacy cipher" exception + // this could mean that the file is maybe not encrypted but the encrypted version is set + if (!$this->supportLegacy && $ignoreCorrectEncVersionCall === true) { + $output->writeln("Attempting to fix the path: \"$path\""); + return $this->correctEncryptedVersion($path, $output, true); + } + return false; } catch (HintException $e) { $this->logger->warning("Issue: " . $e->getMessage()); //If allowOnce is set to false, this becomes recursive. @@ -202,9 +217,10 @@ private function verifyFileContent($path, OutputInterface $output, $ignoreCorrec /** * @param string $path * @param OutputInterface $output + * @param bool $includeZero whether to try zero version for unencrypted file * @return bool */ - private function correctEncryptedVersion($path, OutputInterface $output): bool { + private function correctEncryptedVersion($path, OutputInterface $output, bool $includeZero = false): bool { $fileInfo = $this->view->getFileInfo($path); if (!$fileInfo) { $output->writeln("File info not found for file: \"$path\""); @@ -231,6 +247,17 @@ private function correctEncryptedVersion($path, OutputInterface $output): bool { // Save original encrypted version so we can restore it if decryption fails with all version $originalEncryptedVersion = $encryptedVersion; if ($encryptedVersion >= 0) { + if ($includeZero) { + // try with zero first + $cacheInfo = ['encryptedVersion' => 0, 'encrypted' => 0]; + $cache->put($fileCache->getPath(), $cacheInfo); + $output->writeln("Set the encrypted version to 0 (unencrypted)"); + if ($this->verifyFileContent($path, $output, false) === true) { + $output->writeln("Fixed the file: \"$path\" with version 0 (unencrypted)"); + return true; + } + } + //test by decrementing the value till 1 and if nothing works try incrementing $encryptedVersion--; while ($encryptedVersion > 0) { diff --git a/apps/encryption/tests/Command/FixEncryptedVersionTest.php b/apps/encryption/tests/Command/FixEncryptedVersionTest.php index 22ae239aec23d..14143223264dd 100644 --- a/apps/encryption/tests/Command/FixEncryptedVersionTest.php +++ b/apps/encryption/tests/Command/FixEncryptedVersionTest.php @@ -244,6 +244,46 @@ public function testVersionIsRestoredToOriginalIfNoFixIsFound() { $this->assertEquals(15, $encryptedVersion); } + public function testRepairUnencryptedFileWhenVersionIsSet() { + $this->util->expects($this->once())->method('isMasterKeyEnabled') + ->willReturn(true); + + $view = new View("/" . $this->userId . "/files"); + + // create a file, it's encrypted and also the version is set in the database + $view->touch('hello.txt'); + + $fileInfo1 = $view->getFileInfo('hello.txt'); + + $storage1 = $fileInfo1->getStorage(); + $cache1 = $storage1->getCache(); + $fileCache1 = $cache1->get($fileInfo1->getId()); + + // Now change the encrypted version + $cacheInfo = ['encryptedVersion' => 1, 'encrypted' => 1]; + $cache1->put($fileCache1->getPath(), $cacheInfo); + + $absPath = $view->getLocalFolder(''). '/hello.txt'; + + // create unencrypted file on disk, the version stays + file_put_contents($absPath, 'hello contents'); + + $this->commandTester->execute([ + 'user' => $this->userId + ]); + + $output = $this->commandTester->getDisplay(); + + $this->assertStringContainsString("Verifying the content of file \"/$this->userId/files/hello.txt\" +Attempting to fix the path: \"/$this->userId/files/hello.txt\" +Set the encrypted version to 0 (unencrypted) +The file \"/$this->userId/files/hello.txt\" is: OK +Fixed the file: \"/$this->userId/files/hello.txt\" with version 0 (unencrypted)", $output); + + // the file can be decrypted + $this->assertEquals('hello contents', $view->file_get_contents('hello.txt')); + } + /** * Test commands with a file path */ From 2032ca7457c8bce02148240c5086e6bb2c622aea Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Thu, 26 Aug 2021 10:23:03 +0200 Subject: [PATCH 2/2] Unregister enc stream wrapper for any exception This prevents side effects in tests by properly cleaning up even with expected exceptions. Signed-off-by: Vincent Petry --- lib/private/Files/Stream/Encryption.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/private/Files/Stream/Encryption.php b/lib/private/Files/Stream/Encryption.php index 16d2ca3ce97f6..fc75ead1e45b4 100644 --- a/lib/private/Files/Stream/Encryption.php +++ b/lib/private/Files/Stream/Encryption.php @@ -213,7 +213,7 @@ protected static function wrapSource($source, $context = [], $protocol = null, $ } else { $wrapped = fopen($protocol . '://', $mode, false, $context); } - } catch (\BadMethodCallException $e) { + } catch (\Exception $e) { stream_wrapper_unregister($protocol); throw $e; }