|  | 
|  | 1 | +<?php | 
|  | 2 | + | 
|  | 3 | +declare(strict_types=1); | 
|  | 4 | +/** | 
|  | 5 | + * @copyright Copyright (c) 2022 Robin Appelman <robin@icewind.nl> | 
|  | 6 | + * | 
|  | 7 | + * @license GNU AGPL version 3 or any later version | 
|  | 8 | + * | 
|  | 9 | + * This program is free software: you can redistribute it and/or modify | 
|  | 10 | + * it under the terms of the GNU Affero General Public License as | 
|  | 11 | + * published by the Free Software Foundation, either version 3 of the | 
|  | 12 | + * License, or (at your option) any later version. | 
|  | 13 | + * | 
|  | 14 | + * This program is distributed in the hope that it will be useful, | 
|  | 15 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of | 
|  | 16 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
|  | 17 | + * GNU Affero General Public License for more details. | 
|  | 18 | + * | 
|  | 19 | + * You should have received a copy of the GNU Affero General Public License | 
|  | 20 | + * along with this program.  If not, see <http://www.gnu.org/licenses/>. | 
|  | 21 | + * | 
|  | 22 | + */ | 
|  | 23 | + | 
|  | 24 | +namespace OCA\Encryption\Command; | 
|  | 25 | + | 
|  | 26 | +use OC\Encryption\Util; | 
|  | 27 | +use OC\Files\View; | 
|  | 28 | +use OCP\Files\Config\ICachedMountInfo; | 
|  | 29 | +use OCP\Files\Config\IUserMountCache; | 
|  | 30 | +use OCP\Files\Folder; | 
|  | 31 | +use OCP\Files\File; | 
|  | 32 | +use OCP\Files\IRootFolder; | 
|  | 33 | +use OCP\Files\Node; | 
|  | 34 | +use OCP\IUser; | 
|  | 35 | +use OCP\IUserManager; | 
|  | 36 | +use Symfony\Component\Console\Command\Command; | 
|  | 37 | +use Symfony\Component\Console\Input\InputArgument; | 
|  | 38 | +use Symfony\Component\Console\Input\InputInterface; | 
|  | 39 | +use Symfony\Component\Console\Input\InputOption; | 
|  | 40 | +use Symfony\Component\Console\Output\OutputInterface; | 
|  | 41 | + | 
|  | 42 | +class FixKeyLocation extends Command { | 
|  | 43 | +	private IUserManager $userManager; | 
|  | 44 | +	private IUserMountCache $userMountCache; | 
|  | 45 | +	private Util $encryptionUtil; | 
|  | 46 | +	private IRootFolder $rootFolder; | 
|  | 47 | +	private string $keyRootDirectory; | 
|  | 48 | +	private View $rootView; | 
|  | 49 | + | 
|  | 50 | +	public function __construct(IUserManager $userManager, IUserMountCache $userMountCache, Util $encryptionUtil, IRootFolder $rootFolder) { | 
|  | 51 | +		$this->userManager = $userManager; | 
|  | 52 | +		$this->userMountCache = $userMountCache; | 
|  | 53 | +		$this->encryptionUtil = $encryptionUtil; | 
|  | 54 | +		$this->rootFolder = $rootFolder; | 
|  | 55 | +		$this->keyRootDirectory = rtrim($this->encryptionUtil->getKeyStorageRoot(), '/'); | 
|  | 56 | +		$this->rootView = new View(); | 
|  | 57 | + | 
|  | 58 | +		parent::__construct(); | 
|  | 59 | +	} | 
|  | 60 | + | 
|  | 61 | + | 
|  | 62 | +	protected function configure(): void { | 
|  | 63 | +		parent::configure(); | 
|  | 64 | + | 
|  | 65 | +		$this | 
|  | 66 | +			->setName('encryption:fix-key-location') | 
|  | 67 | +			->setDescription('Fix the location of encryption keys for external storage') | 
|  | 68 | +			->addOption('dry-run', null, InputOption::VALUE_NONE, "Only list files that require key migration, don't try to perform any migration") | 
|  | 69 | +			->addArgument('user', InputArgument::REQUIRED, "User id to fix the key locations for"); | 
|  | 70 | +	} | 
|  | 71 | + | 
|  | 72 | +	protected function execute(InputInterface $input, OutputInterface $output): int { | 
|  | 73 | +		$dryRun = $input->getOption('dry-run'); | 
|  | 74 | +		$userId = $input->getArgument('user'); | 
|  | 75 | +		$user = $this->userManager->get($userId); | 
|  | 76 | +		if (!$user) { | 
|  | 77 | +			$output->writeln("<error>User $userId not found</error>"); | 
|  | 78 | +			return 1; | 
|  | 79 | +		} | 
|  | 80 | + | 
|  | 81 | +		\OC_Util::setupFS($user->getUID()); | 
|  | 82 | + | 
|  | 83 | +		$mounts = $this->getSystemMountsForUser($user); | 
|  | 84 | +		foreach ($mounts as $mount) { | 
|  | 85 | +			$mountRootFolder = $this->rootFolder->get($mount->getMountPoint()); | 
|  | 86 | +			if (!$mountRootFolder instanceof Folder) { | 
|  | 87 | +				$output->writeln("<error>System wide mount point is not a directory, skipping: " . $mount->getMountPoint() . "</error>"); | 
|  | 88 | +				continue; | 
|  | 89 | +			} | 
|  | 90 | + | 
|  | 91 | +			$files = $this->getAllFiles($mountRootFolder); | 
|  | 92 | +			foreach ($files as $file) { | 
|  | 93 | +				if ($this->isKeyStoredForUser($user, $file)) { | 
|  | 94 | +					if ($dryRun) { | 
|  | 95 | +						$output->writeln("<info>" . $file->getPath() . "</info> needs migration"); | 
|  | 96 | +					} else { | 
|  | 97 | +						$output->write("Migrating key for <info>" . $file->getPath() . "</info> "); | 
|  | 98 | +						if ($this->copyKeyAndValidate($user, $file)) { | 
|  | 99 | +							$output->writeln("<info>✓</info>"); | 
|  | 100 | +						} else { | 
|  | 101 | +							$output->writeln("<fg=red>❌</>"); | 
|  | 102 | +							$output->writeln("  Failed to validate key for <error>" . $file->getPath() . "</error>, key will not be migrated"); | 
|  | 103 | +						} | 
|  | 104 | +					} | 
|  | 105 | +				} | 
|  | 106 | +			} | 
|  | 107 | +		} | 
|  | 108 | + | 
|  | 109 | +		return 0; | 
|  | 110 | +	} | 
|  | 111 | + | 
|  | 112 | +	/** | 
|  | 113 | +	 * @param IUser $user | 
|  | 114 | +	 * @return ICachedMountInfo[] | 
|  | 115 | +	 */ | 
|  | 116 | +	private function getSystemMountsForUser(IUser $user): array { | 
|  | 117 | +		return array_filter($this->userMountCache->getMountsForUser($user), function(ICachedMountInfo $mount) use ($user) { | 
|  | 118 | +			$mountPoint = substr($mount->getMountPoint(), strlen($user->getUID() . '/')); | 
|  | 119 | +			return $this->encryptionUtil->isSystemWideMountPoint($mountPoint, $user->getUID()); | 
|  | 120 | +		}); | 
|  | 121 | +	} | 
|  | 122 | + | 
|  | 123 | +	/** | 
|  | 124 | +	 * @param Folder $folder | 
|  | 125 | +	 * @return \Generator<File> | 
|  | 126 | +	 */ | 
|  | 127 | +	private function getAllFiles(Folder $folder) { | 
|  | 128 | +		foreach ($folder->getDirectoryListing() as $child) { | 
|  | 129 | +			if ($child instanceof Folder) { | 
|  | 130 | +				yield from $this->getAllFiles($child); | 
|  | 131 | +			} else { | 
|  | 132 | +				yield $child; | 
|  | 133 | +			} | 
|  | 134 | +		} | 
|  | 135 | +	} | 
|  | 136 | + | 
|  | 137 | +	/** | 
|  | 138 | +	 * Check if the key for a file is stored in the user's keystore and not the system one | 
|  | 139 | +	 * | 
|  | 140 | +	 * @param IUser $user | 
|  | 141 | +	 * @param Node $node | 
|  | 142 | +	 * @return bool | 
|  | 143 | +	 */ | 
|  | 144 | +	private function isKeyStoredForUser(IUser $user, Node $node): bool { | 
|  | 145 | +		$path = trim(substr($node->getPath(), strlen($user->getUID()) + 1), '/'); | 
|  | 146 | +		$systemKeyPath = $this->keyRootDirectory . '/files_encryption/keys/' . $path . '/'; | 
|  | 147 | +		$userKeyPath = $this->keyRootDirectory . '/' . $user->getUID() . '/files_encryption/keys/' . $path . '/'; | 
|  | 148 | + | 
|  | 149 | +		// this uses View instead of the RootFolder because the keys might not be in the cache | 
|  | 150 | +		$systemKeyExists = $this->rootView->file_exists($systemKeyPath); | 
|  | 151 | +		$userKeyExists = $this->rootView->file_exists($userKeyPath); | 
|  | 152 | +		return $userKeyExists && !$systemKeyExists; | 
|  | 153 | +	} | 
|  | 154 | + | 
|  | 155 | +	/** | 
|  | 156 | +	 * Check that the user key stored for a file can decrypt the file | 
|  | 157 | +	 * | 
|  | 158 | +	 * @param IUser $user | 
|  | 159 | +	 * @param File $node | 
|  | 160 | +	 * @return bool | 
|  | 161 | +	 */ | 
|  | 162 | +	private function copyKeyAndValidate(IUser $user, File $node): bool { | 
|  | 163 | +		$path = trim(substr($node->getPath(), strlen($user->getUID()) + 1), '/'); | 
|  | 164 | +		$systemKeyPath = $this->keyRootDirectory . '/files_encryption/keys/' . $path . '/'; | 
|  | 165 | +		$userKeyPath = $this->keyRootDirectory . '/' . $user->getUID() . '/files_encryption/keys/' . $path . '/'; | 
|  | 166 | + | 
|  | 167 | +		$this->rootView->copy($userKeyPath, $systemKeyPath); | 
|  | 168 | +		try { | 
|  | 169 | +			// check that the copied key is valid | 
|  | 170 | +			$fh = $node->fopen('r'); | 
|  | 171 | +			// read a single chunk | 
|  | 172 | +			$data = fread($fh, 8192); | 
|  | 173 | +			if ($data === false) { | 
|  | 174 | +				throw new \Exception("Read failed"); | 
|  | 175 | +			} | 
|  | 176 | + | 
|  | 177 | +			// cleanup wrong key location | 
|  | 178 | +			$this->rootView->rmdir($userKeyPath); | 
|  | 179 | +			return true; | 
|  | 180 | +		} catch (\Exception $e) { | 
|  | 181 | +			// remove the copied key if we know it's invalid | 
|  | 182 | +			$this->rootView->rmdir($systemKeyPath); | 
|  | 183 | +			return false; | 
|  | 184 | +		} | 
|  | 185 | +	} | 
|  | 186 | +} | 
0 commit comments