From dd4ebbd72a2ed4f6030a29e1a1a5a15404c863d3 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 28 Nov 2022 16:04:17 +0100 Subject: [PATCH] add migration for encryption keys in wrong location Signed-off-by: Robin Appelman --- apps/encryption/appinfo/info.xml | 1 + .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../encryption/lib/Command/FixKeyLocation.php | 186 ++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 apps/encryption/lib/Command/FixKeyLocation.php diff --git a/apps/encryption/appinfo/info.xml b/apps/encryption/appinfo/info.xml index 03d443b1d2d..70ffa686925 100644 --- a/apps/encryption/appinfo/info.xml +++ b/apps/encryption/appinfo/info.xml @@ -46,6 +46,7 @@ OCA\Encryption\Command\RecoverUser OCA\Encryption\Command\ScanLegacyFormat OCA\Encryption\Command\FixEncryptedVersion + OCA\Encryption\Command\FixKeyLocation 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 @@ + + * + * @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 . + * + */ + +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("User $userId not found"); + 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("System wide mount point is not a directory, skipping: " . $mount->getMountPoint() . ""); + continue; + } + + $files = $this->getAllFiles($mountRootFolder); + foreach ($files as $file) { + if ($this->isKeyStoredForUser($user, $file)) { + if ($dryRun) { + $output->writeln("" . $file->getPath() . " needs migration"); + } else { + $output->write("Migrating key for " . $file->getPath() . " "); + if ($this->copyKeyAndValidate($user, $file)) { + $output->writeln("✓"); + } else { + $output->writeln("❌"); + $output->writeln(" Failed to validate key for " . $file->getPath() . ", 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 + */ + 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; + } + } +} -- 2.39.5