diff --git a/.gitignore b/.gitignore index de467ec2cf..3395774d23 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ /apps*/* !/apps/files !/apps/files_encryption +!/apps/encryption +!/apps/encryption_dummy !/apps/files_external !/apps/files_sharing !/apps/files_trashbin diff --git a/apps/encryption_dummy/appinfo/app.php b/apps/encryption_dummy/appinfo/app.php new file mode 100644 index 0000000000..fa17e676ed --- /dev/null +++ b/apps/encryption_dummy/appinfo/app.php @@ -0,0 +1,6 @@ +getEncryptionManager(); +$module = new \OCA\Encryption_Dummy\DummyModule(); +$manager->registerEncryptionModule($module); + diff --git a/apps/encryption_dummy/appinfo/info.xml b/apps/encryption_dummy/appinfo/info.xml new file mode 100644 index 0000000000..f62f6fb5dd --- /dev/null +++ b/apps/encryption_dummy/appinfo/info.xml @@ -0,0 +1,20 @@ + + + encryption_dummy + dummy encryption module + + This module does nothing, it is used for testing purpose only + + AGPL + Bjoern Schiessle + 8 + true + false + + + + 166047 + + openssl + + diff --git a/apps/encryption_dummy/appinfo/version b/apps/encryption_dummy/appinfo/version new file mode 100644 index 0000000000..8acdd82b76 --- /dev/null +++ b/apps/encryption_dummy/appinfo/version @@ -0,0 +1 @@ +0.0.1 diff --git a/apps/encryption_dummy/lib/dummymodule.php b/apps/encryption_dummy/lib/dummymodule.php new file mode 100644 index 0000000000..8ca9cd4f9a --- /dev/null +++ b/apps/encryption_dummy/lib/dummymodule.php @@ -0,0 +1,145 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OCA\Encryption_Dummy; + +class DummyModule implements \OCP\Encryption\IEncryptionModule { + + /** @var boolean */ + protected $isWriteOperation; + + /** + * @return string defining the technical unique id + */ + public function getId() { + return "34876934"; + } + + /** + * In comparison to getKey() this function returns a human readable (maybe translated) name + * + * @return string + */ + public function getDisplayName() { + return "Dummy Encryption Module"; + } + + /** + * 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 string $user who read/write the file (null for public access) + * @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, $user, $header, $accessList) { + return array(); + } + + /** + * 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) { + + if ($this->isWriteOperation) { + $storage = \OC::$server->getEncryptionKeyStorage($this->getId()); + $storage->setFileKey($path, 'fileKey', 'foo'); + } + return ''; + } + + /** + * encrypt data + * + * @param string $data you want to encrypt + * @return mixed encrypted data + */ + public function encrypt($data) { + $this->isWriteOperation = true; + return $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) { + $this->isWriteOperation=false; + return $data; + } + + /** + * 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) { + return true; + } + + /** + * should the file be encrypted or not + * + * @param string $path + * @return boolean + */ + public function shouldEncrypt($path) { + if (strpos($path, '/'. \OCP\User::getUser() . '/files/') === 0) { + return true; + } + + return false; + } + + /** + * calculate unencrypted size + * + * @param string $path to file + * @return integer unencrypted size + */ + public function calculateUnencryptedSize($path) { + return 42; + } + + public function getUnencryptedBlockSize() { + return 6126; + } + +} \ No newline at end of file diff --git a/lib/base.php b/lib/base.php index da4b3a47c7..2f03a3200c 100644 --- a/lib/base.php +++ b/lib/base.php @@ -148,7 +148,7 @@ class OC { // search the 3rdparty folder OC::$THIRDPARTYROOT = OC_Config::getValue('3rdpartyroot', null); OC::$THIRDPARTYWEBROOT = OC_Config::getValue('3rdpartyurl', null); - + if (empty(OC::$THIRDPARTYROOT) && empty(OC::$THIRDPARTYWEBROOT)) { if (file_exists(OC::$SERVERROOT . '/3rdparty')) { OC::$THIRDPARTYROOT = OC::$SERVERROOT; @@ -163,7 +163,7 @@ class OC { . ' folder in the ownCloud folder or the folder above.' . ' You can also configure the location in the config.php file.'); } - + // search the apps folder $config_paths = OC_Config::getValue('apps_paths', array()); if (!empty($config_paths)) { @@ -613,6 +613,8 @@ class OC { self::registerShareHooks(); self::registerLogRotate(); self::registerLocalAddressBook(); + self::registerEncryptionWrapper(); + self::registerEncryptionHooks(); //make sure temporary files are cleaned up $tmpManager = \OC::$server->getTempManager(); @@ -669,6 +671,45 @@ class OC { }); } + private static function registerEncryptionWrapper() { + $enabled = self::$server->getEncryptionManager()->isEnabled(); + if ($enabled) { + \OC\Files\Filesystem::addStorageWrapper('oc_encryption', function ($mountPoint, $storage) { + $parameters = array('storage' => $storage, 'mountPoint' => $mountPoint); + $manager = \OC::$server->getEncryptionManager(); + $util = new \OC\Encryption\Util(new \OC\Files\View(), \OC::$server->getUserManager()); + $user = \OC::$server->getUserSession()->getUser(); + $logger = \OC::$server->getLogger(); + $uid = $user ? $user->getUID() : null; + return new \OC\Files\Storage\Wrapper\Encryption($parameters, $manager,$util, $logger, $uid); + }); + } + + } + + private static function registerEncryptionHooks() { + $enabled = self::$server->getEncryptionManager()->isEnabled(); + if ($enabled) { + $user = \OC::$server->getUserSession()->getUser(); + $uid = ''; + if ($user) { + $uid = $user->getUID(); + } + $updater = new \OC\Encryption\Update( + new \OC\Files\View(), + new \OC\Encryption\Util(new \OC\Files\View(), \OC::$server->getUserManager()), + \OC\Files\Filesystem::getMountManager(), + \OC::$server->getEncryptionManager(), + $uid + ); + \OCP\Util::connectHook('OCP\Share', 'post_shared', $updater, 'postShared'); + \OCP\Util::connectHook('OCP\Share', 'post_unshare', $updater, 'postUnshared'); + + //\OCP\Util::connectHook('OC_Filesystem', 'post_umount', 'OCA\Files_Encryption\Hooks', 'postUnmount'); + //\OCP\Util::connectHook('OC_Filesystem', 'umount', 'OCA\Files_Encryption\Hooks', 'preUnmount'); + } + } + /** * register hooks for the cache */ diff --git a/lib/private/encryption/exceptions/encryptionheaderkeyexistsexception.php b/lib/private/encryption/exceptions/encryptionheaderkeyexistsexception.php new file mode 100644 index 0000000000..d401f0323b --- /dev/null +++ b/lib/private/encryption/exceptions/encryptionheaderkeyexistsexception.php @@ -0,0 +1,29 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OC\Encryption\Exceptions; + + +class EncryptionHeaderKeyExistsException extends \Exception { + +} \ No newline at end of file diff --git a/lib/private/encryption/exceptions/modulealreadyexistsexception.php b/lib/private/encryption/exceptions/modulealreadyexistsexception.php new file mode 100644 index 0000000000..41fc0188e2 --- /dev/null +++ b/lib/private/encryption/exceptions/modulealreadyexistsexception.php @@ -0,0 +1,28 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OC\Encryption\Exceptions; + +class ModuleAlreadyExistsException extends \Exception { + +} diff --git a/lib/private/encryption/exceptions/moduledoesnotexistsexception.php b/lib/private/encryption/exceptions/moduledoesnotexistsexception.php new file mode 100644 index 0000000000..5507bd03da --- /dev/null +++ b/lib/private/encryption/exceptions/moduledoesnotexistsexception.php @@ -0,0 +1,28 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OC\Encryption\Exceptions; + +class ModuleDoesNotExistsException extends \Exception { + +} diff --git a/lib/private/encryption/keys/factory.php b/lib/private/encryption/keys/factory.php new file mode 100644 index 0000000000..a214b23861 --- /dev/null +++ b/lib/private/encryption/keys/factory.php @@ -0,0 +1,52 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OC\Encryption\Keys; + +use OC\Encryption\Util; +use OC\Files\View; +use OC\User; + +/** + * Factory provides KeyStorage for different encryption modules + */ +class Factory { + /** @var array */ + protected $instances = array(); + + /** + * get a KeyStorage instance + * + * @param string $encryptionModuleId + * @param View $view + * @param Util $util + * @return Storage + */ + public function get($encryptionModuleId,View $view, Util $util) { + if (!isset($this->instances[$encryptionModuleId])) { + $this->instances[$encryptionModuleId] = new Storage($encryptionModuleId, $view, $util); + } + return $this->instances[$encryptionModuleId]; + } + +} diff --git a/lib/private/encryption/keys/storage.php b/lib/private/encryption/keys/storage.php new file mode 100644 index 0000000000..041db2a2cb --- /dev/null +++ b/lib/private/encryption/keys/storage.php @@ -0,0 +1,320 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OC\Encryption\Keys; + +use OC\Encryption\Util; +use OC\Files\View; +use OCA\Files_Encryption\Exception\EncryptionException; + +class Storage implements \OCP\Encryption\Keys\IStorage { + + /** @var View */ + private $view; + + /** @var Util */ + private $util; + + // base dir where all the file related keys are stored + private $keys_base_dir; + private $encryption_base_dir; + + private $keyCache = array(); + + /** @var string */ + private $encryptionModuleId; + + /** + * @param string $encryptionModuleId + * @param View $view + * @param Util $util + */ + public function __construct($encryptionModuleId, View $view, Util $util) { + $this->view = $view; + $this->util = $util; + $this->encryptionModuleId = $encryptionModuleId; + + $this->encryption_base_dir = '/files_encryption'; + $this->keys_base_dir = $this->encryption_base_dir .'/keys'; + } + + /** + * get user specific key + * + * @param string $uid ID if the user for whom we want the key + * @param string $keyId id of the key + * + * @return mixed key + */ + public function getUserKey($uid, $keyId) { + $path = $this->constructUserKeyPath($keyId, $uid); + return $this->getKey($path); + } + + /** + * get file specific key + * + * @param string $path path to file + * @param string $keyId id of the key + * + * @return mixed key + */ + public function getFileKey($path, $keyId) { + $keyDir = $this->getFileKeyDir($path); + return $this->getKey($keyDir . $keyId); + } + + /** + * get system-wide encryption keys not related to a specific user, + * e.g something like a key for public link shares + * + * @param string $keyId id of the key + * + * @return mixed key + */ + public function getSystemUserKey($keyId) { + $path = $this->constructUserKeyPath($keyId); + return $this->getKey($path); + } + + /** + * set user specific key + * + * @param string $uid ID if the user for whom we want the key + * @param string $keyId id of the key + * @param mixed $key + */ + public function setUserKey($uid, $keyId, $key) { + $path = $this->constructUserKeyPath($keyId, $uid); + return $this->setKey($path, $key); + } + + /** + * set file specific key + * + * @param string $path path to file + * @param string $keyId id of the key + * @param boolean + */ + public function setFileKey($path, $keyId, $key) { + $keyDir = $this->getFileKeyDir($path); + return $this->setKey($keyDir . $keyId, $key); + } + + /** + * set system-wide encryption keys not related to a specific user, + * e.g something like a key for public link shares + * + * @param string $keyId id of the key + * @param mixed $key + * + * @return mixed key + */ + public function setSystemUserKey($keyId, $key) { + $path = $this->constructUserKeyPath($keyId); + return $this->setKey($path, $key); + } + + /** + * delete user specific key + * + * @param string $uid ID if the user for whom we want to delete the key + * @param string $keyId id of the key + * + * @return boolean + */ + public function deleteUserKey($uid, $keyId) { + $path = $this->constructUserKeyPath($keyId, $uid); + return $this->view->unlink($path); + } + + /** + * delete file specific key + * + * @param string $path path to file + * @param string $keyId id of the key + * + * @return boolean + */ + public function deleteFileKey($path, $keyId) { + $keyDir = $this->getFileKeyDir($path); + return $this->view->unlink($keyDir . $keyId); + } + + /** + * delete all file keys for a given file + * + * @param string $path to the file + * @return boolean + */ + public function deleteAllFileKeys($path) { + $keyDir = $this->getFileKeyDir($path); + return $this->view->deleteAll(dirname($keyDir)); + } + + /** + * delete system-wide encryption keys not related to a specific user, + * e.g something like a key for public link shares + * + * @param string $keyId id of the key + * + * @return boolean + */ + public function deleteSystemUserKey($keyId) { + $path = $this->constructUserKeyPath($keyId); + return $this->view->unlink($path); + } + + + /** + * construct path to users key + * + * @param string $keyId + * @param string $uid + * @return string + */ + protected function constructUserKeyPath($keyId, $uid = null) { + + if ($uid === null) { + $path = $this->encryption_base_dir . '/' . $this->encryptionModuleId . '/' . $keyId; + } else { + $path = '/' . $uid . $this->encryption_base_dir . '/' + . $this->encryptionModuleId . '/' . $uid . '.' . $keyId; + } + + return $path; + } + + /** + * read key from hard disk + * + * @param string $path to key + * @return string + */ + private function getKey($path) { + + $key = ''; + + if ($this->view->file_exists($path)) { + if (isset($this->keyCache[$path])) { + $key = $this->keyCache[$path]; + } else { + $key = $this->view->file_get_contents($path); + $this->keyCache[$path] = $key; + } + } + + return $key; + } + + /** + * write key to disk + * + * + * @param string $path path to key directory + * @param string $key key + * @return bool + */ + private function setKey($path, $key) { + $this->keySetPreparation(dirname($path)); + + $result = $this->view->file_put_contents($path, $key); + + if (is_int($result) && $result > 0) { + $this->keyCache[$path] = $key; + return true; + } + + return false; + } + + /** + * get path to key folder for a given file + * + * @param string $path path to the file, relative to data/ + * @return string + * @throws EncryptionException + * @internal param string $keyId + */ + private function getFileKeyDir($path) { + + if ($this->view->is_dir($path)) { + throw new EncryptionException('file was expected but directory was given', EncryptionException::GENERIC); + } + + list($owner, $filename) = $this->util->getUidAndFilename($path); + $filename = $this->util->stripPartialFileExtension($filename); + + // in case of system wide mount points the keys are stored directly in the data directory + if ($this->util->isSystemWideMountPoint($filename)) { + $keyPath = $this->keys_base_dir . $filename . '/'; + } else { + $keyPath = '/' . $owner . $this->keys_base_dir . $filename . '/'; + } + + return \OC\Files\Filesystem::normalizePath($keyPath . $this->encryptionModuleId . '/', false); + } + + /** + * move keys if a file was renamed + * + * @param string $source + * @param string $target + * @param string $owner + * @param bool $systemWide + */ + public function renameKeys($source, $target, $owner, $systemWide) { + if ($systemWide) { + $sourcePath = $this->keys_base_dir . $source . '/'; + $targetPath = $this->keys_base_dir . $target . '/'; + } else { + $sourcePath = '/' . $owner . $this->keys_base_dir . $source . '/'; + $targetPath = '/' . $owner . $this->keys_base_dir . $target . '/'; + } + + if ($this->view->file_exists($sourcePath)) { + $this->keySetPreparation(dirname($targetPath)); + $this->view->rename($sourcePath, $targetPath); + } + } + + /** + * Make preparations to filesystem for saving a keyfile + * + * @param string $path relative to the views root + */ + protected function keySetPreparation($path) { + // If the file resides within a subdirectory, create it + if (!$this->view->file_exists($path)) { + $sub_dirs = explode('/', $path); + $dir = ''; + foreach ($sub_dirs as $sub_dir) { + $dir .= '/' . $sub_dir; + if (!$this->view->is_dir($dir)) { + $this->view->mkdir($dir); + } + } + } + } + +} diff --git a/lib/private/encryption/manager.php b/lib/private/encryption/manager.php new file mode 100644 index 0000000000..5164025239 --- /dev/null +++ b/lib/private/encryption/manager.php @@ -0,0 +1,170 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OC\Encryption; + +use OCP\Encryption\IEncryptionModule; + +class Manager implements \OCP\Encryption\IManager { + + /** @var array */ + protected $encryptionModules; + + /** @var \OCP\IConfig */ + protected $config; + + /** + * @param \OCP\IConfig $config + */ + public function __construct(\OCP\IConfig $config) { + $this->encryptionModules = array(); + $this->config = $config; + } + + /** + * Check if encryption is enabled + * + * @return bool true if enabled, false if not + */ + public function isEnabled() { + + $installed = $this->config->getSystemValue('installed', false); + if (!$installed) { + return false; + } + + $enabled = $this->config->getAppValue('core', 'encryption_enabled', 'no'); + return $enabled === 'yes'; + } + + /** + * Registers an encryption module + * + * @param IEncryptionModule $module + * @throws Exceptions\ModuleAlreadyExistsException + */ + public function registerEncryptionModule(IEncryptionModule $module) { + $id = $module->getId(); + $name = $module->getDisplayName(); + if (isset($this->encryptionModules[$id])) { + $message = 'Id "' . $id . '" already used by encryption module "' . $name . '"'; + throw new Exceptions\ModuleAlreadyExistsException($message); + + } + + $defaultEncryptionModuleId = $this->getDefaultEncryptionModuleId(); + + if (empty($defaultEncryptionModuleId)) { + $this->setDefaultEncryptionModule($id); + } + + $this->encryptionModules[$id] = $module; + } + + /** + * Unregisters an encryption module + * + * @param IEncryptionModule $module + */ + public function unregisterEncryptionModule(IEncryptionModule $module) { + unset($this->encryptionModules[$module->getId()]); + } + + /** + * get a list of all encryption modules + * + * @return IEncryptionModule[] + */ + public function getEncryptionModules() { + return $this->encryptionModules; + } + + /** + * get a specific encryption module + * + * @param string $moduleId + * @return IEncryptionModule + * @throws Exceptions\ModuleDoesNotExistsException + */ + public function getEncryptionModule($moduleId) { + if (isset($this->encryptionModules[$moduleId])) { + return $this->encryptionModules[$moduleId]; + } else { + $message = "Module with id: $moduleId does not exists."; + throw new Exceptions\ModuleDoesNotExistsException($message); + } + } + + /** + * get default encryption module + * + * @return \OCP\Encryption\IEncryptionModule + * @throws Exceptions\ModuleDoesNotExistsException + */ + public function getDefaultEncryptionModule() { + $defaultModuleId = $this->getDefaultEncryptionModuleId(); + if (!empty($defaultModuleId)) { + if (isset($this->encryptionModules[$defaultModuleId])) { + return $this->encryptionModules[$defaultModuleId]; + } else { + $message = 'Default encryption module not loaded'; + throw new Exceptions\ModuleDoesNotExistsException($message); + } + } else { + $message = 'No default encryption module defined'; + throw new Exceptions\ModuleDoesNotExistsException($message); + } + + } + + /** + * set default encryption module Id + * + * @param string $moduleId + * @return bool + */ + public function setDefaultEncryptionModule($moduleId) { + try { + $this->config->setAppValue('core', 'default_encryption_module', $moduleId); + return true; + } catch (\Exception $e) { + return false; + } + + } + + /** + * get default encryption module Id + * + * @return string + */ + protected function getDefaultEncryptionModuleId() { + try { + return $this->config->getAppValue('core', 'default_encryption_module'); + } catch (\Exception $e) { + return ''; + } + } + + +} diff --git a/lib/private/encryption/update.php b/lib/private/encryption/update.php new file mode 100644 index 0000000000..649cf0285a --- /dev/null +++ b/lib/private/encryption/update.php @@ -0,0 +1,111 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OC\Encryption; + +use \OC\Files\Mount; +use \OC\Files\View; + +/** + * update encrypted files, e.g. because a file was shared + */ +class Update { + + /** @var \OC\Files\View */ + protected $view; + + /** @var \OC\Encryption\Util */ + protected $util; + + /** @var \OC\Files\Mount\Manager */ + protected $mountManager; + + /** @var \OC\Encryption\Manager */ + protected $encryptionManager; + + /** @var string */ + protected $uid; + + /** + * + * @param \OC\Files\View $view + * @param \OC\Encryption\Util $util + * @param \OC\Files\Mount\Manager $mountManager + * @param \OC\Encryption\Manager $encryptionManager + * @param string $uid + */ + public function __construct( + View $view, + Util $util, + Mount\Manager $mountManager, + Manager $encryptionManager, + $uid + ) { + + $this->view = $view; + $this->util = $util; + $this->mountManager = $mountManager; + $this->encryptionManager = $encryptionManager; + $this->uid = $uid; + } + + public function postShared($params) { + if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') { + $this->update($params['fileSource']); + } + } + + public function postUnshared($params) { + if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') { + $this->update($params['fileSource']); + } + } + + /** + * update keyfiles and share keys recursively + * + * @param int $fileSource file source id + */ + private function update($fileSource) { + $path = \OC\Files\Filesystem::getPath($fileSource); + $absPath = '/' . $this->uid . '/files' . $path; + + $mount = $this->mountManager->find($path); + $mountPoint = $mount->getMountPoint(); + + // if a folder was shared, get a list of all (sub-)folders + if ($this->view->is_dir($absPath)) { + $allFiles = $this->util->getAllFiles($absPath, $mountPoint); + } else { + $allFiles = array($absPath); + } + + $encryptionModule = $this->encryptionManager->getDefaultEncryptionModule(); + + foreach ($allFiles as $path) { + $usersSharing = $this->util->getSharingUsersArray($path); + $encryptionModule->update($absPath, $usersSharing); + } + } + +} \ No newline at end of file diff --git a/lib/private/encryption/util.php b/lib/private/encryption/util.php new file mode 100644 index 0000000000..2c6ff26684 --- /dev/null +++ b/lib/private/encryption/util.php @@ -0,0 +1,401 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OC\Encryption; + +use OC\Encryption\Exceptions\EncryptionHeaderToLargeException; +use OC\Encryption\Exceptions\EncryptionHeaderKeyExistsException; +use OCP\Encryption\IEncryptionModule; + +class Util { + + const HEADER_START = 'HBEGIN'; + const HEADER_END = 'HEND'; + const HEADER_PADDING_CHAR = '-'; + + const HEADER_ENCRYPTION_MODULE_KEY = 'oc_encryption_module'; + + /** + * block size will always be 8192 for a PHP stream + * @see https://bugs.php.net/bug.php?id=21641 + * @var integer + */ + protected $headerSize = 8192; + + /** + * block size will always be 8192 for a PHP stream + * @see https://bugs.php.net/bug.php?id=21641 + * @var integer + */ + protected $blockSize = 8192; + + /** @var \OC\Files\View */ + protected $view; + + /** @var array */ + protected $ocHeaderKeys; + + /** @var \OC\User\Manager */ + protected $userManager; + + /** @var array paths excluded from encryption */ + protected $excludedPaths; + + /** + * @param \OC\Files\View $view root view + */ + public function __construct(\OC\Files\View $view, \OC\User\Manager $userManager) { + $this->ocHeaderKeys = [ + self::HEADER_ENCRYPTION_MODULE_KEY + ]; + + $this->view = $view; + $this->userManager = $userManager; + + $this->excludedPaths[] = 'files_encryption'; + } + + /** + * read encryption module ID from header + * + * @param array $header + * @return string + */ + public function getEncryptionModuleId(array $header) { + $id = ''; + $encryptionModuleKey = self::HEADER_ENCRYPTION_MODULE_KEY; + + if (isset($header[$encryptionModuleKey])) { + $id = $header[$encryptionModuleKey]; + } + + return $id; + } + + /** + * read header into array + * + * @param string $header + * @return array + */ + public function readHeader($header) { + + $result = array(); + + if (substr($header, 0, strlen(self::HEADER_START)) === self::HEADER_START) { + $endAt = strpos($header, self::HEADER_END); + if ($endAt !== false) { + $header = substr($header, 0, $endAt + strlen(self::HEADER_END)); + + // +1 to not start with an ':' which would result in empty element at the beginning + $exploded = explode(':', substr($header, strlen(self::HEADER_START)+1)); + + $element = array_shift($exploded); + while ($element !== self::HEADER_END) { + $result[$element] = array_shift($exploded); + $element = array_shift($exploded); + } + } + } + + return $result; + } + + /** + * create header for encrypted file + * + * @param array $headerData + * @param IEncryptionModule $encryptionModule + * @return string + * @throws EncryptionHeaderToLargeException if header has to many arguments + * @throws EncryptionHeaderKeyExistsException if header key is already in use + */ + public function createHeader(array $headerData, IEncryptionModule $encryptionModule) { + $header = self::HEADER_START . ':' . self::HEADER_ENCRYPTION_MODULE_KEY . ':' . $encryptionModule->getId() . ':'; + foreach ($headerData as $key => $value) { + if (in_array($key, $this->ocHeaderKeys)) { + throw new EncryptionHeaderKeyExistsException('header key "'. $key . '" already reserved by ownCloud'); + } + $header .= $key . ':' . $value . ':'; + } + $header .= self::HEADER_END; + + if (strlen($header) > $this->getHeaderSize()) { + throw new EncryptionHeaderToLargeException('max header size exceeded', EncryptionException::ENCRYPTION_HEADER_TO_LARGE); + } + + $paddedHeader = str_pad($header, $this->headerSize, self::HEADER_PADDING_CHAR, STR_PAD_RIGHT); + + return $paddedHeader; + } + + /** + * Find, sanitise and format users sharing a file + * @note This wraps other methods into a portable bundle + * @param string $path path relative to current users files folder + * @return array + */ + public function getSharingUsersArray($path) { + + // Make sure that a share key is generated for the owner too + list($owner, $ownerPath) = $this->getUidAndFilename($path); + + // always add owner to the list of users with access to the file + $userIds = array($owner); + + if (!$this->isFile($ownerPath)) { + return array('users' => $userIds, 'public' => false); + } + + $ownerPath = substr($ownerPath, strlen('/files')); + $ownerPath = $this->stripPartialFileExtension($ownerPath); + + // Find out who, if anyone, is sharing the file + $result = \OCP\Share::getUsersSharingFile($ownerPath, $owner); + $userIds = \array_merge($userIds, $result['users']); + $public = $result['public'] || $result['remote']; + + // check if it is a group mount + if (\OCP\App::isEnabled("files_external")) { + $mounts = \OC_Mount_Config::getSystemMountPoints(); + foreach ($mounts as $mount) { + if ($mount['mountpoint'] == substr($ownerPath, 1, strlen($mount['mountpoint']))) { + $mountedFor = $this->getUserWithAccessToMountPoint($mount['applicable']['users'], $mount['applicable']['groups']); + $userIds = array_merge($userIds, $mountedFor); + } + } + } + + // Remove duplicate UIDs + $uniqueUserIds = array_unique($userIds); + + return array('users' => $uniqueUserIds, 'public' => $public); + } + + /** + * go recursively through a dir and collect all files and sub files. + * + * @param string $dir relative to the users files folder + * @param strinf $mountPoint + * @return array with list of files relative to the users files folder + */ + public function getAllFiles($dir, $mountPoint = '') { + $result = array(); + $dirList = array($dir); + + while ($dirList) { + $dir = array_pop($dirList); + $content = $this->view->getDirectoryContent($dir); + + foreach ($content as $c) { + // getDirectoryContent() returns the paths relative to the mount points, so we need + // to re-construct the complete path + $path = ($mountPoint !== '') ? $mountPoint . '/' . $c['path'] : $c['path']; + if ($c['type'] === 'dir') { + $dirList[] = $path; + } else { + $result[] = $path; + } + } + + } + + return $result; + } + + /** + * check if it is a file uploaded by the user stored in data/user/files + * or a metadata file + * + * @param string $path + * @return boolean + */ + protected function isFile($path) { + if (substr($path, 0, strlen('/files/')) === '/files/') { + return true; + } + return false; + } + + /** + * return size of encryption header + * + * @return integer + */ + public function getHeaderSize() { + return $this->headerSize; + } + + /** + * return size of block read by a PHP stream + * + * @return integer + */ + public function getBlockSize() { + return $this->blockSize; + } + + /** + * get the owner and the path for the owner + * + * @param string $path + * @return array + * @throws \BadMethodCallException + */ + public function getUidAndFilename($path) { + + $parts = explode('/', $path); + $uid = ''; + if (count($parts) > 2) { + $uid = $parts[1]; + } + if (!$this->userManager->userExists($uid)) { + throw new \BadMethodCallException('path needs to be relative to the system wide data folder and point to a user specific file'); + } + + $pathinfo = pathinfo($path); + $partfile = false; + $parentFolder = false; + if (array_key_exists('extension', $pathinfo) && $pathinfo['extension'] === 'part') { + // if the real file exists we check this file + $filePath = $pathinfo['dirname'] . '/' . $pathinfo['filename']; + if ($this->view->file_exists($filePath)) { + $pathToCheck = $pathinfo['dirname'] . '/' . $pathinfo['filename']; + } else { // otherwise we look for the parent + $pathToCheck = $pathinfo['dirname']; + $parentFolder = true; + } + $partfile = true; + } else { + $pathToCheck = $path; + } + + $pathToCheck = substr($pathToCheck, strlen('/' . $uid)); + + $this->view->chroot('/' . $uid); + $owner = $this->view->getOwner($pathToCheck); + + // Check that UID is valid + if (!$this->userManager->userExists($owner)) { + throw new \BadMethodCallException('path needs to be relative to the system wide data folder and point to a user specific file'); + } + + \OC\Files\Filesystem::initMountPoints($owner); + + $info = $this->view->getFileInfo($pathToCheck); + $this->view->chroot('/' . $owner); + $ownerPath = $this->view->getPath($info->getId()); + $this->view->chroot('/'); + + if ($parentFolder) { + $ownerPath = $ownerPath . '/'. $pathinfo['filename']; + } + + if ($partfile) { + $ownerPath = $ownerPath . '.' . $pathinfo['extension']; + } + + return array( + $owner, + \OC\Files\Filesystem::normalizePath($ownerPath) + ); + } + + /** + * Remove .path extension from a file path + * @param string $path Path that may identify a .part file + * @return string File path without .part extension + * @note this is needed for reusing keys + */ + public function stripPartialFileExtension($path) { + $extension = pathinfo($path, PATHINFO_EXTENSION); + + if ( $extension === 'part') { + + $newLength = strlen($path) - 5; // 5 = strlen(".part") + $fPath = substr($path, 0, $newLength); + + // if path also contains a transaction id, we remove it too + $extension = pathinfo($fPath, PATHINFO_EXTENSION); + if(substr($extension, 0, 12) === 'ocTransferId') { // 12 = strlen("ocTransferId") + $newLength = strlen($fPath) - strlen($extension) -1; + $fPath = substr($fPath, 0, $newLength); + } + return $fPath; + + } else { + return $path; + } + } + + protected function getUserWithAccessToMountPoint($users, $groups) { + $result = array(); + if (in_array('all', $users)) { + $result = \OCP\User::getUsers(); + } else { + $result = array_merge($result, $users); + foreach ($groups as $group) { + $result = array_merge($result, \OC_Group::usersInGroup($group)); + } + } + + return $result; + } + + /** + * check if the file is stored on a system wide mount point + * @param string $path relative to /data/user with leading '/' + * @return boolean + */ + public function isSystemWideMountPoint($path) { + $normalizedPath = ltrim($path, '/'); + if (\OCP\App::isEnabled("files_external")) { + $mounts = \OC_Mount_Config::getSystemMountPoints(); + foreach ($mounts as $mount) { + if ($mount['mountpoint'] == substr($normalizedPath, 0, strlen($mount['mountpoint']))) { + if ($this->isMountPointApplicableToUser($mount)) { + return true; + } + } + } + } + return false; + } + + /** + * check if it is a path which is excluded by ownCloud from encryption + * + * @param string $path + * @return boolean + */ + public function isExcluded($path) { + $root = explode('/', $path, 2); + if (isset($root[0])) { + if (in_array($root[0], $this->excludedPaths)) { + return true; + } + } + return false; + } + +} diff --git a/lib/private/files/fileinfo.php b/lib/private/files/fileinfo.php index 1acb62033d..c666cde3de 100644 --- a/lib/private/files/fileinfo.php +++ b/lib/private/files/fileinfo.php @@ -148,6 +148,13 @@ class FileInfo implements \OCP\Files\FileInfo, \ArrayAccess { return $this->data['encrypted']; } + /** + * @return int + */ + public function getUnencryptedSize() { + return isset($this->data['unencrypted_size']) ? $this->data['unencrypted_size'] : 0; + } + /** * @return int */ diff --git a/lib/private/files/storage/wrapper/encryption.php b/lib/private/files/storage/wrapper/encryption.php new file mode 100644 index 0000000000..44fc2124f7 --- /dev/null +++ b/lib/private/files/storage/wrapper/encryption.php @@ -0,0 +1,319 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OC\Files\Storage\Wrapper; + +use OC\Encryption\Exceptions\ModuleDoesNotExistsException; + +class Encryption extends Wrapper { + + /** @var string */ + private $mountPoint; + + /** @var \OC\Encryption\Util */ + private $util; + + /** @var \OC\Encryption\Manager */ + private $encryptionManager; + + /** @var \OC\Log */ + private $logger; + + /** @var string */ + private $uid; + + /** @var array */ + private $unencryptedSize; + + /** + * @param array $parameters + * @param \OC\Encryption\Manager $encryptionManager + * @param \OC\Encryption\Util $util + * @param \OC\Log $logger + * @param string $uid user who perform the read/write operation (null for public access) + */ + public function __construct( + $parameters, + \OC\Encryption\Manager $encryptionManager = null, + \OC\Encryption\Util $util = null, + \OC\Log $logger = null, + $uid = null + ) { + + $this->mountPoint = $parameters['mountPoint']; + $this->encryptionManager = $encryptionManager; + $this->util = $util; + $this->logger = $logger; + $this->uid = $uid; + $this->unencryptedSize = array(); + parent::__construct($parameters); + } + + /** + * see http://php.net/manual/en/function.filesize.php + * The result for filesize when called on a folder is required to be 0 + * + * @param string $path + * @return int + */ + public function filesize($path) { + $size = 0; + $fullPath = $this->getFullPath($path); + + $encryptedSize = $this->storage->filesize($path); + + $info = $this->getCache()->get($path); + + if($encryptedSize > 0 && $info['encrypted']) { + $size = $info['unencrypted_size']; + if ($size <= 0) { + $encryptionModule = $this->getEncryptionModule($path); + if ($encryptionModule) { + $size = $encryptionModule->calculateUnencryptedSize($fullPath); + $this->getCache()->update($info['fileid'], array('unencrypted_size' => $size)); + } + } + } else if (isset($this->unencryptedSize[$fullPath]) && isset($info['fileid'])) { + $size = $this->unencryptedSize[$fullPath]; + $info['encrypted'] = true; + $info['unencrypted_size'] = $size; + $info['size'] = $encryptedSize; + $this->getCache()->put($path, $info); + } + + return $size; + } + + /** + * see http://php.net/manual/en/function.file_get_contents.php + * + * @param string $path + * @return string + */ + public function file_get_contents($path) { + + $data = null; + $encryptionModule = $this->getEncryptionModule($path); + + if ($encryptionModule) { + + $handle = $this->fopen($path, 'r'); + + if (is_resource($handle)) { + while (!feof($handle)) { + $data .= fread($handle, $this->util->getBlockSize()); + } + } + } else { + $data = $this->storage->file_get_contents($path); + } + + return $data; + } + + /** + * see http://php.net/manual/en/function.file_put_contents.php + * + * @param string $path + * @param string $data + * @return bool + */ + public function file_put_contents($path, $data) { + // file put content will always be translated to a stream write + $handle = $this->fopen($path, 'w'); + fwrite($handle, $data); + return fclose($handle); + } + + /** + * see http://php.net/manual/en/function.unlink.php + * + * @param string $path + * @return bool + */ + public function unlink($path) { + if ($this->util->isExcluded($path)) { + return $this->storage->unlink($path); + } + + $encryptionModule = $this->getEncryptionModule($path); + if ($encryptionModule) { + $keyStorage = \OC::$server->getEncryptionKeyStorage($encryptionModule->getId()); + $keyStorage->deleteAllFileKeys($this->getFullPath($path)); + } + + return $this->storage->unlink($path); + } + + /** + * see http://php.net/manual/en/function.rename.php + * + * @param string $path1 + * @param string $path2 + * @return bool + */ + public function rename($path1, $path2) { + if ($this->util->isExcluded($path1)) { + return $this->storage->rename($path1, $path2); + } + + $fullPath1 = $this->getFullPath($path1); + list($owner, $source) = $this->util->getUidAndFilename($fullPath1); + $result = $this->storage->rename($path1, $path2); + if ($result) { + $fullPath2 = $this->getFullPath($path2); + $systemWide = $this->util->isSystemWideMountPoint($this->mountPoint); + list(, $target) = $this->util->getUidAndFilename($fullPath2); + $encryptionModule = $this->getEncryptionModule($path2); + if ($encryptionModule) { + $keyStorage = \OC::$server->getEncryptionKeyStorage($encryptionModule->getId()); + $keyStorage->renameKeys($source, $target, $owner, $systemWide); + } + } + + return $result; + } + + /** + * see http://php.net/manual/en/function.copy.php + * + * @param string $path1 + * @param string $path2 + * @return bool + */ + public function copy($path1, $path2) { + // todo copy encryption keys, get users with access to the file and reencrypt + // or is this to encryption module specific? Then we can hand this over + return $this->storage->copy($path1, $path2); + } + + /** + * see http://php.net/manual/en/function.fopen.php + * + * @param string $path + * @param string $mode + * @return resource + */ + public function fopen($path, $mode) { + + $shouldEncrypt = false; + $encryptionModule = null; + $header = $this->getHeader($path); + $fullPath = $this->getFullPath($path); + $encryptionModuleId = $this->util->getEncryptionModuleId($header); + + $size = $unencryptedSize = 0; + if ($this->file_exists($path)) { + $size = $this->storage->filesize($path); + $unencryptedSize = $this->filesize($path); + } + + try { + + if ( + $mode === 'w' + || $mode === 'w+' + || $mode === 'wb' + || $mode === 'wb+' + ) { + if (!empty($encryptionModuleId)) { + $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId); + } else { + $encryptionModule = $this->encryptionManager->getDefaultEncryptionModule(); + } + $shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath); + } else { + // only get encryption module if we found one in the header + if (!empty($encryptionModuleId)) { + $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId); + $shouldEncrypt = true; + } + } + } catch (ModuleDoesNotExistsException $e) { + $this->logger->warning('Encryption module "' . $encryptionModuleId . + '" not found, file will be stored unencrypted'); + } + + if($shouldEncrypt === true && !$this->util->isExcluded($path) && $encryptionModule !== null) { + $source = $this->storage->fopen($path, $mode); + $handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header, + $this->uid, $encryptionModule, $this->storage, $this, $this->util, $mode, + $size, $unencryptedSize); + return $handle; + } else { + return $this->storage->fopen($path, $mode); + } + } + + /** + * return full path, including mount point + * + * @param string $path relative to mount point + * @return string full path including mount point + */ + protected function getFullPath($path) { + return \OC\Files\Filesystem::normalizePath($this->mountPoint . '/' . $path); + } + + /** + * read header from file + * + * @param string $path + * @return array + */ + protected function getHeader($path) { + $header = ''; + if ($this->storage->file_exists($path)) { + $handle = $this->storage->fopen($path, 'r'); + $header = fread($handle, $this->util->getHeaderSize()); + fclose($handle); + } + return $this->util->readHeader($header); + } + + /** + * read encryption module needed to read/write the file located at $path + * + * @param string $path + * @return \OCP\Encryption\IEncryptionModule|null + */ + protected function getEncryptionModule($path) { + $encryptionModule = null; + $header = $this->getHeader($path); + $encryptionModuleId = $this->util->getEncryptionModuleId($header); + if (!empty($encryptionModuleId)) { + try { + $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId); + } catch (ModuleDoesNotExistsException $e) { + $this->logger->critical('Encryption module defined in "' . $path . '" mot loaded!'); + throw $e; + } + } + return $encryptionModule; + } + + public function updateUnencryptedSize($path, $unencryptedSize) { + $this->unencryptedSize[$path] = $unencryptedSize; + } + +} diff --git a/lib/private/files/stream/encryption.php b/lib/private/files/stream/encryption.php new file mode 100644 index 0000000000..ddef9067ba --- /dev/null +++ b/lib/private/files/stream/encryption.php @@ -0,0 +1,385 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OC\Files\Stream; + +use Icewind\Streams\Wrapper; +use OC\Encryption\Exceptions\EncryptionHeaderKeyExistsException; + +class Encryption extends Wrapper { + + /** @var \OC\Encryption\Util */ + protected $util; + + /** @var \OCP\Encryption\IEncryptionModule */ + protected $encryptionModule; + + /** @var \OC\Files\Storage\Storage */ + protected $storage; + + /** @var \OC\Files\Storage\Wrapper\Encryption */ + protected $encryptionStorage; + + /** @var string */ + protected $internalPath; + + /** @var integer */ + protected $size; + + /** @var integer */ + protected $position; + + /** @var integer */ + protected $unencryptedSize; + + /** @var integer */ + protected $unencryptedBlockSize; + + /** @var array */ + protected $header; + + /** @var string */ + protected $fullPath; + + /** + * header data returned by the encryption module, will be written to the file + * in case of a write operation + * + * @var array + */ + protected $newHeader; + + /** + * user who perform the read/write operation null for public access + * + * @var string + */ + protected $uid; + + /** @var bool */ + protected $readOnly; + + /** @var array */ + protected $expectedContextProperties; + + public function __construct() { + $this->expectedContextProperties = array( + 'source', + 'storage', + 'internalPath', + 'fullPath', + 'encryptionModule', + 'header', + 'uid', + 'util', + 'size', + 'unencryptedSize', + 'encryptionStorage' + ); + } + + + /** + * Wraps a stream with the provided callbacks + * + * @param resource $source + * @param string $internalPath relative to mount point + * @param string $fullPath relative to data/ + * @param array $header + * @param sting $uid + * @param \OCP\Encryption\IEncryptionModule $encryptionModule + * @param \OC\Files\Storage\Storage $storage + * @param OC\Files\Storage\Wrapper\Encryption $encStorage + * @param \OC\Encryption\Util $util + * @param string $mode + * @param int $size + * @param int $unencryptedSize + * @return resource + * + * @throws \BadMethodCallException + */ + public static function wrap($source, $internalPath, $fullPath, array $header, + $uid, \OCP\Encryption\IEncryptionModule $encryptionModule, + \OC\Files\Storage\Storage $storage, \OC\Files\Storage\Wrapper\Encryption $encStorage, + \OC\Encryption\Util $util, $mode, $size, $unencryptedSize) { + + $context = stream_context_create(array( + 'ocencryption' => array( + 'source' => $source, + 'storage' => $storage, + 'internalPath' => $internalPath, + 'fullPath' => $fullPath, + 'encryptionModule' => $encryptionModule, + 'header' => $header, + 'uid' => $uid, + 'util' => $util, + 'size' => $size, + 'unencryptedSize' => $unencryptedSize, + 'encryptionStorage' => $encStorage + ) + )); + + return self::wrapSource($source, $mode, $context, 'ocencryption', 'OC\Files\Stream\Encryption'); + } + + /** + * add stream wrapper + * + * @param resource $source + * @param string $mode + * @param array $context + * @param string $protocol + * @param string $class + * @return resource + * @throws \BadMethodCallException + */ + protected static function wrapSource($source, $mode, $context, $protocol, $class) { + try { + stream_wrapper_register($protocol, $class); + if (@rewinddir($source) === false) { + $wrapped = fopen($protocol . '://', $mode, false, $context); + } else { + $wrapped = opendir($protocol . '://', $context); + } + } catch (\BadMethodCallException $e) { + stream_wrapper_unregister($protocol); + throw $e; + } + stream_wrapper_unregister($protocol); + return $wrapped; + } + + /** + * Load the source from the stream context and return the context options + * + * @param string $name + * @return array + * @throws \BadMethodCallException + */ + protected function loadContext($name) { + $context = parent::loadContext($name); + + foreach ($this->expectedContextProperties as $property) { + if (isset($context[$property])) { + $this->{$property} = $context[$property]; + } else { + throw new \BadMethodCallException('Invalid context, "' . $property . '" options not set'); + } + } + return $context; + + } + + public function stream_open($path, $mode, $options, &$opened_path) { + $this->loadContext('ocencryption'); + + $this->position = 0; + $this->unencryptedBlockSize = $this->encryptionModule->getUnencryptedBlockSize(); + + if ( + $mode === 'w' + || $mode === 'w+' + || $mode === 'wb' + || $mode === 'wb+' + ) { + // We're writing a new file so start write counter with 0 bytes + // TODO can we remove this completely? + //$this->unencryptedSize = 0; + //$this->size = 0; + $this->readOnly = false; + } else { + $this->readOnly = true; + } + + $sharePath = $this->fullPath; + if (!$this->storage->file_exists($this->internalPath)) { + $sharePath = dirname($path); + } + + $accessList = $this->util->getSharingUsersArray($sharePath); + $this->newHeader = $this->encryptionModule->begin($this->fullPath, $this->uid, $this->header, $accessList); + + return true; + + } + + public function stream_read($count) { + + $result = ''; + + // skip the header if we read the file from the beginning + if ($this->position === 0 && !empty($this->header)) { + parent::stream_read($this->util->getBlockSize()); + } + + while ($count > 0) { + $remainingLength = $count; + // update the cache of the current block + $data = parent::stream_read($this->util->getBlockSize()); + $decrypted = $this->encryptionModule->decrypt($data); + // determine the relative position in the current block + $blockPosition = ($this->position % $this->unencryptedBlockSize); + // if entire read inside current block then only position needs to be updated + if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) { + $result .= substr($decrypted, $blockPosition, $remainingLength); + $this->position += $remainingLength; + $count = 0; + // otherwise remainder of current block is fetched, the block is flushed and the position updated + } else { + $result .= substr($decrypted, $blockPosition); + $this->position += ($this->unencryptedBlockSize - $blockPosition); + $count -= ($this->unencryptedBlockSize - $blockPosition); + } + } + return $result; + + } + + public function stream_write($data) { + + if ($this->position === 0) { + $this->writeHeader(); + } + + $length = 0; + // loop over $data to fit it in 6126 sized unencrypted blocks + while (strlen($data) > 0) { + $remainingLength = strlen($data); + + // read current block + $currentBlock = parent::stream_read($this->util->getBlockSize()); + $decrypted = $this->encryptionModule->decrypt($currentBlock, $this->uid); + + // for seekable streams the pointer is moved back to the beginning of the encrypted block + // flush will start writing there when the position moves to another block + $positionInFile = floor($this->position / $this->unencryptedBlockSize) * + $this->util->getBlockSize() + $this->util->getHeaderSize(); + $resultFseek = parent::stream_seek($positionInFile); + + // only allow writes on seekable streams, or at the end of the encrypted stream + if ($resultFseek || $positionInFile === $this->size) { + + // determine the relative position in the current block + $blockPosition = ($this->position % $this->unencryptedBlockSize); + // check if $data fits in current block + // if so, overwrite existing data (if any) + // update position and liberate $data + if ($remainingLength < ($this->unencryptedBlockSize - $blockPosition)) { + $decrypted = substr($decrypted, 0, $blockPosition) + . $data . substr($decrypted, $blockPosition + $remainingLength); + $encrypted = $this->encryptionModule->encrypt($decrypted); + parent::stream_write($encrypted); + $this->position += $remainingLength; + $length += $remainingLength; + $data = ''; + // if $data doens't fit the current block, the fill the current block and reiterate + // after the block is filled, it is flushed and $data is updatedxxx + } else { + $decrypted = substr($decrypted, 0, $blockPosition) . + substr($data, 0, $this->unencryptedBlockSize - $blockPosition); + $encrypted = $this->encryptionModule->encrypt($decrypted); + parent::stream_write($encrypted); + $this->position += ($this->unencryptedBlockSize - $blockPosition); + $this->size = max($this->size, $this->stream_tell()); + $length += ($this->unencryptedBlockSize - $blockPosition); + $data = substr($data, $this->unencryptedBlockSize - $blockPosition); + } + } else { + $encrypted = $this->encryptionModule->encrypt($data); + parent::stream_write($encrypted); + $data = ''; + } + } + $this->unencryptedSize = max($this->unencryptedSize, $this->position); + return $length; + } + + public function stream_tell() { + return $this->position; + } + + public function stream_seek($offset, $whence = SEEK_SET) { + + $return = false; + + switch ($whence) { + case SEEK_SET: + if ($offset < $this->unencryptedSize && $offset >= 0) { + $newPosition = $offset; + } + break; + case SEEK_CUR: + if ($offset >= 0) { + $newPosition = $offset + $this->position; + } + break; + case SEEK_END: + if ($this->unencryptedSize + $offset >= 0) { + $newPosition = $this->unencryptedSize + $offset; + } + break; + default: + return $return; + } + + $newFilePosition = floor($newPosition / $this->unencryptedBlockSize) + * $this->util->getBlockSize() + $this->util->getHeaderSize(); + + if (parent::stream_seek($newFilePosition)) { + $this->position = $newPosition; + $return = true; + } + return $return; + + } + + public function stream_close() { + $this->flush(); + return parent::stream_close(); + } + + /** + * tell encryption module that we are done and write remaining data to the file + */ + protected function flush() { + $remainingData = $this->encryptionModule->end($this->fullPath); + if ($this->readOnly === false) { + if(!empty($remainingData)) { + parent::stream_write($remainingData); + } + $this->encryptionStorage->updateUnencryptedSize($this->fullPath, $this->unencryptedSize); + } + } + + + /** + * write header at beginning of encrypted file + * + * @throws EncryptionHeaderKeyExistsException if header key is already in use + */ + private function writeHeader() { + $header = $this->util->createHeader($this->newHeader, $this->encryptionModule); + parent::stream_write($header); + } + +} diff --git a/lib/private/server.php b/lib/private/server.php index fd7a2bea2d..d5b72000dc 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -45,9 +45,18 @@ class Server extends SimpleContainer implements IServerContainer { $this->registerService('ContactsManager', function ($c) { return new ContactsManager(); }); + $this->registerService('PreviewManager', function (Server $c) { return new PreviewManager($c->getConfig()); }); + + $this->registerService('EncryptionManager', function (Server $c) { + return new Encryption\Manager($c->getConfig()); + }); + + $this->registerService('EncryptionKeyStorageFactory', function ($c) { + return new Encryption\Keys\Factory(); + }); $this->registerService('TagMapper', function(Server $c) { return new TagMapper($c->getDatabaseConnection()); }); @@ -347,6 +356,24 @@ class Server extends SimpleContainer implements IServerContainer { return $this->query('ContactsManager'); } + /** + * @return \OC\Encryption\Manager + */ + function getEncryptionManager() { + return $this->query('EncryptionManager'); + } + + /** + * @param string $encryptionModuleId encryption module ID + * + * @return \OCP\Encryption\Keys\IStorage + */ + function getEncryptionKeyStorage($encryptionModuleId) { + $view = new \OC\Files\View(); + $util = new \OC\Encryption\Util($view, \OC::$server->getUserManager()); + return $this->query('EncryptionKeyStorageFactory')->get($encryptionModuleId, $view, $util); + } + /** * The current request object holding all information about the request * currently being processed is returned from this method. diff --git a/lib/public/encryption/iencryptionmodule.php b/lib/public/encryption/iencryptionmodule.php new file mode 100644 index 0000000000..2527e35e63 --- /dev/null +++ b/lib/public/encryption/iencryptionmodule.php @@ -0,0 +1,115 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OCP\Encryption; + +interface IEncryptionModule { + + /** + * @return string defining the technical unique id + */ + public function getId(); + + /** + * In comparison to getKey() this function returns a human readable (maybe translated) name + * + * @return string + */ + public function getDisplayName(); + + /** + * 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 string $user who read/write the file (null for public access) + * @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, $user, $header, $accessList); + + /** + * 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); + + /** + * encrypt data + * + * @param string $data you want to encrypt + * @return mixed encrypted data + */ + public function encrypt($data); + + /** + * decrypt data + * + * @param string $data you want to decrypt + * @return mixed decrypted data + */ + public function decrypt($data); + + /** + * 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); + + /** + * should the file be encrypted or not + * + * @param string $path + * @return boolean + */ + public function shouldEncrypt($path); + + /** + * calculate unencrypted size + * + * @param string $path to file + * @return integer unencrypted size + */ + public function calculateUnencryptedSize($path); + + /** + * get size of the unencrypted payload per block. + * ownCloud read/write files with a block size of 8192 byte + * + * @return integer + */ + public function getUnencryptedBlockSize(); +} diff --git a/lib/public/encryption/imanager.php b/lib/public/encryption/imanager.php new file mode 100644 index 0000000000..9a12e40159 --- /dev/null +++ b/lib/public/encryption/imanager.php @@ -0,0 +1,92 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OCP\Encryption; +// +// TODO: move exceptions to OCP +// +use OC\Encryption\Exceptions\ModuleDoesNotExistsException; +use OC\Encryption\Exceptions\ModuleAlreadyExistsException; + +/** + * This class provides access to files encryption apps. + * + */ +interface IManager { + + /** + * Check if encryption is available (at least one encryption module needs to be enabled) + * + * @return bool true if enabled, false if not + */ + function isEnabled(); + + /** + * Registers an encryption module + * + * @param IEncryptionModule $module + * @throws ModuleAlreadyExistsException + */ + function registerEncryptionModule(IEncryptionModule $module); + + /** + * Unregisters an encryption module + * + * @param IEncryptionModule $module + */ + function unregisterEncryptionModule(IEncryptionModule $module); + + /** + * get a list of all encryption modules + * + * @return array + */ + function getEncryptionModules(); + + + /** + * get a specific encryption module + * + * @param string $moduleId + * @return IEncryptionModule + * @throws ModuleDoesNotExistsException + */ + function getEncryptionModule($moduleId); + + /** + * get default encryption module + * + * @return \OCP\Encryption\IEncryptionModule + * @throws Exceptions\ModuleDoesNotExistsException + */ + public function getDefaultEncryptionModule(); + + /** + * set default encryption module Id + * + * @param string $moduleId + * @return string + */ + public function setDefaultEncryptionModule($moduleId); + +} diff --git a/lib/public/encryption/keys/istorage.php b/lib/public/encryption/keys/istorage.php new file mode 100644 index 0000000000..24f6efd6e5 --- /dev/null +++ b/lib/public/encryption/keys/istorage.php @@ -0,0 +1,117 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace OCP\Encryption\Keys; + +interface IStorage { + + /** + * get user specific key + * + * @param string $uid ID if the user for whom we want the key + * @param string $keyId id of the key + * + * @return mixed key + */ + public function getUserKey($uid, $keyId); + + /** + * get file specific key + * + * @param string $path path to file + * @param string $keyId id of the key + * + * @return mixed key + */ + public function getFileKey($path, $keyId); + + /** + * get system-wide encryption keys not related to a specific user, + * e.g something like a key for public link shares + * + * @param string $keyId id of the key + * + * @return mixed key + */ + public function getSystemUserKey($keyId); + + /** + * set user specific key + * + * @param string $uid ID if the user for whom we want the key + * @param string $keyId id of the key + * @param mixed $key + */ + public function setUserKey($uid, $keyId, $key); + + /** + * set file specific key + * + * @param string $path path to file + * @param string $keyId id of the key + * @param boolean + */ + public function setFileKey($path, $keyId, $key); + + /** + * set system-wide encryption keys not related to a specific user, + * e.g something like a key for public link shares + * + * @param string $keyId id of the key + * @param mixed $key + * + * @return mixed key + */ + public function setSystemUserKey($keyId, $key); + + /** + * delete user specific key + * + * @param string $uid ID if the user for whom we want to delete the key + * @param string $keyId id of the key + * + * @return boolean + */ + public function deleteUserKey($uid, $keyId); + + /** + * delete file specific key + * + * @param string $path path to file + * @param string $keyId id of the key + * + * @return boolean + */ + public function deleteFileKey($path, $keyId); + + /** + * delete system-wide encryption keys not related to a specific user, + * e.g something like a key for public link shares + * + * @param string $keyId id of the key + * + * @return boolean + */ + public function deleteSystemUserKey($keyId); + +} diff --git a/settings/admin.php b/settings/admin.php index 6ec4cef350..34e26c60d8 100644 --- a/settings/admin.php +++ b/settings/admin.php @@ -57,6 +57,23 @@ $template->assign('shareExcludeGroups', $excludeGroups); $excludedGroupsList = $appConfig->getValue('core', 'shareapi_exclude_groups_list', ''); $excludedGroupsList = explode(',', $excludedGroupsList); // FIXME: this should be JSON! $template->assign('shareExcludedGroupsList', implode('|', $excludedGroupsList)); +$template->assign('encryptionEnabled', \OC::$server->getEncryptionManager()->isEnabled()); +$encryptionModules = \OC::$server->getEncryptionManager()->getEncryptionModules(); +try { + $defaultEncryptionModule = \OC::$server->getEncryptionManager()->getDefaultEncryptionModule(); + $defaultEncryptionModuleId = $defaultEncryptionModule->getId(); +} catch (Exception $e) { + $defaultEncryptionModule = null; +} +$encModulues = array(); +foreach ($encryptionModules as $module) { + $encModulues[$module->getId()]['displayName'] = $module->getDisplayName(); + $encModulues[$module->getId()]['default'] = false; + if ($defaultEncryptionModule && $module->getId() === $defaultEncryptionModuleId) { + $encModulues[$module->getId()]['default'] = true; + } +} +$template->assign('encryptionModules', $encModulues); // If the current web root is non-empty but the web root from the config is, // and system cron is used, the URL generator fails to build valid URLs. @@ -118,6 +135,7 @@ $formsAndMore = array_merge($formsAndMore, $formsMap); // add bottom hardcoded forms from the template $formsAndMore[] = array('anchor' => 'backgroundjobs', 'section-name' => $l->t('Cron')); $formsAndMore[] = array('anchor' => 'shareAPI', 'section-name' => $l->t('Sharing')); +$formsAndMore[] = array('anchor' => 'encryptionAPI', 'section-name' => $l->t('Server Side Encryption')); $formsAndMore[] = array('anchor' => 'mail_general_settings', 'section-name' => $l->t('Email Server')); $formsAndMore[] = array('anchor' => 'log-section', 'section-name' => $l->t('Log')); diff --git a/settings/css/settings.css b/settings/css/settings.css index 0716cd2493..75acaf5f72 100644 --- a/settings/css/settings.css +++ b/settings/css/settings.css @@ -386,3 +386,12 @@ doesnotexist:-o-prefocus, .strengthify-wrapper { font-weight: normal; margin-top: 5px; } + +#selectEncryptionModules { + margin-left: 30px; + padding: 10px; +} + +#encryptionModules { + padding: 10px; +} \ No newline at end of file diff --git a/settings/js/admin.js b/settings/js/admin.js index a3c941f08a..d16ab90306 100644 --- a/settings/js/admin.js +++ b/settings/js/admin.js @@ -54,6 +54,22 @@ $(document).ready(function(){ $('#shareAPI p:not(#enable)').toggleClass('hidden', !this.checked); }); + $('#encryptionEnabled').change(function() { + $('#encryptionAPI div#selectEncryptionModules').toggleClass('hidden'); + }); + + $('#encryptionAPI input').change(function() { + var value = $(this).val(); + if ($(this).attr('type') === 'checkbox') { + if (this.checked) { + value = 'yes'; + } else { + value = 'no'; + } + } + OC.AppConfig.setValue('core', $(this).attr('name'), value); + }); + $('#shareAPI input:not(#excludedGroups)').change(function() { var value = $(this).val(); if ($(this).attr('type') === 'checkbox') { diff --git a/settings/templates/admin.php b/settings/templates/admin.php index 23c3a36e28..1e8221f043 100644 --- a/settings/templates/admin.php +++ b/settings/templates/admin.php @@ -356,6 +356,30 @@ if ($_['cronErrors']) {

+
+

t('Server Side Encryption'));?>

+

+ /> +
+

+
+ +

Select default encryption module:

+
+ $module): ?> + > +
+ +
+ +
+
+

t('Email Server'));?>

diff --git a/tests/lib/encryption/keys/storage.php b/tests/lib/encryption/keys/storage.php new file mode 100644 index 0000000000..c2e5bdbd3d --- /dev/null +++ b/tests/lib/encryption/keys/storage.php @@ -0,0 +1,280 @@ + + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library 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 along with this library. If not, see . + */ + +namespace Test\Encryption\Keys; + +use OC\Encryption\Keys\Storage; +use Test\TestCase; + +class StorageTest extends TestCase { + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $util; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $view; + + public function setUp() { + parent::setUp(); + + $this->util = $this->getMockBuilder('OC\Encryption\Util') + ->disableOriginalConstructor() + ->getMock(); + + $this->view = $this->getMockBuilder('OC\Files\View') + ->disableOriginalConstructor() + ->getMock(); + + } + + public function testSetFileKey() { + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(array('user1', '/files/foo.txt')); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(false); + $this->view->expects($this->once()) + ->method('file_put_contents') + ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey'), + $this->equalTo('key')) + ->willReturn(strlen('key')); + + $storage = new Storage('encModule', $this->view, $this->util); + + $this->assertTrue( + $storage->setFileKey('user1/files/foo.txt', 'fileKey', 'key') + ); + } + + public function testGetFileKey() { + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(array('user1', '/files/foo.txt')); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(false); + $this->view->expects($this->once()) + ->method('file_get_contents') + ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn('key'); + $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn(true); + + $storage = new Storage('encModule', $this->view, $this->util); + + $this->assertSame('key', + $storage->getFileKey('user1/files/foo.txt', 'fileKey') + ); + } + + public function testSetFileKeySystemWide() { + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(array('user1', '/files/foo.txt')); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(true); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + $this->view->expects($this->once()) + ->method('file_put_contents') + ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey'), + $this->equalTo('key')) + ->willReturn(strlen('key')); + + $storage = new Storage('encModule', $this->view, $this->util); + + $this->assertTrue( + $storage->setFileKey('user1/files/foo.txt', 'fileKey', 'key') + ); + } + + public function testGetFileKeySystemWide() { + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(array('user1', '/files/foo.txt')); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(true); + $this->view->expects($this->once()) + ->method('file_get_contents') + ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn('key'); + $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn(true); + + $storage = new Storage('encModule', $this->view, $this->util); + + $this->assertSame('key', + $storage->getFileKey('user1/files/foo.txt', 'fileKey') + ); + } + + public function testSetSystemUserKey() { + $this->view->expects($this->once()) + ->method('file_put_contents') + ->with($this->equalTo('/files_encryption/encModule/shareKey_56884'), + $this->equalTo('key')) + ->willReturn(strlen('key')); + + $storage = new Storage('encModule', $this->view, $this->util); + + $this->assertTrue( + $storage->setSystemUserKey('shareKey_56884', 'key') + ); + } + + public function testSetUserKey() { + $this->view->expects($this->once()) + ->method('file_put_contents') + ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey'), + $this->equalTo('key')) + ->willReturn(strlen('key')); + + $storage = new Storage('encModule', $this->view, $this->util); + + $this->assertTrue( + $storage->setUserKey('user1', 'publicKey', 'key') + ); + } + + public function testGetSystemUserKey() { + $this->view->expects($this->once()) + ->method('file_get_contents') + ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) + ->willReturn('key'); + $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) + ->willReturn(true); + + $storage = new Storage('encModule', $this->view, $this->util); + + $this->assertSame('key', + $storage->getSystemUserKey('shareKey_56884') + ); + } + + public function testGetUserKey() { + $this->view->expects($this->once()) + ->method('file_get_contents') + ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) + ->willReturn('key'); + $this->view->expects($this->once()) + ->method('file_exists') + ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) + ->willReturn(true); + + $storage = new Storage('encModule', $this->view, $this->util); + + $this->assertSame('key', + $storage->getUserKey('user1', 'publicKey') + ); + } + + public function testDeleteUserKey() { + $this->view->expects($this->once()) + ->method('unlink') + ->with($this->equalTo('/user1/files_encryption/encModule/user1.publicKey')) + ->willReturn(true); + + $storage = new Storage('encModule', $this->view, $this->util); + + $this->assertTrue( + $storage->deleteUserKey('user1', 'publicKey') + ); + } + + public function testDeleteSystemUserKey() { + $this->view->expects($this->once()) + ->method('unlink') + ->with($this->equalTo('/files_encryption/encModule/shareKey_56884')) + ->willReturn(true); + + $storage = new Storage('encModule', $this->view, $this->util); + + $this->assertTrue( + $storage->deleteSystemUserKey('shareKey_56884') + ); + } + + public function testDeleteFileKeySystemWide() { + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(array('user1', '/files/foo.txt')); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(true); + $this->view->expects($this->once()) + ->method('unlink') + ->with($this->equalTo('/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn(true); + + $storage = new Storage('encModule', $this->view, $this->util); + + $this->assertTrue( + $storage->deleteFileKey('user1/files/foo.txt', 'fileKey') + ); + } + + public function testDeleteFileKey() { + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturn(array('user1', '/files/foo.txt')); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnArgument(0); + $this->util->expects($this->any()) + ->method('isSystemWideMountPoint') + ->willReturn(false); + $this->view->expects($this->once()) + ->method('unlink') + ->with($this->equalTo('/user1/files_encryption/keys/files/foo.txt/encModule/fileKey')) + ->willReturn(true); + + $storage = new Storage('encModule', $this->view, $this->util); + + $this->assertTrue( + $storage->deleteFileKey('user1/files/foo.txt', 'fileKey') + ); + } + +} diff --git a/tests/lib/encryption/managertest.php b/tests/lib/encryption/managertest.php new file mode 100644 index 0000000000..ab297bae0c --- /dev/null +++ b/tests/lib/encryption/managertest.php @@ -0,0 +1,114 @@ +getMock('\OCP\IConfig'); + $m = new Manager($config); + $this->assertFalse($m->isEnabled()); + } + + public function testManagerIsDisabledIfEnabledButNoModules() { + $config = $this->getMock('\OCP\IConfig'); + $config->expects($this->any())->method('getAppValue')->willReturn(true); + $m = new Manager($config); + $this->assertFalse($m->isEnabled()); + } + + public function testManagerIsDisabledIfDisabledButModules() { + $config = $this->getMock('\OCP\IConfig'); + $config->expects($this->any())->method('getAppValue')->willReturn(false); + $em = $this->getMock('\OCP\Encryption\IEncryptionModule'); + $em->expects($this->any())->method('getId')->willReturn(0); + $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); + $m = new Manager($config); + $m->registerEncryptionModule($em); + $this->assertFalse($m->isEnabled()); + } + + public function testManagerIsEnabled() { + $config = $this->getMock('\OCP\IConfig'); + $config->expects($this->any())->method('getSystemValue')->willReturn(true); + $config->expects($this->any())->method('getAppValue')->willReturn('yes'); + $m = new Manager($config); + $this->assertTrue($m->isEnabled()); + } + + /** + * @expectedException \OC\Encryption\Exceptions\ModuleAlreadyExistsException + * @expectedExceptionMessage Id "0" already used by encryption module "TestDummyModule0" + */ + public function testModuleRegistration() { + $config = $this->getMock('\OCP\IConfig'); + $config->expects($this->any())->method('getAppValue')->willReturn('yes'); + $em = $this->getMock('\OCP\Encryption\IEncryptionModule'); + $em->expects($this->any())->method('getId')->willReturn(0); + $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); + $m = new Manager($config); + $m->registerEncryptionModule($em); + $this->assertSame(1, count($m->getEncryptionModules())); + $m->registerEncryptionModule($em); + } + + public function testModuleUnRegistration() { + $config = $this->getMock('\OCP\IConfig'); + $config->expects($this->any())->method('getAppValue')->willReturn(true); + $em = $this->getMock('\OCP\Encryption\IEncryptionModule'); + $em->expects($this->any())->method('getId')->willReturn(0); + $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); + $m = new Manager($config); + $m->registerEncryptionModule($em); + $this->assertSame(1, + count($m->getEncryptionModules()) + ); + $m->unregisterEncryptionModule($em); + $this->assertEmpty($m->getEncryptionModules()); + } + + /** + * @expectedException \OC\Encryption\Exceptions\ModuleDoesNotExistsException + * @expectedExceptionMessage Module with id: unknown does not exists. + */ + public function testGetEncryptionModuleUnknown() { + $config = $this->getMock('\OCP\IConfig'); + $config->expects($this->any())->method('getAppValue')->willReturn(true); + $em = $this->getMock('\OCP\Encryption\IEncryptionModule'); + $em->expects($this->any())->method('getId')->willReturn(0); + $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); + $m = new Manager($config); + $m->registerEncryptionModule($em); + $this->assertSame(1, count($m->getEncryptionModules())); + $m->getEncryptionModule('unknown'); + } + + public function testGetEncryptionModule() { + $config = $this->getMock('\OCP\IConfig'); + $config->expects($this->any())->method('getAppValue')->willReturn(true); + $em = $this->getMock('\OCP\Encryption\IEncryptionModule'); + $em->expects($this->any())->method('getId')->willReturn(0); + $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); + $m = new Manager($config); + $m->registerEncryptionModule($em); + $this->assertSame(1, count($m->getEncryptionModules())); + $en0 = $m->getEncryptionModule(0); + $this->assertEquals(0, $en0->getId()); + } + + public function testGetDefaultEncryptionModule() { + $config = $this->getMock('\OCP\IConfig'); + $config->expects($this->any())->method('getAppValue')->willReturn(true); + $em = $this->getMock('\OCP\Encryption\IEncryptionModule'); + $em->expects($this->any())->method('getId')->willReturn(0); + $em->expects($this->any())->method('getDisplayName')->willReturn('TestDummyModule0'); + $m = new Manager($config); + $m->registerEncryptionModule($em); + $this->assertSame(1, count($m->getEncryptionModules())); + $en0 = $m->getEncryptionModule(0); + $this->assertEquals(0, $en0->getId()); + } +} diff --git a/tests/lib/encryption/utiltest.php b/tests/lib/encryption/utiltest.php new file mode 100644 index 0000000000..00a9ab9c57 --- /dev/null +++ b/tests/lib/encryption/utiltest.php @@ -0,0 +1,101 @@ +view = $this->getMockBuilder('OC\Files\View') + ->disableOriginalConstructor() + ->getMock(); + + $this->userManager = $this->getMockBuilder('OC\User\Manager') + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @dataProvider providesHeadersForEncryptionModule + */ + public function testGetEncryptionModuleId($expected, $header) { + $u = new Util($this->view, $this->userManager); + $id = $u->getEncryptionModuleId($header); + $this->assertEquals($expected, $id); + } + + public function providesHeadersForEncryptionModule() { + return [ + ['', []], + ['', ['1']], + [2, ['oc_encryption_module' => 2]], + ]; + } + + /** + * @dataProvider providesHeaders + */ + public function testReadHeader($header, $expected, $moduleId) { + $expected['oc_encryption_module'] = $moduleId; + $u = new Util($this->view, $this->userManager); + $result = $u->readHeader($header); + $this->assertSameSize($expected, $result); + foreach ($expected as $key => $value) { + $this->assertArrayHasKey($key, $result); + $this->assertSame($value, $result[$key]); + } + } + + /** + * @dataProvider providesHeaders + */ + public function testCreateHeader($expected, $header, $moduleId) { + + $em = $this->getMock('\OCP\Encryption\IEncryptionModule'); + $em->expects($this->any())->method('getId')->willReturn($moduleId); + + $u = new Util($this->view, $this->userManager); + $result = $u->createHeader($header, $em); + $this->assertEquals($expected, $result); + } + + public function providesHeaders() { + return [ + [str_pad('HBEGIN:oc_encryption_module:0:HEND', $this->headerSize, '-', STR_PAD_RIGHT) + , [], '0'], + [str_pad('HBEGIN:oc_encryption_module:0:custom_header:foo:HEND', $this->headerSize, '-', STR_PAD_RIGHT) + , ['custom_header' => 'foo'], '0'], + ]; + } + + /** + * @expectedException \OC\Encryption\Exceptions\EncryptionHeaderKeyExistsException + */ + public function testCreateHeaderFailed() { + + $header = array('header1' => 1, 'header2' => 2, 'oc_encryption_module' => 'foo'); + + $em = $this->getMock('\OCP\Encryption\IEncryptionModule'); + $em->expects($this->any())->method('getId')->willReturn('moduleId'); + + $u = new Util($this->view, $this->userManager); + $u->createHeader($header, $em); + } + +}