diff options
Diffstat (limited to 'apps/encryption/lib/Command/FixEncryptedVersion.php')
-rw-r--r-- | apps/encryption/lib/Command/FixEncryptedVersion.php | 204 |
1 files changed, 105 insertions, 99 deletions
diff --git a/apps/encryption/lib/Command/FixEncryptedVersion.php b/apps/encryption/lib/Command/FixEncryptedVersion.php index 073c1f1438a..462e3a5cc2a 100644 --- a/apps/encryption/lib/Command/FixEncryptedVersion.php +++ b/apps/encryption/lib/Command/FixEncryptedVersion.php @@ -1,78 +1,41 @@ <?php + /** - * @author Sujith Haridasan <sharidasan@owncloud.com> - * @author Ilja Neumann <ineumann@owncloud.com> - * - * @copyright Copyright (c) 2019, ownCloud GmbH - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2019 ownCloud GmbH + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Encryption\Command; +use OC\Files\Storage\Wrapper\Encryption; use OC\Files\View; use OC\ServerNotAvailableException; use OCA\Encryption\Util; +use OCP\Encryption\Exceptions\InvalidHeaderException; use OCP\Files\IRootFolder; use OCP\HintException; use OCP\IConfig; -use OCP\ILogger; +use OCP\IUser; use OCP\IUserManager; +use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class FixEncryptedVersion extends Command { - /** @var IConfig */ - private $config; - - /** @var ILogger */ - private $logger; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var IUserManager */ - private $userManager; - - /** @var Util */ - private $util; - - /** @var View */ - private $view; - - /** @var bool */ - private $supportLegacy; + private bool $supportLegacy = false; public function __construct( - IConfig $config, - ILogger $logger, - IRootFolder $rootFolder, - IUserManager $userManager, - Util $util, - View $view + private IConfig $config, + private LoggerInterface $logger, + private IRootFolder $rootFolder, + private IUserManager $userManager, + private Util $util, + private View $view, ) { - $this->config = $config; - $this->logger = $logger; - $this->rootFolder = $rootFolder; - $this->userManager = $userManager; - $this->util = $util; - $this->view = $view; - $this->supportLegacy = false; - parent::__construct(); } @@ -84,72 +47,89 @@ class FixEncryptedVersion extends Command { ->setDescription('Fix the encrypted version if the encrypted file(s) are not downloadable.') ->addArgument( 'user', - InputArgument::REQUIRED, + InputArgument::OPTIONAL, 'The id of the user whose files need fixing' )->addOption( 'path', 'p', - InputArgument::OPTIONAL, + InputOption::VALUE_REQUIRED, 'Limit files to fix with path, e.g., --path="/Music/Artist". If path indicates a directory, all the files inside directory will be fixed.' + )->addOption( + 'all', + null, + InputOption::VALUE_NONE, + 'Run the fix for all users on the system, mutually exclusive with specifying a user id.' ); } - /** - * @param InputInterface $input - * @param OutputInterface $output - * @return int - */ protected function execute(InputInterface $input, OutputInterface $output): int { - $skipSignatureCheck = $this->config->getSystemValue('encryption_skip_signature_check', false); + $skipSignatureCheck = $this->config->getSystemValueBool('encryption_skip_signature_check', false); $this->supportLegacy = $this->config->getSystemValueBool('encryption.legacy_format_support', false); if ($skipSignatureCheck) { $output->writeln("<error>Repairing is not possible when \"encryption_skip_signature_check\" is set. Please disable this flag in the configuration.</error>\n"); - return 1; + return self::FAILURE; } if (!$this->util->isMasterKeyEnabled()) { $output->writeln("<error>Repairing only works with master key encryption.</error>\n"); - return 1; + return self::FAILURE; } - $user = (string)$input->getArgument('user'); - $pathToWalk = "/$user/files"; - + $user = $input->getArgument('user'); + $all = $input->getOption('all'); $pathOption = \trim(($input->getOption('path') ?? ''), '/'); - if ($pathOption !== "") { - $pathToWalk = "$pathToWalk/$pathOption"; + + if (!$user && !$all) { + $output->writeln('Either a user id or --all needs to be provided'); + return self::FAILURE; } - if ($user === null) { - $output->writeln("<error>No user id provided.</error>\n"); - return 1; + if ($user) { + if ($all) { + $output->writeln('Specifying a user id and --all are mutually exclusive'); + return self::FAILURE; + } + + if ($this->userManager->get($user) === null) { + $output->writeln("<error>User id $user does not exist. Please provide a valid user id</error>"); + return self::FAILURE; + } + + return $this->runForUser($user, $pathOption, $output); } - if ($this->userManager->get($user) === null) { - $output->writeln("<error>User id $user does not exist. Please provide a valid user id</error>"); - return 1; + $result = 0; + $this->userManager->callForSeenUsers(function (IUser $user) use ($pathOption, $output, &$result) { + $output->writeln('Processing files for ' . $user->getUID()); + $result = $this->runForUser($user->getUID(), $pathOption, $output); + return $result === 0; + }); + return $result; + } + + private function runForUser(string $user, string $pathOption, OutputInterface $output): int { + $pathToWalk = "/$user/files"; + if ($pathOption !== '') { + $pathToWalk = "$pathToWalk/$pathOption"; } return $this->walkPathOfUser($user, $pathToWalk, $output); } /** - * @param string $user - * @param string $path - * @param OutputInterface $output * @return int 0 for success, 1 for error */ - private function walkPathOfUser($user, $path, OutputInterface $output): int { + private function walkPathOfUser(string $user, string $path, OutputInterface $output): int { $this->setupUserFs($user); if (!$this->view->file_exists($path)) { $output->writeln("<error>Path \"$path\" does not exist. Please provide a valid path.</error>"); - return 1; + return self::FAILURE; } if ($this->view->is_file($path)) { $output->writeln("Verifying the content of file \"$path\""); $this->verifyFileContent($path, $output); - return 0; + return self::SUCCESS; } $directories = []; $directories[] = $path; @@ -165,16 +145,21 @@ class FixEncryptedVersion extends Command { } } } - return 0; + return self::SUCCESS; } /** - * @param string $path - * @param OutputInterface $output * @param bool $ignoreCorrectEncVersionCall, setting this variable to false avoids recursion */ - private function verifyFileContent($path, OutputInterface $output, $ignoreCorrectEncVersionCall = true): bool { + private function verifyFileContent(string $path, OutputInterface $output, bool $ignoreCorrectEncVersionCall = true): bool { try { + // since we're manually poking around the encrypted state we need to ensure that this isn't cached in the encryption wrapper + $mount = $this->view->getMount($path); + $storage = $mount->getStorage(); + if ($storage && $storage->instanceOfStorage(Encryption::class)) { + $storage->clearIsEncryptedCache(); + } + /** * In encryption, the files are read in a block size of 8192 bytes * Read block size of 8192 and a bit more (808 bytes) @@ -184,14 +169,35 @@ class FixEncryptedVersion extends Command { */ $handle = $this->view->fopen($path, 'rb'); + if ($handle === false) { + $output->writeln("<warning>Failed to open file: \"$path\" skipping</warning>"); + return true; + } + if (\fread($handle, 9001) !== false) { + $fileInfo = $this->view->getFileInfo($path); + if (!$fileInfo) { + $output->writeln("<warning>File info not found for file: \"$path\"</warning>"); + return true; + } + $encryptedVersion = $fileInfo->getEncryptedVersion(); + $stat = $this->view->stat($path); + if (($encryptedVersion == 0) && isset($stat['hasHeader']) && ($stat['hasHeader'] == true)) { + // The file has encrypted to false but has an encryption header + if ($ignoreCorrectEncVersionCall === true) { + // Lets rectify the file by correcting encrypted version + $output->writeln("<info>Attempting to fix the path: \"$path\"</info>"); + return $this->correctEncryptedVersion($path, $output); + } + return false; + } $output->writeln("<info>The file \"$path\" is: OK</info>"); } \fclose($handle); return true; - } catch (ServerNotAvailableException $e) { + } catch (ServerNotAvailableException|InvalidHeaderException $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) { @@ -200,10 +206,10 @@ class FixEncryptedVersion extends Command { } return false; } catch (HintException $e) { - $this->logger->warning("Issue: " . $e->getMessage()); - //If allowOnce is set to false, this becomes recursive. + $this->logger->warning('Issue: ' . $e->getMessage()); + // If allowOnce is set to false, this becomes recursive. if ($ignoreCorrectEncVersionCall === true) { - //Lets rectify the file by correcting encrypted version + // Lets rectify the file by correcting encrypted version $output->writeln("<info>Attempting to fix the path: \"$path\"</info>"); return $this->correctEncryptedVersion($path, $output); } @@ -212,18 +218,19 @@ class FixEncryptedVersion extends Command { } /** - * @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 $includeZero = false): bool { + private function correctEncryptedVersion(string $path, OutputInterface $output, bool $includeZero = false): bool { $fileInfo = $this->view->getFileInfo($path); if (!$fileInfo) { $output->writeln("<warning>File info not found for file: \"$path\"</warning>"); return true; } $fileId = $fileInfo->getId(); + if ($fileId === null) { + $output->writeln("<warning>File info contains no id for file: \"$path\"</warning>"); + return true; + } $encryptedVersion = $fileInfo->getEncryptedVersion(); $wrongEncryptedVersion = $encryptedVersion; @@ -248,27 +255,27 @@ class FixEncryptedVersion extends Command { // try with zero first $cacheInfo = ['encryptedVersion' => 0, 'encrypted' => 0]; $cache->put($fileCache->getPath(), $cacheInfo); - $output->writeln("<info>Set the encrypted version to 0 (unencrypted)</info>"); + $output->writeln('<info>Set the encrypted version to 0 (unencrypted)</info>'); if ($this->verifyFileContent($path, $output, false) === true) { $output->writeln("<info>Fixed the file: \"$path\" with version 0 (unencrypted)</info>"); return true; } } - //test by decrementing the value till 1 and if nothing works try incrementing + // Test by decrementing the value till 1 and if nothing works try incrementing $encryptedVersion--; while ($encryptedVersion > 0) { $cacheInfo = ['encryptedVersion' => $encryptedVersion, 'encrypted' => $encryptedVersion]; $cache->put($fileCache->getPath(), $cacheInfo); $output->writeln("<info>Decrement the encrypted version to $encryptedVersion</info>"); if ($this->verifyFileContent($path, $output, false) === true) { - $output->writeln("<info>Fixed the file: \"$path\" with version " . $encryptedVersion . "</info>"); + $output->writeln("<info>Fixed the file: \"$path\" with version " . $encryptedVersion . '</info>'); return true; } $encryptedVersion--; } - //So decrementing did not work. Now lets increment. Max increment is till 5 + // So decrementing did not work. Now lets increment. Max increment is till 5 $increment = 1; while ($increment <= 5) { /** @@ -285,7 +292,7 @@ class FixEncryptedVersion extends Command { $cache->put($fileCache->getPath(), $cacheInfo); $output->writeln("<info>Increment the encrypted version to $newEncryptedVersion</info>"); if ($this->verifyFileContent($path, $output, false) === true) { - $output->writeln("<info>Fixed the file: \"$path\" with version " . $newEncryptedVersion . "</info>"); + $output->writeln("<info>Fixed the file: \"$path\" with version " . $newEncryptedVersion . '</info>'); return true; } $increment++; @@ -301,9 +308,8 @@ class FixEncryptedVersion extends Command { /** * Setup user file system - * @param string $uid */ - private function setupUserFs($uid): void { + private function setupUserFs(string $uid): void { \OC_Util::tearDownFS(); \OC_Util::setupFS($uid); } |