Passed
Push — master ( b652d1...f0a1e1 )
by Robin
16:07 queued 12s
created

FixKeyLocation::execute()   B

Complexity

Conditions 8
Paths 4

Size

Total Lines 38
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 8
eloc 26
nc 4
nop 2
dl 0
loc 38
rs 8.4444
c 1
b 0
f 1
1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * @copyright Copyright (c) 2022 Robin Appelman <[email protected]>
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression $systemKeyExists of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
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
}
187