aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-09-24 23:37:18 +0200
committerFerdinand Thiessen <opensource@fthiessen.de>2024-10-15 18:33:06 +0200
commitf3aa004b1c18e0a5fe69112b97cf4ac4fd968cb7 (patch)
tree4ad993b2beaba1fd9e624a741c6123073f45874a
parentcd3dc1719b8e84526f2c99634f0dc8bf16380579 (diff)
downloadnextcloud-server-f3aa004b1c18e0a5fe69112b97cf4ac4fd968cb7.tar.gz
nextcloud-server-f3aa004b1c18e0a5fe69112b97cf4ac4fd968cb7.zip
refactor(encryption): Migrate away from Hooks to typed events
Co-authored-by: Ferdinand Thiessen <opensource@fthiessen.de> Co-authored-by: Louis <louis@chmn.me> Co-authored-by: Côme Chilliet <91878298+come-nc@users.noreply.github.com> Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r--apps/encryption/composer/composer/autoload_classmap.php5
-rw-r--r--apps/encryption/composer/composer/autoload_static.php5
-rw-r--r--apps/encryption/lib/AppInfo/Application.php72
-rw-r--r--apps/encryption/lib/Crypto/Crypt.php2
-rw-r--r--apps/encryption/lib/HookManager.php43
-rw-r--r--apps/encryption/lib/Hooks/Contracts/IHook.php17
-rw-r--r--apps/encryption/lib/Hooks/UserHooks.php266
-rw-r--r--apps/encryption/lib/KeyManager.php8
-rw-r--r--apps/encryption/lib/Listeners/UserEventsListener.php143
-rw-r--r--apps/encryption/lib/Services/PassphraseService.php142
-rw-r--r--apps/encryption/lib/Users/Setup.php12
-rw-r--r--apps/encryption/lib/Util.php5
-rw-r--r--apps/encryption/tests/HookManagerTest.php52
-rw-r--r--apps/encryption/tests/Hooks/UserHooksTest.php370
-rw-r--r--apps/encryption/tests/Listeners/UserEventsListenersTest.php258
-rw-r--r--apps/encryption/tests/PassphraseServiceTest.php196
-rw-r--r--lib/private/Log/ExceptionSerializer.php21
17 files changed, 801 insertions, 816 deletions
diff --git a/apps/encryption/composer/composer/autoload_classmap.php b/apps/encryption/composer/composer/autoload_classmap.php
index 059296338b4..814f39653e9 100644
--- a/apps/encryption/composer/composer/autoload_classmap.php
+++ b/apps/encryption/composer/composer/autoload_classmap.php
@@ -26,12 +26,11 @@ return array(
'OCA\\Encryption\\Exceptions\\MultiKeyEncryptException' => $baseDir . '/../lib/Exceptions/MultiKeyEncryptException.php',
'OCA\\Encryption\\Exceptions\\PrivateKeyMissingException' => $baseDir . '/../lib/Exceptions/PrivateKeyMissingException.php',
'OCA\\Encryption\\Exceptions\\PublicKeyMissingException' => $baseDir . '/../lib/Exceptions/PublicKeyMissingException.php',
- 'OCA\\Encryption\\HookManager' => $baseDir . '/../lib/HookManager.php',
- 'OCA\\Encryption\\Hooks\\Contracts\\IHook' => $baseDir . '/../lib/Hooks/Contracts/IHook.php',
- 'OCA\\Encryption\\Hooks\\UserHooks' => $baseDir . '/../lib/Hooks/UserHooks.php',
'OCA\\Encryption\\KeyManager' => $baseDir . '/../lib/KeyManager.php',
+ 'OCA\\Encryption\\Listeners\\UserEventsListener' => $baseDir . '/../lib/Listeners/UserEventsListener.php',
'OCA\\Encryption\\Migration\\SetMasterKeyStatus' => $baseDir . '/../lib/Migration/SetMasterKeyStatus.php',
'OCA\\Encryption\\Recovery' => $baseDir . '/../lib/Recovery.php',
+ 'OCA\\Encryption\\Services\\PassphraseService' => $baseDir . '/../lib/Services/PassphraseService.php',
'OCA\\Encryption\\Session' => $baseDir . '/../lib/Session.php',
'OCA\\Encryption\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php',
'OCA\\Encryption\\Settings\\Personal' => $baseDir . '/../lib/Settings/Personal.php',
diff --git a/apps/encryption/composer/composer/autoload_static.php b/apps/encryption/composer/composer/autoload_static.php
index 6c458eabddd..af5e5192520 100644
--- a/apps/encryption/composer/composer/autoload_static.php
+++ b/apps/encryption/composer/composer/autoload_static.php
@@ -41,12 +41,11 @@ class ComposerStaticInitEncryption
'OCA\\Encryption\\Exceptions\\MultiKeyEncryptException' => __DIR__ . '/..' . '/../lib/Exceptions/MultiKeyEncryptException.php',
'OCA\\Encryption\\Exceptions\\PrivateKeyMissingException' => __DIR__ . '/..' . '/../lib/Exceptions/PrivateKeyMissingException.php',
'OCA\\Encryption\\Exceptions\\PublicKeyMissingException' => __DIR__ . '/..' . '/../lib/Exceptions/PublicKeyMissingException.php',
- 'OCA\\Encryption\\HookManager' => __DIR__ . '/..' . '/../lib/HookManager.php',
- 'OCA\\Encryption\\Hooks\\Contracts\\IHook' => __DIR__ . '/..' . '/../lib/Hooks/Contracts/IHook.php',
- 'OCA\\Encryption\\Hooks\\UserHooks' => __DIR__ . '/..' . '/../lib/Hooks/UserHooks.php',
'OCA\\Encryption\\KeyManager' => __DIR__ . '/..' . '/../lib/KeyManager.php',
+ 'OCA\\Encryption\\Listeners\\UserEventsListener' => __DIR__ . '/..' . '/../lib/Listeners/UserEventsListener.php',
'OCA\\Encryption\\Migration\\SetMasterKeyStatus' => __DIR__ . '/..' . '/../lib/Migration/SetMasterKeyStatus.php',
'OCA\\Encryption\\Recovery' => __DIR__ . '/..' . '/../lib/Recovery.php',
+ 'OCA\\Encryption\\Services\\PassphraseService' => __DIR__ . '/..' . '/../lib/Services/PassphraseService.php',
'OCA\\Encryption\\Session' => __DIR__ . '/..' . '/../lib/Session.php',
'OCA\\Encryption\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php',
'OCA\\Encryption\\Settings\\Personal' => __DIR__ . '/..' . '/../lib/Settings/Personal.php',
diff --git a/apps/encryption/lib/AppInfo/Application.php b/apps/encryption/lib/AppInfo/Application.php
index d683c82286a..600bc300797 100644
--- a/apps/encryption/lib/AppInfo/Application.php
+++ b/apps/encryption/lib/AppInfo/Application.php
@@ -7,14 +7,14 @@
*/
namespace OCA\Encryption\AppInfo;
+use OC\Core\Events\BeforePasswordResetEvent;
+use OC\Core\Events\PasswordResetEvent;
use OCA\Encryption\Crypto\Crypt;
use OCA\Encryption\Crypto\DecryptAll;
use OCA\Encryption\Crypto\EncryptAll;
use OCA\Encryption\Crypto\Encryption;
-use OCA\Encryption\HookManager;
-use OCA\Encryption\Hooks\UserHooks;
use OCA\Encryption\KeyManager;
-use OCA\Encryption\Recovery;
+use OCA\Encryption\Listeners\UserEventsListener;
use OCA\Encryption\Session;
use OCA\Encryption\Users\Setup;
use OCA\Encryption\Util;
@@ -23,7 +23,14 @@ use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Encryption\IManager;
+use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
+use OCP\IL10N;
+use OCP\IUserSession;
+use OCP\User\Events\BeforePasswordUpdatedEvent;
+use OCP\User\Events\PasswordUpdatedEvent;
+use OCP\User\Events\UserCreatedEvent;
+use OCP\User\Events\UserDeletedEvent;
use Psr\Log\LoggerInterface;
class Application extends App implements IBootstrap {
@@ -49,7 +56,7 @@ class Application extends App implements IBootstrap {
}
$context->injectFn($this->registerEncryptionModule(...));
- $context->injectFn($this->registerHooks(...));
+ $context->injectFn($this->registerEventListeners(...));
$context->injectFn($this->setUp(...));
});
}
@@ -57,38 +64,29 @@ class Application extends App implements IBootstrap {
public function setUp(IManager $encryptionManager) {
if ($encryptionManager->isEnabled()) {
/** @var Setup $setup */
- $setup = $this->getContainer()->query(Setup::class);
+ $setup = $this->getContainer()->get(Setup::class);
$setup->setupSystem();
}
}
- /**
- * register hooks
- */
- public function registerHooks(IConfig $config) {
- if (!$config->getSystemValueBool('maintenance')) {
- $container = $this->getContainer();
- $server = $container->getServer();
- // Register our hooks and fire them.
- $hookManager = new HookManager();
-
- $hookManager->registerHook([
- new UserHooks($container->query(KeyManager::class),
- $server->getUserManager(),
- $server->get(LoggerInterface::class),
- $container->query(Setup::class),
- $server->getUserSession(),
- $container->query(Util::class),
- $container->query(Session::class),
- $container->query(Crypt::class),
- $container->query(Recovery::class))
- ]);
+ public function registerEventListeners(IConfig $config, IEventDispatcher $eventDispatcher, IManager $encryptionManager): void {
+ if (!$encryptionManager->isEnabled()) {
+ return;
+ }
- $hookManager->fireHooks();
- } else {
+ if ($config->getSystemValueBool('maintenance')) {
// Logout user if we are in maintenance to force re-login
- $this->getContainer()->getServer()->getUserSession()->logout();
+ $this->getContainer()->get(IUserSession::class)->logout();
+ return;
}
+
+ // No maintenance so register all events
+ $eventDispatcher->addServiceListener(UserCreatedEvent::class, UserEventsListener::class);
+ $eventDispatcher->addServiceListener(UserDeletedEvent::class, UserEventsListener::class);
+ $eventDispatcher->addServiceListener(BeforePasswordUpdatedEvent::class, UserEventsListener::class);
+ $eventDispatcher->addServiceListener(PasswordUpdatedEvent::class, UserEventsListener::class);
+ $eventDispatcher->addServiceListener(BeforePasswordResetEvent::class, UserEventsListener::class);
+ $eventDispatcher->addServiceListener(PasswordResetEvent::class, UserEventsListener::class);
}
public function registerEncryptionModule(IManager $encryptionManager) {
@@ -99,14 +97,14 @@ class Application extends App implements IBootstrap {
Encryption::DISPLAY_NAME,
function () use ($container) {
return new Encryption(
- $container->query(Crypt::class),
- $container->query(KeyManager::class),
- $container->query(Util::class),
- $container->query(Session::class),
- $container->query(EncryptAll::class),
- $container->query(DecryptAll::class),
- $container->getServer()->get(LoggerInterface::class),
- $container->getServer()->getL10N($container->getAppName())
+ $container->get(Crypt::class),
+ $container->get(KeyManager::class),
+ $container->get(Util::class),
+ $container->get(Session::class),
+ $container->get(EncryptAll::class),
+ $container->get(DecryptAll::class),
+ $container->get(LoggerInterface::class),
+ $container->get(IL10N::class),
);
});
}
diff --git a/apps/encryption/lib/Crypto/Crypt.php b/apps/encryption/lib/Crypto/Crypt.php
index b38734dd061..463ca4e22bb 100644
--- a/apps/encryption/lib/Crypto/Crypt.php
+++ b/apps/encryption/lib/Crypto/Crypt.php
@@ -83,7 +83,7 @@ class Crypt {
/**
* create new private/public key-pair for user
*
- * @return array|bool
+ * @return array{publicKey: string, privateKey: string}|false
*/
public function createKeyPair() {
$res = $this->getOpenSSLPKey();
diff --git a/apps/encryption/lib/HookManager.php b/apps/encryption/lib/HookManager.php
deleted file mode 100644
index 6ad56ebad78..00000000000
--- a/apps/encryption/lib/HookManager.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-/**
- * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-namespace OCA\Encryption;
-
-use OCA\Encryption\Hooks\Contracts\IHook;
-
-class HookManager {
- /** @var IHook[] */
- private $hookInstances = [];
-
- /**
- * @param array|IHook $instances
- * - This accepts either a single instance of IHook or an array of instances of IHook
- * @return bool
- */
- public function registerHook($instances) {
- if (is_array($instances)) {
- foreach ($instances as $instance) {
- if (!$instance instanceof IHook) {
- return false;
- }
- $this->hookInstances[] = $instance;
- }
- } elseif ($instances instanceof IHook) {
- $this->hookInstances[] = $instances;
- }
- return true;
- }
-
- public function fireHooks() {
- foreach ($this->hookInstances as $instance) {
- /**
- * Fire off the add hooks method of each instance stored in cache
- */
- $instance->addHooks();
- }
- }
-}
diff --git a/apps/encryption/lib/Hooks/Contracts/IHook.php b/apps/encryption/lib/Hooks/Contracts/IHook.php
deleted file mode 100644
index 5bb8046e230..00000000000
--- a/apps/encryption/lib/Hooks/Contracts/IHook.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php
-
-/**
- * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-namespace OCA\Encryption\Hooks\Contracts;
-
-interface IHook {
- /**
- * Connects Hooks
- *
- * @return null
- */
- public function addHooks();
-}
diff --git a/apps/encryption/lib/Hooks/UserHooks.php b/apps/encryption/lib/Hooks/UserHooks.php
deleted file mode 100644
index e75a1e545d1..00000000000
--- a/apps/encryption/lib/Hooks/UserHooks.php
+++ /dev/null
@@ -1,266 +0,0 @@
-<?php
-
-/**
- * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-namespace OCA\Encryption\Hooks;
-
-use OC\Files\Filesystem;
-use OCA\Encryption\Crypto\Crypt;
-use OCA\Encryption\Hooks\Contracts\IHook;
-use OCA\Encryption\KeyManager;
-use OCA\Encryption\Recovery;
-use OCA\Encryption\Session;
-use OCA\Encryption\Users\Setup;
-use OCA\Encryption\Util;
-use OCP\Encryption\Exceptions\GenericEncryptionException;
-use OCP\IUserManager;
-use OCP\IUserSession;
-use OCP\Util as OCUtil;
-use Psr\Log\LoggerInterface;
-
-class UserHooks implements IHook {
- /**
- * list of user for which we perform a password reset
- * @var array<string, true>
- */
- protected static array $passwordResetUsers = [];
-
- public function __construct(
- private KeyManager $keyManager,
- private IUserManager $userManager,
- private LoggerInterface $logger,
- private Setup $userSetup,
- private IUserSession $userSession,
- private Util $util,
- private Session $session,
- private Crypt $crypt,
- private Recovery $recovery,
- ) {
- }
-
- /**
- * Connects Hooks
- *
- * @return null
- */
- public function addHooks() {
- OCUtil::connectHook('OC_User', 'post_login', $this, 'login');
- OCUtil::connectHook('OC_User', 'logout', $this, 'logout');
-
- // this hooks only make sense if no master key is used
- if ($this->util->isMasterKeyEnabled() === false) {
- OCUtil::connectHook('OC_User',
- 'post_setPassword',
- $this,
- 'setPassphrase');
-
- OCUtil::connectHook('OC_User',
- 'pre_setPassword',
- $this,
- 'preSetPassphrase');
-
- OCUtil::connectHook('\OC\Core\LostPassword\Controller\LostController',
- 'post_passwordReset',
- $this,
- 'postPasswordReset');
-
- OCUtil::connectHook('\OC\Core\LostPassword\Controller\LostController',
- 'pre_passwordReset',
- $this,
- 'prePasswordReset');
-
- OCUtil::connectHook('OC_User',
- 'post_createUser',
- $this,
- 'postCreateUser');
-
- OCUtil::connectHook('OC_User',
- 'post_deleteUser',
- $this,
- 'postDeleteUser');
- }
- }
-
-
- /**
- * Startup encryption backend upon user login
- *
- * @note This method should never be called for users using client side encryption
- * @param array $params
- * @return boolean|null
- */
- public function login($params) {
- // ensure filesystem is loaded
- if (!Filesystem::$loaded) {
- $this->setupFS($params['uid']);
- }
- if ($this->util->isMasterKeyEnabled() === false) {
- $this->userSetup->setupUser($params['uid'], $params['password']);
- }
-
- $this->keyManager->init($params['uid'], $params['password']);
- }
-
- /**
- * remove keys from session during logout
- */
- public function logout() {
- $this->session->clear();
- }
-
- /**
- * setup encryption backend upon user created
- *
- * @note This method should never be called for users using client side encryption
- * @param array $params
- */
- public function postCreateUser($params) {
- $this->userSetup->setupUser($params['uid'], $params['password']);
- }
-
- /**
- * cleanup encryption backend upon user deleted
- *
- * @param array $params : uid, password
- * @note This method should never be called for users using client side encryption
- */
- public function postDeleteUser($params) {
- $this->keyManager->deletePublicKey($params['uid']);
- }
-
- public function prePasswordReset($params) {
- $user = $params['uid'];
- self::$passwordResetUsers[$user] = true;
- }
-
- public function postPasswordReset($params) {
- $uid = $params['uid'];
- $password = $params['password'];
- $this->keyManager->backupUserKeys('passwordReset', $uid);
- $this->keyManager->deleteUserKeys($uid);
- $this->userSetup->setupUser($uid, $password);
- unset(self::$passwordResetUsers[$uid]);
- }
-
- /**
- * If the password can't be changed within Nextcloud, than update the key password in advance.
- *
- * @param array $params : uid, password
- * @return boolean|null
- */
- public function preSetPassphrase($params) {
- $user = $this->userManager->get($params['uid']);
-
- if ($user && !$user->canChangePassword()) {
- $this->setPassphrase($params);
- }
- }
-
- /**
- * Change a user's encryption passphrase
- *
- * @param array $params keys: uid, password
- * @return boolean|null
- */
- public function setPassphrase($params) {
- // if we are in the process to resetting a user password, we have nothing
- // to do here
- if (isset(self::$passwordResetUsers[$params['uid']])) {
- return true;
- }
-
- // Get existing decrypted private key
- $user = $this->userSession->getUser();
-
- // current logged in user changes their own password
- if ($user && $params['uid'] === $user->getUID()) {
- $privateKey = $this->session->getPrivateKey();
-
- // Encrypt private key with new user pwd as passphrase
- $encryptedPrivateKey = $this->crypt->encryptPrivateKey($privateKey, $params['password'], $params['uid']);
-
- // Save private key
- if ($encryptedPrivateKey) {
- $this->keyManager->setPrivateKey($user->getUID(),
- $this->crypt->generateHeader() . $encryptedPrivateKey);
- } else {
- $this->logger->error('Encryption could not update users encryption password');
- }
-
- // 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 re-encrypt file keys
- $userId = $params['uid'];
- $this->initMountPoints($userId);
- $recoveryPassword = $params['recoveryPassword'] ?? null;
-
- $recoveryKeyId = $this->keyManager->getRecoveryKeyId();
- $recoveryKey = $this->keyManager->getSystemPrivateKey($recoveryKeyId);
- try {
- $decryptedRecoveryKey = $this->crypt->decryptPrivateKey($recoveryKey, $recoveryPassword);
- } catch (\Exception $e) {
- $decryptedRecoveryKey = false;
- }
- if ($decryptedRecoveryKey === false) {
- $message = 'Can not decrypt the recovery key. Maybe you provided the wrong password. Try again.';
- throw new GenericEncryptionException($message, $message);
- }
-
- // 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 (
- ($this->recovery->isRecoveryEnabledForUser($userId) && $recoveryPassword)
- || !$this->keyManager->userHasKeys($userId)
- || !$this->util->userHasFiles($userId)
- ) {
- // backup old keys
- //$this->backupAllKeys('recovery');
-
- $newUserPassword = $params['password'];
-
- $keyPair = $this->crypt->createKeyPair();
-
- // Save public key
- $this->keyManager->setPublicKey($userId, $keyPair['publicKey']);
-
- // Encrypt private key with new password
- $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $newUserPassword, $userId);
-
- if ($encryptedKey) {
- $this->keyManager->setPrivateKey($userId, $this->crypt->generateHeader() . $encryptedKey);
-
- if ($recoveryPassword) { // if recovery key is set we can re-encrypt the key files
- $this->recovery->recoverUsersFiles($recoveryPassword, $userId);
- }
- } else {
- $this->logger->error('Encryption Could not update users encryption password');
- }
- }
- }
- }
-
- /**
- * init mount points for given user
- *
- * @param string $user
- * @throws \OC\User\NoUserException
- */
- protected function initMountPoints($user) {
- Filesystem::initMountPoints($user);
- }
-
- /**
- * setup file system for user
- *
- * @param string $uid user id
- */
- protected function setupFS($uid) {
- \OC_Util::setupFS($uid);
- }
-}
diff --git a/apps/encryption/lib/KeyManager.php b/apps/encryption/lib/KeyManager.php
index 0c9c02760a8..f694e6550f1 100644
--- a/apps/encryption/lib/KeyManager.php
+++ b/apps/encryption/lib/KeyManager.php
@@ -287,11 +287,9 @@ class KeyManager {
/**
* Decrypt private key and store it
*
- * @param string $uid user id
- * @param string $passPhrase users password
* @return boolean
*/
- public function init($uid, $passPhrase) {
+ public function init(string $uid, ?string $passPhrase) {
$this->session->setStatus(Session::INIT_EXECUTED);
try {
@@ -300,6 +298,10 @@ class KeyManager {
$passPhrase = $this->getMasterKeyPassword();
$privateKey = $this->getSystemPrivateKey($uid);
} else {
+ if ($passPhrase === null) {
+ $this->logger->warning('Master key is disabled but not passphrase provided.');
+ return false;
+ }
$privateKey = $this->getPrivateKey($uid);
}
$privateKey = $this->crypt->decryptPrivateKey($privateKey, $passPhrase, $uid);
diff --git a/apps/encryption/lib/Listeners/UserEventsListener.php b/apps/encryption/lib/Listeners/UserEventsListener.php
new file mode 100644
index 00000000000..694640a0103
--- /dev/null
+++ b/apps/encryption/lib/Listeners/UserEventsListener.php
@@ -0,0 +1,143 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Encryption\Listeners;
+
+use OC\Core\Events\BeforePasswordResetEvent;
+use OC\Core\Events\PasswordResetEvent;
+use OC\Files\SetupManager;
+use OCA\Encryption\KeyManager;
+use OCA\Encryption\Services\PassphraseService;
+use OCA\Encryption\Session;
+use OCA\Encryption\Users\Setup;
+use OCA\Encryption\Util;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\User\Events\BeforePasswordUpdatedEvent;
+use OCP\User\Events\PasswordUpdatedEvent;
+use OCP\User\Events\UserCreatedEvent;
+use OCP\User\Events\UserDeletedEvent;
+use OCP\User\Events\UserLoggedInEvent;
+use OCP\User\Events\UserLoggedOutEvent;
+
+/**
+ * @template-implements IEventListener<UserCreatedEvent|UserDeletedEvent|UserLoggedInEvent|UserLoggedOutEvent|BeforePasswordUpdatedEvent|PasswordUpdatedEvent|BeforePasswordResetEvent|PasswordResetEvent>
+ */
+class UserEventsListener implements IEventListener {
+
+ public function __construct(
+ private Util $util,
+ private Setup $userSetup,
+ private Session $session,
+ private KeyManager $keyManager,
+ private IUserManager $userManager,
+ private IUserSession $userSession,
+ private SetupManager $setupManager,
+ private PassphraseService $passphraseService,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if ($event instanceof UserCreatedEvent) {
+ $this->onUserCreated($event->getUid(), $event->getPassword());
+ } elseif ($event instanceof UserDeletedEvent) {
+ $this->onUserDeleted($event->getUid());
+ } elseif ($event instanceof UserLoggedInEvent) {
+ $this->onUserLogin($event->getUser(), $event->getPassword());
+ } elseif ($event instanceof UserLoggedOutEvent) {
+ $this->onUserLogout();
+ } elseif ($event instanceof BeforePasswordUpdatedEvent) {
+ $this->onBeforePasswordUpdated($event->getUser(), $event->getPassword(), $event->getRecoveryPassword());
+ } elseif ($event instanceof PasswordUpdatedEvent) {
+ $this->onPasswordUpdated($event->getUid(), $event->getPassword(), $event->getRecoveryPassword());
+ } elseif ($event instanceof BeforePasswordResetEvent) {
+ $this->onBeforePasswordReset($event->getUid());
+ } elseif ($event instanceof PasswordResetEvent) {
+ $this->onPasswordReset($event->getUid(), $event->getPassword());
+ }
+ }
+
+ /**
+ * Startup encryption backend upon user login
+ */
+ private function onUserLogin(IUser $user, ?string $password): void {
+ // ensure filesystem is loaded
+ $this->setupManager->setupForUser($user);
+ if ($this->util->isMasterKeyEnabled() === false) {
+ // Skip if no master key and the password is not provided
+ if ($password === null) {
+ return;
+ }
+
+ $this->userSetup->setupUser($user->getUID(), $password);
+ }
+
+ $this->keyManager->init($user->getUID(), $password);
+ }
+
+ /**
+ * Remove keys from session during logout
+ */
+ private function onUserLogout(): void {
+ $this->session->clear();
+ }
+
+ /**
+ * Setup encryption backend upon user created
+ *
+ * This method should never be called for users using client side encryption
+ */
+ protected function onUserCreated(string $userId, string $password): void {
+ $this->userSetup->setupUser($userId, $password);
+ }
+
+ /**
+ * Cleanup encryption backend upon user deleted
+ *
+ * This method should never be called for users using client side encryption
+ */
+ protected function onUserDeleted(string $userId): void {
+ $this->keyManager->deletePublicKey($userId);
+ }
+
+ /**
+ * If the password can't be changed within Nextcloud, than update the key password in advance.
+ */
+ public function onBeforePasswordUpdated(IUser $user, string $password, ?string $recoveryPassword = null): void {
+ if (!$user->canChangePassword()) {
+ $this->passphraseService->setPassphraseForUser($user->getUID(), $password, $recoveryPassword);
+ }
+ }
+
+ /**
+ * Change a user's encryption passphrase
+ */
+ public function onPasswordUpdated(string $userId, string $password, ?string $recoveryPassword): void {
+ $this->passphraseService->setPassphraseForUser($userId, $password, $recoveryPassword);
+ }
+
+ /**
+ * Set user password resetting state to allow ignoring "reset"-requests on password update
+ */
+ public function onBeforePasswordReset(string $userId): void {
+ $this->passphraseService->setProcessingReset($userId);
+ }
+
+ /**
+ * Create new encryption keys on password reset and backup the old one
+ */
+ public function onPasswordReset(string $userId, string $password): void {
+ $this->keyManager->backupUserKeys('passwordReset', $userId);
+ $this->keyManager->deleteUserKeys($userId);
+ $this->userSetup->setupUser($userId, $password);
+ $this->passphraseService->setProcessingReset($userId, false);
+ }
+}
diff --git a/apps/encryption/lib/Services/PassphraseService.php b/apps/encryption/lib/Services/PassphraseService.php
new file mode 100644
index 00000000000..4b3e83375f5
--- /dev/null
+++ b/apps/encryption/lib/Services/PassphraseService.php
@@ -0,0 +1,142 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Encryption\Services;
+
+use OCA\Encryption\Crypto\Crypt;
+use OCA\Encryption\KeyManager;
+use OCA\Encryption\Recovery;
+use OCA\Encryption\Session;
+use OCA\Encryption\Util;
+use OCP\Encryption\Exceptions\GenericEncryptionException;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
+
+class PassphraseService {
+
+ /** @var array<string, bool> */
+ private static array $passwordResetUsers = [];
+
+ public function __construct(
+ private Util $util,
+ private Crypt $crypt,
+ private Session $session,
+ private Recovery $recovery,
+ private KeyManager $keyManager,
+ private LoggerInterface $logger,
+ private IUserManager $userManager,
+ private IUserSession $userSession,
+ ) {
+ }
+
+ public function setProcessingReset(string $uid, bool $processing = true): void {
+ if ($processing) {
+ self::$passwordResetUsers[$uid] = true;
+ } else {
+ unset(self::$passwordResetUsers[$uid]);
+ }
+ }
+
+ /**
+ * Change a user's encryption passphrase
+ */
+ public function setPassphraseForUser(string $userId, string $password, ?string $recoveryPassword = null): bool {
+ // if we are in the process to resetting a user password, we have nothing
+ // to do here
+ if (isset(self::$passwordResetUsers[$userId])) {
+ return true;
+ }
+
+ // Check user exists on backend
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ return false;
+ }
+
+ // Get existing decrypted private key
+ $currentUser = $this->userSession->getUser();
+
+ // current logged in user changes his own password
+ if ($currentUser !== null && $userId === $currentUser->getUID()) {
+ $privateKey = $this->session->getPrivateKey();
+
+ // Encrypt private key with new user pwd as passphrase
+ $encryptedPrivateKey = $this->crypt->encryptPrivateKey($privateKey, $password, $userId);
+
+ // Save private key
+ if ($encryptedPrivateKey !== false) {
+ $key = $this->crypt->generateHeader() . $encryptedPrivateKey;
+ $this->keyManager->setPrivateKey($userId, $key);
+ return true;
+ }
+
+ $this->logger->error('Encryption could not update users encryption password');
+
+ // 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 re-encrypt file keys
+ $recoveryPassword = $recoveryPassword ?? '';
+ $this->initMountPoints($user);
+
+ $recoveryKeyId = $this->keyManager->getRecoveryKeyId();
+ $recoveryKey = $this->keyManager->getSystemPrivateKey($recoveryKeyId);
+ try {
+ $this->crypt->decryptPrivateKey($recoveryKey, $recoveryPassword);
+ } catch (\Exception) {
+ $message = 'Can not decrypt the recovery key. Maybe you provided the wrong password. Try again.';
+ throw new GenericEncryptionException($message, $message);
+ }
+
+ // 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 (
+ ($this->recovery->isRecoveryEnabledForUser($userId) && $recoveryPassword !== '')
+ || !$this->keyManager->userHasKeys($userId)
+ || !$this->util->userHasFiles($userId)
+ ) {
+ $keyPair = $this->crypt->createKeyPair();
+ if ($keyPair === false) {
+ $this->logger->error('Could not create new private key-pair for user.');
+ return false;
+ }
+
+ // Save public key
+ $this->keyManager->setPublicKey($userId, $keyPair['publicKey']);
+
+ // Encrypt private key with new password
+ $encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $password, $userId);
+ if ($encryptedKey === false) {
+ $this->logger->error('Encryption could not update users encryption password');
+ return false;
+ }
+
+ $this->keyManager->setPrivateKey($userId, $this->crypt->generateHeader() . $encryptedKey);
+
+ if ($recoveryPassword !== '') {
+ // if recovery key is set we can re-encrypt the key files
+ $this->recovery->recoverUsersFiles($recoveryPassword, $userId);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Init mount points for given user
+ */
+ private function initMountPoints(IUser $user): void {
+ \OC\Files\Filesystem::initMountPoints($user);
+ }
+}
diff --git a/apps/encryption/lib/Users/Setup.php b/apps/encryption/lib/Users/Setup.php
index 30e7c5461cc..f2189d6dab2 100644
--- a/apps/encryption/lib/Users/Setup.php
+++ b/apps/encryption/lib/Users/Setup.php
@@ -11,15 +11,11 @@ use OCA\Encryption\Crypto\Crypt;
use OCA\Encryption\KeyManager;
class Setup {
- /** @var Crypt */
- private $crypt;
- /** @var KeyManager */
- private $keyManager;
- public function __construct(Crypt $crypt,
- KeyManager $keyManager) {
- $this->crypt = $crypt;
- $this->keyManager = $keyManager;
+ public function __construct(
+ private Crypt $crypt,
+ private KeyManager $keyManager,
+ ) {
}
/**
diff --git a/apps/encryption/lib/Util.php b/apps/encryption/lib/Util.php
index 6ca4d2c1e1e..61656ddcda3 100644
--- a/apps/encryption/lib/Util.php
+++ b/apps/encryption/lib/Util.php
@@ -25,11 +25,7 @@ class Util {
private IConfig $config,
private IUserManager $userManager,
) {
- $this->files = $files;
- $this->crypt = $crypt;
$this->user = $userSession->isLoggedIn() ? $userSession->getUser() : false;
- $this->config = $config;
- $this->userManager = $userManager;
}
/**
@@ -140,4 +136,5 @@ class Util {
public function getStorage($path) {
return $this->files->getMount($path)->getStorage();
}
+
}
diff --git a/apps/encryption/tests/HookManagerTest.php b/apps/encryption/tests/HookManagerTest.php
deleted file mode 100644
index f831690580f..00000000000
--- a/apps/encryption/tests/HookManagerTest.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php
-
-/**
- * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-namespace OCA\Encryption\Tests;
-
-use OCA\Encryption\HookManager;
-use OCA\Encryption\Hooks\Contracts\IHook;
-use OCP\IConfig;
-use Test\TestCase;
-
-class HookManagerTest extends TestCase {
-
- /**
- * @var HookManager
- */
- private static $instance;
-
-
- public function testRegisterHookWithArray(): void {
- self::$instance->registerHook([
- $this->getMockBuilder(IHook::class)->disableOriginalConstructor()->getMock(),
- $this->getMockBuilder(IHook::class)->disableOriginalConstructor()->getMock(),
- $this->createMock(IConfig::class)
- ]);
-
- $hookInstances = self::invokePrivate(self::$instance, 'hookInstances');
- // Make sure our type checking works
- $this->assertCount(2, $hookInstances);
- }
-
-
-
- public static function setUpBeforeClass(): void {
- parent::setUpBeforeClass();
- // have to make instance static to preserve data between tests
- self::$instance = new HookManager();
- }
-
-
- public function testRegisterHooksWithInstance(): void {
- $mock = $this->getMockBuilder(IHook::class)->disableOriginalConstructor()->getMock();
- /** @var IHook $mock */
- self::$instance->registerHook($mock);
-
- $hookInstances = self::invokePrivate(self::$instance, 'hookInstances');
- $this->assertCount(3, $hookInstances);
- }
-}
diff --git a/apps/encryption/tests/Hooks/UserHooksTest.php b/apps/encryption/tests/Hooks/UserHooksTest.php
deleted file mode 100644
index 072a20de846..00000000000
--- a/apps/encryption/tests/Hooks/UserHooksTest.php
+++ /dev/null
@@ -1,370 +0,0 @@
-<?php
-
-/**
- * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-namespace OCA\Encryption\Tests\Hooks;
-
-use OCA\Encryption\Crypto\Crypt;
-use OCA\Encryption\Hooks\UserHooks;
-use OCA\Encryption\KeyManager;
-use OCA\Encryption\Recovery;
-use OCA\Encryption\Session;
-use OCA\Encryption\Users\Setup;
-use OCA\Encryption\Util;
-use OCP\IUser;
-use OCP\IUserManager;
-use OCP\IUserSession;
-use PHPUnit\Framework\MockObject\MockObject;
-use Psr\Log\LoggerInterface;
-use Test\TestCase;
-
-/**
- * Class UserHooksTest
- *
- * @group DB
- * @package OCA\Encryption\Tests\Hooks
- */
-class UserHooksTest extends TestCase {
- /**
- * @var \PHPUnit\Framework\MockObject\MockObject
- */
- private $utilMock;
- /**
- * @var \PHPUnit\Framework\MockObject\MockObject
- */
- private $recoveryMock;
- /**
- * @var \PHPUnit\Framework\MockObject\MockObject
- */
- private $sessionMock;
- /**
- * @var \PHPUnit\Framework\MockObject\MockObject
- */
- private $keyManagerMock;
- /**
- * @var \PHPUnit\Framework\MockObject\MockObject
- */
- private $userManagerMock;
-
- /**
- * @var \PHPUnit\Framework\MockObject\MockObject
- */
- private $userSetupMock;
- /**
- * @var \PHPUnit\Framework\MockObject\MockObject
- */
- private $userSessionMock;
- /**
- * @var MockObject|IUser
- */
- private $user;
- /**
- * @var \PHPUnit\Framework\MockObject\MockObject
- */
- private $cryptMock;
- /**
- * @var \PHPUnit\Framework\MockObject\MockObject
- */
- private $loggerMock;
- /**
- * @var UserHooks
- */
- private $instance;
-
- private $params = ['uid' => 'testUser', 'password' => 'password'];
-
- public function testLogin(): void {
- $this->userSetupMock->expects($this->once())
- ->method('setupUser')
- ->willReturnOnConsecutiveCalls(true, false);
-
- $this->keyManagerMock->expects($this->once())
- ->method('init')
- ->with('testUser', 'password');
-
- $this->assertNull($this->instance->login($this->params));
- }
-
- public function testLogout(): void {
- $this->sessionMock->expects($this->once())
- ->method('clear');
- $this->instance->logout();
- $this->addToAssertionCount(1);
- }
-
- public function testPostCreateUser(): void {
- $this->userSetupMock->expects($this->once())
- ->method('setupUser');
-
- $this->instance->postCreateUser($this->params);
- $this->addToAssertionCount(1);
- }
-
- public function testPostDeleteUser(): void {
- $this->keyManagerMock->expects($this->once())
- ->method('deletePublicKey')
- ->with('testUser');
-
- $this->instance->postDeleteUser($this->params);
- $this->addToAssertionCount(1);
- }
-
- public function testPrePasswordReset(): void {
- $params = ['uid' => 'user1'];
- $expected = ['user1' => true];
- $this->instance->prePasswordReset($params);
- $passwordResetUsers = $this->invokePrivate($this->instance, 'passwordResetUsers');
-
- $this->assertSame($expected, $passwordResetUsers);
- }
-
- public function testPostPasswordReset(): void {
- $params = ['uid' => 'user1', 'password' => 'password'];
- $this->invokePrivate($this->instance, 'passwordResetUsers', [['user1' => true]]);
- $this->keyManagerMock->expects($this->once())->method('backupUserKeys')
- ->with('passwordReset', 'user1');
- $this->keyManagerMock->expects($this->once())->method('deleteUserKeys')
- ->with('user1');
- $this->userSetupMock->expects($this->once())->method('setupUser')
- ->with('user1', 'password');
-
- $this->instance->postPasswordReset($params);
- $passwordResetUsers = $this->invokePrivate($this->instance, 'passwordResetUsers');
- $this->assertEmpty($passwordResetUsers);
- }
-
- /**
- * @dataProvider dataTestPreSetPassphrase
- */
- public function testPreSetPassphrase($canChange): void {
- /** @var UserHooks | \PHPUnit\Framework\MockObject\MockObject $instance */
- $instance = $this->getMockBuilder(UserHooks::class)
- ->setConstructorArgs(
- [
- $this->keyManagerMock,
- $this->userManagerMock,
- $this->loggerMock,
- $this->userSetupMock,
- $this->userSessionMock,
- $this->utilMock,
- $this->sessionMock,
- $this->cryptMock,
- $this->recoveryMock
- ]
- )
- ->setMethods(['setPassphrase'])
- ->getMock();
-
- $userMock = $this->createMock(IUser::class);
-
- $this->userManagerMock->expects($this->once())
- ->method('get')
- ->with($this->params['uid'])
- ->willReturn($userMock);
- $userMock->expects($this->once())
- ->method('canChangePassword')
- ->willReturn($canChange);
-
- if ($canChange) {
- // in this case the password will be changed in the post hook
- $instance->expects($this->never())->method('setPassphrase');
- } else {
- // if user can't change the password we update the encryption
- // key password already in the pre hook
- $instance->expects($this->once())
- ->method('setPassphrase')
- ->with($this->params);
- }
-
- $instance->preSetPassphrase($this->params);
- }
-
- public function dataTestPreSetPassphrase() {
- return [
- [true],
- [false]
- ];
- }
-
- public function XtestSetPassphrase() {
- $this->sessionMock->expects($this->once())
- ->method('getPrivateKey')
- ->willReturn(true);
-
- $this->cryptMock->expects($this->exactly(4))
- ->method('encryptPrivateKey')
- ->willReturn(true);
-
- $this->cryptMock->expects($this->any())
- ->method('generateHeader')
- ->willReturn(Crypt::HEADER_START . ':Cipher:test:' . Crypt::HEADER_END);
-
- $this->keyManagerMock->expects($this->exactly(4))
- ->method('setPrivateKey')
- ->willReturnCallback(function ($user, $key): void {
- $header = substr($key, 0, strlen(Crypt::HEADER_START));
- $this->assertSame(
- Crypt::HEADER_START,
- $header, 'every encrypted file should start with a header');
- });
-
- $this->assertNull($this->instance->setPassphrase($this->params));
- $this->params['recoveryPassword'] = 'password';
-
- $this->recoveryMock->expects($this->exactly(3))
- ->method('isRecoveryEnabledForUser')
- ->with('testUser1')
- ->willReturnOnConsecutiveCalls(true, false);
-
-
- $this->instance = $this->getMockBuilder(UserHooks::class)
- ->setConstructorArgs(
- [
- $this->keyManagerMock,
- $this->userManagerMock,
- $this->loggerMock,
- $this->userSetupMock,
- $this->userSessionMock,
- $this->utilMock,
- $this->sessionMock,
- $this->cryptMock,
- $this->recoveryMock
- ]
- )->setMethods(['initMountPoints'])->getMock();
-
- $this->instance->expects($this->exactly(3))->method('initMountPoints');
-
- $this->params['uid'] = 'testUser1';
-
- // Test first if statement
- $this->assertNull($this->instance->setPassphrase($this->params));
-
- // Test Second if conditional
- $this->keyManagerMock->expects($this->exactly(2))
- ->method('userHasKeys')
- ->with('testUser1')
- ->willReturn(true);
-
- $this->assertNull($this->instance->setPassphrase($this->params));
-
- // Test third and final if condition
- $this->utilMock->expects($this->once())
- ->method('userHasFiles')
- ->with('testUser1')
- ->willReturn(false);
-
- $this->cryptMock->expects($this->once())
- ->method('createKeyPair');
-
- $this->keyManagerMock->expects($this->once())
- ->method('setPrivateKey');
-
- $this->recoveryMock->expects($this->once())
- ->method('recoverUsersFiles')
- ->with('password', 'testUser1');
-
- $this->assertNull($this->instance->setPassphrase($this->params));
- }
-
- public function testSetPassphraseResetUserMode(): void {
- $params = ['uid' => 'user1', 'password' => 'password'];
- $this->invokePrivate($this->instance, 'passwordResetUsers', [[$params['uid'] => true]]);
- $this->sessionMock->expects($this->never())->method('getPrivateKey');
- $this->keyManagerMock->expects($this->never())->method('setPrivateKey');
- $this->assertTrue($this->instance->setPassphrase($params));
- $this->invokePrivate($this->instance, 'passwordResetUsers', [[]]);
- }
-
- public function XtestSetPasswordNoUser() {
- $userSessionMock = $this->getMockBuilder(IUserSession::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $userSessionMock->expects($this->any())->method('getUser')->willReturn(null);
-
- $this->recoveryMock->expects($this->once())
- ->method('isRecoveryEnabledForUser')
- ->with('testUser')
- ->willReturn(false);
-
- $userHooks = $this->getMockBuilder(UserHooks::class)
- ->setConstructorArgs(
- [
- $this->keyManagerMock,
- $this->userManagerMock,
- $this->loggerMock,
- $this->userSetupMock,
- $userSessionMock,
- $this->utilMock,
- $this->sessionMock,
- $this->cryptMock,
- $this->recoveryMock
- ]
- )->setMethods(['initMountPoints'])->getMock();
-
- /** @var UserHooks $userHooks */
- $this->assertNull($userHooks->setPassphrase($this->params));
- }
-
- protected function setUp(): void {
- parent::setUp();
- $this->loggerMock = $this->createMock(LoggerInterface::class);
- $this->keyManagerMock = $this->getMockBuilder(KeyManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->userManagerMock = $this->getMockBuilder(IUserManager::class)
- ->disableOriginalConstructor()
- ->getMock();
- $this->userSetupMock = $this->getMockBuilder(Setup::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->user = $this->createMock(IUser::class);
- $this->user->expects($this->any())
- ->method('getUID')
- ->willReturn('testUser');
-
- $this->userSessionMock = $this->createMock(IUserSession::class);
- $this->userSessionMock->expects($this->any())
- ->method('getUser')
- ->willReturn($this->user);
-
- $utilMock = $this->getMockBuilder(Util::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $sessionMock = $this->getMockBuilder(Session::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->cryptMock = $this->getMockBuilder(Crypt::class)
- ->disableOriginalConstructor()
- ->getMock();
- $recoveryMock = $this->getMockBuilder(Recovery::class)
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->sessionMock = $sessionMock;
- $this->recoveryMock = $recoveryMock;
- $this->utilMock = $utilMock;
- $this->utilMock->expects($this->any())->method('isMasterKeyEnabled')->willReturn(false);
-
- $this->instance = $this->getMockBuilder(UserHooks::class)
- ->setConstructorArgs(
- [
- $this->keyManagerMock,
- $this->userManagerMock,
- $this->loggerMock,
- $this->userSetupMock,
- $this->userSessionMock,
- $this->utilMock,
- $this->sessionMock,
- $this->cryptMock,
- $this->recoveryMock
- ]
- )->setMethods(['setupFS'])->getMock();
- }
-}
diff --git a/apps/encryption/tests/Listeners/UserEventsListenersTest.php b/apps/encryption/tests/Listeners/UserEventsListenersTest.php
new file mode 100644
index 00000000000..cb31523f105
--- /dev/null
+++ b/apps/encryption/tests/Listeners/UserEventsListenersTest.php
@@ -0,0 +1,258 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Encryption\Tests\Listeners;
+
+use OC\Core\Events\BeforePasswordResetEvent;
+use OC\Core\Events\PasswordResetEvent;
+use OC\Files\SetupManager;
+use OCA\Encryption\KeyManager;
+use OCA\Encryption\Listeners\UserEventsListener;
+use OCA\Encryption\Services\PassphraseService;
+use OCA\Encryption\Session;
+use OCA\Encryption\Users\Setup;
+use OCA\Encryption\Util;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\User\Events\BeforePasswordUpdatedEvent;
+use OCP\User\Events\PasswordUpdatedEvent;
+use OCP\User\Events\UserCreatedEvent;
+use OCP\User\Events\UserDeletedEvent;
+use OCP\User\Events\UserLoggedInEvent;
+use OCP\User\Events\UserLoggedOutEvent;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class UserEventsListenersTest extends TestCase {
+
+ protected Util&MockObject $util;
+ protected Setup&MockObject $userSetup;
+ protected Session&MockObject $session;
+ protected KeyManager&MockObject $keyManager;
+ protected IUserManager&MockObject $userManager;
+ protected IUserSession&MockObject $userSession;
+ protected SetupManager&MockObject $setupManager;
+ protected PassphraseService&MockObject $passphraseService;
+
+ protected UserEventsListener $instance;
+
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->util = $this->createMock(Util::class);
+ $this->userSetup = $this->createMock(Setup::class);
+ $this->session = $this->createMock(Session::class);
+ $this->keyManager = $this->createMock(KeyManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->setupManager = $this->createMock(SetupManager::class);
+ $this->passphraseService = $this->createMock(PassphraseService::class);
+
+ $this->instance = new UserEventsListener(
+ $this->util,
+ $this->userSetup,
+ $this->session,
+ $this->keyManager,
+ $this->userManager,
+ $this->userSession,
+ $this->setupManager,
+ $this->passphraseService,
+ );
+ }
+
+ public function testLogin(): void {
+ $this->userSetup->expects(self::once())
+ ->method('setupUser')
+ ->willReturn(true);
+
+ $this->keyManager->expects(self::once())
+ ->method('init')
+ ->with('testUser', 'password');
+
+ $this->util->method('isMasterKeyEnabled')->willReturn(false);
+
+ $user = $this->createMock(IUser::class);
+ $user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('testUser');
+ $event = $this->createMock(UserLoggedInEvent::class);
+ $event->expects(self::atLeastOnce())
+ ->method('getUser')
+ ->willReturn($user);
+ $event->expects(self::atLeastOnce())
+ ->method('getPassword')
+ ->willReturn('password');
+
+ $this->instance->handle($event);
+ }
+
+ public function testLoginMasterKey(): void {
+ $this->util->method('isMasterKeyEnabled')->willReturn(true);
+
+ $this->userSetup->expects(self::never())
+ ->method('setupUser');
+
+ $this->keyManager->expects(self::once())
+ ->method('init')
+ ->with('testUser', 'password');
+
+ $user = $this->createMock(IUser::class);
+ $user->expects(self::any())
+ ->method('getUID')
+ ->willReturn('testUser');
+
+ $event = $this->createMock(UserLoggedInEvent::class);
+ $event->expects(self::atLeastOnce())
+ ->method('getUser')
+ ->willReturn($user);
+ $event->expects(self::atLeastOnce())
+ ->method('getPassword')
+ ->willReturn('password');
+
+ $this->instance->handle($event);
+ }
+
+ public function testLogout(): void {
+ $this->session->expects(self::once())
+ ->method('clear');
+
+ $event = $this->createMock(UserLoggedOutEvent::class);
+ $this->instance->handle($event);
+ }
+
+ public function testUserCreated(): void {
+ $this->userSetup->expects(self::once())
+ ->method('setupUser')
+ ->with('testUser', 'password');
+
+ $event = $this->createMock(UserCreatedEvent::class);
+ $event->expects(self::atLeastOnce())
+ ->method('getUid')
+ ->willReturn('testUser');
+ $event->expects(self::atLeastOnce())
+ ->method('getPassword')
+ ->willReturn('password');
+
+ $this->instance->handle($event);
+ }
+
+ public function testUserDeleted(): void {
+ $this->keyManager->expects(self::once())
+ ->method('deletePublicKey')
+ ->with('testUser');
+
+ $event = $this->createMock(UserDeletedEvent::class);
+ $event->expects(self::atLeastOnce())
+ ->method('getUid')
+ ->willReturn('testUser');
+ $this->instance->handle($event);
+ }
+
+ public function testBeforePasswordUpdated(): void {
+ $this->passphraseService->expects(self::never())
+ ->method('setPassphraseForUser');
+
+ $user = $this->createMock(IUser::class);
+ $user->expects(self::atLeastOnce())
+ ->method('canChangePassword')
+ ->willReturn(true);
+
+ $event = $this->createMock(BeforePasswordUpdatedEvent::class);
+ $event->expects(self::atLeastOnce())
+ ->method('getUser')
+ ->willReturn($user);
+ $event->expects(self::atLeastOnce())
+ ->method('getPassword')
+ ->willReturn('password');
+ $this->instance->handle($event);
+ }
+
+ public function testBeforePasswordUpdated_CannotChangePassword(): void {
+ $this->passphraseService->expects(self::once())
+ ->method('setPassphraseForUser')
+ ->with('testUser', 'password');
+
+ $user = $this->createMock(IUser::class);
+ $user->expects(self::atLeastOnce())
+ ->method('getUID')
+ ->willReturn('testUser');
+ $user->expects(self::atLeastOnce())
+ ->method('canChangePassword')
+ ->willReturn(false);
+
+ $event = $this->createMock(BeforePasswordUpdatedEvent::class);
+ $event->expects(self::atLeastOnce())
+ ->method('getUser')
+ ->willReturn($user);
+ $event->expects(self::atLeastOnce())
+ ->method('getPassword')
+ ->willReturn('password');
+ $this->instance->handle($event);
+ }
+
+ public function testPasswordUpdated(): void {
+ $this->passphraseService->expects(self::once())
+ ->method('setPassphraseForUser')
+ ->with('testUser', 'password');
+
+ $event = $this->createMock(PasswordUpdatedEvent::class);
+ $event->expects(self::atLeastOnce())
+ ->method('getUid')
+ ->willReturn('testUser');
+ $event->expects(self::atLeastOnce())
+ ->method('getPassword')
+ ->willReturn('password');
+
+ $this->instance->handle($event);
+ }
+
+ public function testBeforePasswordReset(): void {
+ $this->passphraseService->expects(self::once())
+ ->method('setProcessingReset')
+ ->with('testUser');
+
+ $event = $this->createMock(BeforePasswordResetEvent::class);
+ $event->expects(self::atLeastOnce())
+ ->method('getUid')
+ ->willReturn('testUser');
+ $this->instance->handle($event);
+ }
+
+ public function testPasswordReset(): void {
+ // backup required
+ $this->keyManager->expects(self::once())
+ ->method('backupUserKeys')
+ ->with('passwordReset', 'testUser');
+ // delete old keys
+ $this->keyManager->expects(self::once())
+ ->method('deleteUserKeys')
+ ->with('testUser');
+ // create new keys
+ $this->userSetup->expects(self::once())
+ ->method('setupUser')
+ ->with('testUser', 'password');
+ // reset ends
+ $this->passphraseService->expects(self::once())
+ ->method('setProcessingReset')
+ ->with('testUser', false);
+
+ $event = $this->createMock(PasswordResetEvent::class);
+ $event->expects(self::atLeastOnce())
+ ->method('getUid')
+ ->willReturn('testUser');
+ $event->expects(self::atLeastOnce())
+ ->method('getPassword')
+ ->willReturn('password');
+ $this->instance->handle($event);
+ }
+
+}
diff --git a/apps/encryption/tests/PassphraseServiceTest.php b/apps/encryption/tests/PassphraseServiceTest.php
new file mode 100644
index 00000000000..c2dc9d8173c
--- /dev/null
+++ b/apps/encryption/tests/PassphraseServiceTest.php
@@ -0,0 +1,196 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Encryption\Tests;
+
+use OCA\Encryption\Crypto\Crypt;
+use OCA\Encryption\KeyManager;
+use OCA\Encryption\Recovery;
+use OCA\Encryption\Services\PassphraseService;
+use OCA\Encryption\Session;
+use OCA\Encryption\Util;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+/**
+ * @group DB
+ */
+class PassphraseServiceTest extends TestCase {
+
+ protected Util&MockObject $util;
+ protected Crypt&MockObject $crypt;
+ protected Session&MockObject $session;
+ protected Recovery&MockObject $recovery;
+ protected KeyManager&MockObject $keyManager;
+ protected IUserManager&MockObject $userManager;
+ protected IUserSession&MockObject $userSession;
+
+ protected PassphraseService $instance;
+
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->util = $this->createMock(Util::class);
+ $this->crypt = $this->createMock(Crypt::class);
+ $this->session = $this->createMock(Session::class);
+ $this->recovery = $this->createMock(Recovery::class);
+ $this->keyManager = $this->createMock(KeyManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+
+ $this->instance = new PassphraseService(
+ $this->util,
+ $this->crypt,
+ $this->session,
+ $this->recovery,
+ $this->keyManager,
+ $this->createMock(LoggerInterface::class),
+ $this->userManager,
+ $this->userSession,
+ );
+ }
+
+ public function testSetProcessingReset(): void {
+ $this->instance->setProcessingReset('userId');
+ $this->assertEquals(['userId' => true], $this->invokePrivate($this->instance, 'passwordResetUsers'));
+ }
+
+ public function testUnsetProcessingReset(): void {
+ $this->instance->setProcessingReset('userId');
+ $this->assertEquals(['userId' => true], $this->invokePrivate($this->instance, 'passwordResetUsers'));
+ $this->instance->setProcessingReset('userId', false);
+ $this->assertEquals([], $this->invokePrivate($this->instance, 'passwordResetUsers'));
+ }
+
+ /**
+ * Check that the passphrase setting skips if a reset is processed
+ */
+ public function testSetPassphraseResetUserMode(): void {
+ $this->session->expects(self::never())
+ ->method('getPrivateKey');
+ $this->keyManager->expects(self::never())
+ ->method('setPrivateKey');
+
+ $this->instance->setProcessingReset('userId');
+ $this->assertTrue($this->instance->setPassphraseForUser('userId', 'password'));
+ }
+
+ public function testSetPassphrase_currentUser() {
+ $instance = $this->getMockBuilder(PassphraseService::class)
+ ->onlyMethods(['initMountPoints'])
+ ->setConstructorArgs([
+ $this->util,
+ $this->crypt,
+ $this->session,
+ $this->recovery,
+ $this->keyManager,
+ $this->createMock(LoggerInterface::class),
+ $this->userManager,
+ $this->userSession,
+ ])
+ ->getMock();
+
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('testUser');
+ $this->userSession->expects(self::atLeastOnce())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->userManager->expects(self::atLeastOnce())
+ ->method('get')
+ ->with('testUser')
+ ->willReturn($user);
+ $this->session->expects(self::any())
+ ->method('getPrivateKey')
+ ->willReturn('private-key');
+ $this->crypt->expects(self::any())
+ ->method('encryptPrivateKey')
+ ->with('private-key')
+ ->willReturn('encrypted-key');
+ $this->crypt->expects(self::any())
+ ->method('generateHeader')
+ ->willReturn('crypt-header: ');
+
+ $this->keyManager->expects(self::atLeastOnce())
+ ->method('setPrivateKey')
+ ->with('testUser', 'crypt-header: encrypted-key');
+
+ $this->assertTrue($instance->setPassphraseForUser('testUser', 'password'));
+ }
+
+ public function testSetPassphrase_currentUserFails() {
+ $instance = $this->getMockBuilder(PassphraseService::class)
+ ->onlyMethods(['initMountPoints'])
+ ->setConstructorArgs([
+ $this->util,
+ $this->crypt,
+ $this->session,
+ $this->recovery,
+ $this->keyManager,
+ $this->createMock(LoggerInterface::class),
+ $this->userManager,
+ $this->userSession,
+ ])
+ ->getMock();
+
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('testUser');
+ $this->userManager->expects(self::atLeastOnce())
+ ->method('get')
+ ->with('testUser')
+ ->willReturn($user);
+ $this->userSession->expects(self::atLeastOnce())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->session->expects(self::any())
+ ->method('getPrivateKey')
+ ->willReturn('private-key');
+ $this->crypt->expects(self::any())
+ ->method('encryptPrivateKey')
+ ->with('private-key')
+ ->willReturn(false);
+
+ $this->keyManager->expects(self::never())
+ ->method('setPrivateKey');
+
+ $this->assertFalse($instance->setPassphraseForUser('testUser', 'password'));
+ }
+
+ public function testSetPassphrase_currentUserNotExists() {
+ $instance = $this->getMockBuilder(PassphraseService::class)
+ ->onlyMethods(['initMountPoints'])
+ ->setConstructorArgs([
+ $this->util,
+ $this->crypt,
+ $this->session,
+ $this->recovery,
+ $this->keyManager,
+ $this->createMock(LoggerInterface::class),
+ $this->userManager,
+ $this->userSession,
+ ])
+ ->getMock();
+
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('testUser');
+ $this->userManager->expects(self::atLeastOnce())
+ ->method('get')
+ ->with('testUser')
+ ->willReturn(null);
+ $this->userSession->expects(self::never())
+ ->method('getUser');
+ $this->keyManager->expects(self::never())
+ ->method('setPrivateKey');
+
+ $this->assertFalse($instance->setPassphraseForUser('testUser', 'password'));
+ }
+
+}
diff --git a/lib/private/Log/ExceptionSerializer.php b/lib/private/Log/ExceptionSerializer.php
index 25e52e66f39..904107f6998 100644
--- a/lib/private/Log/ExceptionSerializer.php
+++ b/lib/private/Log/ExceptionSerializer.php
@@ -14,8 +14,9 @@ use OCA\Encryption\Controller\RecoveryController;
use OCA\Encryption\Controller\SettingsController;
use OCA\Encryption\Crypto\Crypt;
use OCA\Encryption\Crypto\Encryption;
-use OCA\Encryption\Hooks\UserHooks;
use OCA\Encryption\KeyManager;
+use OCA\Encryption\Listeners\UserEventsListener;
+use OCA\Encryption\Services\PassphraseService;
use OCA\Encryption\Session;
use OCP\HintException;
@@ -169,14 +170,16 @@ class ExceptionSerializer {
\OCA\Encryption\Users\Setup::class => [
'setupUser',
],
- UserHooks::class => [
- 'login',
- 'postCreateUser',
- 'postDeleteUser',
- 'prePasswordReset',
- 'postPasswordReset',
- 'preSetPassphrase',
- 'setPassphrase',
+ UserEventsListener::class => [
+ 'handle',
+ 'onUserCreated',
+ 'onUserLogin',
+ 'onBeforePasswordUpdated',
+ 'onPasswordUpdated',
+ 'onPasswordReset',
+ ],
+ PassphraseService::class => [
+ 'setPassphraseForUser',
],
];