From a96fe1d9aba36dca263979d5116548f4f01b8339 Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 26 Nov 2025 14:05:29 -0500 Subject: [PATCH] feat(encryption): improve encryption:encrypt-all validation, safety & logging - Add stronger pre-run validation and safer execution for the encryption:encrypt-all command: - more internal state checks before proceeding - require explicit "yes" (not just "yANYTHING" confirmation to proceed due to sensitivity of action - better guarantee restoration of state (maintenance/trashbin) via finally block - explicitly log key failures - improve user-facing help/warnings/guidance Signed-off-by: Josh --- core/Command/Encryption/EncryptAll.php | 152 ++++++++++++++++--------- 1 file changed, 97 insertions(+), 55 deletions(-) diff --git a/core/Command/Encryption/EncryptAll.php b/core/Command/Encryption/EncryptAll.php index f2c991471b6..34c4f791f79 100644 --- a/core/Command/Encryption/EncryptAll.php +++ b/core/Command/Encryption/EncryptAll.php @@ -10,6 +10,7 @@ namespace OC\Core\Command\Encryption; use OCP\App\IAppManager; use OCP\Encryption\IManager; use OCP\IConfig; +use Psr\Log\LoggerInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; @@ -24,17 +25,113 @@ class EncryptAll extends Command { protected IAppManager $appManager, protected IConfig $config, protected QuestionHelper $questionHelper, + private LoggerInterface $logger, ) { parent::__construct(); } + protected function configure(): void { + parent::configure(); + + $this + ->setName('encryption:encrypt-all') + ->setDescription('Encrypt all users\' files using Nextcloud Server Side Encryption') + ->setHelp( + "This will encrypt all files server-side for all users.\n" . + "Maintenance mode will be enabled automatically.\n" . + "Users should not access their files during this process!\n" . + "WARNING: Please read the documentation prior to utilizing this feature to avoid data loss!" + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + + // Handle container environment TTY problems to avoid confusion + if (!$input->isInteractive()) { + $output->writeln('Invalid TTY.'); + $output->writeln("If running inside Docker, use 'docker exec -it' or equivalent."); + $output->writeln(''); + return self::FAILURE; + } + + // Prevent running if SSE isn't enabled + if ($this->encryptionManager->isEnabled() === false) { + $output->writeln('Server Side Encryption is not enabled; unable to encrypt files.'); + return self::FAILURE; + } + + // Prevent running if no valid SSE modules are registered + $modules = $this->encryptionManager->getEncryptionModules(); + if (empty($modules)) { + $output->writeln('No Server Side Encryption modules are registered; unable to encrypt files.'); + return self::FAILURE; + } + + // Prevent running if a default SSE module isn't already configured + $defaultModuleId = $this->encryptionManager->getDefaultEncryptionModuleId(); + if ($defaultModuleId === '') { + $output->writeln('A default Server Side Encryption module is not configured; unable to encrypt files.'); + return self::FAILURE; + } + + // Prevent running if the default SSE module isn't valid + if (!isset($modules[$defaultModuleId])) { + $output->writeln('The current default Server Side Encryption module does not exist: ' . $defaultModuleId . '; unable to encrypt files.'); + return self::FAILURE; + } + + // Prevent running if maintenance mode is already enabled + if ($this->config->getSystemValueBool('maintenance')) { + $output->writeln('This command cannot be run with maintenance mode enabled.'); + return self::FAILURE; + } + + // TODO: Might make sense to add some additional readiness checks here such as the readiness of key storage/etc + + $output->writeln("\n"); + $output->writeln('You are about to encrypt all files stored in your Nextcloud installation.'); + $output->writeln('Depending on the number and size of files, this may take a long time.'); + $output->writeln('Please ensure that no user accesses their files during this process!'); + $output->writeln('Note: The encryption module you use and its settings determine which files get encrypted.'); + $output->writeln('Reminder: If External Storage is included in encryption, those files will no longer be accessible outside Nextcloud.'); + $output->writeln('WARNING: Please read the documentation prior to utilizing this feature to avoid data loss!'); + $output->writeln(''); + + // require "yes" to be typed in fully since this is a sensitive action + $question = new ConfirmationQuestion('Do you really want to continue? (yes/NO) ', false, '/^yes$/i'); + if (!$this->questionHelper->ask($input, $output, $question)) { + $output->writeln("\n" . 'Aborted.'); + return self::FAILURE; + } + + // Requirements before proceeding: disable trash bin, enable maintenance mode + $this->forceMaintenanceAndTrashbin(); + + // Encrypt all the files + try { + $defaultModule = $this->encryptionManager->getEncryptionModule(); + $defaultModule->encryptAll($input, $output); + } catch (\Throwable $ex) { + $output->writeln('Encryption failed: ' . $ex->getMessage() . ''); + $this->logger->error('encryption:encrypt-all failed', ['exception' => $ex]); + return self::FAILURE; + } finally { + // restore state no matter what (XXX: Better coverage than prior behavior; though I'm not convinced either is ideal) + $this->resetMaintenanceAndTrashbin(); + } + // If we made it here, we're good + return self::SUCCESS; + } + /** * Set maintenance mode and disable the trashbin app + * TODO: The "why?" should be noted here. */ protected function forceMaintenanceAndTrashbin(): void { $this->wasTrashbinEnabled = (bool)$this->appManager->isEnabledForUser('files_trashbin'); $this->config->setSystemValue('maintenance', true); $this->appManager->disableApp('files_trashbin'); + // TODO: Determine whether files_versions should be disabled temporarily too } /** @@ -46,59 +143,4 @@ class EncryptAll extends Command { $this->appManager->enableApp('files_trashbin'); } } - - protected function configure() { - parent::configure(); - - $this->setName('encryption:encrypt-all'); - $this->setDescription('Encrypt all files for all users'); - $this->setHelp( - 'This will encrypt all files for all users. ' - . 'Please make sure that no user access his files during this process!' - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - if (!$input->isInteractive()) { - $output->writeln('Invalid TTY.'); - $output->writeln('If you are trying to execute the command in a Docker '); - $output->writeln("container, do not forget to execute 'docker exec' with"); - $output->writeln("the '-i' and '-t' options."); - $output->writeln(''); - return 1; - } - - if ($this->encryptionManager->isEnabled() === false) { - throw new \Exception('Server side encryption is not enabled'); - } - - if ($this->config->getSystemValueBool('maintenance')) { - $output->writeln('This command cannot be run with maintenance mode enabled.'); - return self::FAILURE; - } - - $output->writeln("\n"); - $output->writeln('You are about to encrypt all files stored in your Nextcloud installation.'); - $output->writeln('Depending on the number of available files, and their size, this may take quite some time.'); - $output->writeln('Please ensure that no user accesses their files during this time!'); - $output->writeln('Note: The encryption module you use determines which files get encrypted.'); - $output->writeln(''); - $question = new ConfirmationQuestion('Do you really want to continue? (y/n) ', false); - if ($this->questionHelper->ask($input, $output, $question)) { - $this->forceMaintenanceAndTrashbin(); - - try { - $defaultModule = $this->encryptionManager->getEncryptionModule(); - $defaultModule->encryptAll($input, $output); - } catch (\Exception $ex) { - $this->resetMaintenanceAndTrashbin(); - throw $ex; - } - - $this->resetMaintenanceAndTrashbin(); - return self::SUCCESS; - } - $output->writeln('aborted'); - return self::FAILURE; - } }