diff options
Diffstat (limited to 'core/Command/Encryption')
-rw-r--r-- | core/Command/Encryption/ChangeKeyStorageRoot.php | 229 | ||||
-rw-r--r-- | core/Command/Encryption/DecryptAll.php | 142 | ||||
-rw-r--r-- | core/Command/Encryption/Disable.php | 38 | ||||
-rw-r--r-- | core/Command/Encryption/Enable.php | 58 | ||||
-rw-r--r-- | core/Command/Encryption/EncryptAll.php | 104 | ||||
-rw-r--r-- | core/Command/Encryption/ListModules.php | 71 | ||||
-rw-r--r-- | core/Command/Encryption/MigrateKeyStorage.php | 212 | ||||
-rw-r--r-- | core/Command/Encryption/SetDefaultModule.php | 59 | ||||
-rw-r--r-- | core/Command/Encryption/ShowKeyStorageRoot.php | 37 | ||||
-rw-r--r-- | core/Command/Encryption/Status.php | 38 |
10 files changed, 988 insertions, 0 deletions
diff --git a/core/Command/Encryption/ChangeKeyStorageRoot.php b/core/Command/Encryption/ChangeKeyStorageRoot.php new file mode 100644 index 00000000000..3049fd2ca08 --- /dev/null +++ b/core/Command/Encryption/ChangeKeyStorageRoot.php @@ -0,0 +1,229 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Core\Command\Encryption; + +use OC\Encryption\Keys\Storage; +use OC\Encryption\Util; +use OC\Files\Filesystem; +use OC\Files\View; +use OCP\IConfig; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; + +class ChangeKeyStorageRoot extends Command { + public function __construct( + protected View $rootView, + protected IUserManager $userManager, + protected IConfig $config, + protected Util $util, + protected QuestionHelper $questionHelper, + ) { + parent::__construct(); + } + + protected function configure() { + parent::configure(); + $this + ->setName('encryption:change-key-storage-root') + ->setDescription('Change key storage root') + ->addArgument( + 'newRoot', + InputArgument::OPTIONAL, + 'new root of the key storage relative to the data folder' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $oldRoot = $this->util->getKeyStorageRoot(); + $newRoot = $input->getArgument('newRoot'); + + if ($newRoot === null) { + $question = new ConfirmationQuestion('No storage root given, do you want to reset the key storage root to the default location? (y/n) ', false); + if (!$this->questionHelper->ask($input, $output, $question)) { + return 1; + } + $newRoot = ''; + } + + $oldRootDescription = $oldRoot !== '' ? $oldRoot : 'default storage location'; + $newRootDescription = $newRoot !== '' ? $newRoot : 'default storage location'; + $output->writeln("Change key storage root from <info>$oldRootDescription</info> to <info>$newRootDescription</info>"); + $success = $this->moveAllKeys($oldRoot, $newRoot, $output); + if ($success) { + $this->util->setKeyStorageRoot($newRoot); + $output->writeln(''); + $output->writeln("Key storage root successfully changed to <info>$newRootDescription</info>"); + return 0; + } + return 1; + } + + /** + * move keys to new key storage root + * + * @param string $oldRoot + * @param string $newRoot + * @param OutputInterface $output + * @return bool + * @throws \Exception + */ + protected function moveAllKeys($oldRoot, $newRoot, OutputInterface $output) { + $output->writeln('Start to move keys:'); + + if ($this->rootView->is_dir($oldRoot) === false) { + $output->writeln('No old keys found: Nothing needs to be moved'); + return false; + } + + $this->prepareNewRoot($newRoot); + $this->moveSystemKeys($oldRoot, $newRoot); + $this->moveUserKeys($oldRoot, $newRoot, $output); + + return true; + } + + /** + * prepare new key storage + * + * @param string $newRoot + * @throws \Exception + */ + protected function prepareNewRoot($newRoot) { + if ($this->rootView->is_dir($newRoot) === false) { + throw new \Exception("New root folder doesn't exist. Please create the folder or check the permissions and try again."); + } + + $result = $this->rootView->file_put_contents( + $newRoot . '/' . Storage::KEY_STORAGE_MARKER, + 'Nextcloud will detect this folder as key storage root only if this file exists' + ); + + if (!$result) { + throw new \Exception("Can't access the new root folder. Please check the permissions and make sure that the folder is in your data folder"); + } + } + + + /** + * move system key folder + * + * @param string $oldRoot + * @param string $newRoot + */ + protected function moveSystemKeys($oldRoot, $newRoot) { + if ( + $this->rootView->is_dir($oldRoot . '/files_encryption') + && $this->targetExists($newRoot . '/files_encryption') === false + ) { + $this->rootView->rename($oldRoot . '/files_encryption', $newRoot . '/files_encryption'); + } + } + + + /** + * setup file system for the given user + * + * @param string $uid + */ + protected function setupUserFS($uid) { + \OC_Util::tearDownFS(); + \OC_Util::setupFS($uid); + } + + + /** + * iterate over each user and move the keys to the new storage + * + * @param string $oldRoot + * @param string $newRoot + * @param OutputInterface $output + */ + protected function moveUserKeys($oldRoot, $newRoot, OutputInterface $output) { + $progress = new ProgressBar($output); + $progress->start(); + + + foreach ($this->userManager->getBackends() as $backend) { + $limit = 500; + $offset = 0; + do { + $users = $backend->getUsers('', $limit, $offset); + foreach ($users as $user) { + $progress->advance(); + $this->setupUserFS($user); + $this->moveUserEncryptionFolder($user, $oldRoot, $newRoot); + } + $offset += $limit; + } while (count($users) >= $limit); + } + $progress->finish(); + } + + /** + * move user encryption folder to new root folder + * + * @param string $user + * @param string $oldRoot + * @param string $newRoot + * @throws \Exception + */ + protected function moveUserEncryptionFolder($user, $oldRoot, $newRoot) { + if ($this->userManager->userExists($user)) { + $source = $oldRoot . '/' . $user . '/files_encryption'; + $target = $newRoot . '/' . $user . '/files_encryption'; + if ( + $this->rootView->is_dir($source) + && $this->targetExists($target) === false + ) { + $this->prepareParentFolder($newRoot . '/' . $user); + $this->rootView->rename($source, $target); + } + } + } + + /** + * Make preparations to filesystem for saving a key file + * + * @param string $path relative to data/ + */ + protected function prepareParentFolder($path) { + $path = Filesystem::normalizePath($path); + // If the file resides within a subdirectory, create it + if ($this->rootView->file_exists($path) === false) { + $sub_dirs = explode('/', ltrim($path, '/')); + $dir = ''; + foreach ($sub_dirs as $sub_dir) { + $dir .= '/' . $sub_dir; + if ($this->rootView->file_exists($dir) === false) { + $this->rootView->mkdir($dir); + } + } + } + } + + /** + * check if target already exists + * + * @param $path + * @return bool + * @throws \Exception + */ + protected function targetExists($path) { + if ($this->rootView->file_exists($path)) { + throw new \Exception("new folder '$path' already exists"); + } + + return false; + } +} diff --git a/core/Command/Encryption/DecryptAll.php b/core/Command/Encryption/DecryptAll.php new file mode 100644 index 00000000000..92e2ba787e2 --- /dev/null +++ b/core/Command/Encryption/DecryptAll.php @@ -0,0 +1,142 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Core\Command\Encryption; + +use OCP\App\IAppManager; +use OCP\Encryption\IManager; +use OCP\IConfig; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; + +class DecryptAll extends Command { + protected bool $wasTrashbinEnabled = false; + protected bool $wasMaintenanceModeEnabled = false; + + public function __construct( + protected IManager $encryptionManager, + protected IAppManager $appManager, + protected IConfig $config, + protected \OC\Encryption\DecryptAll $decryptAll, + protected QuestionHelper $questionHelper, + ) { + parent::__construct(); + } + + /** + * Set maintenance mode and disable the trashbin app + */ + protected function forceMaintenanceAndTrashbin(): void { + $this->wasTrashbinEnabled = $this->appManager->isEnabledForUser('files_trashbin'); + $this->wasMaintenanceModeEnabled = $this->config->getSystemValueBool('maintenance'); + $this->config->setSystemValue('maintenance', true); + $this->appManager->disableApp('files_trashbin'); + } + + /** + * Reset the maintenance mode and re-enable the trashbin app + */ + protected function resetMaintenanceAndTrashbin(): void { + $this->config->setSystemValue('maintenance', $this->wasMaintenanceModeEnabled); + if ($this->wasTrashbinEnabled) { + $this->appManager->enableApp('files_trashbin'); + } + } + + protected function configure() { + parent::configure(); + + $this->setName('encryption:decrypt-all'); + $this->setDescription('Disable server-side encryption and decrypt all files'); + $this->setHelp( + 'This will disable server-side encryption and decrypt all files for ' + . 'all users if it is supported by your encryption module. ' + . 'Please make sure that no user access his files during this process!' + ); + $this->addArgument( + 'user', + InputArgument::OPTIONAL, + 'user for which you want to decrypt all files (optional)', + '' + ); + } + + 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; + } + + $isMaintenanceModeEnabled = $this->config->getSystemValue('maintenance', false); + if ($isMaintenanceModeEnabled) { + $output->writeln('Maintenance mode must be disabled when starting decryption,'); + $output->writeln('in order to load the relevant encryption modules correctly.'); + $output->writeln('Your instance will automatically be put to maintenance mode'); + $output->writeln('during the actual decryption of the files.'); + return 1; + } + + try { + if ($this->encryptionManager->isEnabled() === true) { + $output->write('Disable server side encryption... '); + $this->config->setAppValue('core', 'encryption_enabled', 'no'); + $output->writeln('done.'); + } else { + $output->writeln('Server side encryption not enabled. Nothing to do.'); + return 0; + } + + $uid = $input->getArgument('user'); + if ($uid === '') { + $message = 'your Nextcloud'; + } else { + $message = "$uid's account"; + } + + $output->writeln("\n"); + $output->writeln("You are about to start to decrypt all files stored in $message."); + $output->writeln('It will depend on the encryption module and your setup if this is possible.'); + $output->writeln('Depending on the number and size of your files this can take some time'); + $output->writeln('Please make sure that no user access his files during this process!'); + $output->writeln(''); + $question = new ConfirmationQuestion('Do you really want to continue? (y/n) ', false); + if ($this->questionHelper->ask($input, $output, $question)) { + $this->forceMaintenanceAndTrashbin(); + $user = $input->getArgument('user'); + $result = $this->decryptAll->decryptAll($input, $output, $user); + if ($result === false) { + $output->writeln(' aborted.'); + $output->writeln('Server side encryption remains enabled'); + $this->config->setAppValue('core', 'encryption_enabled', 'yes'); + } elseif ($uid !== '') { + $output->writeln('Server side encryption remains enabled'); + $this->config->setAppValue('core', 'encryption_enabled', 'yes'); + } + $this->resetMaintenanceAndTrashbin(); + return 0; + } + $output->write('Enable server side encryption... '); + $this->config->setAppValue('core', 'encryption_enabled', 'yes'); + $output->writeln('done.'); + $output->writeln('aborted'); + return 1; + } catch (\Exception $e) { + // enable server side encryption again if something went wrong + $this->config->setAppValue('core', 'encryption_enabled', 'yes'); + $this->resetMaintenanceAndTrashbin(); + throw $e; + } + } +} diff --git a/core/Command/Encryption/Disable.php b/core/Command/Encryption/Disable.php new file mode 100644 index 00000000000..91d4ac82d23 --- /dev/null +++ b/core/Command/Encryption/Disable.php @@ -0,0 +1,38 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Core\Command\Encryption; + +use OCP\IConfig; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Disable extends Command { + public function __construct( + protected IConfig $config, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('encryption:disable') + ->setDescription('Disable encryption') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + if ($this->config->getAppValue('core', 'encryption_enabled', 'no') !== 'yes') { + $output->writeln('Encryption is already disabled'); + } else { + $this->config->setAppValue('core', 'encryption_enabled', 'no'); + $output->writeln('<info>Encryption disabled</info>'); + } + return 0; + } +} diff --git a/core/Command/Encryption/Enable.php b/core/Command/Encryption/Enable.php new file mode 100644 index 00000000000..2c476185692 --- /dev/null +++ b/core/Command/Encryption/Enable.php @@ -0,0 +1,58 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Core\Command\Encryption; + +use OCP\Encryption\IManager; +use OCP\IConfig; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Enable extends Command { + public function __construct( + protected IConfig $config, + protected IManager $encryptionManager, + ) { + parent::__construct(); + } + + protected function configure() { + $this + ->setName('encryption:enable') + ->setDescription('Enable encryption') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + if ($this->config->getAppValue('core', 'encryption_enabled', 'no') === 'yes') { + $output->writeln('Encryption is already enabled'); + } else { + $this->config->setAppValue('core', 'encryption_enabled', 'yes'); + $output->writeln('<info>Encryption enabled</info>'); + } + $output->writeln(''); + + $modules = $this->encryptionManager->getEncryptionModules(); + if (empty($modules)) { + $output->writeln('<error>No encryption module is loaded</error>'); + return 1; + } + $defaultModule = $this->config->getAppValue('core', 'default_encryption_module', null); + if ($defaultModule === null) { + $output->writeln('<error>No default module is set</error>'); + return 1; + } + if (!isset($modules[$defaultModule])) { + $output->writeln('<error>The current default module does not exist: ' . $defaultModule . '</error>'); + return 1; + } + $output->writeln('Default module: ' . $defaultModule); + + return 0; + } +} diff --git a/core/Command/Encryption/EncryptAll.php b/core/Command/Encryption/EncryptAll.php new file mode 100644 index 00000000000..f2c991471b6 --- /dev/null +++ b/core/Command/Encryption/EncryptAll.php @@ -0,0 +1,104 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Core\Command\Encryption; + +use OCP\App\IAppManager; +use OCP\Encryption\IManager; +use OCP\IConfig; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; + +class EncryptAll extends Command { + protected bool $wasTrashbinEnabled = false; + + public function __construct( + protected IManager $encryptionManager, + protected IAppManager $appManager, + protected IConfig $config, + protected QuestionHelper $questionHelper, + ) { + parent::__construct(); + } + + /** + * Set maintenance mode and disable the trashbin app + */ + protected function forceMaintenanceAndTrashbin(): void { + $this->wasTrashbinEnabled = (bool)$this->appManager->isEnabledForUser('files_trashbin'); + $this->config->setSystemValue('maintenance', true); + $this->appManager->disableApp('files_trashbin'); + } + + /** + * Reset the maintenance mode and re-enable the trashbin app + */ + protected function resetMaintenanceAndTrashbin(): void { + $this->config->setSystemValue('maintenance', false); + if ($this->wasTrashbinEnabled) { + $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('<error>This command cannot be run with maintenance mode enabled.</error>'); + 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; + } +} diff --git a/core/Command/Encryption/ListModules.php b/core/Command/Encryption/ListModules.php new file mode 100644 index 00000000000..bf02c29f432 --- /dev/null +++ b/core/Command/Encryption/ListModules.php @@ -0,0 +1,71 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Core\Command\Encryption; + +use OC\Core\Command\Base; +use OCP\Encryption\IManager; +use OCP\IConfig; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ListModules extends Base { + public function __construct( + protected IManager $encryptionManager, + protected IConfig $config, + ) { + parent::__construct(); + } + + protected function configure() { + parent::configure(); + + $this + ->setName('encryption:list-modules') + ->setDescription('List all available encryption modules') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $isMaintenanceModeEnabled = $this->config->getSystemValue('maintenance', false); + if ($isMaintenanceModeEnabled) { + $output->writeln('Maintenance mode must be disabled when listing modules'); + $output->writeln('in order to list the relevant encryption modules correctly.'); + return 1; + } + + $encryptionModules = $this->encryptionManager->getEncryptionModules(); + $defaultEncryptionModuleId = $this->encryptionManager->getDefaultEncryptionModuleId(); + + $encModules = []; + foreach ($encryptionModules as $module) { + $encModules[$module['id']]['displayName'] = $module['displayName']; + $encModules[$module['id']]['default'] = $module['id'] === $defaultEncryptionModuleId; + } + $this->writeModuleList($input, $output, $encModules); + return 0; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @param array $items + */ + protected function writeModuleList(InputInterface $input, OutputInterface $output, $items) { + if ($input->getOption('output') === self::OUTPUT_FORMAT_PLAIN) { + array_walk($items, function (&$item): void { + if (!$item['default']) { + $item = $item['displayName']; + } else { + $item = $item['displayName'] . ' [default*]'; + } + }); + } + + $this->writeArrayInOutputFormat($input, $output, $items); + } +} diff --git a/core/Command/Encryption/MigrateKeyStorage.php b/core/Command/Encryption/MigrateKeyStorage.php new file mode 100644 index 00000000000..937b17cde5f --- /dev/null +++ b/core/Command/Encryption/MigrateKeyStorage.php @@ -0,0 +1,212 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Core\Command\Encryption; + +use OC\Encryption\Keys\Storage; +use OC\Encryption\Util; +use OC\Files\View; +use OCP\IConfig; +use OCP\IUserManager; +use OCP\Security\ICrypto; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class MigrateKeyStorage extends Command { + public function __construct( + protected View $rootView, + protected IUserManager $userManager, + protected IConfig $config, + protected Util $util, + private ICrypto $crypto, + ) { + parent::__construct(); + } + + protected function configure(): void { + parent::configure(); + $this + ->setName('encryption:migrate-key-storage-format') + ->setDescription('Migrate the format of the keystorage to a newer format'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $root = $this->util->getKeyStorageRoot(); + + $output->writeln('Updating key storage format'); + $this->updateKeys($root, $output); + $output->writeln('Key storage format successfully updated'); + + return 0; + } + + /** + * Move keys to new key storage root + * + * @throws \Exception + */ + protected function updateKeys(string $root, OutputInterface $output): bool { + $output->writeln('Start to update the keys:'); + + $this->updateSystemKeys($root, $output); + $this->updateUsersKeys($root, $output); + $this->config->deleteSystemValue('encryption.key_storage_migrated'); + return true; + } + + /** + * Move system key folder + */ + protected function updateSystemKeys(string $root, OutputInterface $output): void { + if (!$this->rootView->is_dir($root . '/files_encryption')) { + return; + } + + $this->traverseKeys($root . '/files_encryption', null, $output); + } + + private function traverseKeys(string $folder, ?string $uid, OutputInterface $output): void { + $listing = $this->rootView->getDirectoryContent($folder); + + foreach ($listing as $node) { + if ($node['mimetype'] === 'httpd/unix-directory') { + continue; + } + + if ($node['name'] === 'fileKey' + || str_ends_with($node['name'], '.privateKey') + || str_ends_with($node['name'], '.publicKey') + || str_ends_with($node['name'], '.shareKey')) { + $path = $folder . '/' . $node['name']; + + $content = $this->rootView->file_get_contents($path); + + if ($content === false) { + $output->writeln("<error>Failed to open path $path</error>"); + continue; + } + + try { + $this->crypto->decrypt($content); + continue; + } catch (\Exception $e) { + // Ignore we now update the data. + } + + $data = [ + 'key' => base64_encode($content), + 'uid' => $uid, + ]; + + $enc = $this->crypto->encrypt(json_encode($data)); + $this->rootView->file_put_contents($path, $enc); + } + } + } + + private function traverseFileKeys(string $folder, OutputInterface $output): void { + $listing = $this->rootView->getDirectoryContent($folder); + + foreach ($listing as $node) { + if ($node['mimetype'] === 'httpd/unix-directory') { + $this->traverseFileKeys($folder . '/' . $node['name'], $output); + } else { + $endsWith = function (string $haystack, string $needle): bool { + $length = strlen($needle); + if ($length === 0) { + return true; + } + + return (substr($haystack, -$length) === $needle); + }; + + if ($node['name'] === 'fileKey' + || $endsWith($node['name'], '.privateKey') + || $endsWith($node['name'], '.publicKey') + || $endsWith($node['name'], '.shareKey')) { + $path = $folder . '/' . $node['name']; + + $content = $this->rootView->file_get_contents($path); + + if ($content === false) { + $output->writeln("<error>Failed to open path $path</error>"); + continue; + } + + try { + $this->crypto->decrypt($content); + continue; + } catch (\Exception $e) { + // Ignore we now update the data. + } + + $data = [ + 'key' => base64_encode($content) + ]; + + $enc = $this->crypto->encrypt(json_encode($data)); + $this->rootView->file_put_contents($path, $enc); + } + } + } + } + + + /** + * setup file system for the given user + */ + protected function setupUserFS(string $uid): void { + \OC_Util::tearDownFS(); + \OC_Util::setupFS($uid); + } + + + /** + * iterate over each user and move the keys to the new storage + */ + protected function updateUsersKeys(string $root, OutputInterface $output): void { + $progress = new ProgressBar($output); + $progress->start(); + + foreach ($this->userManager->getBackends() as $backend) { + $limit = 500; + $offset = 0; + do { + $users = $backend->getUsers('', $limit, $offset); + foreach ($users as $user) { + $progress->advance(); + $this->setupUserFS($user); + $this->updateUserKeys($root, $user, $output); + } + $offset += $limit; + } while (count($users) >= $limit); + } + $progress->finish(); + } + + /** + * move user encryption folder to new root folder + * + * @throws \Exception + */ + protected function updateUserKeys(string $root, string $user, OutputInterface $output): void { + if ($this->userManager->userExists($user)) { + $source = $root . '/' . $user . '/files_encryption/OC_DEFAULT_MODULE'; + if ($this->rootView->is_dir($source)) { + $this->traverseKeys($source, $user, $output); + } + + $source = $root . '/' . $user . '/files_encryption/keys'; + if ($this->rootView->is_dir($source)) { + $this->traverseFileKeys($source, $output); + } + } + } +} diff --git a/core/Command/Encryption/SetDefaultModule.php b/core/Command/Encryption/SetDefaultModule.php new file mode 100644 index 00000000000..d10872afd38 --- /dev/null +++ b/core/Command/Encryption/SetDefaultModule.php @@ -0,0 +1,59 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Core\Command\Encryption; + +use OCP\Encryption\IManager; +use OCP\IConfig; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class SetDefaultModule extends Command { + public function __construct( + protected IManager $encryptionManager, + protected IConfig $config, + ) { + parent::__construct(); + } + + protected function configure() { + parent::configure(); + + $this + ->setName('encryption:set-default-module') + ->setDescription('Set the encryption default module') + ->addArgument( + 'module', + InputArgument::REQUIRED, + 'ID of the encryption module that should be used' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $isMaintenanceModeEnabled = $this->config->getSystemValue('maintenance', false); + if ($isMaintenanceModeEnabled) { + $output->writeln('Maintenance mode must be disabled when setting default module,'); + $output->writeln('in order to load the relevant encryption modules correctly.'); + return 1; + } + + $moduleId = $input->getArgument('module'); + + if ($moduleId === $this->encryptionManager->getDefaultEncryptionModuleId()) { + $output->writeln('"' . $moduleId . '"" is already the default module'); + } elseif ($this->encryptionManager->setDefaultEncryptionModule($moduleId)) { + $output->writeln('<info>Set default module to "' . $moduleId . '"</info>'); + } else { + $output->writeln('<error>The specified module "' . $moduleId . '" does not exist</error>'); + return 1; + } + return 0; + } +} diff --git a/core/Command/Encryption/ShowKeyStorageRoot.php b/core/Command/Encryption/ShowKeyStorageRoot.php new file mode 100644 index 00000000000..8cf97076249 --- /dev/null +++ b/core/Command/Encryption/ShowKeyStorageRoot.php @@ -0,0 +1,37 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Core\Command\Encryption; + +use OC\Encryption\Util; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ShowKeyStorageRoot extends Command { + public function __construct( + protected Util $util, + ) { + parent::__construct(); + } + + protected function configure() { + parent::configure(); + $this + ->setName('encryption:show-key-storage-root') + ->setDescription('Show current key storage root'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $currentRoot = $this->util->getKeyStorageRoot(); + + $rootDescription = $currentRoot !== '' ? $currentRoot : 'default storage location (data/)'; + + $output->writeln("Current key storage root: <info>$rootDescription</info>"); + return 0; + } +} diff --git a/core/Command/Encryption/Status.php b/core/Command/Encryption/Status.php new file mode 100644 index 00000000000..6e4f7d16d0c --- /dev/null +++ b/core/Command/Encryption/Status.php @@ -0,0 +1,38 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Core\Command\Encryption; + +use OC\Core\Command\Base; +use OCP\Encryption\IManager; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class Status extends Base { + public function __construct( + protected IManager $encryptionManager, + ) { + parent::__construct(); + } + + protected function configure() { + parent::configure(); + + $this + ->setName('encryption:status') + ->setDescription('Lists the current status of encryption') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->writeArrayInOutputFormat($input, $output, [ + 'enabled' => $this->encryptionManager->isEnabled(), + 'defaultModule' => $this->encryptionManager->getDefaultEncryptionModuleId(), + ]); + return 0; + } +} |