* * 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; /** * @brief Class to manage registration of hooks an various helper methods * @package OCA\Encryption */ class Helper { private static $tmpFileMapping; // Map tmp files to files in data/user/files /** * @brief register share related hooks * */ public static function registerShareHooks() { \OCP\Util::connectHook('OCP\Share', 'pre_shared', 'OCA\Encryption\Hooks', 'preShared'); \OCP\Util::connectHook('OCP\Share', 'post_shared', 'OCA\Encryption\Hooks', 'postShared'); \OCP\Util::connectHook('OCP\Share', 'post_unshare', 'OCA\Encryption\Hooks', 'postUnshare'); } /** * @brief register user related hooks * */ public static function registerUserHooks() { \OCP\Util::connectHook('OC_User', 'post_login', 'OCA\Encryption\Hooks', 'login'); \OCP\Util::connectHook('OC_User', 'post_setPassword', 'OCA\Encryption\Hooks', 'setPassphrase'); \OCP\Util::connectHook('OC_User', 'pre_setPassword', 'OCA\Encryption\Hooks', 'preSetPassphrase'); \OCP\Util::connectHook('OC_User', 'post_createUser', 'OCA\Encryption\Hooks', 'postCreateUser'); \OCP\Util::connectHook('OC_User', 'post_deleteUser', 'OCA\Encryption\Hooks', 'postDeleteUser'); } /** * @brief register filesystem related hooks * */ public static function registerFilesystemHooks() { \OCP\Util::connectHook('OC_Filesystem', 'rename', 'OCA\Encryption\Hooks', 'preRename'); \OCP\Util::connectHook('OC_Filesystem', 'post_rename', 'OCA\Encryption\Hooks', 'postRename'); \OCP\Util::connectHook('OC_Filesystem', 'post_delete', 'OCA\Encryption\Hooks', 'postDelete'); \OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Encryption\Hooks', 'preDelete'); } /** * @brief register app management related hooks * */ public static function registerAppHooks() { \OCP\Util::connectHook('OC_App', 'pre_disable', 'OCA\Encryption\Hooks', 'preDisable'); \OCP\Util::connectHook('OC_App', 'post_disable', 'OCA\Encryption\Hooks', 'postEnable'); } /** * @brief setup user for files_encryption * * @param Util $util * @param string $password * @return bool */ public static function setupUser($util, $password) { // Check files_encryption infrastructure is ready for action if (!$util->ready()) { \OCP\Util::writeLog('Encryption library', 'User account "' . $util->getUserId() . '" is not ready for encryption; configuration started', \OCP\Util::DEBUG); if (!$util->setupServerSide($password)) { return false; } } return true; } /** * @brief enable recovery * * @param $recoveryKeyId * @param string $recoveryPassword * @internal param \OCA\Encryption\Util $util * @internal param string $password * @return bool */ public static function adminEnableRecovery($recoveryKeyId, $recoveryPassword) { $view = new \OC\Files\View('/'); $appConfig = \OC::$server->getAppConfig(); if ($recoveryKeyId === null) { $recoveryKeyId = 'recovery_' . substr(md5(time()), 0, 8); $appConfig->setValue('files_encryption', 'recoveryKeyId', $recoveryKeyId); } if (!$view->is_dir('/owncloud_private_key')) { $view->mkdir('/owncloud_private_key'); } if ( (!$view->file_exists("/public-keys/" . $recoveryKeyId . ".public.key") || !$view->file_exists("/owncloud_private_key/" . $recoveryKeyId . ".private.key")) ) { $keypair = \OCA\Encryption\Crypt::createKeypair(); \OC_FileProxy::$enabled = false; // Save public key if (!$view->is_dir('/public-keys')) { $view->mkdir('/public-keys'); } $view->file_put_contents('/public-keys/' . $recoveryKeyId . '.public.key', $keypair['publicKey']); // Encrypt private key empty passphrase $encryptedPrivateKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent($keypair['privateKey'], $recoveryPassword); // Save private key $view->file_put_contents('/owncloud_private_key/' . $recoveryKeyId . '.private.key', $encryptedPrivateKey); \OC_FileProxy::$enabled = true; // Set recoveryAdmin as enabled $appConfig->setValue('files_encryption', 'recoveryAdminEnabled', 1); $return = true; } else { // get recovery key and check the password $util = new \OCA\Encryption\Util(new \OC_FilesystemView('/'), \OCP\User::getUser()); $return = $util->checkRecoveryPassword($recoveryPassword); if ($return) { $appConfig->setValue('files_encryption', 'recoveryAdminEnabled', 1); } } return $return; } /** * @brief Check if a path is a .part file * @param string $path Path that may identify a .part file * @return bool */ public static function isPartialFilePath($path) { $extension = pathinfo($path, PATHINFO_EXTENSION); if ( $extension === 'part') { return true; } else { return false; } } /** * @brief 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 static function stripPartialFileExtension($path) { $extension = pathinfo($path, PATHINFO_EXTENSION); if ( $extension === 'part') { $newLength = strlen($path) - 5; // 5 = strlen(".part") = strlen(".etmp") $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; } } /** * @brief disable recovery * * @param string $recoveryPassword * @return bool */ public static function adminDisableRecovery($recoveryPassword) { $util = new Util(new \OC_FilesystemView('/'), \OCP\User::getUser()); $return = $util->checkRecoveryPassword($recoveryPassword); if ($return) { // Set recoveryAdmin as disabled \OC::$server->getAppConfig()->setValue('files_encryption', 'recoveryAdminEnabled', 0); } return $return; } /** * @brief checks if access is public/anonymous user * @return bool */ public static function isPublicAccess() { if (\OCP\User::getUser() === false) { return true; } else { return false; } } /** * @brief Format a path to be relative to the /user/files/ directory * @param string $path the absolute path * @return string e.g. turns '/admin/files/test.txt' into 'test.txt' */ public static function stripUserFilesPath($path) { $trimmed = ltrim($path, '/'); $split = explode('/', $trimmed); // it is not a file relative to data/user/files if (count($split) < 3 || $split[1] !== 'files') { return false; } $sliced = array_slice($split, 2); $relPath = implode('/', $sliced); return $relPath; } /** * @brief try to get the user from the path if no user is logged in * @param string $path * @return mixed user or false if we couldn't determine a user */ public static function getUser($path) { $user = \OCP\User::getUser(); // if we are logged in, then we return the userid if ($user) { return $user; } // if no user is logged in we try to access a publicly shared files. // In this case we need to try to get the user from the path $trimmed = ltrim($path, '/'); $split = explode('/', $trimmed); // it is not a file relative to data/user/files if (count($split) < 2 || ($split[1] !== 'files' && $split[1] !== 'cache')) { return false; } $user = $split[0]; if (\OCP\User::userExists($user)) { return $user; } return false; } /** * @brief get path to the corresponding file in data/user/files if path points * to a version or to a file in cache * @param string $path path to a version or a file in the trash * @return string path to corresponding file relative to data/user/files */ public static function getPathToRealFile($path) { $trimmed = ltrim($path, '/'); $split = explode('/', $trimmed); $result = false; if (count($split) >= 3 && ($split[1] === "files_versions" || $split[1] === 'cache')) { $sliced = array_slice($split, 2); $result = implode('/', $sliced); if ($split[1] === "files_versions") { // we skip user/files $sliced = array_slice($split, 2); $relPath = implode('/', $sliced); //remove the last .v $result = substr($relPath, 0, strrpos($relPath, '.v')); } if ($split[1] === "cache") { // we skip /user/cache/transactionId $sliced = array_slice($split, 3); $result = implode('/', $sliced); //prepare the folders self::mkdirr($path, new \OC\Files\View('/')); } } return $result; } /** * @brief create directory recursively * @param string $path * @param \OC\Files\View $view */ public static function mkdirr($path, $view) { $dirname = \OC_Filesystem::normalizePath(dirname($path)); $dirParts = explode('/', $dirname); $dir = ""; foreach ($dirParts as $part) { $dir = $dir . '/' . $part; if (!$view->file_exists($dir)) { $view->mkdir($dir); } } } /** * @brief redirect to a error page * @param Session $session */ public static function redirectToErrorPage($session, $errorCode = null) { if ($errorCode === null) { $init = $session->getInitialized(); switch ($init) { case \OCA\Encryption\Session::INIT_EXECUTED: $errorCode = \OCA\Encryption\Crypt::ENCRYPTION_PRIVATE_KEY_NOT_VALID_ERROR; break; case \OCA\Encryption\Session::NOT_INITIALIZED: $errorCode = \OCA\Encryption\Crypt::ENCRYPTION_NOT_INITIALIZED_ERROR; break; default: $errorCode = \OCA\Encryption\Crypt::ENCRYPTION_UNKNOWN_ERROR; } } $location = \OC_Helper::linkToAbsolute('apps/files_encryption/files', 'error.php'); $post = 0; if(count($_POST) > 0) { $post = 1; } if(defined('PHPUNIT_RUN') and PHPUNIT_RUN) { throw new \Exception("Encryption error: $errorCode"); } header('Location: ' . $location . '?p=' . $post . '&errorCode=' . $errorCode); exit(); } /** * check requirements for encryption app. * @return bool true if requirements are met */ public static function checkRequirements() { $result = true; //openssl extension needs to be loaded $result &= extension_loaded("openssl"); // we need php >= 5.3.3 $result &= version_compare(phpversion(), '5.3.3', '>='); return (bool) $result; } /** * check some common errors if the server isn't configured properly for encryption * @return bool true if configuration seems to be OK */ public static function checkConfiguration() { if(self::getOpenSSLPkey()) { return true; } else { while ($msg = openssl_error_string()) { \OCP\Util::writeLog('Encryption library', 'openssl_pkey_new() fails: ' . $msg, \OCP\Util::ERROR); } return false; } } /** * Create an openssl pkey with config-supplied settings * WARNING: This initializes a new private keypair, which is computationally expensive * @return resource The pkey resource created */ public static function getOpenSSLPkey() { return openssl_pkey_new(self::getOpenSSLConfig()); } /** * Return an array of OpenSSL config options, default + config * Used for multiple OpenSSL functions * @return array The combined defaults and config settings */ public static function getOpenSSLConfig() { $config = array('private_key_bits' => 4096); $config = array_merge(\OCP\Config::getSystemValue('openssl', array()), $config); return $config; } /** * @brief glob uses different pattern than regular expressions, escape glob pattern only * @param string $path unescaped path * @return string path */ public static function escapeGlobPattern($path) { return preg_replace('/(\*|\?|\[)/', '[$1]', $path); } /** * @brief remember from which file the tmp file (getLocalFile() call) was created * @param string $tmpFile path of tmp file * @param string $originalFile path of the original file relative to data/ */ public static function addTmpFileToMapper($tmpFile, $originalFile) { self::$tmpFileMapping[$tmpFile] = $originalFile; } /** * @brief get the path of the original file * @param string $tmpFile path of the tmp file * @return string|false path of the original file or false */ public static function getPathFromTmpFile($tmpFile) { if (isset(self::$tmpFileMapping[$tmpFile])) { return self::$tmpFileMapping[$tmpFile]; } return false; } }