628 lines
19 KiB
PHP
628 lines
19 KiB
PHP
<?php
|
|
|
|
/**
|
|
* ownCloud
|
|
*
|
|
* @copyright (C) 2014 ownCloud, Inc.
|
|
*
|
|
* @author Sam Tuke <samtuke@owncloud.org>
|
|
* @author Bjoern Schiessle <schiessle@owncloud.com>
|
|
*
|
|
* 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;
|
|
|
|
use OC\Files\Filesystem;
|
|
|
|
/**
|
|
* Class for hook specific logic
|
|
*/
|
|
class Hooks {
|
|
|
|
// file for which we want to rename the keys after the rename operation was successful
|
|
private static $renamedFiles = array();
|
|
// file for which we want to delete the keys after the delete operation was successful
|
|
private static $deleteFiles = array();
|
|
// file for which we want to delete the keys after the delete operation was successful
|
|
private static $unmountedFiles = array();
|
|
|
|
/**
|
|
* Startup encryption backend upon user login
|
|
* @note This method should never be called for users using client side encryption
|
|
*/
|
|
public static function login($params) {
|
|
|
|
if (\OCP\App::isEnabled('files_encryption') === false) {
|
|
return true;
|
|
}
|
|
|
|
|
|
$l = new \OC_L10N('files_encryption');
|
|
|
|
$view = new \OC\Files\View('/');
|
|
|
|
// ensure filesystem is loaded
|
|
if (!\OC\Files\Filesystem::$loaded) {
|
|
\OC_Util::setupFS($params['uid']);
|
|
}
|
|
|
|
$privateKey = \OCA\Encryption\Keymanager::getPrivateKey($view, $params['uid']);
|
|
|
|
// if no private key exists, check server configuration
|
|
if (!$privateKey) {
|
|
//check if all requirements are met
|
|
if (!Helper::checkRequirements() || !Helper::checkConfiguration()) {
|
|
$error_msg = $l->t("Missing requirements.");
|
|
$hint = $l->t('Please make sure that OpenSSL together with the PHP extension is enabled and configured properly. For now, the encryption app has been disabled.');
|
|
\OC_App::disable('files_encryption');
|
|
\OCP\Util::writeLog('Encryption library', $error_msg . ' ' . $hint, \OCP\Util::ERROR);
|
|
\OCP\Template::printErrorPage($error_msg, $hint);
|
|
}
|
|
}
|
|
|
|
$util = new Util($view, $params['uid']);
|
|
|
|
// setup user, if user not ready force relogin
|
|
if (Helper::setupUser($util, $params['password']) === false) {
|
|
return false;
|
|
}
|
|
|
|
$session = $util->initEncryption($params);
|
|
|
|
// Check if first-run file migration has already been performed
|
|
$ready = false;
|
|
$migrationStatus = $util->getMigrationStatus();
|
|
if ($migrationStatus === Util::MIGRATION_OPEN && $session !== false) {
|
|
$ready = $util->beginMigration();
|
|
} elseif ($migrationStatus === Util::MIGRATION_IN_PROGRESS) {
|
|
// refuse login as long as the initial encryption is running
|
|
sleep(5);
|
|
\OCP\User::logout();
|
|
return false;
|
|
}
|
|
|
|
$result = true;
|
|
|
|
// If migration not yet done
|
|
if ($ready) {
|
|
|
|
// Encrypt existing user files
|
|
try {
|
|
$result = $util->encryptAll('/' . $params['uid'] . '/' . 'files');
|
|
} catch (\Exception $ex) {
|
|
\OCP\Util::writeLog('Encryption library', 'Initial encryption failed! Error: ' . $ex->getMessage(), \OCP\Util::FATAL);
|
|
$result = false;
|
|
}
|
|
|
|
if ($result) {
|
|
\OC_Log::write(
|
|
'Encryption library', 'Encryption of existing files belonging to "' . $params['uid'] . '" completed'
|
|
, \OC_Log::INFO
|
|
);
|
|
// Register successful migration in DB
|
|
$util->finishMigration();
|
|
} else {
|
|
\OCP\Util::writeLog('Encryption library', 'Initial encryption failed!', \OCP\Util::FATAL);
|
|
$util->resetMigrationStatus();
|
|
\OCP\User::logout();
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* remove keys from session during logout
|
|
*/
|
|
public static function logout() {
|
|
$session = new \OCA\Encryption\Session(new \OC\Files\View());
|
|
$session->removeKeys();
|
|
}
|
|
|
|
/**
|
|
* setup encryption backend upon user created
|
|
* @note This method should never be called for users using client side encryption
|
|
*/
|
|
public static function postCreateUser($params) {
|
|
|
|
if (\OCP\App::isEnabled('files_encryption')) {
|
|
$view = new \OC\Files\View('/');
|
|
$util = new Util($view, $params['uid']);
|
|
Helper::setupUser($util, $params['password']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* cleanup encryption backend upon user deleted
|
|
* @note This method should never be called for users using client side encryption
|
|
*/
|
|
public static function postDeleteUser($params) {
|
|
|
|
if (\OCP\App::isEnabled('files_encryption')) {
|
|
Keymanager::deletePublicKey(new \OC\Files\View(), $params['uid']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the password can't be changed within ownCloud, than update the key password in advance.
|
|
*/
|
|
public static function preSetPassphrase($params) {
|
|
if (\OCP\App::isEnabled('files_encryption')) {
|
|
if ( ! \OC_User::canUserChangePassword($params['uid']) ) {
|
|
self::setPassphrase($params);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change a user's encryption passphrase
|
|
* @param array $params keys: uid, password
|
|
*/
|
|
public static function setPassphrase($params) {
|
|
if (\OCP\App::isEnabled('files_encryption') === false) {
|
|
return true;
|
|
}
|
|
|
|
// Only attempt to change passphrase if server-side encryption
|
|
// is in use (client-side encryption does not have access to
|
|
// the necessary keys)
|
|
if (Crypt::mode() === 'server') {
|
|
|
|
$view = new \OC\Files\View('/');
|
|
$session = new \OCA\Encryption\Session($view);
|
|
|
|
// Get existing decrypted private key
|
|
$privateKey = $session->getPrivateKey();
|
|
|
|
if ($params['uid'] === \OCP\User::getUser() && $privateKey) {
|
|
|
|
// Encrypt private key with new user pwd as passphrase
|
|
$encryptedPrivateKey = Crypt::symmetricEncryptFileContent($privateKey, $params['password'], Helper::getCipher());
|
|
|
|
// Save private key
|
|
if ($encryptedPrivateKey) {
|
|
Keymanager::setPrivateKey($encryptedPrivateKey, \OCP\User::getUser());
|
|
} else {
|
|
\OCP\Util::writeLog('files_encryption', 'Could not update users encryption password', \OCP\Util::ERROR);
|
|
}
|
|
|
|
// NOTE: Session does not need to be updated as the
|
|
// private key has not changed, only the passphrase
|
|
// used to decrypt it has changed
|
|
|
|
|
|
} else { // admin changed the password for a different user, create new keys and reencrypt file keys
|
|
|
|
$user = $params['uid'];
|
|
$util = new Util($view, $user);
|
|
$recoveryPassword = isset($params['recoveryPassword']) ? $params['recoveryPassword'] : null;
|
|
|
|
// we generate new keys if...
|
|
// ...we have a recovery password and the user enabled the recovery key
|
|
// ...encryption was activated for the first time (no keys exists)
|
|
// ...the user doesn't have any files
|
|
if (($util->recoveryEnabledForUser() && $recoveryPassword)
|
|
|| !$util->userKeysExists()
|
|
|| !$view->file_exists($user . '/files')) {
|
|
|
|
// backup old keys
|
|
$util->backupAllKeys('recovery');
|
|
|
|
$newUserPassword = $params['password'];
|
|
|
|
// make sure that the users home is mounted
|
|
\OC\Files\Filesystem::initMountPoints($user);
|
|
|
|
$keypair = Crypt::createKeypair();
|
|
|
|
// Disable encryption proxy to prevent recursive calls
|
|
$proxyStatus = \OC_FileProxy::$enabled;
|
|
\OC_FileProxy::$enabled = false;
|
|
|
|
// Save public key
|
|
Keymanager::setPublicKey($keypair['publicKey'], $user);
|
|
|
|
// Encrypt private key with new password
|
|
$encryptedKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent($keypair['privateKey'], $newUserPassword, Helper::getCipher());
|
|
if ($encryptedKey) {
|
|
Keymanager::setPrivateKey($encryptedKey, $user);
|
|
|
|
if ($recoveryPassword) { // if recovery key is set we can re-encrypt the key files
|
|
$util = new Util($view, $user);
|
|
$util->recoverUsersFiles($recoveryPassword);
|
|
}
|
|
} else {
|
|
\OCP\Util::writeLog('files_encryption', 'Could not update users encryption password', \OCP\Util::ERROR);
|
|
}
|
|
|
|
\OC_FileProxy::$enabled = $proxyStatus;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* after password reset we create a new key pair for the user
|
|
*
|
|
* @param array $params
|
|
*/
|
|
public static function postPasswordReset($params) {
|
|
$uid = $params['uid'];
|
|
$password = $params['password'];
|
|
|
|
$util = new Util(new \OC\Files\View(), $uid);
|
|
$util->replaceUserKeys($password);
|
|
}
|
|
|
|
/*
|
|
* check if files can be encrypted to every user.
|
|
*/
|
|
/**
|
|
* @param array $params
|
|
*/
|
|
public static function preShared($params) {
|
|
|
|
if (\OCP\App::isEnabled('files_encryption') === false) {
|
|
return true;
|
|
}
|
|
|
|
$l = new \OC_L10N('files_encryption');
|
|
$users = array();
|
|
$view = new \OC\Files\View('/');
|
|
|
|
switch ($params['shareType']) {
|
|
case \OCP\Share::SHARE_TYPE_USER:
|
|
$users[] = $params['shareWith'];
|
|
break;
|
|
case \OCP\Share::SHARE_TYPE_GROUP:
|
|
$users = \OC_Group::usersInGroup($params['shareWith']);
|
|
break;
|
|
}
|
|
|
|
$notConfigured = array();
|
|
foreach ($users as $user) {
|
|
if (!Keymanager::publicKeyExists($view, $user)) {
|
|
$notConfigured[] = $user;
|
|
}
|
|
}
|
|
|
|
if (count($notConfigured) > 0) {
|
|
$params['run'] = false;
|
|
$params['error'] = $l->t('Following users are not set up for encryption:') . ' ' . join(', ' , $notConfigured);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* update share keys if a file was shared
|
|
*/
|
|
public static function postShared($params) {
|
|
|
|
if (\OCP\App::isEnabled('files_encryption') === false) {
|
|
return true;
|
|
}
|
|
|
|
if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') {
|
|
|
|
$path = \OC\Files\Filesystem::getPath($params['fileSource']);
|
|
|
|
self::updateKeyfiles($path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* update keyfiles and share keys recursively
|
|
*
|
|
* @param string $path to the file/folder
|
|
*/
|
|
private static function updateKeyfiles($path) {
|
|
$view = new \OC\Files\View('/');
|
|
$userId = \OCP\User::getUser();
|
|
$session = new \OCA\Encryption\Session($view);
|
|
$util = new Util($view, $userId);
|
|
$sharingEnabled = \OCP\Share::isEnabled();
|
|
|
|
$mountManager = \OC\Files\Filesystem::getMountManager();
|
|
$mount = $mountManager->find('/' . $userId . '/files' . $path);
|
|
$mountPoint = $mount->getMountPoint();
|
|
|
|
// if a folder was shared, get a list of all (sub-)folders
|
|
if ($view->is_dir('/' . $userId . '/files' . $path)) {
|
|
$allFiles = $util->getAllFiles($path, $mountPoint);
|
|
} else {
|
|
$allFiles = array($path);
|
|
}
|
|
|
|
foreach ($allFiles as $path) {
|
|
$usersSharing = $util->getSharingUsersArray($sharingEnabled, $path);
|
|
$util->setSharedFileKeyfiles($session, $usersSharing, $path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* unshare file/folder from a user with whom you shared the file before
|
|
*/
|
|
public static function postUnshare($params) {
|
|
|
|
if (\OCP\App::isEnabled('files_encryption') === false) {
|
|
return true;
|
|
}
|
|
|
|
if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') {
|
|
|
|
$view = new \OC\Files\View('/');
|
|
$userId = \OCP\User::getUser();
|
|
$util = new Util($view, $userId);
|
|
$path = \OC\Files\Filesystem::getPath($params['fileSource']);
|
|
|
|
// for group shares get a list of the group members
|
|
if ($params['shareType'] === \OCP\Share::SHARE_TYPE_GROUP) {
|
|
$userIds = \OC_Group::usersInGroup($params['shareWith']);
|
|
} else {
|
|
if ($params['shareType'] === \OCP\Share::SHARE_TYPE_LINK) {
|
|
$userIds = array($util->getPublicShareKeyId());
|
|
} else {
|
|
$userIds = array($params['shareWith']);
|
|
}
|
|
}
|
|
|
|
$mountManager = \OC\Files\Filesystem::getMountManager();
|
|
$mount = $mountManager->find('/' . $userId . '/files' . $path);
|
|
$mountPoint = $mount->getMountPoint();
|
|
|
|
// if we unshare a folder we need a list of all (sub-)files
|
|
if ($params['itemType'] === 'folder') {
|
|
$allFiles = $util->getAllFiles($path, $mountPoint);
|
|
} else {
|
|
$allFiles = array($path);
|
|
}
|
|
|
|
foreach ($allFiles as $path) {
|
|
|
|
// check if the user still has access to the file, otherwise delete share key
|
|
$sharingUsers = $util->getSharingUsersArray(true, $path);
|
|
|
|
// Unshare every user who no longer has access to the file
|
|
$delUsers = array_diff($userIds, $sharingUsers);
|
|
$keyPath = Keymanager::getKeyPath($view, $util, $path);
|
|
|
|
// delete share key
|
|
Keymanager::delShareKey($view, $delUsers, $keyPath, $userId, $path);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* mark file as renamed so that we know the original source after the file was renamed
|
|
* @param array $params with the old path and the new path
|
|
*/
|
|
public static function preRename($params) {
|
|
self::preRenameOrCopy($params, 'rename');
|
|
}
|
|
|
|
/**
|
|
* mark file as copied so that we know the original source after the file was copied
|
|
* @param array $params with the old path and the new path
|
|
*/
|
|
public static function preCopy($params) {
|
|
self::preRenameOrCopy($params, 'copy');
|
|
}
|
|
|
|
private static function preRenameOrCopy($params, $operation) {
|
|
$user = \OCP\User::getUser();
|
|
$view = new \OC\Files\View('/');
|
|
$util = new Util($view, $user);
|
|
|
|
// we only need to rename the keys if the rename happens on the same mountpoint
|
|
// otherwise we perform a stream copy, so we get a new set of keys
|
|
$mp1 = $view->getMountPoint('/' . $user . '/files/' . $params['oldpath']);
|
|
$mp2 = $view->getMountPoint('/' . $user . '/files/' . $params['newpath']);
|
|
|
|
$oldKeysPath = Keymanager::getKeyPath($view, $util, $params['oldpath']);
|
|
|
|
if ($mp1 === $mp2) {
|
|
self::$renamedFiles[$params['oldpath']] = array(
|
|
'operation' => $operation,
|
|
'oldKeysPath' => $oldKeysPath,
|
|
);
|
|
} else {
|
|
self::$renamedFiles[$params['oldpath']] = array(
|
|
'operation' => 'cleanup',
|
|
'oldKeysPath' => $oldKeysPath,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* after a file is renamed/copied, rename/copy its keyfile and share-keys also fix the file size and fix also the sharing
|
|
*
|
|
* @param array $params array with oldpath and newpath
|
|
*/
|
|
public static function postRenameOrCopy($params) {
|
|
|
|
if (\OCP\App::isEnabled('files_encryption') === false) {
|
|
return true;
|
|
}
|
|
|
|
$view = new \OC\Files\View('/');
|
|
$userId = \OCP\User::getUser();
|
|
$util = new Util($view, $userId);
|
|
|
|
if (isset(self::$renamedFiles[$params['oldpath']]['operation']) &&
|
|
isset(self::$renamedFiles[$params['oldpath']]['oldKeysPath'])) {
|
|
$operation = self::$renamedFiles[$params['oldpath']]['operation'];
|
|
$oldKeysPath = self::$renamedFiles[$params['oldpath']]['oldKeysPath'];
|
|
unset(self::$renamedFiles[$params['oldpath']]);
|
|
if ($operation === 'cleanup') {
|
|
return $view->unlink($oldKeysPath);
|
|
}
|
|
} else {
|
|
\OCP\Util::writeLog('Encryption library', "can't get path and owner from the file before it was renamed", \OCP\Util::DEBUG);
|
|
return false;
|
|
}
|
|
|
|
list($ownerNew, $pathNew) = $util->getUidAndFilename($params['newpath']);
|
|
|
|
if ($util->isSystemWideMountPoint($pathNew)) {
|
|
$newKeysPath = 'files_encryption/keys/' . $pathNew;
|
|
} else {
|
|
$newKeysPath = $ownerNew . '/files_encryption/keys/' . $pathNew;
|
|
}
|
|
|
|
// create key folders if it doesn't exists
|
|
if (!$view->file_exists(dirname($newKeysPath))) {
|
|
$view->mkdir(dirname($newKeysPath));
|
|
}
|
|
|
|
$view->$operation($oldKeysPath, $newKeysPath);
|
|
|
|
// update sharing-keys
|
|
self::updateKeyfiles($params['newpath']);
|
|
}
|
|
|
|
/**
|
|
* set migration status and the init status back to '0' so that all new files get encrypted
|
|
* if the app gets enabled again
|
|
* @param array $params contains the app ID
|
|
*/
|
|
public static function preDisable($params) {
|
|
if ($params['app'] === 'files_encryption') {
|
|
|
|
\OC::$server->getConfig()->deleteAppFromAllUsers('files_encryption');
|
|
|
|
$session = new \OCA\Encryption\Session(new \OC\Files\View('/'));
|
|
$session->setInitialized(\OCA\Encryption\Session::NOT_INITIALIZED);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* set the init status to 'NOT_INITIALIZED' (0) if the app gets enabled
|
|
* @param array $params contains the app ID
|
|
*/
|
|
public static function postEnable($params) {
|
|
if ($params['app'] === 'files_encryption') {
|
|
$session = new \OCA\Encryption\Session(new \OC\Files\View('/'));
|
|
$session->setInitialized(\OCA\Encryption\Session::NOT_INITIALIZED);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* if the file was really deleted we remove the encryption keys
|
|
* @param array $params
|
|
* @return boolean|null
|
|
*/
|
|
public static function postDelete($params) {
|
|
|
|
$path = $params[\OC\Files\Filesystem::signal_param_path];
|
|
|
|
if (!isset(self::$deleteFiles[$path])) {
|
|
return true;
|
|
}
|
|
|
|
$deletedFile = self::$deleteFiles[$path];
|
|
$keyPath = $deletedFile['keyPath'];
|
|
|
|
// we don't need to remember the file any longer
|
|
unset(self::$deleteFiles[$path]);
|
|
|
|
$view = new \OC\Files\View('/');
|
|
|
|
// return if the file still exists and wasn't deleted correctly
|
|
if ($view->file_exists('/' . \OCP\User::getUser() . '/files/' . $path)) {
|
|
return true;
|
|
}
|
|
|
|
// Delete keyfile & shareKey so it isn't orphaned
|
|
$view->unlink($keyPath);
|
|
|
|
}
|
|
|
|
/**
|
|
* remember the file which should be deleted and it's owner
|
|
* @param array $params
|
|
* @return boolean|null
|
|
*/
|
|
public static function preDelete($params) {
|
|
$view = new \OC\Files\View('/');
|
|
$path = $params[\OC\Files\Filesystem::signal_param_path];
|
|
|
|
// skip this method if the trash bin is enabled or if we delete a file
|
|
// outside of /data/user/files
|
|
if (\OCP\App::isEnabled('files_trashbin')) {
|
|
return true;
|
|
}
|
|
|
|
$util = new Util($view, \OCP\USER::getUser());
|
|
|
|
$keysPath = Keymanager::getKeyPath($view, $util, $path);
|
|
|
|
self::$deleteFiles[$path] = array(
|
|
'keyPath' => $keysPath);
|
|
}
|
|
|
|
/**
|
|
* unmount file from yourself
|
|
* remember files/folders which get unmounted
|
|
*/
|
|
public static function preUnmount($params) {
|
|
$view = new \OC\Files\View('/');
|
|
$user = \OCP\User::getUser();
|
|
$path = $params[\OC\Files\Filesystem::signal_param_path];
|
|
|
|
$util = new Util($view, $user);
|
|
list($owner, $ownerPath) = $util->getUidAndFilename($path);
|
|
|
|
$keysPath = Keymanager::getKeyPath($view, $util, $path);
|
|
|
|
self::$unmountedFiles[$path] = array(
|
|
'keyPath' => $keysPath,
|
|
'owner' => $owner,
|
|
'ownerPath' => $ownerPath
|
|
);
|
|
}
|
|
|
|
/**
|
|
* unmount file from yourself
|
|
*/
|
|
public static function postUnmount($params) {
|
|
|
|
$path = $params[\OC\Files\Filesystem::signal_param_path];
|
|
$user = \OCP\User::getUser();
|
|
|
|
if (!isset(self::$unmountedFiles[$path])) {
|
|
return true;
|
|
}
|
|
|
|
$umountedFile = self::$unmountedFiles[$path];
|
|
$keyPath = $umountedFile['keyPath'];
|
|
$owner = $umountedFile['owner'];
|
|
$ownerPath = $umountedFile['ownerPath'];
|
|
|
|
$view = new \OC\Files\View();
|
|
|
|
// we don't need to remember the file any longer
|
|
unset(self::$unmountedFiles[$path]);
|
|
|
|
// check if the user still has access to the file, otherwise delete share key
|
|
$sharingUsers = \OCP\Share::getUsersSharingFile($path, $user);
|
|
if (!in_array(\OCP\User::getUser(), $sharingUsers['users'])) {
|
|
Keymanager::delShareKey($view, array(\OCP\User::getUser()), $keyPath, $owner, $ownerPath);
|
|
}
|
|
}
|
|
|
|
}
|