f282a5cff0
[encryption] support aes 256
499 lines
14 KiB
PHP
Executable file
499 lines
14 KiB
PHP
Executable file
<?php
|
|
|
|
/**
|
|
* ownCloud
|
|
*
|
|
* @author Florin Peter
|
|
* @copyright 2013 Florin Peter <owncloud@florin-peter.de>
|
|
*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
namespace OCA\Encryption;
|
|
|
|
/**
|
|
* 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
|
|
|
|
/**
|
|
* 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');
|
|
}
|
|
|
|
/**
|
|
* register user related hooks
|
|
*
|
|
*/
|
|
public static function registerUserHooks() {
|
|
|
|
\OCP\Util::connectHook('OC_User', 'post_login', 'OCA\Encryption\Hooks', 'login');
|
|
\OCP\Util::connectHook('OC_User', 'logout', 'OCA\Encryption\Hooks', 'logout');
|
|
\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');
|
|
}
|
|
|
|
/**
|
|
* 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', 'postRenameOrCopy');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'copy', 'OCA\Encryption\Hooks', 'preCopy');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'post_copy', 'OCA\Encryption\Hooks', 'postRenameOrCopy');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'post_delete', 'OCA\Encryption\Hooks', 'postDelete');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Encryption\Hooks', 'preDelete');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'post_umount', 'OCA\Encryption\Hooks', 'postUmount');
|
|
\OCP\Util::connectHook('OC_Filesystem', 'umount', 'OCA\Encryption\Hooks', 'preUmount');
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* enable recovery
|
|
*
|
|
* @param string $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']);
|
|
|
|
$cipher = \OCA\Encryption\Helper::getCipher();
|
|
$encryptedKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent($keypair['privateKey'], $recoveryPassword, $cipher);
|
|
if ($encryptedKey) {
|
|
Keymanager::setPrivateSystemKey($encryptedKey, $recoveryKeyId . '.private.key');
|
|
// Set recoveryAdmin as enabled
|
|
$appConfig->setValue('files_encryption', 'recoveryAdminEnabled', 1);
|
|
$return = true;
|
|
}
|
|
|
|
\OC_FileProxy::$enabled = true;
|
|
|
|
} else { // get recovery key and check the password
|
|
$util = new \OCA\Encryption\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) {
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* create directory recursively
|
|
* @param string $path
|
|
* @param \OC\Files\View $view
|
|
*/
|
|
public static function mkdirr($path, $view) {
|
|
$dirname = \OC\Files\Filesystem::normalizePath(dirname($path));
|
|
$dirParts = explode('/', $dirname);
|
|
$dir = "";
|
|
foreach ($dirParts as $part) {
|
|
$dir = $dir . '/' . $part;
|
|
if (!$view->file_exists($dir)) {
|
|
$view->mkdir($dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* find all share keys for a given file
|
|
* @param string $path to the file
|
|
* @param \OC\Files\View $view view, relative to data/
|
|
* @return array list of files, path relative to data/
|
|
*/
|
|
public static function findShareKeys($path, $view) {
|
|
$result = array();
|
|
$pathinfo = pathinfo($path);
|
|
$dirContent = $view->opendir($pathinfo['dirname']);
|
|
|
|
if (is_resource($dirContent)) {
|
|
while (($file = readdir($dirContent)) !== false) {
|
|
if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
|
|
if (preg_match("/" . $pathinfo['filename'] . ".(.*).shareKey/", $file)) {
|
|
$result[] = $pathinfo['dirname'] . '/' . $file;
|
|
}
|
|
}
|
|
}
|
|
closedir($dirContent);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* read the cipher used for encryption from the config.php
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function getCipher() {
|
|
|
|
$cipher = \OCP\Config::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;
|
|
}
|
|
}
|
|
|