Browse Source

Merge pull request #48332 from nextcloud/chore/migrate-encryption-away-from-hooks

refactor(encryption): Migrate from hooks to events
pull/47959/merge
Ferdinand Thiessen 1 year ago
committed by GitHub
parent
commit
5b007b7b32
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      apps/encryption/composer/composer/autoload_classmap.php
  2. 5
      apps/encryption/composer/composer/autoload_static.php
  3. 72
      apps/encryption/lib/AppInfo/Application.php
  4. 2
      apps/encryption/lib/Crypto/Crypt.php
  5. 43
      apps/encryption/lib/HookManager.php
  6. 17
      apps/encryption/lib/Hooks/Contracts/IHook.php
  7. 266
      apps/encryption/lib/Hooks/UserHooks.php
  8. 8
      apps/encryption/lib/KeyManager.php
  9. 143
      apps/encryption/lib/Listeners/UserEventsListener.php
  10. 142
      apps/encryption/lib/Services/PassphraseService.php
  11. 12
      apps/encryption/lib/Users/Setup.php
  12. 5
      apps/encryption/lib/Util.php
  13. 52
      apps/encryption/tests/HookManagerTest.php
  14. 370
      apps/encryption/tests/Hooks/UserHooksTest.php
  15. 258
      apps/encryption/tests/Listeners/UserEventsListenersTest.php
  16. 196
      apps/encryption/tests/PassphraseServiceTest.php
  17. 7
      core/Events/BeforePasswordResetEvent.php
  18. 7
      core/Events/PasswordResetEvent.php
  19. 21
      lib/private/Log/ExceptionSerializer.php
  20. 2
      lib/public/AppFramework/Bootstrap/IRegistrationContext.php
  21. 7
      lib/public/User/Events/PasswordUpdatedEvent.php
  22. 7
      lib/public/User/Events/UserDeletedEvent.php
  23. 7
      lib/public/User/Events/UserLoggedInEvent.php

5
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',

5
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',

72
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),
);
});
}

2
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();

43
apps/encryption/lib/HookManager.php

@ -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();
}
}
}

17
apps/encryption/lib/Hooks/Contracts/IHook.php

@ -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();
}

266
apps/encryption/lib/Hooks/UserHooks.php

@ -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);
}
}

8
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);

143
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);
}
}

142
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);
}
}

12
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,
) {
}
/**

5
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();
}
}

52
apps/encryption/tests/HookManagerTest.php

@ -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);
}
}

370
apps/encryption/tests/Hooks/UserHooksTest.php

@ -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();
}
}

258
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);
}
}

196
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'));
}
}

7
core/Events/BeforePasswordResetEvent.php

@ -34,6 +34,13 @@ class BeforePasswordResetEvent extends Event {
return $this->user;
}
/**
* @since 31.0.0
*/
public function getUid(): string {
return $this->user->getUID();
}
/**
* @since 25.0.0
*/

7
core/Events/PasswordResetEvent.php

@ -34,6 +34,13 @@ class PasswordResetEvent extends Event {
return $this->user;
}
/**
* @since 31.0.0
*/
public function getUid(): string {
return $this->user->getUID();
}
/**
* @since 25.0.0
*/

21
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',
],
];

2
lib/public/AppFramework/Bootstrap/IRegistrationContext.php

@ -69,7 +69,7 @@ interface IRegistrationContext {
* @param string $name
* @param callable $factory
* @psalm-param callable(\Psr\Container\ContainerInterface): mixed $factory
* @param bool $shared
* @param bool $shared If set to true the factory result will be cached otherwise every query will call the factory again
*
* @return void
* @see IContainer::registerService()

7
lib/public/User/Events/PasswordUpdatedEvent.php

@ -49,6 +49,13 @@ class PasswordUpdatedEvent extends Event {
return $this->user;
}
/**
* @since 31.0.0
*/
public function getUid(): string {
return $this->user->getUID();
}
/**
* @return string
* @since 18.0.0

7
lib/public/User/Events/UserDeletedEvent.php

@ -34,4 +34,11 @@ class UserDeletedEvent extends Event {
public function getUser(): IUser {
return $this->user;
}
/**
* @since 31.0.0
*/
public function getUid(): string {
return $this->user->getUID();
}
}

7
lib/public/User/Events/UserLoggedInEvent.php

@ -45,6 +45,13 @@ class UserLoggedInEvent extends Event {
return $this->user;
}
/**
* @since 31.0.0
*/
public function getUid(): string {
return $this->user->getUID();
}
/**
* @since 21.0.0
*/

Loading…
Cancel
Save