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