Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 90 additions & 48 deletions core/Command/Encryption/EncryptAll.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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;
Expand All @@ -24,81 +25,122 @@ public function __construct(
protected IAppManager $appManager,
protected IConfig $config,
protected QuestionHelper $questionHelper,
private LoggerInterface $logger,
) {
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() {
protected function configure(): void {
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!'
$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 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('<error>Invalid TTY.</error>');
$output->writeln("<comment>If running inside Docker, use 'docker exec -it' or equivalent.</comment>");
$output->writeln('');
return 1;
return self::FAILURE;
}

// Prevent running if SSE isn't enabled
if ($this->encryptionManager->isEnabled() === false) {
throw new \Exception('Server side encryption is not enabled');
$output->writeln('<error>Server Side Encryption is not enabled; unable to encrypt files.</error>');
return self::FAILURE;
}

// Prevent running if no valid SSE modules are registered
$modules = $this->encryptionManager->getEncryptionModules();
if (empty($modules)) {
$output->writeln('<error>No Server Side Encryption modules are registered; unable to encrypt files.</error>');
return self::FAILURE;
}

// Prevent running if a default SSE module isn't already configured
$defaultModuleId = $this->encryptionManager->getDefaultEncryptionModuleId();
if ($defaultModuleId === '') {
$output->writeln('<error>A default Server Side Encryption module is not configured; unable to encrypt files.</error>');
return self::FAILURE;
}

// Prevent running if the default SSE module isn't valid
if (!isset($modules[$defaultModuleId])) {
$output->writeln('<error>The current default Server Side Encryption module does not exist: ' . $defaultModuleId . '; unable to encrypt files.</error>');
return self::FAILURE;
}

// Prevent running if maintenance mode is already enabled
if ($this->config->getSystemValueBool('maintenance')) {
$output->writeln('<error>This command cannot be run with maintenance mode enabled.</error>');
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 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('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('');
$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;
}

// 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('<error>Encryption failed: ' . $ex->getMessage() . '</error>');
$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();
return self::SUCCESS;
}
$output->writeln('aborted');
return self::FAILURE;
// 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
}

/**
* 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');
}
}
}
Loading