Passed
Push — master ( 0b39e7...65b5e6 )
by Morris
13:40 queued 28s
created

MigrateKeyStorage::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 6
c 1
b 0
f 0
nc 1
nop 5
dl 0
loc 7
rs 10
1
<?php
2
3
declare(strict_types=1);
4
/**
5
 * @copyright Copyright (c) 2020, Roeland Jago Douma <[email protected]>
6
 *
7
 * @author Roeland Jago Douma <[email protected]>
8
 *
9
 * @license GNU AGPL version 3 or any later version
10
 *
11
 * This program is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License as
13
 * published by the Free Software Foundation, either version 3 of the
14
 * License, or (at your option) any later version.
15
 *
16
 * This program is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 * GNU Affero General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU Affero General Public License
22
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23
 *
24
 */
25
26
27
namespace OC\Core\Command\Encryption;
28
29
use OC\Encryption\Keys\Storage;
30
use OC\Encryption\Util;
31
use OC\Files\View;
32
use OCP\IConfig;
33
use OCP\IUserManager;
34
use OCP\Security\ICrypto;
35
use Symfony\Component\Console\Command\Command;
36
use Symfony\Component\Console\Helper\ProgressBar;
37
use Symfony\Component\Console\Helper\QuestionHelper;
38
use Symfony\Component\Console\Input\InputInterface;
39
use Symfony\Component\Console\Output\OutputInterface;
40
41
class MigrateKeyStorage extends Command {
42
43
	/** @var View */
44
	protected $rootView;
45
46
	/** @var IUserManager */
47
	protected $userManager;
48
49
	/** @var IConfig */
50
	protected $config;
51
52
	/** @var Util */
53
	protected $util;
54
55
	/** @var QuestionHelper */
56
	protected $questionHelper;
57
	/**
58
	 * @var ICrypto
59
	 */
60
	private $crypto;
61
62
	public function __construct(View $view, IUserManager $userManager, IConfig $config, Util $util, ICrypto $crypto) {
63
		parent::__construct();
64
		$this->rootView = $view;
65
		$this->userManager = $userManager;
66
		$this->config = $config;
67
		$this->util = $util;
68
		$this->crypto = $crypto;
69
	}
70
71
	protected function configure() {
72
		parent::configure();
73
		$this
74
			->setName('encryption:migrate-key-storage-format')
75
			->setDescription('Migrate the format of the keystorage to a newer format');
76
	}
77
78
	protected function execute(InputInterface $input, OutputInterface $output): int {
79
		$root = $this->util->getKeyStorageRoot();
80
81
		$output->writeln("Updating key storage format");
82
		$this->updateKeys($root, $output);
83
		$output->writeln("Key storage format successfully updated");
84
85
		return 0;
86
	}
87
88
	/**
89
	 * move keys to new key storage root
90
	 *
91
	 * @param string $root
92
	 * @param OutputInterface $output
93
	 * @return bool
94
	 * @throws \Exception
95
	 */
96
	protected function updateKeys(string $root, OutputInterface $output) {
97
		$output->writeln("Start to update the keys:");
98
99
		$this->updateSystemKeys($root);
100
		$this->updateUsersKeys($root, $output);
101
		$this->config->deleteSystemValue('encryption.key_storage_migrated');
102
		return true;
103
	}
104
105
	/**
106
	 * move system key folder
107
	 *
108
	 * @param string $root
109
	 */
110
	protected function updateSystemKeys($root) {
111
		if (!$this->rootView->is_dir($root . '/files_encryption')) {
112
			return;
113
		}
114
115
		$this->traverseKeys($root . '/files_encryption', null);
116
	}
117
118
	private function traverseKeys(string $folder, ?string $uid) {
119
		$listing = $this->rootView->getDirectoryContent($folder);
120
121
		foreach ($listing as $node) {
122
			if ($node['mimetype'] === 'httpd/unix-directory') {
123
				//ignore
124
			} else {
125
				$endsWith = function ($haystack, $needle) {
126
					$length = strlen($needle);
127
					if ($length === 0) {
128
						return true;
129
					}
130
131
					return (substr($haystack, -$length) === $needle);
132
				};
133
134
				if ($node['name'] === 'fileKey' ||
135
					$endsWith($node['name'], '.privateKey') ||
136
					$endsWith($node['name'], '.publicKey') ||
137
					$endsWith($node['name'], '.shareKey')) {
138
					$path = $folder . '/' . $node['name'];
139
140
					$content = $this->rootView->file_get_contents($path);
141
142
					try {
143
						$this->crypto->decrypt($content);
144
						continue;
145
					} catch (\Exception $e) {
146
						// Ignore we now update the data.
147
					}
148
149
					$data = [
150
						'key' => base64_encode($content),
151
						'uid' => $uid,
152
					];
153
154
					$enc = $this->crypto->encrypt(json_encode($data));
155
					$this->rootView->file_put_contents($path, $enc);
156
				}
157
			}
158
		}
159
	}
160
161
	private function traverseFileKeys(string $folder) {
162
		$listing = $this->rootView->getDirectoryContent($folder);
163
164
		foreach ($listing as $node) {
165
			if ($node['mimetype'] === 'httpd/unix-directory') {
166
				$this->traverseFileKeys($folder . '/' . $node['name']);
167
			} else {
168
				$endsWith = function ($haystack, $needle) {
169
					$length = strlen($needle);
170
					if ($length === 0) {
171
						return true;
172
					}
173
174
					return (substr($haystack, -$length) === $needle);
175
				};
176
177
				if ($node['name'] === 'fileKey' ||
178
					$endsWith($node['name'], '.privateKey') ||
179
					$endsWith($node['name'], '.publicKey') ||
180
					$endsWith($node['name'], '.shareKey')) {
181
					$path = $folder . '/' . $node['name'];
182
183
					$content = $this->rootView->file_get_contents($path);
184
185
					try {
186
						$this->crypto->decrypt($content);
187
						continue;
188
					} catch (\Exception $e) {
189
						// Ignore we now update the data.
190
					}
191
192
					$data = [
193
						'key' => base64_encode($content)
194
					];
195
196
					$enc = $this->crypto->encrypt(json_encode($data));
197
					$this->rootView->file_put_contents($path, $enc);
198
				}
199
			}
200
		}
201
	}
202
203
204
	/**
205
	 * setup file system for the given user
206
	 *
207
	 * @param string $uid
208
	 */
209
	protected function setupUserFS($uid) {
210
		\OC_Util::tearDownFS();
211
		\OC_Util::setupFS($uid);
212
	}
213
214
215
	/**
216
	 * iterate over each user and move the keys to the new storage
217
	 *
218
	 * @param string $root
219
	 * @param OutputInterface $output
220
	 */
221
	protected function updateUsersKeys(string $root, OutputInterface $output) {
222
		$progress = new ProgressBar($output);
223
		$progress->start();
224
225
		foreach ($this->userManager->getBackends() as $backend) {
226
			$limit = 500;
227
			$offset = 0;
228
			do {
229
				$users = $backend->getUsers('', $limit, $offset);
230
				foreach ($users as $user) {
231
					$progress->advance();
232
					$this->setupUserFS($user);
233
					$this->updateUserKeys($root, $user);
234
				}
235
				$offset += $limit;
236
			} while (count($users) >= $limit);
237
		}
238
		$progress->finish();
239
	}
240
241
	/**
242
	 * move user encryption folder to new root folder
243
	 *
244
	 * @param string $root
245
	 * @param string $user
246
	 * @throws \Exception
247
	 */
248
	protected function updateUserKeys(string $root, string $user) {
249
		if ($this->userManager->userExists($user)) {
250
			$source = $root . '/' . $user . '/files_encryption/OC_DEFAULT_MODULE';
251
			if ($this->rootView->is_dir($source)) {
252
				$this->traverseKeys($source, $user);
253
			}
254
255
			$source = $root . '/' . $user . '/files_encryption/keys';
256
			if ($this->rootView->is_dir($source)) {
257
				$this->traverseFileKeys($source);
258
			}
259
		}
260
	}
261
}
262