From 0c2f9ca849ef41232511cf576cc9a9de2caa43f0 Mon Sep 17 00:00:00 2001 From: Clark Tomlinson Date: Tue, 24 Mar 2015 17:29:10 -0400 Subject: [PATCH] Updating keystorage movement and fixing hooks --- apps/encryption/appinfo/encryption.php | 43 +- apps/encryption/hooks/userhooks.php | 213 +++------- apps/encryption/lib/crypto/Encryption.php | 17 +- apps/encryption/lib/crypto/crypt.php | 153 ++++++- apps/encryption/lib/hookmanager.php | 2 + apps/encryption/lib/keymanager.php | 324 +++++++++++---- apps/encryption/lib/migrator.php | 123 ------ apps/encryption/lib/recovery.php | 58 ++- apps/encryption/lib/users/setup.php | 20 +- apps/encryption/lib/util.php | 394 +++++++++++++++++++ apps/encryption/settings/settings-admin.php | 8 +- apps/encryption/tests/lib/KeyManagerTest.php | 39 +- apps/encryption/tests/lib/MigratorTest.php | 3 +- apps/files_encryption/lib/hooks.php | 226 +++++++++++ lib/private/server.php | 2 +- 15 files changed, 1202 insertions(+), 423 deletions(-) delete mode 100644 apps/encryption/lib/migrator.php create mode 100644 apps/encryption/lib/util.php diff --git a/apps/encryption/appinfo/encryption.php b/apps/encryption/appinfo/encryption.php index f2ab89aadef..1a4c17eb60a 100644 --- a/apps/encryption/appinfo/encryption.php +++ b/apps/encryption/appinfo/encryption.php @@ -21,16 +21,16 @@ namespace OCA\Encryption\AppInfo; +use OC\Files\Filesystem; +use OC\Files\View; 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 OCA\Encryption\Util; use OCP\App; use OCP\AppFramework\IAppContainer; use OCP\Encryption\IManager; @@ -81,14 +81,7 @@ class Encryption extends \OCP\AppFramework\App { $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() + new UserHooks($container->query('KeyManager'), $server->getLogger(), $container->query('UserSetup'), $server->getUserSession(), new \OCP\Util(), $container->query('Util')), ]); $hookManager->fireHooks(); @@ -103,7 +96,7 @@ class Encryption extends \OCP\AppFramework\App { * */ public function registerEncryptionModule() { -// $this->encryptionManager->registerEncryptionModule(new \OCA\Encryption\Crypto\Encryption()); + $this->encryptionManager->registerEncryptionModule(new \OCA\Encryption\Crypto\Encryption()); } /** @@ -124,10 +117,13 @@ class Encryption extends \OCP\AppFramework\App { function (IAppContainer $c) { $server = $c->getServer(); - return new KeyManager($server->getEncryptionKeyStorage(), + return new KeyManager($server->getEncryptionKeyStorage('encryption'), $c->query('Crypt'), $server->getConfig(), - $server->getUserSession()); + $server->getUserSession(), + $server->getMemCacheFactory(), + $server->getLogger() + ); }); @@ -141,7 +137,7 @@ class Encryption extends \OCP\AppFramework\App { $server->getSecureRandom(), $c->query('KeyManager'), $server->getConfig(), - $server->getEncryptionKeyStorage()); + $server->getEncryptionKeyStorage('encryption')); }); $container->registerService('UserSetup', @@ -157,13 +153,26 @@ class Encryption extends \OCP\AppFramework\App { function (IAppContainer $c) { $server = $c->getServer(); - return new Migrator($server->getUserSession(), - $server->getConfig(), + return new Migrator($server->getConfig(), $server->getUserManager(), $server->getLogger(), $c->query('Crypt')); }); + $container->registerService('Util', + function (IAppContainer $c) { + $server = $c->getServer(); + + return new Util(new View(), + new Filesystem(), + $c->query('Crypt'), + $c->query('KeyManager'), + $server->getLogger(), + $server->getUserSession(), + $server->getConfig() + ); + }); + } /** diff --git a/apps/encryption/hooks/userhooks.php b/apps/encryption/hooks/userhooks.php index 79de70a6d02..096fd3beb93 100644 --- a/apps/encryption/hooks/userhooks.php +++ b/apps/encryption/hooks/userhooks.php @@ -22,15 +22,14 @@ namespace OCA\Encryption\Hooks; +use OCP\Util as OCUtil; 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 OCA\Encryption\Util; use Test\User; class UserHooks implements IHook { @@ -46,14 +45,14 @@ class UserHooks implements IHook { * @var Setup */ private $userSetup; - /** - * @var Migrator - */ - private $migrator; /** * @var IUserSession */ private $user; + /** + * @var Util + */ + private $util; /** * UserHooks constructor. @@ -61,17 +60,19 @@ class UserHooks implements IHook { * @param KeyManager $keyManager * @param ILogger $logger * @param Setup $userSetup - * @param Migrator $migrator * @param IUserSession $user + * @param OCUtil $ocUtil + * @param Util $util + * @internal param Migrator $migrator */ public function __construct( - KeyManager $keyManager, ILogger $logger, Setup $userSetup, Migrator $migrator, IUserSession $user) { + KeyManager $keyManager, ILogger $logger, Setup $userSetup, IUserSession $user, OCUtil $ocUtil, Util $util) { $this->keyManager = $keyManager; $this->logger = $logger; $this->userSetup = $userSetup; - $this->migrator = $migrator; $this->user = $user; + $this->util = $util; } /** @@ -80,12 +81,24 @@ class UserHooks implements IHook { * @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'); + OCUtil::connectHook('OC_User', 'post_login', $this, 'login'); + OCUtil::connectHook('OC_User', 'logout', $this, 'logout'); + OCUtil::connectHook('OC_User', + 'post_setPassword', + $this, + 'setPassphrase'); + OCUtil::connectHook('OC_User', + 'pre_setPassword', + $this, + 'preSetPassphrase'); + OCUtil::connectHook('OC_User', + 'post_createUser', + $this, + 'postCreateUser'); + OCUtil::connectHook('OC_User', + 'post_deleteUser', + $this, + 'postDeleteUser'); } @@ -93,6 +106,8 @@ class UserHooks implements IHook { * Startup encryption backend upon user login * * @note This method should never be called for users using client side encryption + * @param array $params + * @return bool */ public function login($params) { @@ -107,198 +122,76 @@ class UserHooks implements IHook { } // 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(); + if (!$this->userSetup->setupUser($params['uid'], $params['password'])) { 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; + $this->keyManager->init($params['uid'], $params['password']); } /** * remove keys from session during logout */ public function logout() { - $session = new Session(new \OC\Files\View()); - $session->removeKeys(); + KeyManager::$cacheFactory->clear(); } /** * setup encryption backend upon user created * * @note This method should never be called for users using client side encryption + * @param array $params */ 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']); + if (App::isEnabled('encryption')) { + $this->userSetup->setupUser($params['uid'], $params['password']); } } /** * cleanup encryption backend upon user deleted * + * @param array $params : uid, password * @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 (App::isEnabled('encryption')) { + $this->keyManager->deletePublicKey($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 + * @param array $params : uid, password + * @return bool */ - 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); - } + public function preSetPassphrase($params) { + if (App::isEnabled('encryption')) { - \OC_FileProxy::$enabled = $proxyStatus; + if (!$this->user->getUser()->canChangePassword()) { + if (App::isEnabled('encryption') === false) { + return true; } + $this->keyManager->setPassphrase($params, + $this->user, + $this->util); } } } + /** * 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); + $this->keyManager->replaceUserKeys($params['uid']); + $this->userSetup->setupServerSide($params['uid'], $password); } } diff --git a/apps/encryption/lib/crypto/Encryption.php b/apps/encryption/lib/crypto/Encryption.php index 123581b83ac..b8429d7124e 100644 --- a/apps/encryption/lib/crypto/Encryption.php +++ b/apps/encryption/lib/crypto/Encryption.php @@ -18,7 +18,7 @@ class Encryption extends Crypt implements IEncryptionModule { * @return string defining the technical unique id */ public function getId() { - // TODO: Implement getId() method. + return md5($this->getDisplayName()); } /** @@ -27,7 +27,7 @@ class Encryption extends Crypt implements IEncryptionModule { * @return string */ public function getDisplayName() { - // TODO: Implement getDisplayName() method. + return 'ownCloud Default Encryption'; } /** @@ -44,7 +44,9 @@ class Encryption extends Crypt implements IEncryptionModule { * or if no additional data is needed return a empty array */ public function begin($path, $header, $accessList) { + // TODO: Implement begin() method. + // return additional header information that needs to be written i.e. cypher used } /** @@ -68,6 +70,7 @@ class Encryption extends Crypt implements IEncryptionModule { */ public function encrypt($data) { // Todo: xxx Update Signature and usages + // passphrase is file key decrypted with user private/share key $this->symmetricEncryptFileContent($data); } @@ -113,4 +116,14 @@ class Encryption extends Crypt implements IEncryptionModule { public function calculateUnencryptedSize($path) { // TODO: Implement calculateUnencryptedSize() method. } + + /** + * get size of the unencrypted payload per block. + * ownCloud read/write files with a block size of 8192 byte + * + * @return integer + */ + public function getUnencryptedBlockSize() { + // TODO: Implement getUnencryptedBlockSize() method. + } } diff --git a/apps/encryption/lib/crypto/crypt.php b/apps/encryption/lib/crypto/crypt.php index 8018f11a370..f9fe4f9bece 100644 --- a/apps/encryption/lib/crypto/crypt.php +++ b/apps/encryption/lib/crypto/crypt.php @@ -25,6 +25,8 @@ namespace OCA\Encryption\Crypto; use OC\Encryption\Exceptions\DecryptionFailedException; use OC\Encryption\Exceptions\EncryptionFailedException; use OC\Encryption\Exceptions\GenericEncryptionException; +use OCA\Files_Encryption\Exception\MultiKeyDecryptException; +use OCA\Files_Encryption\Exception\MultiKeyEncryptException; use OCP\IConfig; use OCP\ILogger; use OCP\IUser; @@ -83,12 +85,17 @@ class Crypt { $res = $this->getOpenSSLPKey(); if (!$res) { - $log->error("Encryption Library could'nt generate users key-pair for {$this->user->getUID()}", ['app' => 'encryption']); + $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']); + $log->error('Encryption library openssl_pkey_new() fails: ' . openssl_error_string(), + ['app' => 'encryption']); } - } elseif (openssl_pkey_export($res, $privateKey, null, $this->getOpenSSLConfig())) { + } elseif (openssl_pkey_export($res, + $privateKey, + null, + $this->getOpenSSLConfig())) { $keyDetails = openssl_pkey_get_details($res); $publicKey = $keyDetails['key']; @@ -97,9 +104,11 @@ class Crypt { 'privateKey' => $privateKey ]; } - $log->error('Encryption library couldn\'t export users private key, please check your servers openSSL configuration.' . $user->getUID(), ['app' => 'encryption']); + $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']); + $log->error('Encryption Library:' . openssl_error_string(), + ['app' => 'encryption']); } return false; @@ -118,7 +127,9 @@ class Crypt { */ private function getOpenSSLConfig() { $config = ['private_key_bits' => 4096]; - $config = array_merge(\OC::$server->getConfig()->getSystemValue('openssl', []), $config); + $config = array_merge(\OC::$server->getConfig()->getSystemValue('openssl', + []), + $config); return $config; } @@ -131,14 +142,18 @@ class Crypt { public function symmetricEncryptFileContent($plainContent, $passphrase) { if (!$plainContent) { - $this->logger->error('Encryption Library, symmetrical encryption failed no content given', ['app' => 'encryption']); + $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()); + $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); @@ -146,7 +161,8 @@ class Crypt { return $padded; } catch (EncryptionFailedException $e) { $message = 'Could not encrypt file content (code: ' . $e->getCode() . '): '; - $this->logger->error('files_encryption' . $message . $e->getMessage(), ['app' => 'encryption']); + $this->logger->error('files_encryption' . $message . $e->getMessage(), + ['app' => 'encryption']); return false; } @@ -161,11 +177,16 @@ class Crypt { * @throws EncryptionFailedException */ private function encrypt($plainContent, $iv, $passphrase = '', $cipher = self::DEFAULT_CIPHER) { - $encryptedContent = openssl_encrypt($plainContent, $cipher, $passphrase, false, $iv); + $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']); + $this->logger->error($error . openssl_error_string(), + ['app' => 'encryption']); throw new EncryptionFailedException($error); } @@ -177,8 +198,9 @@ class Crypt { */ 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']); + 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; } @@ -214,10 +236,14 @@ class Crypt { // 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)); + $recoveryKey = substr($recoveryKey, + strpos($recoveryKey, + self::HEADEREND) + strlen(self::HEADERSTART)); } - $plainKey = $this->symmetricDecryptFileContent($recoveryKey, $password, $cipher); + $plainKey = $this->symmetricDecryptFileContent($recoveryKey, + $password, + $cipher); // Check if this is a valid private key $res = openssl_get_privatekey($plainKey); @@ -246,7 +272,10 @@ class Crypt { $catFile = $this->splitIv($noPadding); - $plainContent = $this->decrypt($catFile['encrypted'], $catFile['iv'], $passphrase, $cipher); + $plainContent = $this->decrypt($catFile['encrypted'], + $catFile['iv'], + $passphrase, + $cipher); if ($plainContent) { return $plainContent; @@ -296,7 +325,11 @@ class Crypt { * @throws DecryptionFailedException */ private function decrypt($encryptedContent, $iv, $passphrase = '', $cipher = self::DEFAULT_CIPHER) { - $plainContent = openssl_decrypt($encryptedContent, $cipher, $passphrase, false, $iv); + $plainContent = openssl_decrypt($encryptedContent, + $cipher, + $passphrase, + false, + $iv); if ($plainContent) { return $plainContent; @@ -317,7 +350,8 @@ class Crypt { $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)); + $exploded = explode(':', + substr($header, strlen(self::HEADERSTART) + 1)); $element = array_shift($exploded); @@ -339,7 +373,8 @@ class Crypt { 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']); + $this->logger->error('Encryption Library: Insecure symmetric key was generated using openssl_random_psudo_bytes()', + ['app' => 'encryption']); } /* @@ -351,5 +386,85 @@ class Crypt { // If we ever get here we've failed anyway no need for an else throw new GenericEncryptionException('Generating IV Failed'); } + + /** + * Check if a file's contents contains an IV and is symmetrically encrypted + * + * @param $content + * @return bool + */ + public function isCatFileContent($content) { + if (!$content) { + return false; + } + + $noPadding = $this->removePadding($content); + + // Fetch encryption metadata from end of file + $meta = substr($noPadding, -22); + + // Fetch identifier from start of metadata + $identifier = substr($meta, 0, 6); + + if ($identifier === '00iv00') { + return true; + } + return false; + } + + /** + * @param $encKeyFile + * @param $shareKey + * @param $privateKey + * @return mixed + * @throws MultiKeyDecryptException + */ + public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) { + if (!$encKeyFile) { + throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content'); + } + + if (openssl_open($encKeyFile, $plainContent, $shareKey, $privateKey)) { + return $plainContent; + } else { + throw new MultiKeyDecryptException('multikeydecrypt with share key failed'); + } + } + + /** + * @param $plainContent + * @param array $keyFiles + * @return array + * @throws MultiKeyEncryptException + */ + public function multiKeyEncrypt($plainContent, array $keyFiles) { + // openssl_seal returns false without errors if plaincontent is empty + // so trigger our own error + if (empty($plainContent)) { + throw new MultiKeyEncryptException('Cannot multikeyencrypt empty plain content'); + } + + // Set empty vars to be set by openssl by reference + $sealed = ''; + $shareKeys = []; + $mappedShareKeys = []; + + if (openssl_seal($plainContent, $sealed, $shareKeys, $keyFiles)) { + $i = 0; + + // Ensure each shareKey is labelled with its coreesponding keyid + foreach ($keyFiles as $userId => $publicKey) { + $mappedShareKeys[$userId] = $shareKeys[$i]; + $i++; + } + + return [ + 'keys' => $mappedShareKeys, + 'data' => $sealed + ]; + } else { + throw new MultiKeyEncryptException('multikeyencryption failed ' . openssl_error_string()); + } + } } diff --git a/apps/encryption/lib/hookmanager.php b/apps/encryption/lib/hookmanager.php index a535230a6a7..9e6d9cd855c 100644 --- a/apps/encryption/lib/hookmanager.php +++ b/apps/encryption/lib/hookmanager.php @@ -54,6 +54,8 @@ class HookManager { public function fireHooks() { foreach ($this->hookInstances as $instance) { /** + * Fire off the add hooks method of each instance stored in cache + * * @var $instance IHook */ $instance->addHooks(); diff --git a/apps/encryption/lib/keymanager.php b/apps/encryption/lib/keymanager.php index 272bf0849c2..83b24c79b8c 100644 --- a/apps/encryption/lib/keymanager.php +++ b/apps/encryption/lib/keymanager.php @@ -22,21 +22,27 @@ namespace OCA\Encryption; +use OC\Encryption\Exceptions\DecryptionFailedException; use OC\Encryption\Exceptions\PrivateKeyMissingException; use OC\Encryption\Exceptions\PublicKeyMissingException; use OCA\Encryption\Crypto\Crypt; -use OCP\Encryption\IKeyStorage; +use OCP\Encryption\Keys\IStorage; +use OCP\ICache; +use OCP\ICacheFactory; use OCP\IConfig; -use OCP\IUser; +use OCP\ILogger; use OCP\IUserSession; class KeyManager { /** - * @var IKeyStorage + * @var ICache + */ + public static $cacheFactory; + /** + * @var IStorage */ private $keyStorage; - /** * @var Crypt */ @@ -53,35 +59,143 @@ class KeyManager { * @var string UserID */ private $keyId; + /** + * @var string + */ + private $publicKeyId = 'public'; + /** + * @var string + */ + private $privateKeyId = 'private'; /** * @var string */ - private $publicKeyId = '.public'; + private $shareKeyId = 'sharekey'; + /** * @var string */ - private $privateKeyId = '.private'; + private $fileKeyId = 'filekey'; /** * @var IConfig */ private $config; + /** + * @var ILogger + */ + private $log; /** - * @param IKeyStorage $keyStorage + * @param IStorage $keyStorage * @param Crypt $crypt * @param IConfig $config * @param IUserSession $userSession + * @param ICacheFactory $cacheFactory + * @param ILogger $log */ - public function __construct(IKeyStorage $keyStorage, Crypt $crypt, IConfig $config, IUserSession $userSession) { + public function __construct(IStorage $keyStorage, Crypt $crypt, IConfig $config, IUserSession $userSession, ICacheFactory $cacheFactory, ILogger $log) { $this->keyStorage = $keyStorage; $this->crypt = $crypt; $this->config = $config; - $this->recoveryKeyId = $this->config->getAppValue('encryption', 'recoveryKeyId'); - $this->publicShareKeyId = $this->config->getAppValue('encryption', 'publicShareKeyId'); + $this->recoveryKeyId = $this->config->getAppValue('encryption', + 'recoveryKeyId'); + $this->publicShareKeyId = $this->config->getAppValue('encryption', + 'publicShareKeyId'); $this->keyId = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : false; + self::$cacheFactory = $cacheFactory; + self::$cacheFactory = self::$cacheFactory->create('encryption'); + $this->log = $log; + } + + /** + * @return bool + */ + public function recoveryKeyExists() { + return (strlen($this->keyStorage->getSystemUserKey($this->recoveryKeyId)) !== 0); + } + + /** + * @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 string $uid + * @param string $password + * @param string $keyPair + * @return bool + */ + public function storeKeyPair($uid, $password, $keyPair) { + // Save Public Key + $this->setPublicKey($uid, $keyPair['publicKey']); + + $encryptedKey = $this->crypt->symmetricEncryptFileContent($keyPair['privateKey'], + $password); + + if ($encryptedKey) { + $this->setPrivateKey($uid, $encryptedKey); + $this->config->setAppValue('encryption', 'recoveryAdminEnabled', 1); + 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); + } + + /** + * Decrypt private key and store it + * + * @param string $uid userid + * @param string $passPhrase users password + * @return ICache + */ + public function init($uid, $passPhrase) { + try { + $privateKey = $this->getPrivateKey($uid); + $privateKey = $this->crypt->decryptPrivateKey($privateKey, + $passPhrase); + } catch (PrivateKeyMissingException $e) { + return false; + } catch (DecryptionFailedException $e) { + return false; + } + + self::$cacheFactory->set('privateKey', $privateKey); + self::$cacheFactory->set('initStatus', true); + + + return self::$cacheFactory; } /** @@ -90,7 +204,8 @@ class KeyManager { * @throws PrivateKeyMissingException */ public function getPrivateKey($userId) { - $privateKey = $this->keyStorage->getUserKey($userId, $this->privateKeyId); + $privateKey = $this->keyStorage->getUserKey($userId, + $this->privateKeyId); if (strlen($privateKey) !== 0) { return $privateKey; @@ -99,24 +214,105 @@ class KeyManager { } /** - * @param $userId + * @param $path * @return mixed - * @throws PublicKeyMissingException */ - public function getPublicKey($userId) { - $publicKey = $this->keyStorage->getUserKey($userId, $this->publicKeyId); + public function getFileKey($path) { + return $this->keyStorage->getFileKey($path, $this->fileKeyId); + } - if (strlen($publicKey) !== 0) { - return $publicKey; - } - throw new PublicKeyMissingException(); + /** + * @param $path + * @return mixed + */ + public function getShareKey($path) { + return $this->keyStorage->getFileKey($path, $this->keyId . $this->shareKeyId); } /** + * Change a user's encryption passphrase + * + * @param array $params keys: uid, password + * @param IUserSession $user + * @param Util $util * @return bool */ - public function recoveryKeyExists() { - return (strlen($this->keyStorage->getSystemUserKey($this->recoveryKeyId)) !== 0); + public function setPassphrase($params, IUserSession $user, Util $util) { + + // Only attempt to change passphrase if server-side encryption + // is in use (client-side encryption does not have access to + // the necessary keys) + if ($this->crypt->mode() === 'server') { + + // Get existing decrypted private key + $privateKey = self::$cacheFactory->get('privateKey'); + + if ($params['uid'] === $user->getUser()->getUID() && $privateKey) { + + // Encrypt private key with new user pwd as passphrase + $encryptedPrivateKey = $this->crypt->symmetricEncryptFileContent($privateKey, + $params['password']); + + // Save private key + if ($encryptedPrivateKey) { + $this->setPrivateKey($user->getUser()->getUID(), + $encryptedPrivateKey); + } else { + $this->log->error('Encryption could not update users encryption password'); + } + + // 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']; + $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) + + || !$this->userHasKeys($user) + || !$util->userHasFiles($user) + ) { + + // backup old keys + $this->backupAllKeys('recovery'); + + $newUserPassword = $params['password']; + + $keypair = $this->crypt->createKeyPair(); + + // Disable encryption proxy to prevent recursive calls + $proxyStatus = \OC_FileProxy::$enabled; + \OC_FileProxy::$enabled = false; + + // Save public key + $this->setPublicKey($user, $keypair['publicKey']); + + // Encrypt private key with new password + $encryptedKey = $this->crypt->symmetricEncryptFileContent($keypair['privateKey'], + $newUserPassword); + + if ($encryptedKey) { + $this->setPrivateKey($user, $encryptedKey); + + if ($recoveryPassword) { // if recovery key is set we can re-encrypt the key files + $util->recoverUsersFiles($recoveryPassword); + } + } else { + $this->log->error('Encryption Could not update users encryption password'); + } + + \OC_FileProxy::$enabled = $proxyStatus; + } + } + } } /** @@ -136,82 +332,70 @@ class KeyManager { } /** - * @param $password - * @return bool + * @param $userId + * @return mixed + * @throws PublicKeyMissingException */ - public function checkRecoveryPassword($password) { - $recoveryKey = $this->keyStorage->getSystemUserKey($this->recoveryKeyId); - $decryptedRecoveryKey = $this->crypt->decryptPrivateKey($recoveryKey, $password); + public function getPublicKey($userId) { + $publicKey = $this->keyStorage->getUserKey($userId, $this->publicKeyId); - if ($decryptedRecoveryKey) { - return true; + if (strlen($publicKey) !== 0) { + return $publicKey; } - return false; + throw new PublicKeyMissingException(); } /** - * @param $userId - * @param $key - * @return bool + * @param $purpose + * @param bool $timestamp + * @param bool $includeUserKeys */ - public function setPublicKey($userId, $key) { - return $this->keyStorage->setUserKey($userId, $this->publicKeyId, $key); + public function backupAllKeys($purpose, $timestamp = true, $includeUserKeys = true) { +// $backupDir = $this->keyStorage->; } /** - * @param $userId - * @param $key - * @return bool + * @param string $uid */ - public function setPrivateKey($userId, $key) { - return $this->keyStorage->setUserKey($userId, $this->privateKeyId, $key); + public function replaceUserKeys($uid) { + $this->backupAllKeys('password_reset'); + $this->deletePublicKey($uid); + $this->deletePrivateKey($uid); } - /** - * @param $password - * @param $keyPair + * @param $uid * @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; + public function deletePublicKey($uid) { + return $this->keyStorage->deleteUserKey($uid, $this->publicKeyId); } /** + * @param $uid * @return bool */ - public function ready() { - return $this->keyStorage->ready(); + private function deletePrivateKey($uid) { + return $this->keyStorage->deleteUserKey($uid, $this->privateKeyId); } - /** - * @return \OCP\ICache - * @throws PrivateKeyMissingException + * @param array $userIds + * @return array + * @throws PublicKeyMissingException */ - public function init() { - try { - $privateKey = $this->getPrivateKey($this->keyId); - } catch (PrivateKeyMissingException $e) { - return false; - } + public function getPublicKeys(array $userIds) { + $keys = []; - $cache = \OC::$server->getMemCacheFactory(); + foreach ($userIds as $userId) { + try { + $keys[$userId] = $this->getPublicKey($userId); + } catch (PublicKeyMissingException $e) { + continue; + } + } - $cacheInstance = $cache->create('Encryption'); - $cacheInstance->set('privateKey', $privateKey); + return $keys; - return $cacheInstance; } - } diff --git a/apps/encryption/lib/migrator.php b/apps/encryption/lib/migrator.php deleted file mode 100644 index 8f7823cb1ae..00000000000 --- a/apps/encryption/lib/migrator.php +++ /dev/null @@ -1,123 +0,0 @@ - - * @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 index 88350e96c53..457184b4b96 100644 --- a/apps/encryption/lib/recovery.php +++ b/apps/encryption/lib/recovery.php @@ -22,11 +22,12 @@ namespace OCA\Encryption; -use OC\Files\View; use OCA\Encryption\Crypto\Crypt; -use OCP\Encryption\IKeyStorage; +use OCP\Encryption\Keys\IStorage; use OCP\IConfig; use OCP\IUser; +use OCP\IUserSession; +use OCP\PreConditionNotMetException; use OCP\Security\ISecureRandom; class Recovery { @@ -58,20 +59,20 @@ class Recovery { private $keyStorage; /** - * @param IUser $user + * @param IUserSession $user * @param Crypt $crypt * @param ISecureRandom $random * @param KeyManager $keyManager * @param IConfig $config - * @param IKeyStorage $keyStorage + * @param IStorage $keyStorage */ - public function __construct(IUser $user, + public function __construct(IUserSession $user, Crypt $crypt, ISecureRandom $random, KeyManager $keyManager, IConfig $config, - IKeyStorage $keyStorage) { - $this->user = $user; + IStorage $keyStorage) { + $this->user = $user && $user->isLoggedIn() ? $user->getUser() : false; $this->crypt = $crypt; $this->random = $random; $this->keyManager = $keyManager; @@ -97,7 +98,7 @@ class Recovery { if (!$keyManager->recoveryKeyExists()) { $keyPair = $this->crypt->createKeyPair(); - return $this->keyManager->storeKeyPair($password, $keyPair); + return $this->keyManager->storeKeyPair($this->user->getUID(), $password, $keyPair); } if ($keyManager->checkRecoveryPassword($password)) { @@ -131,4 +132,45 @@ class Recovery { // No idea new way to do this.... } + /** + * @return bool + */ + public function recoveryEnabledForUser() { + $recoveryMode = $this->config->getUserValue($this->user->getUID(), + 'encryption', + 'recoveryEnabled', + 0); + + return ($recoveryMode === '1'); + } + /** + * @param $enabled + * @return bool + */ + public function setRecoveryForUser($enabled) { + $value = $enabled ? '1' : '0'; + + try { + $this->config->setUserValue($this->user->getUID(), + 'encryption', + 'recoveryEnabled', + $value); + return true; + } catch (PreConditionNotMetException $e) { + return false; + } + } + + /** + * @param $recoveryPassword + */ + public function recoverUsersFiles($recoveryPassword) { + // todo: get system private key here +// $this->keyManager->get + $privateKey = $this->crypt->decryptPrivateKey($encryptedKey, + $recoveryPassword); + + $this->recoverAllFiles('/', $privateKey); + } + } diff --git a/apps/encryption/lib/users/setup.php b/apps/encryption/lib/users/setup.php index 123d6973be9..662a4b4b6af 100644 --- a/apps/encryption/lib/users/setup.php +++ b/apps/encryption/lib/users/setup.php @@ -39,24 +39,24 @@ class Setup extends \OCA\Encryption\Setup { } /** - * @param $password + * @param $uid userid + * @param $password user 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); - } + public function setupUser($uid, $password) { + return $this->setupServerSide($uid, $password); } /** - * @param $password + * @param $uid userid + * @param $password user password * @return bool */ - private function setupServerSide($password) { + public function setupServerSide($uid, $password) { // Check if user already has keys - if (!$this->keyManager->userHasKeys($this->user->getUID())) { - return $this->keyManager->storeKeyPair($password, $this->crypt->createKeyPair()); + if (!$this->keyManager->userHasKeys($uid)) { + return $this->keyManager->storeKeyPair($uid, $password, + $this->crypt->createKeyPair()); } return true; } diff --git a/apps/encryption/lib/util.php b/apps/encryption/lib/util.php new file mode 100644 index 00000000000..5cc658a3136 --- /dev/null +++ b/apps/encryption/lib/util.php @@ -0,0 +1,394 @@ + + * @since 3/17/15, 10:31 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 + * + */ + + +namespace OCA\Encryption; + + +use OC\Files\Filesystem; +use OC\Files\View; +use OCA\Encryption\Crypto\Crypt; +use OCA\Files_Versions\Storage; +use OCP\App; +use OCP\IConfig; +use OCP\ILogger; +use OCP\IUser; +use OCP\IUserSession; +use OCP\PreConditionNotMetException; +use OCP\Share; + +class Util { + /** + * @var View + */ + private $files; + /** + * @var Filesystem + */ + private $filesystem; + /** + * @var Crypt + */ + private $crypt; + /** + * @var KeyManager + */ + private $keyManager; + /** + * @var ILogger + */ + private $logger; + /** + * @var bool|IUser + */ + private $user; + /** + * @var IConfig + */ + private $config; + + /** + * Util constructor. + * + * @param View $files + * @param Filesystem $filesystem + * @param Crypt $crypt + * @param KeyManager $keyManager + * @param ILogger $logger + * @param IUserSession $userSession + * @param IConfig $config + */ + public function __construct( + View $files, + Filesystem $filesystem, + Crypt $crypt, + KeyManager $keyManager, + ILogger $logger, + IUserSession $userSession, + IConfig $config + ) { + $this->files = $files; + $this->filesystem = $filesystem; + $this->crypt = $crypt; + $this->keyManager = $keyManager; + $this->logger = $logger; + $this->user = $userSession && $userSession->isLoggedIn() ? $userSession->getUser() : false; + $this->config = $config; + } + + /** + * @param $dirPath + * @param bool $found + * @return array|bool + */ + private function findEncryptedFiles($dirPath, &$found = false) { + + if ($found === false) { + $found = [ + 'plain' => [], + 'encrypted' => [], + 'broken' => [], + ]; + } + + if ($this->files->is_dir($dirPath) && $handle = $this->files->opendir($dirPath)) { + if (is_resource($handle)) { + while (($file = readdir($handle) !== false)) { + if ($file !== '.' && $file !== '..') { + + // Skip stray part files + if ($this->isPartialFilePath($file)) { + continue; + } + + $filePath = $dirPath . '/' . $this->files->getRelativePath('/' . $file); + $relPath = $this->stripUserFilesPath($filePath); + + // If the path is a directory, search its contents + if ($this->files->is_dir($filePath)) { + // Recurse back + $this->findEncryptedFiles($filePath); + + /* + * If the path is a file, + * determine where they got re-enabled :/ + */ + } elseif ($this->files->is_file($filePath)) { + $isEncryptedPath = $this->isEncryptedPath($filePath); + + /** + * If the file is encrypted + * + * @note: if the userId is + * empty or not set, file will + * be detected as plain + * @note: this is inefficient; + * scanning every file like this + * will eat server resources :( + * fixMe: xxx find better way + */ + if ($isEncryptedPath) { + $fileKey = $this->keyManager->getFileKey($relPath); + $shareKey = $this->keyManager->getShareKey($relPath); + // If file is encrypted but now file key is available, throw exception + if (!$fileKey || !$shareKey) { + $this->logger->error('Encryption library, no keys avilable to decrypt the file: ' . $file); + $found['broken'][] = [ + 'name' => $file, + 'path' => $filePath, + ]; + } else { + $found['encrypted'][] = [ + 'name' => $file, + 'path' => $filePath + ]; + } + } else { + $found['plain'][] = [ + 'name' => $file, + 'path' => $filePath + ]; + } + } + } + + } + } + } + + return $found; + } + + /** + * @param $path + * @return bool + */ + private function isPartialFilePath($path) { + $extension = pathinfo($path, PATHINFO_EXTENSION); + + if ($extension === 'part') { + return true; + } + return false; + } + + /** + * @param $filePath + * @return bool|string + */ + private function stripUserFilesPath($filePath) { + $split = $this->splitPath($filePath); + + // It is not a file relative to data/user/files + if (count($split) < 4 || $split[2] !== 'files') { + return false; + } + + $sliced = array_slice($split, 3); + + return implode('/', $sliced); + + } + + /** + * @param $filePath + * @return array + */ + private function splitPath($filePath) { + $normalized = $this->filesystem->normalizePath($filePath); + + return explode('/', $normalized); + } + + /** + * @param $filePath + * @return bool + */ + private function isEncryptedPath($filePath) { + $data = ''; + + // We only need 24 bytes from the last chunck + if ($this->files->file_exists($filePath)) { + $handle = $this->files->fopen($filePath, 'r'); + if (is_resource($handle)) { + // Suppress fseek warning, we handle the case that fseek + // doesn't work in the else branch + if (@fseek($handle, -24, SEEK_END) === 0) { + $data = fgets($handle); + } else { + // if fseek failed on the storage we create a local copy + // from the file and read this one + fclose($handle); + $localFile = $this->files->getLocalFile($filePath); + $handle = fopen($localFile, 'r'); + + if (is_resource($handle) && fseek($handle, + -24, + SEEK_END) === 0 + ) { + $data = fgets($handle); + } + } + fclose($handle); + return $this->crypt->isCatfileContent($data); + } + } + } + + /** + * @return bool + */ + public function recoveryEnabledForUser() { + $recoveryMode = $this->config->getUserValue($this->user->getUID(), + 'encryption', + 'recoveryEnabled', + 0); + + return ($recoveryMode === '1'); + } + + /** + * @param $enabled + * @return bool + */ + public function setRecoveryForUser($enabled) { + $value = $enabled ? '1' : '0'; + + try { + $this->config->setUserValue($this->user->getUID(), + 'encryption', + 'recoveryEnabled', + $value); + return true; + } catch (PreConditionNotMetException $e) { + return false; + } + } + + /** + * @param $recoveryPassword + */ + public function recoverUsersFiles($recoveryPassword) { + // todo: get system private key here +// $this->keyManager->get + $privateKey = $this->crypt->decryptPrivateKey($encryptedKey, + $recoveryPassword); + + $this->recoverAllFiles('/', $privateKey); + } + + /** + * @param string $uid + * @return bool + */ + public function userHasFiles($uid) { + return $this->files->file_exists($uid . '/files'); + } + + /** + * @param $path + * @param $privateKey + */ + private function recoverAllFiles($path, $privateKey) { + // Todo relocate to storage + $dirContent = $this->files->getDirectoryContent($path); + + foreach ($dirContent as $item) { + // Get relative path from encryption/keyfiles + $filePath = substr($item['path'], strlen('encryption/keys')); + if ($this->files->is_dir($this->user->getUID() . '/files' . '/' . $filePath)) { + $this->recoverAllFiles($filePath . '/', $privateKey); + } else { + $this->recoverFile($filePath, $privateKey); + } + } + + } + + /** + * @param $filePath + * @param $privateKey + */ + private function recoverFile($filePath, $privateKey) { + $sharingEnabled = Share::isEnabled(); + + // Find out who, if anyone, is sharing the file + if ($sharingEnabled) { + $result = Share::getUsersSharingFile($filePath, + $this->user->getUID(), + true); + $userIds = $result['users']; + $userIds[] = 'public'; + } else { + $userIds = [ + $this->user->getUID(), + $this->recoveryKeyId + ]; + } + $filteredUids = $this->filterShareReadyUsers($userIds); + + // Decrypt file key + $encKeyFile = $this->keyManager->getFileKey($filePath); + $shareKey = $this->keyManager->getShareKey($filePath); + $plainKeyFile = $this->crypt->multiKeyDecrypt($encKeyFile, + $shareKey, + $privateKey); + + // Encrypt the file key again to all users, this time with the new publick keyt for the recovered user + $userPublicKeys = $this->keyManager->getPublicKeys($filteredUids['ready']); + $multiEncryptionKey = $this->crypt->multiKeyEncrypt($plainKeyFile, + $userPublicKeys); + + $this->keyManager->setFileKeys($multiEncryptionKey['data']); + $this->keyManager->setShareKeys($multiEncryptionKey['keys']); + } + + /** + * @param $userIds + * @return array + */ + private function filterShareReadyUsers($userIds) { + // This array will collect the filtered IDs + $readyIds = $unreadyIds = []; + + // Loop though users and create array of UIDs that need new keyfiles + foreach ($userIds as $user) { + // Check that the user is encryption capable, or is the + // public system user (for public shares) + if ($this->isUserReady($user)) { + // construct array of ready UIDs for keymanager + $readyIds[] = $user; + } else { + // Construct array of unready UIDs for keymanager + $unreadyIds[] = $user; + + // Log warning; we cant do necessary setup here + // because we don't have the user passphrase + $this->logger->warning('Encryption Library ' . $this->user->getUID() . ' is not setup for encryption'); + } + } + return [ + 'ready' => $readyIds, + 'unready' => $unreadyIds + ]; + } + +} diff --git a/apps/encryption/settings/settings-admin.php b/apps/encryption/settings/settings-admin.php index 0f5d56a3734..d3acdfe2ba4 100644 --- a/apps/encryption/settings/settings-admin.php +++ b/apps/encryption/settings/settings-admin.php @@ -6,17 +6,17 @@ * See the COPYING-README file. */ +use OCA\Encryption\KeyManager; + \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(); +$recoveryAdminEnabled = \OC::$server->getConfig()->getAppValue('encryption', 'recoveryAdminEnabled', '0'); $tmpl->assign('recoveryEnabled', $recoveryAdminEnabled); -$tmpl->assign('initStatus', $initStatus); +$tmpl->assign('initStatus', KeyManager::$cacheFactory->get('initStatus')); \OCP\Util::addscript('files_encryption', 'settings-admin'); \OCP\Util::addscript('core', 'multiselect'); diff --git a/apps/encryption/tests/lib/KeyManagerTest.php b/apps/encryption/tests/lib/KeyManagerTest.php index 260e69a73bf..01c5e1d2d07 100644 --- a/apps/encryption/tests/lib/KeyManagerTest.php +++ b/apps/encryption/tests/lib/KeyManagerTest.php @@ -27,33 +27,42 @@ class KeyManagerTest extends TestCase { */ private $dummyKeys; + /** + * + */ public function setUp() { parent::setUp(); - $keyStorageMock = $this->getMock('OCP\Encryption\IKeyStorage'); - $cryptMock = $this->getMockBuilder('OCA\Encryption\Crypt') + $keyStorageMock = $this->getMock('OCP\Encryption\Keys\IStorage'); + $keyStorageMock->method('getUserKey') + ->will($this->returnValue(false)); + $keyStorageMock->method('setUserKey') + ->will($this->returnValue(true)); + $cryptMock = $this->getMockBuilder('OCA\Encryption\Crypto\Crypt') ->disableOriginalConstructor() ->getMock(); $configMock = $this->getMock('OCP\IConfig'); - $userMock = $this->getMock('OCP\IUser'); - $userMock->expects($this->once()) + $userMock = $this->getMock('OCP\IUserSession'); + $userMock ->method('getUID') ->will($this->returnValue('admin')); + $cacheMock = $this->getMock('OCP\ICacheFactory'); + $logMock = $this->getMock('OCP\ILogger'); $this->userId = 'admin'; - $this->instance = new KeyManager($keyStorageMock, $cryptMock, $configMock, $userMock); + $this->instance = new KeyManager($keyStorageMock, $cryptMock, $configMock, $userMock, $cacheMock, $logMock); $this->dummyKeys = ['public' => 'randomweakpublickeyhere', 'private' => 'randomweakprivatekeyhere']; } /** - * @expectedException OC\Encryption\Exceptions\PrivateKeyMissingException + * @expectedException \OC\Encryption\Exceptions\PrivateKeyMissingException */ public function testGetPrivateKey() { $this->assertFalse($this->instance->getPrivateKey($this->userId)); } /** - * @expectedException OC\Encryption\Exceptions\PublicKeyMissingException + * @expectedException \OC\Encryption\Exceptions\PublicKeyMissingException */ public function testGetPublicKey() { $this->assertFalse($this->instance->getPublicKey($this->userId)); @@ -73,18 +82,34 @@ class KeyManagerTest extends TestCase { $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)); } + /** + * + */ + public function testInit() { + $this->assertFalse($this->instance->init($this->userId, 'pass')); + } + } diff --git a/apps/encryption/tests/lib/MigratorTest.php b/apps/encryption/tests/lib/MigratorTest.php index a9d57b34209..0d30b9865b9 100644 --- a/apps/encryption/tests/lib/MigratorTest.php +++ b/apps/encryption/tests/lib/MigratorTest.php @@ -51,8 +51,7 @@ class MigratorTest extends TestCase { parent::setUp(); $cryptMock = $this->getMockBuilder('OCA\Encryption\Crypto\Crypt')->disableOriginalConstructor()->getMock(); - $this->instance = new Migrator($this->getMock('OCP\IUser'), - $this->getMock('OCP\IConfig'), + $this->instance = new Migrator($this->getMock('OCP\IConfig'), $this->getMock('OCP\IUserManager'), $this->getMock('OCP\ILogger'), $cryptMock); diff --git a/apps/files_encryption/lib/hooks.php b/apps/files_encryption/lib/hooks.php index 536e512bdb2..4a29ffaaedf 100644 --- a/apps/files_encryption/lib/hooks.php +++ b/apps/files_encryption/lib/hooks.php @@ -34,8 +34,234 @@ 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. */ diff --git a/lib/private/server.php b/lib/private/server.php index 8c5169f229e..6a2e45aa59d 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -702,7 +702,7 @@ class Server extends SimpleContainer implements IServerContainer { * * @return \OCP\Security\ISecureRandom */ - function getSecureRandom() { +function getSecureRandom() { return $this->query('SecureRandom'); } -- 2.39.5