]> source.dussan.org Git - nextcloud-server.git/commitdiff
add migration for encryption keys in wrong location 35107/head
authorRobin Appelman <robin@icewind.nl>
Mon, 28 Nov 2022 15:04:17 +0000 (16:04 +0100)
committerRobin Appelman <robin@icewind.nl>
Mon, 28 Nov 2022 15:12:11 +0000 (16:12 +0100)
Signed-off-by: Robin Appelman <robin@icewind.nl>
apps/encryption/appinfo/info.xml
apps/encryption/composer/composer/autoload_classmap.php
apps/encryption/composer/composer/autoload_static.php
apps/encryption/lib/Command/FixKeyLocation.php [new file with mode: 0644]

index 03d443b1d2d4c94e9431a68920a671e9199ddb51..70ffa6869254db991cce29f77e7346b6833785c2 100644 (file)
@@ -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>
index 0ce1e86f8a6b23911ad92f44f6d1c318dfc1d299..9f9ab4e406f3dde406798cc81278440766fa94c5 100644 (file)
@@ -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',
index fc1fcbcf63b4ed946dbac076bdd4de306b5f166a..8f50f0649970fb2d042bcfb3d8df05e026eda914 100644 (file)
@@ -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 (file)
index 0000000..5339247
--- /dev/null
@@ -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;
+               }
+       }
+}