summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorRobin Appelman <robin@icewind.nl>2022-11-28 16:04:17 +0100
committerVincent Petry (Rebase PR Action) <PVince81@users.noreply.github.com>2022-12-16 12:57:12 +0000
commit1660ca655dd71bd8db78836026322acb61d46b21 (patch)
tree67bd40d730eb7bda20d4fc6bcdd4e191db14b09d /apps
parent104d5fae79ecb93857cc7c06b55e0962bd623a06 (diff)
downloadnextcloud-server-1660ca655dd71bd8db78836026322acb61d46b21.tar.gz
nextcloud-server-1660ca655dd71bd8db78836026322acb61d46b21.zip
add migration for encryption keys in wrong location
Signed-off-by: Robin Appelman <robin@icewind.nl>
Diffstat (limited to 'apps')
-rw-r--r--apps/encryption/appinfo/info.xml1
-rw-r--r--apps/encryption/composer/composer/autoload_classmap.php1
-rw-r--r--apps/encryption/composer/composer/autoload_static.php1
-rw-r--r--apps/encryption/lib/Command/FixKeyLocation.php186
4 files changed, 189 insertions, 0 deletions
diff --git a/apps/encryption/appinfo/info.xml b/apps/encryption/appinfo/info.xml
index ca03c51fc2b..d80a6d7ef53 100644
--- a/apps/encryption/appinfo/info.xml
+++ b/apps/encryption/appinfo/info.xml
@@ -46,6 +46,7 @@
<command>OCA\Encryption\Command\RecoverUser</command>
<command>OCA\Encryption\Command\ScanLegacyFormat</command>
<command>OCA\Encryption\Command\FixEncryptedVersion</command>
+ <command>OCA\Encryption\Command\FixKeyLocation</command>
</commands>
<settings>
diff --git a/apps/encryption/composer/composer/autoload_classmap.php b/apps/encryption/composer/composer/autoload_classmap.php
index 0ce1e86f8a6..9f9ab4e406f 100644
--- a/apps/encryption/composer/composer/autoload_classmap.php
+++ b/apps/encryption/composer/composer/autoload_classmap.php
@@ -11,6 +11,7 @@ return array(
'OCA\\Encryption\\Command\\DisableMasterKey' => $baseDir . '/../lib/Command/DisableMasterKey.php',
'OCA\\Encryption\\Command\\EnableMasterKey' => $baseDir . '/../lib/Command/EnableMasterKey.php',
'OCA\\Encryption\\Command\\FixEncryptedVersion' => $baseDir . '/../lib/Command/FixEncryptedVersion.php',
+ 'OCA\\Encryption\\Command\\FixKeyLocation' => $baseDir . '/../lib/Command/FixKeyLocation.php',
'OCA\\Encryption\\Command\\RecoverUser' => $baseDir . '/../lib/Command/RecoverUser.php',
'OCA\\Encryption\\Command\\ScanLegacyFormat' => $baseDir . '/../lib/Command/ScanLegacyFormat.php',
'OCA\\Encryption\\Controller\\RecoveryController' => $baseDir . '/../lib/Controller/RecoveryController.php',
diff --git a/apps/encryption/composer/composer/autoload_static.php b/apps/encryption/composer/composer/autoload_static.php
index fc1fcbcf63b..8f50f064997 100644
--- a/apps/encryption/composer/composer/autoload_static.php
+++ b/apps/encryption/composer/composer/autoload_static.php
@@ -26,6 +26,7 @@ class ComposerStaticInitEncryption
'OCA\\Encryption\\Command\\DisableMasterKey' => __DIR__ . '/..' . '/../lib/Command/DisableMasterKey.php',
'OCA\\Encryption\\Command\\EnableMasterKey' => __DIR__ . '/..' . '/../lib/Command/EnableMasterKey.php',
'OCA\\Encryption\\Command\\FixEncryptedVersion' => __DIR__ . '/..' . '/../lib/Command/FixEncryptedVersion.php',
+ 'OCA\\Encryption\\Command\\FixKeyLocation' => __DIR__ . '/..' . '/../lib/Command/FixKeyLocation.php',
'OCA\\Encryption\\Command\\RecoverUser' => __DIR__ . '/..' . '/../lib/Command/RecoverUser.php',
'OCA\\Encryption\\Command\\ScanLegacyFormat' => __DIR__ . '/..' . '/../lib/Command/ScanLegacyFormat.php',
'OCA\\Encryption\\Controller\\RecoveryController' => __DIR__ . '/..' . '/../lib/Controller/RecoveryController.php',
diff --git a/apps/encryption/lib/Command/FixKeyLocation.php b/apps/encryption/lib/Command/FixKeyLocation.php
new file mode 100644
index 00000000000..5339247ae19
--- /dev/null
+++ b/apps/encryption/lib/Command/FixKeyLocation.php
@@ -0,0 +1,186 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * 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
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\Encryption\Command;
+
+use OC\Encryption\Util;
+use OC\Files\View;
+use OCP\Files\Config\ICachedMountInfo;
+use OCP\Files\Config\IUserMountCache;
+use OCP\Files\Folder;
+use OCP\Files\File;
+use OCP\Files\IRootFolder;
+use OCP\Files\Node;
+use OCP\IUser;
+use OCP\IUserManager;
+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 FixKeyLocation extends Command {
+ private IUserManager $userManager;
+ private IUserMountCache $userMountCache;
+ private Util $encryptionUtil;
+ private IRootFolder $rootFolder;
+ private string $keyRootDirectory;
+ private View $rootView;
+
+ public function __construct(IUserManager $userManager, IUserMountCache $userMountCache, Util $encryptionUtil, IRootFolder $rootFolder) {
+ $this->userManager = $userManager;
+ $this->userMountCache = $userMountCache;
+ $this->encryptionUtil = $encryptionUtil;
+ $this->rootFolder = $rootFolder;
+ $this->keyRootDirectory = rtrim($this->encryptionUtil->getKeyStorageRoot(), '/');
+ $this->rootView = new View();
+
+ parent::__construct();
+ }
+
+
+ protected function configure(): void {
+ parent::configure();
+
+ $this
+ ->setName('encryption:fix-key-location')
+ ->setDescription('Fix the location of encryption keys for external storage')
+ ->addOption('dry-run', null, InputOption::VALUE_NONE, "Only list files that require key migration, don't try to perform any migration")
+ ->addArgument('user', InputArgument::REQUIRED, "User id to fix the key locations for");
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $dryRun = $input->getOption('dry-run');
+ $userId = $input->getArgument('user');
+ $user = $this->userManager->get($userId);
+ if (!$user) {
+ $output->writeln("<error>User $userId not found</error>");
+ return 1;
+ }
+
+ \OC_Util::setupFS($user->getUID());
+
+ $mounts = $this->getSystemMountsForUser($user);
+ foreach ($mounts as $mount) {
+ $mountRootFolder = $this->rootFolder->get($mount->getMountPoint());
+ if (!$mountRootFolder instanceof Folder) {
+ $output->writeln("<error>System wide mount point is not a directory, skipping: " . $mount->getMountPoint() . "</error>");
+ continue;
+ }
+
+ $files = $this->getAllFiles($mountRootFolder);
+ foreach ($files as $file) {
+ if ($this->isKeyStoredForUser($user, $file)) {
+ if ($dryRun) {
+ $output->writeln("<info>" . $file->getPath() . "</info> needs migration");
+ } else {
+ $output->write("Migrating key for <info>" . $file->getPath() . "</info> ");
+ if ($this->copyKeyAndValidate($user, $file)) {
+ $output->writeln("<info>✓</info>");
+ } else {
+ $output->writeln("<fg=red>❌</>");
+ $output->writeln(" Failed to validate key for <error>" . $file->getPath() . "</error>, key will not be migrated");
+ }
+ }
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * @param IUser $user
+ * @return ICachedMountInfo[]
+ */
+ private function getSystemMountsForUser(IUser $user): array {
+ return array_filter($this->userMountCache->getMountsForUser($user), function(ICachedMountInfo $mount) use ($user) {
+ $mountPoint = substr($mount->getMountPoint(), strlen($user->getUID() . '/'));
+ return $this->encryptionUtil->isSystemWideMountPoint($mountPoint, $user->getUID());
+ });
+ }
+
+ /**
+ * @param Folder $folder
+ * @return \Generator<File>
+ */
+ private function getAllFiles(Folder $folder) {
+ foreach ($folder->getDirectoryListing() as $child) {
+ if ($child instanceof Folder) {
+ yield from $this->getAllFiles($child);
+ } else {
+ yield $child;
+ }
+ }
+ }
+
+ /**
+ * Check if the key for a file is stored in the user's keystore and not the system one
+ *
+ * @param IUser $user
+ * @param Node $node
+ * @return bool
+ */
+ private function isKeyStoredForUser(IUser $user, Node $node): bool {
+ $path = trim(substr($node->getPath(), strlen($user->getUID()) + 1), '/');
+ $systemKeyPath = $this->keyRootDirectory . '/files_encryption/keys/' . $path . '/';
+ $userKeyPath = $this->keyRootDirectory . '/' . $user->getUID() . '/files_encryption/keys/' . $path . '/';
+
+ // this uses View instead of the RootFolder because the keys might not be in the cache
+ $systemKeyExists = $this->rootView->file_exists($systemKeyPath);
+ $userKeyExists = $this->rootView->file_exists($userKeyPath);
+ return $userKeyExists && !$systemKeyExists;
+ }
+
+ /**
+ * Check that the user key stored for a file can decrypt the file
+ *
+ * @param IUser $user
+ * @param File $node
+ * @return bool
+ */
+ private function copyKeyAndValidate(IUser $user, File $node): bool {
+ $path = trim(substr($node->getPath(), strlen($user->getUID()) + 1), '/');
+ $systemKeyPath = $this->keyRootDirectory . '/files_encryption/keys/' . $path . '/';
+ $userKeyPath = $this->keyRootDirectory . '/' . $user->getUID() . '/files_encryption/keys/' . $path . '/';
+
+ $this->rootView->copy($userKeyPath, $systemKeyPath);
+ try {
+ // check that the copied key is valid
+ $fh = $node->fopen('r');
+ // read a single chunk
+ $data = fread($fh, 8192);
+ if ($data === false) {
+ throw new \Exception("Read failed");
+ }
+
+ // cleanup wrong key location
+ $this->rootView->rmdir($userKeyPath);
+ return true;
+ } catch (\Exception $e) {
+ // remove the copied key if we know it's invalid
+ $this->rootView->rmdir($systemKeyPath);
+ return false;
+ }
+ }
+}