1<?php
2/**
3 * @author Björn Schießle <bjoern@schiessle.org>
4 * @author Roeland Jago Douma <rullzer@owncloud.com>
5 * @author Thomas Müller <thomas.mueller@tmit.eu>
6 *
7 * @copyright Copyright (c) 2019, ownCloud GmbH
8 * @license AGPL-3.0
9 *
10 * This code is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU Affero General Public License, version 3,
12 * as published by the Free Software Foundation.
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, version 3,
20 * along with this program.  If not, see <http://www.gnu.org/licenses/>
21 *
22 */
23
24namespace OCA\Encryption\Crypto;
25
26use OC\Encryption\Exceptions\DecryptionFailedException;
27use OC\Files\View;
28use OCA\Encryption\KeyManager;
29use OCA\Encryption\Users\Setup;
30use OCA\Encryption\Util;
31use OCP\IConfig;
32use OCP\IL10N;
33use OCP\IUserManager;
34use OCP\Mail\IMailer;
35use OCP\Security\ISecureRandom;
36use Symfony\Component\Console\Helper\ProgressBar;
37use Symfony\Component\Console\Helper\QuestionHelper;
38use Symfony\Component\Console\Helper\Table;
39use Symfony\Component\Console\Input\InputInterface;
40use Symfony\Component\Console\Output\OutputInterface;
41use Symfony\Component\Console\Question\ConfirmationQuestion;
42
43class EncryptAll {
44
45	/** @var Setup */
46	protected $userSetup;
47
48	/** @var IUserManager */
49	protected $userManager;
50
51	/** @var View */
52	protected $rootView;
53
54	/** @var KeyManager */
55	protected $keyManager;
56
57	/** @var Util */
58	protected $util;
59
60	/** @var array  */
61	protected $userPasswords;
62
63	/** @var  IConfig */
64	protected $config;
65
66	/** @var IMailer */
67	protected $mailer;
68
69	/** @var  IL10N */
70	protected $l;
71
72	/** @var  QuestionHelper */
73	protected $questionHelper;
74
75	/** @var  OutputInterface */
76	protected $output;
77
78	/** @var  InputInterface */
79	protected $input;
80
81	/** @var ISecureRandom */
82	protected $secureRandom;
83
84	/**
85	 * @param Setup $userSetup
86	 * @param IUserManager $userManager
87	 * @param View $rootView
88	 * @param KeyManager $keyManager
89	 * @param Util $util
90	 * @param IConfig $config
91	 * @param IMailer $mailer
92	 * @param IL10N $l
93	 * @param QuestionHelper $questionHelper
94	 * @param ISecureRandom $secureRandom
95	 */
96	public function __construct(
97		Setup $userSetup,
98		IUserManager $userManager,
99		View $rootView,
100		KeyManager $keyManager,
101		Util $util,
102		IConfig $config,
103		IMailer $mailer,
104		IL10N $l,
105		QuestionHelper $questionHelper,
106		ISecureRandom $secureRandom
107	) {
108		$this->userSetup = $userSetup;
109		$this->userManager = $userManager;
110		$this->rootView = $rootView;
111		$this->keyManager = $keyManager;
112		$this->util = $util;
113		$this->config = $config;
114		$this->mailer = $mailer;
115		$this->l = $l;
116		$this->questionHelper = $questionHelper;
117		$this->secureRandom = $secureRandom;
118		// store one time passwords for the users
119		$this->userPasswords = [];
120	}
121
122	/**
123	 * Call this method only when no master key is created.
124	 *
125	 * @return bool true when masterkey and sharekey is created else false
126	 */
127	public function createMasterKey() {
128		$this->keyManager->setPublicShareKeyIDAndMasterKeyId();
129
130		/**
131		 * Call validateShareKey method, to check if public share exists,
132		 * else create one.
133		 */
134		$this->keyManager->validateShareKey();
135		/**
136		 * Same here, check if public masterkey exists else
137		 * create one.
138		 */
139		$this->keyManager->validateMasterKey();
140		return (!empty($this->keyManager->getPublicShareKey()) && !empty($this->keyManager->getPublicMasterKey()));
141	}
142
143	/**
144	 * start to encrypt all files
145	 *
146	 * @param InputInterface $input
147	 * @param OutputInterface $output
148	 */
149	public function encryptAll(InputInterface $input, OutputInterface $output) {
150		$this->input = $input;
151		$this->output = $output;
152
153		$headline = 'Encrypt all files with the ' . Encryption::DISPLAY_NAME;
154		$this->output->writeln("\n");
155		$this->output->writeln($headline);
156		$this->output->writeln(\str_pad('', \strlen($headline), '='));
157		$this->output->writeln("\n");
158
159		if ($this->util->isMasterKeyEnabled()) {
160			$this->output->writeln('Use master key to encrypt all files.');
161			$this->keyManager->validateMasterKey();
162		} else {
163			//create private/public keys for each user and store the private key password
164			$this->output->writeln('Create key-pair for every user');
165			$this->output->writeln('------------------------------');
166			$this->output->writeln('');
167			$this->output->writeln('This module will encrypt all files in the users files folder initially.');
168			$this->output->writeln('Already existing versions and files in the trash bin will not be encrypted.');
169			$this->output->writeln('');
170			$this->createKeyPairs();
171		}
172
173		//setup users file system and encrypt all files one by one (take should encrypt setting of storage into account)
174		$this->output->writeln("\n");
175		$this->output->writeln('Start to encrypt users files');
176		$this->output->writeln('----------------------------');
177		$this->output->writeln('');
178		$this->encryptAllUsersFiles();
179		if ($this->util->isMasterKeyEnabled() === false) {
180			//send-out or display password list and write it to a file
181			$this->output->writeln("\n");
182			$this->output->writeln('Generated encryption key passwords');
183			$this->output->writeln('----------------------------------');
184			$this->output->writeln('');
185			$this->outputPasswords();
186		}
187		$this->output->writeln("\n");
188	}
189
190	/**
191	 * create key-pair for every user
192	 */
193	protected function createKeyPairs() {
194		$this->output->writeln("\n");
195		$progress = new ProgressBar($this->output);
196		$progress->setFormat(" %message% \n [%bar%]");
197		$progress->start();
198
199		foreach ($this->userManager->getBackends() as $backend) {
200			$limit = 500;
201			$offset = 0;
202			do {
203				$users = $backend->getUsers('', $limit, $offset);
204				foreach ($users as $user) {
205					if ($this->keyManager->userHasKeys($user) === false) {
206						$progress->setMessage('Create key-pair for ' . $user);
207						$progress->advance();
208						$this->setupUserFS($user);
209						$password = $this->generateOneTimePassword($user);
210						$this->userSetup->setupUser($user, $password);
211					} else {
212						// users which already have a key-pair will be stored with a
213						// empty password and filtered out later
214						$this->userPasswords[$user] = '';
215					}
216				}
217				$offset += $limit;
218			} while (\count($users) >= $limit);
219		}
220
221		$progress->setMessage('Key-pair created for all users');
222		$progress->finish();
223	}
224
225	/**
226	 * iterate over all user and encrypt their files
227	 */
228	protected function encryptAllUsersFiles() {
229		$this->output->writeln("\n");
230		$progress = new ProgressBar($this->output);
231		$progress->setFormat(" %message% \n [%bar%]");
232		$progress->start();
233		$numberOfUsers = \count($this->userPasswords);
234		$userNo = 1;
235		if ($this->util->isMasterKeyEnabled()) {
236			$this->encryptAllUserFilesWithMasterKey($progress);
237		} else {
238			foreach ($this->userPasswords as $uid => $password) {
239				$userCount = "$uid ($userNo of $numberOfUsers)";
240				$this->encryptUsersFiles($uid, $progress, $userCount);
241				$userNo++;
242			}
243		}
244		$progress->setMessage("all files encrypted");
245		$progress->finish();
246	}
247
248	/**
249	 * encrypt all user files with the master key
250	 *
251	 * @param ProgressBar $progress
252	 */
253	protected function encryptAllUserFilesWithMasterKey(ProgressBar $progress) {
254		$userNo = 1;
255		foreach ($this->userManager->getBackends() as $backend) {
256			$limit = 500;
257			$offset = 0;
258			do {
259				$users = $backend->getUsers('', $limit, $offset);
260				foreach ($users as $user) {
261					$userCount = "$user ($userNo)";
262					$this->encryptUsersFiles($user, $progress, $userCount);
263					$userNo++;
264				}
265				$offset += $limit;
266			} while (\count($users) >= $limit);
267		}
268	}
269
270	/**
271	 * encrypt files from the given user
272	 *
273	 * @param string $uid
274	 * @param ProgressBar $progress
275	 * @param string $userCount
276	 */
277	protected function encryptUsersFiles($uid, ProgressBar $progress, $userCount) {
278		$this->setupUserFS($uid);
279		$directories = [];
280		$directories[] = '/' . $uid . '/files';
281
282		while ($root = \array_pop($directories)) {
283			$content = $this->rootView->getDirectoryContent($root);
284			foreach ($content as $file) {
285				// only encrypt files owned by the user, exclude incoming local shares, and incoming federated shares
286				if ($file->getStorage()->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) {
287					continue;
288				}
289				$path = $root . '/' . $file['name'];
290				if ($this->rootView->is_dir($path)) {
291					$directories[] = $path;
292					continue;
293				} else {
294					$progress->setMessage("encrypt files for user $userCount: $path");
295					$progress->advance();
296					if ($this->encryptFile($path) === false) {
297						$progress->setMessage("encrypt files for user $userCount: $path (already encrypted)");
298						$progress->advance();
299					}
300				}
301			}
302		}
303	}
304
305	/**
306	 * encrypt file
307	 *
308	 * @param string $path
309	 * @return bool
310	 */
311	protected function encryptFile($path) {
312		$source = $path;
313		$target = $path . '.encrypted.' . $this->getTimeStamp() . '.part';
314
315		try {
316			$version = $this->keyManager->getVersion($source, $this->rootView);
317			if ($version > 0) {
318				return false;
319			}
320			$this->rootView->copy($source, $target);
321			$this->rootView->rename($target, $source);
322		} catch (DecryptionFailedException $e) {
323			if ($this->rootView->file_exists($target)) {
324				$this->rootView->unlink($target);
325			}
326			return false;
327		}
328
329		return true;
330	}
331
332	/**
333	 * output one-time encryption passwords
334	 */
335	protected function outputPasswords() {
336		$table = new Table($this->output);
337		$table->setHeaders(['Username', 'Private key password']);
338
339		//create rows
340		$newPasswords = [];
341		$unchangedPasswords = [];
342		foreach ($this->userPasswords as $uid => $password) {
343			if (empty($password)) {
344				$unchangedPasswords[] = $uid;
345			} else {
346				$newPasswords[] = [$uid, $password];
347			}
348		}
349
350		if (empty($newPasswords)) {
351			$this->output->writeln("\nAll users already had a key-pair, no further action needed.\n");
352			return;
353		}
354
355		$table->setRows($newPasswords);
356		$table->render();
357
358		if (!empty($unchangedPasswords)) {
359			$this->output->writeln("\nThe following users already had a key-pair which was reused without setting a new password:\n");
360			foreach ($unchangedPasswords as $uid) {
361				$this->output->writeln("    $uid");
362			}
363		}
364
365		$this->writePasswordsToFile($newPasswords);
366
367		$this->output->writeln('');
368		$question = new ConfirmationQuestion('Do you want to send the passwords directly to the users by mail? (y/n) ', false);
369		if ($this->questionHelper->ask($this->input, $this->output, $question)) {
370			$this->sendPasswordsByMail();
371		}
372	}
373
374	/**
375	 * write one-time encryption passwords to a csv file
376	 *
377	 * @param array $passwords
378	 */
379	protected function writePasswordsToFile(array $passwords) {
380		$fp = $this->rootView->fopen('oneTimeEncryptionPasswords.csv', 'w');
381		foreach ($passwords as $pwd) {
382			\fputcsv($fp, $pwd);
383		}
384		\fclose($fp);
385		$this->output->writeln("\n");
386		$this->output->writeln('A list of all newly created passwords was written to data/oneTimeEncryptionPasswords.csv');
387		$this->output->writeln('');
388		$this->output->writeln('Each of these users need to login to the web interface, go to the');
389		$this->output->writeln('personal settings section "ownCloud basic encryption module" and');
390		$this->output->writeln('update the private key password to match the login password again by');
391		$this->output->writeln('entering the one-time password into the "old log-in password" field');
392		$this->output->writeln('and their current login password');
393	}
394
395	/**
396	 * setup user file system
397	 *
398	 * @param string $uid
399	 */
400	protected function setupUserFS($uid) {
401		\OC_Util::tearDownFS();
402		\OC_Util::setupFS($uid);
403	}
404
405	/**
406	 * get current timestamp
407	 *
408	 * @return int
409	 */
410	protected function getTimeStamp() {
411		return \time();
412	}
413
414	/**
415	 * generate one time password for the user and store it in a array
416	 *
417	 * @param string $uid
418	 * @return string password
419	 */
420	protected function generateOneTimePassword($uid) {
421		$password = $this->secureRandom->generate(8);
422		$this->userPasswords[$uid] = $password;
423		return $password;
424	}
425
426	/**
427	 * send encryption key passwords to the users by mail
428	 */
429	protected function sendPasswordsByMail() {
430		$noMail = [];
431
432		$this->output->writeln('');
433		$progress = new ProgressBar($this->output, \count($this->userPasswords));
434		$progress->start();
435
436		foreach ($this->userPasswords as $uid => $password) {
437			$progress->advance();
438			if (!empty($password)) {
439				$recipient = $this->userManager->get($uid);
440				$recipientDisplayName = $recipient->getDisplayName();
441				$to = $recipient->getEMailAddress();
442
443				if ($to === '') {
444					$noMail[] = $uid;
445					continue;
446				}
447
448				$subject = (string)$this->l->t('one-time password for server-side-encryption');
449				list($htmlBody, $textBody) = $this->createMailBody($password);
450
451				// send it out now
452				try {
453					$message = $this->mailer->createMessage();
454					$message->setSubject($subject);
455					$message->setTo([$to => $recipientDisplayName]);
456					$message->setHtmlBody($htmlBody);
457					$message->setPlainBody($textBody);
458					$message->setFrom([
459						\OCP\Util::getDefaultEmailAddress('admin-noreply')
460					]);
461
462					$this->mailer->send($message);
463				} catch (\Exception $e) {
464					$noMail[] = $uid;
465				}
466			}
467		}
468
469		$progress->finish();
470
471		if (empty($noMail)) {
472			$this->output->writeln("\n\nPassword successfully send to all users");
473		} else {
474			$table = new Table($this->output);
475			$table->setHeaders(['Username', 'Private key password']);
476			$this->output->writeln("\n\nCould not send password to following users:\n");
477			$rows = [];
478			foreach ($noMail as $uid) {
479				$rows[] = [$uid, $this->userPasswords[$uid]];
480			}
481			$table->setRows($rows);
482			$table->render();
483		}
484	}
485
486	/**
487	 * create mail body for plain text and html mail
488	 *
489	 * @param string $password one-time encryption password
490	 * @return array an array of the html mail body and the plain text mail body
491	 */
492	protected function createMailBody($password) {
493		$html = new \OC_Template("encryption", "mail", "");
494		$html->assign('password', $password);
495		$htmlMail = $html->fetchPage();
496
497		$plainText = new \OC_Template("encryption", "altmail", "");
498		$plainText->assign('password', $password);
499		$plainTextMail = $plainText->fetchPage();
500
501		return [$htmlMail, $plainTextMail];
502	}
503}
504