diff options
author | Clark Tomlinson <fallen013@gmail.com> | 2015-02-24 13:05:19 -0500 |
---|---|---|
committer | Thomas Müller <thomas.mueller@tmit.eu> | 2015-04-07 13:30:27 +0200 |
commit | 39733c8da1c12cc79b7d650edf2ea1074330ee5f (patch) | |
tree | 9d072f0ebd7c0a185c5d6afeb345b5d0ae55295e /apps | |
parent | 63e7fe608a5f507c5d2b417c45cf26589d091ebc (diff) | |
download | nextcloud-server-39733c8da1c12cc79b7d650edf2ea1074330ee5f.tar.gz nextcloud-server-39733c8da1c12cc79b7d650edf2ea1074330ee5f.zip |
Initial commit
Diffstat (limited to 'apps')
24 files changed, 2233 insertions, 226 deletions
diff --git a/apps/encryption/appinfo/app.php b/apps/encryption/appinfo/app.php new file mode 100644 index 00000000000..72e7fc42ca0 --- /dev/null +++ b/apps/encryption/appinfo/app.php @@ -0,0 +1,33 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 2/19/15, 9:52 AM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +use OCA\Encryption\AppInfo\Encryption; + +if (!OC::$CLI) { + $di = \OC::$server; + $app = new Encryption('encryption', + [], + $di->getEncryptionManager(), + $di->getConfig()); + + $app->boot(); +} + diff --git a/apps/encryption/appinfo/encryption.php b/apps/encryption/appinfo/encryption.php new file mode 100644 index 00000000000..f2ab89aadef --- /dev/null +++ b/apps/encryption/appinfo/encryption.php @@ -0,0 +1,182 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 3/11/15, 11:03 AM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\Encryption\AppInfo; + + +use OCA\Encryption\Crypto\Crypt; +use OCA\Encryption\HookManager; +use OCA\Encryption\Hooks\AppHooks; +use OCA\Encryption\Hooks\FileSystemHooks; +use OCA\Encryption\Hooks\ShareHooks; +use OCA\Encryption\Hooks\UserHooks; +use OCA\Encryption\KeyManager; +use OCA\Encryption\Migrator; +use OCA\Encryption\Recovery; +use OCA\Encryption\Users\Setup; +use OCP\App; +use OCP\AppFramework\IAppContainer; +use OCP\Encryption\IManager; +use OCP\IConfig; + + +class Encryption extends \OCP\AppFramework\App { + /** + * @var IManager + */ + private $encryptionManager; + /** + * @var IConfig + */ + private $config; + + /** + * @param $appName + * @param array $urlParams + * @param IManager $encryptionManager + * @param IConfig $config + */ + public function __construct($appName, $urlParams = array(), IManager $encryptionManager, IConfig $config) { + parent::__construct($appName, $urlParams); + $this->encryptionManager = $encryptionManager; + $this->config = $config; + } + + /** + * + */ + public function boot() { + $this->registerServices(); + $this->registerHooks(); + $this->registerEncryptionModule(); + $this->registerSettings(); + } + + /** + * + */ + public function registerHooks() { + if (!$this->config->getSystemValue('maintenance', false)) { + + $container = $this->getContainer(); + $server = $container->getServer(); + // Register our hooks and fire them. + $hookManager = new HookManager(); + + $hookManager->registerHook([ + new UserHooks($container->query('KeyManager'), + $server->getLogger(), + $container->query('UserSetup'), + $container->query('Migrator'), + $server->getUserSession()), +// new ShareHooks(), +// new FileSystemHooks(), +// new AppHooks() + ]); + + $hookManager->fireHooks(); + + } else { + // Logout user if we are in maintenance to force re-login + $this->getContainer()->getServer()->getUserSession()->logout(); + } + } + + /** + * + */ + public function registerEncryptionModule() { +// $this->encryptionManager->registerEncryptionModule(new \OCA\Encryption\Crypto\Encryption()); + } + + /** + * + */ + public function registerServices() { + $container = $this->getContainer(); + + $container->registerService('Crypt', + function (IAppContainer $c) { + $server = $c->getServer(); + return new Crypt($server->getLogger(), + $server->getUserSession(), + $server->getConfig()); + }); + + $container->registerService('KeyManager', + function (IAppContainer $c) { + $server = $c->getServer(); + + return new KeyManager($server->getEncryptionKeyStorage(), + $c->query('Crypt'), + $server->getConfig(), + $server->getUserSession()); + }); + + + $container->registerService('Recovery', + function (IAppContainer $c) { + $server = $c->getServer(); + + return new Recovery( + $server->getUserSession(), + $c->query('Crypt'), + $server->getSecureRandom(), + $c->query('KeyManager'), + $server->getConfig(), + $server->getEncryptionKeyStorage()); + }); + + $container->registerService('UserSetup', + function (IAppContainer $c) { + $server = $c->getServer(); + return new Setup($server->getLogger(), + $server->getUserSession(), + $c->query('Crypt'), + $c->query('KeyManager')); + }); + + $container->registerService('Migrator', + function (IAppContainer $c) { + $server = $c->getServer(); + + return new Migrator($server->getUserSession(), + $server->getConfig(), + $server->getUserManager(), + $server->getLogger(), + $c->query('Crypt')); + }); + + } + + /** + * + */ + public function registerSettings() { + +// script('encryption', 'encryption'); +// script('encryption', 'detect-migration'); + + + // Register settings scripts + App::registerAdmin('encryption', 'settings/settings-admin'); + App::registerPersonal('encryption', 'settings/settings-personal'); + } +} diff --git a/apps/encryption/appinfo/info.xml b/apps/encryption/appinfo/info.xml new file mode 100644 index 00000000000..e4a7d790e9c --- /dev/null +++ b/apps/encryption/appinfo/info.xml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<info> + <id>encryption</id> + <description> + This application encrypts all files accessed by ownCloud at rest, + wherever they are stored. As an example, with this application + enabled, external cloud based Amazon S3 storage will be encrypted, + protecting this data on storage outside of the control of the Admin. + When this application is enabled for the first time, all files are + encrypted as users log in and are prompted for their password. The + recommended recovery key option enables recovery of files in case + the key is lost. + Note that this app encrypts all files that are touched by ownCloud, + so external storage providers and applications such as SharePoint + will see new files encrypted when they are accessed. Encryption is + based on AES 128 or 256 bit keys. More information is available in + the Encryption documentation + </description> +<name>Encryption</name> + <license>AGPL</license> + <author>Bjoern Schiessle, Clark Tomlinson</author> + <requiremin>8</requiremin> + <shipped>true</shipped> + <documentation> + <user>user-encryption</user> + <admin>admin-encryption</admin> + </documentation> + <rememberlogin>false</rememberlogin> + <types> + <filesystem/> + </types> + <dependencies> + <lib>openssl</lib> + </dependencies> + +</info> diff --git a/apps/encryption/appinfo/routes.php b/apps/encryption/appinfo/routes.php new file mode 100644 index 00000000000..a86f3717ce9 --- /dev/null +++ b/apps/encryption/appinfo/routes.php @@ -0,0 +1,39 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 2/19/15, 11:22 AM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + + +use OCP\AppFramework\App; + +(new App('encryption'))->registerRoutes($this, array('routes' => array( + + [ + 'name' => 'recovery#adminRecovery', + 'url' => '/ajax/adminRecovery', + 'verb' => 'POST' + ], + [ + 'name' => 'recovery#userRecovery', + 'url' => '/ajax/userRecovery', + 'verb' => 'POST' + ] + + +))); diff --git a/apps/encryption/controller/recoverycontroller.php b/apps/encryption/controller/recoverycontroller.php new file mode 100644 index 00000000000..abea8993336 --- /dev/null +++ b/apps/encryption/controller/recoverycontroller.php @@ -0,0 +1,106 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 2/19/15, 11:25 AM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Encryption\Controller; + + +use OCA\Encryption\Recovery; +use OCP\AppFramework\Controller; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IRequest; +use OCP\JSON; +use Symfony\Component\HttpFoundation\JsonResponse; + +class RecoveryController extends Controller { + /** + * @var IConfig + */ + private $config; + /** + * @var IL10N + */ + private $l; + /** + * @var Recovery + */ + private $recovery; + + /** + * @param string $AppName + * @param IRequest $request + * @param IConfig $config + * @param IL10N $l10n + * @param Recovery $recovery + */ + public function __construct($AppName, IRequest $request, IConfig $config, IL10N $l10n, Recovery $recovery) { + parent::__construct($AppName, $request); + $this->config = $config; + $this->l = $l10n; + $this->recovery = $recovery; + } + + public function adminRecovery($recoveryPassword, $confirmPassword, $adminEnableRecovery) { + // Check if both passwords are the same + if (empty($recoveryPassword)) { + $errorMessage = $this->l->t('Missing recovery key password'); + return new JsonResponse(['data' => ['message' => $errorMessage]], 500); + } + + if (empty($confirmPassword)) { + $errorMessage = $this->l->t('Please repeat the recovery key password'); + return new JsonResponse(['data' => ['message' => $errorMessage]], 500); + } + + if ($recoveryPassword !== $confirmPassword) { + $errorMessage = $this->l->t('Repeated recovery key password does not match the provided recovery key password'); + return new JsonResponse(['data' => ['message' => $errorMessage]], 500); + } + + // Enable recoveryAdmin + $recoveryKeyId = $this->config->getAppValue('encryption', 'recoveryKeyId'); + + if (isset($adminEnableRecovery) && $adminEnableRecovery === '1') { + if ($this->recovery->enableAdminRecovery($recoveryKeyId, $recoveryPassword)) { + return new JsonResponse(['data' => array('message' => $this->l->t('Recovery key successfully enabled'))]); + } + return new JsonResponse(['data' => array('message' => $this->l->t('Could not enable recovery key. Please check your recovery key password!'))]); + } elseif (isset($adminEnableRecovery) && $adminEnableRecovery === '0') { + if ($this->recovery->disableAdminRecovery($recoveryKeyId, $recoveryPassword)) { + return new JsonResponse(['data' => array('message' => $this->l->t('Recovery key successfully disabled'))]); + } + return new JsonResponse(['data' => array('message' => $this->l->t('Could not disable recovery key. Please check your recovery key password!'))]); + } + } + + public function userRecovery($userEnableRecovery) { + if (isset($userEnableRecovery) && ($userEnableRecovery === '0' || $userEnableRecovery === '1')) { + $userId = $this->user->getUID(); + if ($userEnableRecovery === '1') { + // Todo xxx figure out if we need keyid's here or what. + return $this->recovery->addRecoveryKeys(); + } + // Todo xxx see :98 + return $this->recovery->removeRecoveryKeys(); + } + } + +} diff --git a/apps/encryption/hooks/apphooks.php b/apps/encryption/hooks/apphooks.php new file mode 100644 index 00000000000..713e9cadef6 --- /dev/null +++ b/apps/encryption/hooks/apphooks.php @@ -0,0 +1,37 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 2/19/15, 10:02 AM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Encryption\Hooks; + +use OCA\Encryption\Hooks\Contracts\IHook; +use OCP\Util; + +class AppHooks implements IHook { + /** + * Connects Hooks + * + * @return null + */ + public function addHooks() { + Util::connectHook('OC_App', 'pre_disable', 'OCA\Encryption\Hooks', 'preDisable'); + Util::connectHook('OC_App', 'post_disable', 'OCA\Encryption\Hooks', 'postEnable'); + } +} diff --git a/apps/encryption/hooks/contracts/ihook.php b/apps/encryption/hooks/contracts/ihook.php new file mode 100644 index 00000000000..2cc01fd7c9b --- /dev/null +++ b/apps/encryption/hooks/contracts/ihook.php @@ -0,0 +1,32 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 2/19/15, 10:03 AM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Encryption\Hooks\Contracts; + + +interface IHook { + /** + * Connects Hooks + * + * @return null + */ + public function addHooks(); +} diff --git a/apps/encryption/hooks/filesystemhooks.php b/apps/encryption/hooks/filesystemhooks.php new file mode 100644 index 00000000000..fda6b75b299 --- /dev/null +++ b/apps/encryption/hooks/filesystemhooks.php @@ -0,0 +1,46 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 2/19/15, 10:02 AM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Encryption\Hooks; + + +use OCA\Encryption\Hooks\Contracts\IHook; +use OCP\Util; + +class FileSystemHooks implements IHook { + + /** + * Connects Hooks + * + * @return null + */ + public function addHooks() { + Util::connectHook('OC_Filesystem', 'rename', 'OCA\Encryption\Hooks', 'preRename'); + Util::connectHook('OC_Filesystem', 'post_rename', 'OCA\Encryption\Hooks', 'postRenameOrCopy'); + Util::connectHook('OC_Filesystem', 'copy', 'OCA\Encryption\Hooks', 'preCopy'); + Util::connectHook('OC_Filesystem', 'post_copy', 'OCA\Encryption\Hooks', 'postRenameOrCopy'); + Util::connectHook('OC_Filesystem', 'post_delete', 'OCA\Encryption\Hooks', 'postDelete'); + Util::connectHook('OC_Filesystem', 'delete', 'OCA\Encryption\Hooks', 'preDelete'); + Util::connectHook('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', 'OCA\Encryption\Hooks', 'postPasswordReset'); + Util::connectHook('OC_Filesystem', 'post_umount', 'OCA\Encryption\Hooks', 'postUnmount'); + Util::connectHook('OC_Filesystem', 'umount', 'OCA\Encryption\Hooks', 'preUnmount'); + } +} diff --git a/apps/encryption/hooks/sharehooks.php b/apps/encryption/hooks/sharehooks.php new file mode 100644 index 00000000000..fc50712b821 --- /dev/null +++ b/apps/encryption/hooks/sharehooks.php @@ -0,0 +1,40 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 2/19/15, 10:02 AM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Encryption\Hooks; + + +use OCA\Encryption\Hooks\Contracts\IHook; +use OCP\Util; + +class ShareHooks implements IHook { + + /** + * Connects Hooks + * + * @return null + */ + public function addHooks() { + Util::connectHook('OCP\Share', 'pre_shared', 'OCA\Encryption\Hooks', 'preShared'); + Util::connectHook('OCP\Share', 'post_shared', 'OCA\Encryption\Hooks', 'postShared'); + Util::connectHook('OCP\Share', 'post_unshare', 'OCA\Encryption\Hooks', 'postUnshare'); + } +} diff --git a/apps/encryption/hooks/userhooks.php b/apps/encryption/hooks/userhooks.php new file mode 100644 index 00000000000..79de70a6d02 --- /dev/null +++ b/apps/encryption/hooks/userhooks.php @@ -0,0 +1,304 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 2/19/15, 10:02 AM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Encryption\Hooks; + + +use OCA\Encryption\Hooks\Contracts\IHook; +use OCA\Encryption\KeyManager; +use OCA\Encryption\Migrator; +use OCA\Encryption\RequirementsChecker; +use OCA\Encryption\Users\Setup; +use OCP\App; +use OCP\ILogger; +use OCP\IUserSession; +use OCP\Util; +use Test\User; + +class UserHooks implements IHook { + /** + * @var KeyManager + */ + private $keyManager; + /** + * @var ILogger + */ + private $logger; + /** + * @var Setup + */ + private $userSetup; + /** + * @var Migrator + */ + private $migrator; + /** + * @var IUserSession + */ + private $user; + + /** + * UserHooks constructor. + * + * @param KeyManager $keyManager + * @param ILogger $logger + * @param Setup $userSetup + * @param Migrator $migrator + * @param IUserSession $user + */ + public function __construct( + KeyManager $keyManager, ILogger $logger, Setup $userSetup, Migrator $migrator, IUserSession $user) { + + $this->keyManager = $keyManager; + $this->logger = $logger; + $this->userSetup = $userSetup; + $this->migrator = $migrator; + $this->user = $user; + } + + /** + * Connects Hooks + * + * @return null + */ + public function addHooks() { + Util::connectHook('OC_User', 'post_login', $this, 'login'); + Util::connectHook('OC_User', 'logout', $this, 'logout'); + Util::connectHook('OC_User', 'post_setPassword', $this, 'setPassphrase'); + Util::connectHook('OC_User', 'pre_setPassword', $this, 'preSetPassphrase'); + Util::connectHook('OC_User', 'post_createUser', $this, 'postCreateUser'); + Util::connectHook('OC_User', 'post_deleteUser', $this, 'postDeleteUser'); + } + + + /** + * Startup encryption backend upon user login + * + * @note This method should never be called for users using client side encryption + */ + public function login($params) { + + if (!App::isEnabled('encryption')) { + return true; + } + + // ensure filesystem is loaded + // Todo: update? + if (!\OC\Files\Filesystem::$loaded) { + \OC_Util::setupFS($params['uid']); + } + + // setup user, if user not ready force relogin + if (!$this->userSetup->setupUser($params['password'])) { + return false; + } + + $cache = $this->keyManager->init(); + + // Check if first-run file migration has already been performed + $ready = false; + $migrationStatus = $this->migrator->getStatus($params['uid']); + if ($migrationStatus === Migrator::$migrationOpen && $cache !== false) { + $ready = $this->migrator->beginMigration(); + } elseif ($migrationStatus === Migrator::$migrationInProgress) { + // refuse login as long as the initial encryption is running + sleep(5); + $this->user->logout(); + return false; + } + + $result = true; + + // If migration not yet done + if ($ready) { + + // Encrypt existing user files + try { + $result = $util->encryptAll('/' . $params['uid'] . '/' . 'files'); + } catch (\Exception $ex) { + \OCP\Util::writeLog('Encryption library', 'Initial encryption failed! Error: ' . $ex->getMessage(), \OCP\Util::FATAL); + $result = false; + } + + if ($result) { + \OC_Log::write( + 'Encryption library', 'Encryption of existing files belonging to "' . $params['uid'] . '" completed' + , \OC_Log::INFO + ); + // Register successful migration in DB + $util->finishMigration(); + } else { + \OCP\Util::writeLog('Encryption library', 'Initial encryption failed!', \OCP\Util::FATAL); + $util->resetMigrationStatus(); + \OCP\User::logout(); + } + } + + return $result; + } + + /** + * remove keys from session during logout + */ + public function logout() { + $session = new Session(new \OC\Files\View()); + $session->removeKeys(); + } + + /** + * setup encryption backend upon user created + * + * @note This method should never be called for users using client side encryption + */ + public function postCreateUser($params) { + + if (App::isEnabled('files_encryption')) { + $view = new \OC\Files\View('/'); + $util = new Util($view, $params['uid']); + Helper::setupUser($util, $params['password']); + } + } + + /** + * cleanup encryption backend upon user deleted + * + * @note This method should never be called for users using client side encryption + */ + public function postDeleteUser($params) { + + if (App::isEnabled('files_encryption')) { + Keymanager::deletePublicKey(new \OC\Files\View(), $params['uid']); + } + } + + /** + * If the password can't be changed within ownCloud, than update the key password in advance. + */ + public function preSetPassphrase($params) { + if (App::isEnabled('files_encryption')) { + if (!\OC_User::canUserChangePassword($params['uid'])) { + self::setPassphrase($params); + } + } + } + + /** + * Change a user's encryption passphrase + * + * @param array $params keys: uid, password + */ + public function setPassphrase($params) { + if (App::isEnabled('files_encryption') === false) { + return true; + } + + // Only attempt to change passphrase if server-side encryption + // is in use (client-side encryption does not have access to + // the necessary keys) + if (Crypt::mode() === 'server') { + + $view = new \OC\Files\View('/'); + $session = new Session($view); + + // Get existing decrypted private key + $privateKey = $session->getPrivateKey(); + + if ($params['uid'] === \OCP\User::getUser() && $privateKey) { + + // Encrypt private key with new user pwd as passphrase + $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($privateKey, $params['password'], Helper::getCipher()); + + // Save private key + if ($encryptedPrivateKey) { + Keymanager::setPrivateKey($encryptedPrivateKey, \OCP\User::getUser()); + } else { + \OCP\Util::writeLog('files_encryption', 'Could not update users encryption password', \OCP\Util::ERROR); + } + + // NOTE: Session does not need to be updated as the + // private key has not changed, only the passphrase + // used to decrypt it has changed + + + } else { // admin changed the password for a different user, create new keys and reencrypt file keys + + $user = $params['uid']; + $util = new Util($view, $user); + $recoveryPassword = isset($params['recoveryPassword']) ? $params['recoveryPassword'] : null; + + // we generate new keys if... + // ...we have a recovery password and the user enabled the recovery key + // ...encryption was activated for the first time (no keys exists) + // ...the user doesn't have any files + if (($util->recoveryEnabledForUser() && $recoveryPassword) + || !$util->userKeysExists() + || !$view->file_exists($user . '/files') + ) { + + // backup old keys + $util->backupAllKeys('recovery'); + + $newUserPassword = $params['password']; + + // make sure that the users home is mounted + \OC\Files\Filesystem::initMountPoints($user); + + $keypair = Crypt::createKeypair(); + + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + // Save public key + Keymanager::setPublicKey($keypair['publicKey'], $user); + + // Encrypt private key with new password + $encryptedKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], $newUserPassword, Helper::getCipher()); + if ($encryptedKey) { + Keymanager::setPrivateKey($encryptedKey, $user); + + if ($recoveryPassword) { // if recovery key is set we can re-encrypt the key files + $util = new Util($view, $user); + $util->recoverUsersFiles($recoveryPassword); + } + } else { + \OCP\Util::writeLog('files_encryption', 'Could not update users encryption password', \OCP\Util::ERROR); + } + + \OC_FileProxy::$enabled = $proxyStatus; + } + } + } + } + + /** + * after password reset we create a new key pair for the user + * + * @param array $params + */ + public function postPasswordReset($params) { + $uid = $params['uid']; + $password = $params['password']; + + $util = new Util(new \OC\Files\View(), $uid); + $util->replaceUserKeys($password); + } +} diff --git a/apps/encryption/lib/crypto/Encryption.php b/apps/encryption/lib/crypto/Encryption.php new file mode 100644 index 00000000000..123581b83ac --- /dev/null +++ b/apps/encryption/lib/crypto/Encryption.php @@ -0,0 +1,116 @@ +<?php + /** + * @author Clark Tomlinson <fallen013@gmail.com> + * @since 3/6/15, 2:28 PM + * @link http:/www.clarkt.com + * @copyright Clark Tomlinson © 2015 + * + */ + +namespace OCA\Encryption\Crypto; + + +use OCP\Encryption\IEncryptionModule; + +class Encryption extends Crypt implements IEncryptionModule { + + /** + * @return string defining the technical unique id + */ + public function getId() { + // TODO: Implement getId() method. + } + + /** + * In comparison to getKey() this function returns a human readable (maybe translated) name + * + * @return string + */ + public function getDisplayName() { + // TODO: Implement getDisplayName() method. + } + + /** + * start receiving chunks from a file. This is the place where you can + * perform some initial step before starting encrypting/decrypting the + * chunks + * + * @param string $path to the file + * @param array $header contains the header data read from the file + * @param array $accessList who has access to the file contains the key 'users' and 'public' + * + * $return array $header contain data as key-value pairs which should be + * written to the header, in case of a write operation + * or if no additional data is needed return a empty array + */ + public function begin($path, $header, $accessList) { + // TODO: Implement begin() method. + } + + /** + * last chunk received. This is the place where you can perform some final + * operation and return some remaining data if something is left in your + * buffer. + * + * @param string $path to the file + * @return string remained data which should be written to the file in case + * of a write operation + */ + public function end($path) { + // TODO: Implement end() method. + } + + /** + * encrypt data + * + * @param string $data you want to encrypt + * @return mixed encrypted data + */ + public function encrypt($data) { + // Todo: xxx Update Signature and usages + $this->symmetricEncryptFileContent($data); + } + + /** + * decrypt data + * + * @param string $data you want to decrypt + * @param string $user decrypt as user (null for public access) + * @return mixed decrypted data + */ + public function decrypt($data, $user) { + // Todo: xxx Update Usages? + $this->symmetricDecryptFileContent($data, $user); + } + + /** + * update encrypted file, e.g. give additional users access to the file + * + * @param string $path path to the file which should be updated + * @param array $accessList who has access to the file contains the key 'users' and 'public' + * @return boolean + */ + public function update($path, $accessList) { + // TODO: Implement update() method. + } + + /** + * should the file be encrypted or not + * + * @param string $path + * @return boolean + */ + public function shouldEncrypt($path) { + // TODO: Implement shouldEncrypt() method. + } + + /** + * calculate unencrypted size + * + * @param string $path to file + * @return integer unencrypted size + */ + public function calculateUnencryptedSize($path) { + // TODO: Implement calculateUnencryptedSize() method. + } +} diff --git a/apps/encryption/lib/crypto/crypt.php b/apps/encryption/lib/crypto/crypt.php new file mode 100644 index 00000000000..8018f11a370 --- /dev/null +++ b/apps/encryption/lib/crypto/crypt.php @@ -0,0 +1,355 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 2/19/15, 1:42 PM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Encryption\Crypto; + + +use OC\Encryption\Exceptions\DecryptionFailedException; +use OC\Encryption\Exceptions\EncryptionFailedException; +use OC\Encryption\Exceptions\GenericEncryptionException; +use OCP\IConfig; +use OCP\ILogger; +use OCP\IUser; +use OCP\IUserSession; + +class Crypt { + + const ENCRYPTION_UKNOWN_ERROR = -1; + const ENCRYPTION_NOT_INIALIZED_ERROR = 1; + const ENCRYPTIION_PRIVATE_KEY_NOT_VALID_ERROR = 2; + const ENCRYPTION_NO_SHARE_KEY_FOUND = 3; + + const BLOCKSIZE = 8192; + const DEFAULT_CIPHER = 'AES-256-CFB'; + + const HEADERSTART = 'HBEGIN'; + const HEADEREND = 'HEND'; + /** + * @var ILogger + */ + private $logger; + /** + * @var IUser + */ + private $user; + /** + * @var IConfig + */ + private $config; + + /** + * @param ILogger $logger + * @param IUserSession $userSession + * @param IConfig $config + */ + public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config) { + $this->logger = $logger; + $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser() : false; + $this->config = $config; + } + + /** + * @param null $user + * @return string + */ + public function mode($user = null) { + return 'server'; + } + + /** + * + */ + public function createKeyPair() { + + $log = $this->logger; + $res = $this->getOpenSSLPKey(); + + if (!$res) { + $log->error("Encryption Library could'nt generate users key-pair for {$this->user->getUID()}", ['app' => 'encryption']); + + if (openssl_error_string()) { + $log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(), ['app' => 'encryption']); + } + } elseif (openssl_pkey_export($res, $privateKey, null, $this->getOpenSSLConfig())) { + $keyDetails = openssl_pkey_get_details($res); + $publicKey = $keyDetails['key']; + + return [ + 'publicKey' => $publicKey, + 'privateKey' => $privateKey + ]; + } + $log->error('Encryption library couldn\'t export users private key, please check your servers openSSL configuration.' . $user->getUID(), ['app' => 'encryption']); + if (openssl_error_string()) { + $log->error('Encryption Library:' . openssl_error_string(), ['app' => 'encryption']); + } + + return false; + } + + /** + * @return resource + */ + public function getOpenSSLPKey() { + $config = $this->getOpenSSLConfig(); + return openssl_pkey_new($config); + } + + /** + * @return array + */ + private function getOpenSSLConfig() { + $config = ['private_key_bits' => 4096]; + $config = array_merge(\OC::$server->getConfig()->getSystemValue('openssl', []), $config); + return $config; + } + + /** + * @param $plainContent + * @param $passphrase + * @return bool|string + * @throws GenericEncryptionException + */ + public function symmetricEncryptFileContent($plainContent, $passphrase) { + + if (!$plainContent) { + $this->logger->error('Encryption Library, symmetrical encryption failed no content given', ['app' => 'encryption']); + return false; + } + + $iv = $this->generateIv(); + + try { + $encryptedContent = $this->encrypt($plainContent, $iv, $passphrase, $this->getCipher()); + // combine content to encrypt the IV identifier and actual IV + $catFile = $this->concatIV($encryptedContent, $iv); + $padded = $this->addPadding($catFile); + + return $padded; + } catch (EncryptionFailedException $e) { + $message = 'Could not encrypt file content (code: ' . $e->getCode() . '): '; + $this->logger->error('files_encryption' . $message . $e->getMessage(), ['app' => 'encryption']); + return false; + } + + } + + /** + * @param $plainContent + * @param $iv + * @param string $passphrase + * @param string $cipher + * @return string + * @throws EncryptionFailedException + */ + private function encrypt($plainContent, $iv, $passphrase = '', $cipher = self::DEFAULT_CIPHER) { + $encryptedContent = openssl_encrypt($plainContent, $cipher, $passphrase, false, $iv); + + if (!$encryptedContent) { + $error = 'Encryption (symmetric) of content failed'; + $this->logger->error($error . openssl_error_string(), ['app' => 'encryption']); + throw new EncryptionFailedException($error); + } + + return $encryptedContent; + } + + /** + * @return mixed|string + */ + public function getCipher() { + $cipher = $this->config->getSystemValue('cipher', self::DEFAULT_CIPHER); + if ($cipher !== 'AES-256-CFB' || $cipher !== 'AES-128-CFB') { + $this->logger->warning('Wrong cipher defined in config.php only AES-128-CFB and AES-256-CFB are supported. Fall back' . self::DEFAULT_CIPHER, ['app' => 'encryption']); + $cipher = self::DEFAULT_CIPHER; + } + + return $cipher; + } + + /** + * @param $encryptedContent + * @param $iv + * @return string + */ + private function concatIV($encryptedContent, $iv) { + return $encryptedContent . '00iv00' . $iv; + } + + /** + * @param $data + * @return string + */ + private function addPadding($data) { + return $data . 'xx'; + } + + /** + * @param $recoveryKey + * @param $password + * @return bool|string + */ + public function decryptPrivateKey($recoveryKey, $password) { + + $header = $this->parseHeader($recoveryKey); + $cipher = $this->getCipher($header); + + // If we found a header we need to remove it from the key we want to decrypt + if (!empty($header)) { + $recoveryKey = substr($recoveryKey, strpos($recoveryKey, self::HEADEREND) + strlen(self::HEADERSTART)); + } + + $plainKey = $this->symmetricDecryptFileContent($recoveryKey, $password, $cipher); + + // Check if this is a valid private key + $res = openssl_get_privatekey($plainKey); + if (is_resource($res)) { + $sslInfo = openssl_pkey_get_details($res); + if (!isset($sslInfo['key'])) { + return false; + } + } else { + return false; + } + + return $plainKey; + } + + /** + * @param $keyFileContents + * @param string $passphrase + * @param string $cipher + * @return bool|string + * @throws DecryptionFailedException + */ + public function symmetricDecryptFileContent($keyFileContents, $passphrase = '', $cipher = self::DEFAULT_CIPHER) { + // Remove Padding + $noPadding = $this->removePadding($keyFileContents); + + $catFile = $this->splitIv($noPadding); + + $plainContent = $this->decrypt($catFile['encrypted'], $catFile['iv'], $passphrase, $cipher); + + if ($plainContent) { + return $plainContent; + } + + return false; + } + + /** + * @param $padded + * @return bool|string + */ + private function removePadding($padded) { + if (substr($padded, -2) === 'xx') { + return substr($padded, 0, -2); + } + return false; + } + + /** + * @param $catFile + * @return array + */ + private function splitIv($catFile) { + // Fetch encryption metadata from end of file + $meta = substr($catFile, -22); + + // Fetch IV from end of file + $iv = substr($meta, -16); + + // Remove IV and IV Identifier text to expose encrypted content + + $encrypted = substr($catFile, 0, -22); + + return [ + 'encrypted' => $encrypted, + 'iv' => $iv + ]; + } + + /** + * @param $encryptedContent + * @param $iv + * @param string $passphrase + * @param string $cipher + * @return string + * @throws DecryptionFailedException + */ + private function decrypt($encryptedContent, $iv, $passphrase = '', $cipher = self::DEFAULT_CIPHER) { + $plainContent = openssl_decrypt($encryptedContent, $cipher, $passphrase, false, $iv); + + if ($plainContent) { + return $plainContent; + } else { + throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed'); + } + } + + /** + * @param $data + * @return array + */ + private function parseHeader($data) { + $result = []; + + if (substr($data, 0, strlen(self::HEADERSTART)) === self::HEADERSTART) { + $endAt = strpos($data, self::HEADEREND); + $header = substr($data, 0, $endAt + strlen(self::HEADEREND)); + + // +1 not to start with an ':' which would result in empty element at the beginning + $exploded = explode(':', substr($header, strlen(self::HEADERSTART) + 1)); + + $element = array_shift($exploded); + + while ($element != self::HEADEREND) { + $result[$element] = array_shift($exploded); + $element = array_shift($exploded); + } + } + + return $result; + } + + /** + * @return string + * @throws GenericEncryptionException + */ + private function generateIv() { + $random = openssl_random_pseudo_bytes(12, $strong); + if ($random) { + if (!$strong) { + // If OpenSSL indicates randomness is insecure log error + $this->logger->error('Encryption Library: Insecure symmetric key was generated using openssl_random_psudo_bytes()', ['app' => 'encryption']); + } + + /* + * We encode the iv purely for string manipulation + * purposes -it gets decoded before use + */ + return base64_encode($random); + } + // If we ever get here we've failed anyway no need for an else + throw new GenericEncryptionException('Generating IV Failed'); + } +} + diff --git a/apps/encryption/lib/hookmanager.php b/apps/encryption/lib/hookmanager.php new file mode 100644 index 00000000000..a535230a6a7 --- /dev/null +++ b/apps/encryption/lib/hookmanager.php @@ -0,0 +1,64 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 2/19/15, 10:13 AM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Encryption; + + +use OCA\Encryption\Hooks\Contracts\IHook; + +class HookManager { + + private $hookInstances = []; + + /** + * @param array|IHook $instances + * - This accepts either a single instance of IHook or an array of instances of IHook + * @return bool + */ + public function registerHook($instances) { + if (is_array($instances)) { + foreach ($instances as $instance) { + if (!$instance instanceof IHook) { + return false; + } + $this->hookInstances[] = $instance; + return true; + } + + } + $this->hookInstances[] = $instances; + return true; + } + + /** + * + */ + public function fireHooks() { + foreach ($this->hookInstances as $instance) { + /** + * @var $instance IHook + */ + $instance->addHooks(); + } + + } + +} diff --git a/apps/encryption/lib/keymanager.php b/apps/encryption/lib/keymanager.php new file mode 100644 index 00000000000..272bf0849c2 --- /dev/null +++ b/apps/encryption/lib/keymanager.php @@ -0,0 +1,217 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 2/19/15, 1:20 PM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Encryption; + + +use OC\Encryption\Exceptions\PrivateKeyMissingException; +use OC\Encryption\Exceptions\PublicKeyMissingException; +use OCA\Encryption\Crypto\Crypt; +use OCP\Encryption\IKeyStorage; +use OCP\IConfig; +use OCP\IUser; +use OCP\IUserSession; + +class KeyManager { + + /** + * @var IKeyStorage + */ + private $keyStorage; + + /** + * @var Crypt + */ + private $crypt; + /** + * @var string + */ + private $recoveryKeyId; + /** + * @var string + */ + private $publicShareKeyId; + /** + * @var string UserID + */ + private $keyId; + + /** + * @var string + */ + private $publicKeyId = '.public'; + /** + * @var string + */ + private $privateKeyId = '.private'; + /** + * @var IConfig + */ + private $config; + + /** + * @param IKeyStorage $keyStorage + * @param Crypt $crypt + * @param IConfig $config + * @param IUserSession $userSession + */ + public function __construct(IKeyStorage $keyStorage, Crypt $crypt, IConfig $config, IUserSession $userSession) { + + $this->keyStorage = $keyStorage; + $this->crypt = $crypt; + $this->config = $config; + $this->recoveryKeyId = $this->config->getAppValue('encryption', 'recoveryKeyId'); + $this->publicShareKeyId = $this->config->getAppValue('encryption', 'publicShareKeyId'); + $this->keyId = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : false; + + } + + /** + * @param $userId + * @return mixed + * @throws PrivateKeyMissingException + */ + public function getPrivateKey($userId) { + $privateKey = $this->keyStorage->getUserKey($userId, $this->privateKeyId); + + if (strlen($privateKey) !== 0) { + return $privateKey; + } + throw new PrivateKeyMissingException(); + } + + /** + * @param $userId + * @return mixed + * @throws PublicKeyMissingException + */ + public function getPublicKey($userId) { + $publicKey = $this->keyStorage->getUserKey($userId, $this->publicKeyId); + + if (strlen($publicKey) !== 0) { + return $publicKey; + } + throw new PublicKeyMissingException(); + } + + /** + * @return bool + */ + public function recoveryKeyExists() { + return (strlen($this->keyStorage->getSystemUserKey($this->recoveryKeyId)) !== 0); + } + + /** + * @param $userId + * @return bool + */ + public function userHasKeys($userId) { + try { + $this->getPrivateKey($userId); + $this->getPublicKey($userId); + } catch (PrivateKeyMissingException $e) { + return false; + } catch (PublicKeyMissingException $e) { + return false; + } + return true; + } + + /** + * @param $password + * @return bool + */ + public function checkRecoveryPassword($password) { + $recoveryKey = $this->keyStorage->getSystemUserKey($this->recoveryKeyId); + $decryptedRecoveryKey = $this->crypt->decryptPrivateKey($recoveryKey, $password); + + if ($decryptedRecoveryKey) { + return true; + } + return false; + } + + /** + * @param $userId + * @param $key + * @return bool + */ + public function setPublicKey($userId, $key) { + return $this->keyStorage->setUserKey($userId, $this->publicKeyId, $key); + } + + /** + * @param $userId + * @param $key + * @return bool + */ + public function setPrivateKey($userId, $key) { + return $this->keyStorage->setUserKey($userId, $this->privateKeyId, $key); + } + + + /** + * @param $password + * @param $keyPair + * @return bool + */ + public function storeKeyPair($password, $keyPair) { + // Save Public Key + $this->setPublicKey($this->keyId, $keyPair['publicKey']); + + $encryptedKey = $this->crypt->symmetricEncryptFileContent($keyPair['privateKey'], $password); + + if ($encryptedKey) { + $this->setPrivateKey($this->keyId, $encryptedKey); + $this->config->setAppValue('encryption', 'recoveryAdminEnabled', 1); + return true; + } + return false; + } + + /** + * @return bool + */ + public function ready() { + return $this->keyStorage->ready(); + } + + + /** + * @return \OCP\ICache + * @throws PrivateKeyMissingException + */ + public function init() { + try { + $privateKey = $this->getPrivateKey($this->keyId); + } catch (PrivateKeyMissingException $e) { + return false; + } + + $cache = \OC::$server->getMemCacheFactory(); + + $cacheInstance = $cache->create('Encryption'); + $cacheInstance->set('privateKey', $privateKey); + + return $cacheInstance; + } + +} diff --git a/apps/encryption/lib/migrator.php b/apps/encryption/lib/migrator.php new file mode 100644 index 00000000000..8f7823cb1ae --- /dev/null +++ b/apps/encryption/lib/migrator.php @@ -0,0 +1,123 @@ +<?php +/** + * @author Clark Tomlinson <fallen013@gmail.com> + * @since 3/9/15, 2:44 PM + * @link http:/www.clarkt.com + * @copyright Clark Tomlinson © 2015 + * + */ + +namespace OCA\Encryption; + + +use OCA\Encryption\Crypto\Crypt; +use OCP\IConfig; +use OCP\ILogger; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\PreConditionNotMetException; + +class Migrator { + + /** + * @var bool + */ + private $status = false; + /** + * @var IUserManager + */ + private $user; + /** + * @var IConfig + */ + private $config; + /** + * @var string + */ + public static $migrationOpen = '0'; + /** + * @var string + */ + public static $migrationInProgress = '-1'; + /** + * @var string + */ + public static $migrationComplete = '1'; + /** + * @var IUserManager + */ + private $userManager; + /** + * @var ILogger + */ + private $log; + /** + * @var Crypt + */ + private $crypt; + + /** + * Migrator constructor. + * + * @param IUserSession $userSession + * @param IConfig $config + * @param IUserManager $userManager + * @param ILogger $log + * @param Crypt $crypt + */ + public function __construct(IUserSession $userSession, IConfig $config, IUserManager $userManager, ILogger $log, Crypt $crypt) { + $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser() : false; + $this->config = $config; + $this->userManager = $userManager; + $this->log = $log; + $this->crypt = $crypt; + } + + /** + * @param $userId + * @return bool|string + */ + public function getStatus($userId) { + if ($this->userManager->userExists($userId)) { + $this->status = $this->config->getUserValue($userId, 'encryption', 'migrationStatus', false); + + if (!$this->status) { + $this->config->setUserValue($userId, 'encryption', 'migrationStatus', self::$migrationOpen); + $this->status = self::$migrationOpen; + } + } + + return $this->status; + } + + /** + * @return bool + */ + public function beginMigration() { + $status = $this->setMigrationStatus(self::$migrationInProgress, self::$migrationOpen); + + if ($status) { + $this->log->info('Encryption Library Start migration to encrypt for ' . $this->user->getUID()); + return $status; + } + $this->log->warning('Encryption Library Could not activate migration for ' . $this->user->getUID() . '. Probably another process already started the inital encryption'); + return $status; + } + + /** + * @param $status + * @param bool $preCondition + * @return bool + */ + private function setMigrationStatus($status, $preCondition = false) { + // Convert to string if preCondition is set + $preCondition = ($preCondition === false) ? false : (string)$preCondition; + + try { + $this->config->setUserValue($this->user->getUID(), 'encryption', 'migrationStatus', (string)$status, $preCondition); + return true; + } catch (PreConditionNotMetException $e) { + return false; + } + } +} diff --git a/apps/encryption/lib/recovery.php b/apps/encryption/lib/recovery.php new file mode 100644 index 00000000000..88350e96c53 --- /dev/null +++ b/apps/encryption/lib/recovery.php @@ -0,0 +1,134 @@ +<?php +/** + * @author Clark Tomlinson <clark@owncloud.com> + * @since 2/19/15, 11:45 AM + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Encryption; + + +use OC\Files\View; +use OCA\Encryption\Crypto\Crypt; +use OCP\Encryption\IKeyStorage; +use OCP\IConfig; +use OCP\IUser; +use OCP\Security\ISecureRandom; + +class Recovery { + + + /** + * @var null|IUser + */ + protected $user; + /** + * @var Crypt + */ + protected $crypt; + /** + * @var ISecureRandom + */ + private $random; + /** + * @var KeyManager + */ + private $keyManager; + /** + * @var IConfig + */ + private $config; + /** + * @var IEncryptionKeyStorage + */ + private $keyStorage; + + /** + * @param IUser $user + * @param Crypt $crypt + * @param ISecureRandom $random + * @param KeyManager $keyManager + * @param IConfig $config + * @param IKeyStorage $keyStorage + */ + public function __construct(IUser $user, + Crypt $crypt, + ISecureRandom $random, + KeyManager $keyManager, + IConfig $config, + IKeyStorage $keyStorage) { + $this->user = $user; + $this->crypt = $crypt; + $this->random = $random; + $this->keyManager = $keyManager; + $this->config = $config; + $this->keyStorage = $keyStorage; + } + + /** + * @param $recoveryKeyId + * @param $password + * @return bool + */ + public function enableAdminRecovery($recoveryKeyId, $password) { + $appConfig = $this->config; + + if ($recoveryKeyId === null) { + $recoveryKeyId = $this->random->getLowStrengthGenerator(); + $appConfig->setAppValue('encryption', 'recoveryKeyId', $recoveryKeyId); + } + + $keyManager = $this->keyManager; + + if (!$keyManager->recoveryKeyExists()) { + $keyPair = $this->crypt->createKeyPair(); + + return $this->keyManager->storeKeyPair($password, $keyPair); + } + + if ($keyManager->checkRecoveryPassword($password)) { + $appConfig->setAppValue('encryption', 'recoveryAdminEnabled', 1); + return true; + } + + return false; + } + + /** + * @param $recoveryPassword + * @return bool + */ + public function disableAdminRecovery($recoveryPassword) { + $keyManager = $this->keyManager; + + if ($keyManager->checkRecoveryPassword($recoveryPassword)) { + // Set recoveryAdmin as disabled + $this->config->setAppValue('encryption', 'recoveryAdminEnabled', 0); + return true; + } + return false; + } + + public function addRecoveryKeys($keyId) { + // No idea new way to do this.... + } + + public function removeRecoveryKeys() { + // No idea new way to do this.... + } + +} diff --git a/apps/encryption/lib/setup.php b/apps/encryption/lib/setup.php new file mode 100644 index 00000000000..cc8f00f0a40 --- /dev/null +++ b/apps/encryption/lib/setup.php @@ -0,0 +1,38 @@ +<?php +/** + * @author Clark Tomlinson <fallen013@gmail.com> + * @since 3/6/15, 11:30 AM + * @link http:/www.clarkt.com + * @copyright Clark Tomlinson © 2015 + * + */ + +namespace OCA\Encryption; + + +use OCP\ILogger; +use OCP\IUser; +use OCP\IUserSession; + +class Setup { + /** + * @var ILogger + */ + protected $logger; + /** + * @var IUser + */ + protected $user; + + /** + * Setup constructor. + * + * @param ILogger $logger + * @param IUserSession $userSession + */ + public function __construct(ILogger $logger, IUserSession $userSession) { + $this->logger = $logger; + $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : false; + + } +} diff --git a/apps/encryption/lib/users/setup.php b/apps/encryption/lib/users/setup.php new file mode 100644 index 00000000000..123d6973be9 --- /dev/null +++ b/apps/encryption/lib/users/setup.php @@ -0,0 +1,63 @@ +<?php +/** + * @author Clark Tomlinson <fallen013@gmail.com> + * @since 3/6/15, 11:36 AM + * @link http:/www.clarkt.com + * @copyright Clark Tomlinson © 2015 + * + */ + +namespace OCA\Encryption\Users; + + +use OCA\Encryption\Crypto\Crypt; +use OCA\Encryption\KeyManager; +use OCP\ILogger; +use OCP\IUserSession; + +class Setup extends \OCA\Encryption\Setup { + /** + * @var Crypt + */ + private $crypt; + /** + * @var KeyManager + */ + private $keyManager; + + + /** + * @param ILogger $logger + * @param IUserSession $userSession + * @param Crypt $crypt + * @param KeyManager $keyManager + */ + public function __construct(ILogger $logger, IUserSession $userSession, Crypt $crypt, KeyManager $keyManager) { + parent::__construct($logger, $userSession); + $this->crypt = $crypt; + $this->keyManager = $keyManager; + } + + /** + * @param $password + * @return bool + */ + public function setupUser($password) { + if ($this->keyManager->ready()) { + $this->logger->debug('Encryption Library: User Account ' . $this->user->getUID() . ' Is not ready for encryption; configuration started'); + return $this->setupServerSide($password); + } + } + + /** + * @param $password + * @return bool + */ + private function setupServerSide($password) { + // Check if user already has keys + if (!$this->keyManager->userHasKeys($this->user->getUID())) { + return $this->keyManager->storeKeyPair($password, $this->crypt->createKeyPair()); + } + return true; + } +} diff --git a/apps/encryption/settings/settings-admin.php b/apps/encryption/settings/settings-admin.php new file mode 100644 index 00000000000..0f5d56a3734 --- /dev/null +++ b/apps/encryption/settings/settings-admin.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright (c) 2011 Robin Appelman <icewind@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +\OC_Util::checkAdminUser(); + +$tmpl = new OCP\Template('files_encryption', 'settings-admin'); + +// Check if an adminRecovery account is enabled for recovering files after lost pwd +$recoveryAdminEnabled = \OC::$server->getAppConfig()->getValue('files_encryption', 'recoveryAdminEnabled', '0'); +$session = new \OCA\Files_Encryption\Session(new \OC\Files\View('/')); +$initStatus = $session->getInitialized(); + +$tmpl->assign('recoveryEnabled', $recoveryAdminEnabled); +$tmpl->assign('initStatus', $initStatus); + +\OCP\Util::addscript('files_encryption', 'settings-admin'); +\OCP\Util::addscript('core', 'multiselect'); + +return $tmpl->fetchPage(); diff --git a/apps/encryption/settings/settings-personal.php b/apps/encryption/settings/settings-personal.php new file mode 100644 index 00000000000..fe2d846f50a --- /dev/null +++ b/apps/encryption/settings/settings-personal.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright (c) 2013 Sam Tuke <samtuke@owncloud.com> + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +// Add CSS stylesheet +\OC_Util::addStyle('files_encryption', 'settings-personal'); + +$tmpl = new OCP\Template('files_encryption', 'settings-personal'); + +$user = \OCP\USER::getUser(); +$view = new \OC\Files\View('/'); +$util = new \OCA\Files_Encryption\Util($view, $user); +$session = new \OCA\Files_Encryption\Session($view); + +$privateKeySet = $session->getPrivateKey() !== false; +// did we tried to initialize the keys for this session? +$initialized = $session->getInitialized(); + +$recoveryAdminEnabled = \OC::$server->getAppConfig()->getValue('files_encryption', 'recoveryAdminEnabled'); +$recoveryEnabledForUser = $util->recoveryEnabledForUser(); + +$result = false; + +if ($recoveryAdminEnabled || !$privateKeySet) { + + \OCP\Util::addscript('files_encryption', 'settings-personal'); + + $tmpl->assign('recoveryEnabled', $recoveryAdminEnabled); + $tmpl->assign('recoveryEnabledForUser', $recoveryEnabledForUser); + $tmpl->assign('privateKeySet', $privateKeySet); + $tmpl->assign('initialized', $initialized); + + $result = $tmpl->fetchPage(); +} + +return $result; + diff --git a/apps/encryption/tests/lib/KeyManagerTest.php b/apps/encryption/tests/lib/KeyManagerTest.php new file mode 100644 index 00000000000..260e69a73bf --- /dev/null +++ b/apps/encryption/tests/lib/KeyManagerTest.php @@ -0,0 +1,90 @@ +<?php +/** + * @author Clark Tomlinson <fallen013@gmail.com> + * @since 3/5/15, 10:53 AM + * @link http:/www.clarkt.com + * @copyright Clark Tomlinson © 2015 + * + */ + +namespace OCA\Encryption\Tests; + + +use OCA\Encryption\KeyManager; +use Test\TestCase; + +class KeyManagerTest extends TestCase { + /** + * @var KeyManager + */ + private $instance; + /** + * @var + */ + private $userId; + /** + * @var + */ + private $dummyKeys; + + public function setUp() { + parent::setUp(); + $keyStorageMock = $this->getMock('OCP\Encryption\IKeyStorage'); + $cryptMock = $this->getMockBuilder('OCA\Encryption\Crypt') + ->disableOriginalConstructor() + ->getMock(); + $configMock = $this->getMock('OCP\IConfig'); + $userMock = $this->getMock('OCP\IUser'); + $userMock->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('admin')); + $this->userId = 'admin'; + $this->instance = new KeyManager($keyStorageMock, $cryptMock, $configMock, $userMock); + + $this->dummyKeys = ['public' => 'randomweakpublickeyhere', + 'private' => 'randomweakprivatekeyhere']; + } + + /** + * @expectedException OC\Encryption\Exceptions\PrivateKeyMissingException + */ + public function testGetPrivateKey() { + $this->assertFalse($this->instance->getPrivateKey($this->userId)); + } + + /** + * @expectedException OC\Encryption\Exceptions\PublicKeyMissingException + */ + public function testGetPublicKey() { + $this->assertFalse($this->instance->getPublicKey($this->userId)); + } + + /** + * + */ + public function testRecoveryKeyExists() { + $this->assertFalse($this->instance->recoveryKeyExists()); + } + + /** + * + */ + public function testCheckRecoveryKeyPassword() { + $this->assertFalse($this->instance->checkRecoveryPassword('pass')); + } + + public function testSetPublicKey() { + + $this->assertTrue($this->instance->setPublicKey($this->userId, $this->dummyKeys['public'])); + } + + public function testSetPrivateKey() { + $this->assertTrue($this->instance->setPrivateKey($this->userId, $this->dummyKeys['private'])); + } + + public function testUserHasKeys() { + $this->assertFalse($this->instance->userHasKeys($this->userId)); + } + + +} diff --git a/apps/encryption/tests/lib/MigratorTest.php b/apps/encryption/tests/lib/MigratorTest.php new file mode 100644 index 00000000000..a9d57b34209 --- /dev/null +++ b/apps/encryption/tests/lib/MigratorTest.php @@ -0,0 +1,62 @@ +<?php +/** + * @author Clark Tomlinson <fallen013@gmail.com> + * @since 3/9/15, 2:56 PM + * @link http:/www.clarkt.com + * @copyright Clark Tomlinson © 2015 + * + */ + +namespace OCA\Encryption\Tests; + + +use OCA\Encryption\Migrator; +use Test\TestCase; + +class MigratorTest extends TestCase { + + /** + * @var Migrator + */ + private $instance; + + /** + * + */ + public function testGetStatus() { + $this->assertFalse($this->instance->getStatus('admin')); + } + + /** + * + */ + public function testBeginMigration() { + $this->assertTrue($this->instance->beginMigration()); + } + + /** + * + */ + public function testSetMigrationStatus() { + $this->assertTrue(\Test_Helper::invokePrivate($this->instance, + 'setMigrationStatus', + ['0', '-1']) + ); + } + + /** + * + */ + protected function setUp() { + parent::setUp(); + + $cryptMock = $this->getMockBuilder('OCA\Encryption\Crypto\Crypt')->disableOriginalConstructor()->getMock(); + $this->instance = new Migrator($this->getMock('OCP\IUser'), + $this->getMock('OCP\IConfig'), + $this->getMock('OCP\IUserManager'), + $this->getMock('OCP\ILogger'), + $cryptMock); + } + + +} diff --git a/apps/encryption/tests/lib/RequirementsCheckerTest.php b/apps/encryption/tests/lib/RequirementsCheckerTest.php new file mode 100644 index 00000000000..97ddd76b750 --- /dev/null +++ b/apps/encryption/tests/lib/RequirementsCheckerTest.php @@ -0,0 +1,51 @@ +<?php +/** + * @author Clark Tomlinson <fallen013@gmail.com> + * @since 3/6/15, 10:36 AM + * @link http:/www.clarkt.com + * @copyright Clark Tomlinson © 2015 + * + */ + +namespace OCA\Encryption\Tests; + + +use OCA\Encryption\RequirementsChecker; +use Test\TestCase; + +class RequirementsCheckerTest extends TestCase { + /** + * @var RequirementsChecker + */ + private $instance; + + /** + * + */ + protected function setUp() { + parent::setUp(); + $log = $this->getMock('OCP\ILogger'); + $crypt = $this->getMockBuilder('OCA\Encryption\Crypt') + ->disableOriginalConstructor() + ->getMock(); + $crypt + ->method('getOpenSSLPkey') + ->will($this->returnValue(true)); + $this->instance = new RequirementsChecker($crypt, $log); + } + + /** + * + */ + public function testCanCheckConfigration() { + $this->assertTrue($this->instance->checkConfiguration()); + } + + /** + * + */ + public function testCanCheckRequiredExtensions() { + $this->assertTrue($this->instance->checkExtensions()); + } + +} diff --git a/apps/files_encryption/lib/hooks.php b/apps/files_encryption/lib/hooks.php index 4a29ffaaedf..536e512bdb2 100644 --- a/apps/files_encryption/lib/hooks.php +++ b/apps/files_encryption/lib/hooks.php @@ -34,234 +34,8 @@ class Hooks { // file for which we want to delete the keys after the delete operation was successful private static $unmountedFiles = array(); - /** - * Startup encryption backend upon user login - * @note This method should never be called for users using client side encryption - */ - public static function login($params) { - - if (\OCP\App::isEnabled('files_encryption') === false) { - return true; - } - - - $l = new \OC_L10N('files_encryption'); - - $view = new \OC\Files\View('/'); - - // ensure filesystem is loaded - if (!\OC\Files\Filesystem::$loaded) { - \OC_Util::setupFS($params['uid']); - } - - $privateKey = Keymanager::getPrivateKey($view, $params['uid']); - - // if no private key exists, check server configuration - if (!$privateKey) { - //check if all requirements are met - if (!Helper::checkRequirements() || !Helper::checkConfiguration()) { - $error_msg = $l->t("Missing requirements."); - $hint = $l->t('Please make sure that OpenSSL together with the PHP extension is enabled and configured properly. For now, the encryption app has been disabled.'); - \OC_App::disable('files_encryption'); - \OCP\Util::writeLog('Encryption library', $error_msg . ' ' . $hint, \OCP\Util::ERROR); - \OCP\Template::printErrorPage($error_msg, $hint); - } - } - - $util = new Util($view, $params['uid']); - - // setup user, if user not ready force relogin - if (Helper::setupUser($util, $params['password']) === false) { - return false; - } - - $session = $util->initEncryption($params); - - // Check if first-run file migration has already been performed - $ready = false; - $migrationStatus = $util->getMigrationStatus(); - if ($migrationStatus === Util::MIGRATION_OPEN && $session !== false) { - $ready = $util->beginMigration(); - } elseif ($migrationStatus === Util::MIGRATION_IN_PROGRESS) { - // refuse login as long as the initial encryption is running - sleep(5); - \OCP\User::logout(); - return false; - } - - $result = true; - - // If migration not yet done - if ($ready) { - - // Encrypt existing user files - try { - $result = $util->encryptAll('/' . $params['uid'] . '/' . 'files'); - } catch (\Exception $ex) { - \OCP\Util::writeLog('Encryption library', 'Initial encryption failed! Error: ' . $ex->getMessage(), \OCP\Util::FATAL); - $result = false; - } - - if ($result) { - \OC_Log::write( - 'Encryption library', 'Encryption of existing files belonging to "' . $params['uid'] . '" completed' - , \OC_Log::INFO - ); - // Register successful migration in DB - $util->finishMigration(); - } else { - \OCP\Util::writeLog('Encryption library', 'Initial encryption failed!', \OCP\Util::FATAL); - $util->resetMigrationStatus(); - \OCP\User::logout(); - } - } - - return $result; - } - - /** - * remove keys from session during logout - */ - public static function logout() { - $session = new Session(new \OC\Files\View()); - $session->removeKeys(); - } - - /** - * setup encryption backend upon user created - * @note This method should never be called for users using client side encryption - */ - public static function postCreateUser($params) { - - if (\OCP\App::isEnabled('files_encryption')) { - $view = new \OC\Files\View('/'); - $util = new Util($view, $params['uid']); - Helper::setupUser($util, $params['password']); - } - } - - /** - * cleanup encryption backend upon user deleted - * @note This method should never be called for users using client side encryption - */ - public static function postDeleteUser($params) { - - if (\OCP\App::isEnabled('files_encryption')) { - Keymanager::deletePublicKey(new \OC\Files\View(), $params['uid']); - } - } - - /** - * If the password can't be changed within ownCloud, than update the key password in advance. - */ - public static function preSetPassphrase($params) { - if (\OCP\App::isEnabled('files_encryption')) { - if ( ! \OC_User::canUserChangePassword($params['uid']) ) { - self::setPassphrase($params); - } - } - } - - /** - * Change a user's encryption passphrase - * @param array $params keys: uid, password - */ - public static function setPassphrase($params) { - if (\OCP\App::isEnabled('files_encryption') === false) { - return true; - } - - // Only attempt to change passphrase if server-side encryption - // is in use (client-side encryption does not have access to - // the necessary keys) - if (Crypt::mode() === 'server') { - - $view = new \OC\Files\View('/'); - $session = new Session($view); - - // Get existing decrypted private key - $privateKey = $session->getPrivateKey(); - - if ($params['uid'] === \OCP\User::getUser() && $privateKey) { - - // Encrypt private key with new user pwd as passphrase - $encryptedPrivateKey = Crypt::symmetricEncryptFileContent($privateKey, $params['password'], Helper::getCipher()); - - // Save private key - if ($encryptedPrivateKey) { - Keymanager::setPrivateKey($encryptedPrivateKey, \OCP\User::getUser()); - } else { - \OCP\Util::writeLog('files_encryption', 'Could not update users encryption password', \OCP\Util::ERROR); - } - - // NOTE: Session does not need to be updated as the - // private key has not changed, only the passphrase - // used to decrypt it has changed - } else { // admin changed the password for a different user, create new keys and reencrypt file keys - - $user = $params['uid']; - $util = new Util($view, $user); - $recoveryPassword = isset($params['recoveryPassword']) ? $params['recoveryPassword'] : null; - - // we generate new keys if... - // ...we have a recovery password and the user enabled the recovery key - // ...encryption was activated for the first time (no keys exists) - // ...the user doesn't have any files - if (($util->recoveryEnabledForUser() && $recoveryPassword) - || !$util->userKeysExists() - || !$view->file_exists($user . '/files')) { - - // backup old keys - $util->backupAllKeys('recovery'); - - $newUserPassword = $params['password']; - - // make sure that the users home is mounted - \OC\Files\Filesystem::initMountPoints($user); - - $keypair = Crypt::createKeypair(); - - // Disable encryption proxy to prevent recursive calls - $proxyStatus = \OC_FileProxy::$enabled; - \OC_FileProxy::$enabled = false; - - // Save public key - Keymanager::setPublicKey($keypair['publicKey'], $user); - - // Encrypt private key with new password - $encryptedKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], $newUserPassword, Helper::getCipher()); - if ($encryptedKey) { - Keymanager::setPrivateKey($encryptedKey, $user); - - if ($recoveryPassword) { // if recovery key is set we can re-encrypt the key files - $util = new Util($view, $user); - $util->recoverUsersFiles($recoveryPassword); - } - } else { - \OCP\Util::writeLog('files_encryption', 'Could not update users encryption password', \OCP\Util::ERROR); - } - - \OC_FileProxy::$enabled = $proxyStatus; - } - } - } - } - - /** - * after password reset we create a new key pair for the user - * - * @param array $params - */ - public static function postPasswordReset($params) { - $uid = $params['uid']; - $password = $params['password']; - - $util = new Util(new \OC\Files\View(), $uid); - $util->replaceUserKeys($password); - } - /* * check if files can be encrypted to every user. */ |