533 lines
15 KiB
PHP
533 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* @author Bjoern Schiessle <schiessle@owncloud.com>
|
|
* @author Björn Schießle <schiessle@owncloud.com>
|
|
* @author Florin Peter <github@florin-peter.de>
|
|
* @author Joas Schilling <nickvergessen@gmx.de>
|
|
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
|
|
* @author Lukas Reschke <lukas@owncloud.com>
|
|
* @author Morris Jobke <hey@morrisjobke.de>
|
|
* @author Owen Winkler <a_github@midnightcircus.com>
|
|
* @author Robin Appelman <icewind@owncloud.com>
|
|
* @author Robin McCorkell <rmccorkell@karoshi.org.uk>
|
|
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
|
*
|
|
* @copyright Copyright (c) 2015, ownCloud, Inc.
|
|
* @license AGPL-3.0
|
|
*
|
|
* This code is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License, version 3,
|
|
* as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License, version 3,
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
*
|
|
*/
|
|
namespace OCA\Files_Encryption;
|
|
|
|
/**
|
|
* Class to manage registration of hooks an various helper methods
|
|
* @package OCA\Files_Encryption
|
|
*/
|
|
class Helper {
|
|
|
|
private static $tmpFileMapping; // Map tmp files to files in data/user/files
|
|
|
|
/**
|
|
* register share related hooks
|
|
*
|
|
*/
|
|
public static function registerShareHooks() {
|
|
|
|
\OCP\Util::connectHook('OCP\Share', 'pre_shared', 'OCA\Files_Encryption\Hooks', 'preShared');
|
|
\OCP\Util::connectHook('OCP\Share', 'post_shared', 'OCA\Files_Encryption\Hooks', 'postShared');
|
|
\OCP\Util::connectHook('OCP\Share', 'post_unshare', 'OCA\Files_Encryption\Hooks', 'postUnshare');
|
|
}
|
|
|
|
/**
|
|
* register user related hooks
|
|
*
|
|
*/
|
|
public static function registerUserHooks() {
|
|
|
|
\OCP\Util::connectHook('OC_User', 'post_login', 'OCA\Files_Encryption\Hooks', 'login');
|
|
\OCP\Util::connectHook('OC_User', 'logout', 'OCA\Files_Encryption\Hooks', 'logout');
|
|
\OCP\Util::connectHook('OC_User', 'post_setPassword', 'OCA\Files_Encryption\Hooks', 'setPassphrase');
|
|
\OCP\Util::connectHook('OC_User', 'pre_setPassword', 'OCA\Files_Encryption\Hooks', 'preSetPassphrase');
|
|
\OCP\Util::connectHook('OC_User', 'post_createUser', 'OCA\Files_Encryption\Hooks', 'postCreateUser');
|
|
\OCP\Util::connectHook('OC_User', 'post_deleteUser', 'OCA\Files_Encryption\Hooks', 'postDeleteUser');
|
|
}
|
|
|
|
/**
|
|
* register filesystem related hooks
|
|
*
|
|
*/
|
|
public static function registerFilesystemHooks() {
|
|
|
|
\OCP\Util::connectHook('OC_Filesystem', 'rename', 'OCA\Files_Encryption\Hooks', 'preRename');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'post_rename', 'OCA\Files_Encryption\Hooks', 'postRenameOrCopy');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'copy', 'OCA\Files_Encryption\Hooks', 'preCopy');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'post_copy', 'OCA\Files_Encryption\Hooks', 'postRenameOrCopy');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'post_delete', 'OCA\Files_Encryption\Hooks', 'postDelete');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Encryption\Hooks', 'preDelete');
|
|
\OCP\Util::connectHook('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', 'OCA\Files_Encryption\Hooks', 'postPasswordReset');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'post_umount', 'OCA\Files_Encryption\Hooks', 'postUnmount');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'umount', 'OCA\Files_Encryption\Hooks', 'preUnmount');
|
|
}
|
|
|
|
/**
|
|
* register app management related hooks
|
|
*
|
|
*/
|
|
public static function registerAppHooks() {
|
|
|
|
\OCP\Util::connectHook('OC_App', 'pre_disable', 'OCA\Files_Encryption\Hooks', 'preDisable');
|
|
\OCP\Util::connectHook('OC_App', 'post_disable', 'OCA\Files_Encryption\Hooks', 'postEnable');
|
|
}
|
|
|
|
/**
|
|
* setup user for files_encryption
|
|
*
|
|
* @param Util $util
|
|
* @param string $password
|
|
* @return bool
|
|
*/
|
|
public static function setupUser(Util $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;
|
|
}
|
|
|
|
/**
|
|
* get recovery key id
|
|
*
|
|
* @return string|bool recovery key ID or false
|
|
*/
|
|
public static function getRecoveryKeyId() {
|
|
$appConfig = \OC::$server->getAppConfig();
|
|
$key = $appConfig->getValue('files_encryption', 'recoveryKeyId');
|
|
|
|
return ($key === null) ? false : $key;
|
|
}
|
|
|
|
public static function getPublicShareKeyId() {
|
|
$appConfig = \OC::$server->getAppConfig();
|
|
$key = $appConfig->getValue('files_encryption', 'publicShareKeyId');
|
|
|
|
return ($key === null) ? false : $key;
|
|
}
|
|
|
|
/**
|
|
* enable recovery
|
|
*
|
|
* @param string $recoveryKeyId
|
|
* @param string $recoveryPassword
|
|
* @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 (!Keymanager::recoveryKeyExists($view)) {
|
|
|
|
$keypair = Crypt::createKeypair();
|
|
|
|
// Save public key
|
|
Keymanager::setPublicKey($keypair['publicKey'], $recoveryKeyId);
|
|
|
|
$cipher = Helper::getCipher();
|
|
$encryptedKey = Crypt::symmetricEncryptFileContent($keypair['privateKey'], $recoveryPassword, $cipher);
|
|
if ($encryptedKey) {
|
|
Keymanager::setPrivateSystemKey($encryptedKey, $recoveryKeyId);
|
|
// Set recoveryAdmin as enabled
|
|
$appConfig->setValue('files_encryption', 'recoveryAdminEnabled', 1);
|
|
$return = true;
|
|
}
|
|
|
|
} else { // get recovery key and check the password
|
|
$util = new Util(new \OC\Files\View('/'), \OCP\User::getUser());
|
|
$return = $util->checkRecoveryPassword($recoveryPassword);
|
|
if ($return) {
|
|
$appConfig->setValue('files_encryption', 'recoveryAdminEnabled', 1);
|
|
}
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* disable recovery
|
|
*
|
|
* @param string $recoveryPassword
|
|
* @return bool
|
|
*/
|
|
public static function adminDisableRecovery($recoveryPassword) {
|
|
$util = new Util(new \OC\Files\View('/'), \OCP\User::getUser());
|
|
$return = $util->checkRecoveryPassword($recoveryPassword);
|
|
|
|
if ($return) {
|
|
// Set recoveryAdmin as disabled
|
|
\OC::$server->getAppConfig()->setValue('files_encryption', 'recoveryAdminEnabled', 0);
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* checks if access is public/anonymous user
|
|
* @return bool
|
|
*/
|
|
public static function isPublicAccess() {
|
|
if (\OCP\User::getUser() === false) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
$split = self::splitPath($path);
|
|
|
|
// it is not a file relative to data/user/files
|
|
if (count($split) < 4 || $split[2] !== 'files') {
|
|
return false;
|
|
}
|
|
|
|
$sliced = array_slice($split, 3);
|
|
$relPath = implode('/', $sliced);
|
|
|
|
return $relPath;
|
|
}
|
|
|
|
/**
|
|
* try to get the user from the path if no user is logged in
|
|
* @param string $path
|
|
* @return string 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
|
|
return self::getUserFromPath($path);
|
|
}
|
|
|
|
/**
|
|
* extract user from path
|
|
*
|
|
* @param string $path
|
|
* @return string user id
|
|
* @throws Exception\EncryptionException
|
|
*/
|
|
public static function getUserFromPath($path) {
|
|
$split = self::splitPath($path);
|
|
|
|
if (count($split) > 2 && (
|
|
$split[2] === 'files' || $split[2] === 'files_versions' || $split[2] === 'cache' || $split[2] === 'files_trashbin')) {
|
|
|
|
$user = $split[1];
|
|
|
|
if (\OCP\User::userExists($user)) {
|
|
return $user;
|
|
}
|
|
}
|
|
|
|
throw new Exception\EncryptionException('Could not determine user', Exception\EncryptionException::GENERIC);
|
|
}
|
|
|
|
/**
|
|
* get path to the corresponding file in data/user/files if path points
|
|
* to a file in cache
|
|
*
|
|
* @param string $path path to a file in cache
|
|
* @return string path to corresponding file relative to data/user/files
|
|
* @throws Exception\EncryptionException
|
|
*/
|
|
public static function getPathFromCachedFile($path) {
|
|
$split = self::splitPath($path);
|
|
|
|
if (count($split) < 5) {
|
|
throw new Exception\EncryptionException('no valid cache file path', Exception\EncryptionException::GENERIC);
|
|
}
|
|
|
|
// we skip /user/cache/transactionId
|
|
$sliced = array_slice($split, 4);
|
|
|
|
return implode('/', $sliced);
|
|
}
|
|
|
|
|
|
/**
|
|
* get path to the corresponding file in data/user/files for a version
|
|
*
|
|
* @param string $path path to a version
|
|
* @return string path to corresponding file relative to data/user/files
|
|
* @throws Exception\EncryptionException
|
|
*/
|
|
public static function getPathFromVersion($path) {
|
|
$split = self::splitPath($path);
|
|
|
|
if (count($split) < 4) {
|
|
throw new Exception\EncryptionException('no valid path to a version', Exception\EncryptionException::GENERIC);
|
|
}
|
|
|
|
// we skip user/files_versions
|
|
$sliced = array_slice($split, 3);
|
|
$relPath = implode('/', $sliced);
|
|
//remove the last .v
|
|
$realPath = substr($relPath, 0, strrpos($relPath, '.v'));
|
|
|
|
return $realPath;
|
|
}
|
|
|
|
/**
|
|
* create directory recursively
|
|
*
|
|
* @param string $path
|
|
* @param \OC\Files\View $view
|
|
*/
|
|
public static function mkdirr($path, \OC\Files\View $view) {
|
|
$dirParts = self::splitPath(dirname($path));
|
|
$dir = "";
|
|
foreach ($dirParts as $part) {
|
|
$dir = $dir . '/' . $part;
|
|
if (!$view->file_exists($dir)) {
|
|
$view->mkdir($dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* redirect to a error page
|
|
* @param Session $session
|
|
* @param int|null $errorCode
|
|
* @throws \Exception
|
|
*/
|
|
public static function redirectToErrorPage(Session $session, $errorCode = null) {
|
|
|
|
if ($errorCode === null) {
|
|
$init = $session->getInitialized();
|
|
switch ($init) {
|
|
case Session::INIT_EXECUTED:
|
|
$errorCode = Crypt::ENCRYPTION_PRIVATE_KEY_NOT_VALID_ERROR;
|
|
break;
|
|
case Session::NOT_INITIALIZED:
|
|
$errorCode = Crypt::ENCRYPTION_NOT_INITIALIZED_ERROR;
|
|
break;
|
|
default:
|
|
$errorCode = 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() {
|
|
|
|
//openssl extension needs to be loaded
|
|
return extension_loaded("openssl");
|
|
|
|
}
|
|
|
|
/**
|
|
* 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(\OC::$server->getConfig()->getSystemValue('openssl', array()), $config);
|
|
return $config;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* detect file type, encryption can read/write regular files, versions
|
|
* and cached files
|
|
*
|
|
* @param string $path
|
|
* @return int
|
|
* @throws Exception\EncryptionException
|
|
*/
|
|
public static function detectFileType($path) {
|
|
$parts = self::splitPath($path);
|
|
|
|
if (count($parts) > 2) {
|
|
switch ($parts[2]) {
|
|
case 'files':
|
|
return Util::FILE_TYPE_FILE;
|
|
case 'files_versions':
|
|
return Util::FILE_TYPE_VERSION;
|
|
case 'cache':
|
|
return Util::FILE_TYPE_CACHE;
|
|
}
|
|
}
|
|
|
|
// thow exception if we couldn't detect a valid file type
|
|
throw new Exception\EncryptionException('Could not detect file type', Exception\EncryptionException::GENERIC);
|
|
}
|
|
|
|
/**
|
|
* read the cipher used for encryption from the config.php
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function getCipher() {
|
|
|
|
$cipher = \OC::$server->getConfig()->getSystemValue('cipher', Crypt::DEFAULT_CIPHER);
|
|
|
|
if ($cipher !== 'AES-256-CFB' && $cipher !== 'AES-128-CFB') {
|
|
\OCP\Util::writeLog('files_encryption',
|
|
'wrong cipher defined in config.php, only AES-128-CFB and AES-256-CFB is supported. Fall back ' . Crypt::DEFAULT_CIPHER,
|
|
\OCP\Util::WARN);
|
|
|
|
$cipher = Crypt::DEFAULT_CIPHER;
|
|
}
|
|
|
|
return $cipher;
|
|
}
|
|
|
|
public static function splitPath($path) {
|
|
$normalized = \OC\Files\Filesystem::normalizePath($path);
|
|
return explode('/', $normalized);
|
|
}
|
|
|
|
}
|
|
|