Browse Source
Merge pull request #48332 from nextcloud/chore/migrate-encryption-away-from-hooks
Merge pull request #48332 from nextcloud/chore/migrate-encryption-away-from-hooks
refactor(encryption): Migrate from hooks to eventspull/47959/merge
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 837 additions and 817 deletions
-
5apps/encryption/composer/composer/autoload_classmap.php
-
5apps/encryption/composer/composer/autoload_static.php
-
72apps/encryption/lib/AppInfo/Application.php
-
2apps/encryption/lib/Crypto/Crypt.php
-
43apps/encryption/lib/HookManager.php
-
17apps/encryption/lib/Hooks/Contracts/IHook.php
-
266apps/encryption/lib/Hooks/UserHooks.php
-
8apps/encryption/lib/KeyManager.php
-
143apps/encryption/lib/Listeners/UserEventsListener.php
-
142apps/encryption/lib/Services/PassphraseService.php
-
12apps/encryption/lib/Users/Setup.php
-
5apps/encryption/lib/Util.php
-
52apps/encryption/tests/HookManagerTest.php
-
370apps/encryption/tests/Hooks/UserHooksTest.php
-
258apps/encryption/tests/Listeners/UserEventsListenersTest.php
-
196apps/encryption/tests/PassphraseServiceTest.php
-
7core/Events/BeforePasswordResetEvent.php
-
7core/Events/PasswordResetEvent.php
-
21lib/private/Log/ExceptionSerializer.php
-
2lib/public/AppFramework/Bootstrap/IRegistrationContext.php
-
7lib/public/User/Events/PasswordUpdatedEvent.php
-
7lib/public/User/Events/UserDeletedEvent.php
-
7lib/public/User/Events/UserLoggedInEvent.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(); |
|||
} |
|||
} |
|||
} |
@ -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(); |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
|
|||
} |
@ -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')); |
|||
} |
|||
|
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue