create new encryption keys on password reset and backup the old onetags/v12.0.0beta1
use OCA\Encryption\Recovery; | use OCA\Encryption\Recovery; | ||||
class UserHooks implements IHook { | class UserHooks implements IHook { | ||||
/** | |||||
* list of user for which we perform a password reset | |||||
* @var array | |||||
*/ | |||||
protected static $passwordResetUsers = []; | |||||
/** | /** | ||||
* @var KeyManager | * @var KeyManager | ||||
*/ | */ | ||||
$this, | $this, | ||||
'preSetPassphrase'); | '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', | OCUtil::connectHook('OC_User', | ||||
'post_createUser', | 'post_createUser', | ||||
$this, | $this, | ||||
} | } | ||||
} | } | ||||
public function prePasswordReset($params) { | |||||
if (App::isEnabled('encryption')) { | |||||
$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 ownCloud, than update the key password in advance. | * If the password can't be changed within ownCloud, than update the key password in advance. | ||||
* | * | ||||
* @return boolean|null | * @return boolean|null | ||||
*/ | */ | ||||
public function preSetPassphrase($params) { | public function preSetPassphrase($params) { | ||||
if (App::isEnabled('encryption')) { | |||||
$user = $this->userManager->get($params['uid']); | |||||
$user = $this->userManager->get($params['uid']); | |||||
if ($user && !$user->canChangePassword()) { | |||||
$this->setPassphrase($params); | |||||
} | |||||
if ($user && !$user->canChangePassword()) { | |||||
$this->setPassphrase($params); | |||||
} | } | ||||
} | } | ||||
*/ | */ | ||||
public function setPassphrase($params) { | 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 | // Get existing decrypted private key | ||||
$privateKey = $this->session->getPrivateKey(); | $privateKey = $this->session->getPrivateKey(); | ||||
$user = $this->user->getUser(); | $user = $this->user->getUser(); | ||||
Filesystem::initMountPoints($user); | Filesystem::initMountPoints($user); | ||||
} | } | ||||
/** | |||||
* after password reset we create a new key pair for the user | |||||
* | |||||
* @param array $params | |||||
*/ | |||||
public function postPasswordReset($params) { | |||||
$password = $params['password']; | |||||
$this->keyManager->deleteUserKeys($params['uid']); | |||||
$this->userSetup->setupUser($params['uid'], $password); | |||||
} | |||||
/** | /** | ||||
* setup file system for user | * setup file system for user | ||||
* | * |
/** | /** | ||||
* @param string $purpose | * @param string $purpose | ||||
* @param bool $timestamp | |||||
* @param bool $includeUserKeys | |||||
* @param string $uid | |||||
*/ | */ | ||||
public function backupAllKeys($purpose, $timestamp = true, $includeUserKeys = true) { | |||||
// $backupDir = $this->keyStorage->; | |||||
public function backupUserKeys($purpose, $uid) { | |||||
$this->keyStorage->backupUserKeys(Encryption::ID, $purpose, $uid); | |||||
} | } | ||||
/** | /** | ||||
* @param string $uid | * @param string $uid | ||||
*/ | */ | ||||
public function deleteUserKeys($uid) { | public function deleteUserKeys($uid) { | ||||
$this->backupAllKeys('password_reset'); | |||||
$this->deletePublicKey($uid); | $this->deletePublicKey($uid); | ||||
$this->deletePrivateKey($uid); | $this->deletePrivateKey($uid); | ||||
} | } |
$this->assertTrue(true); | $this->assertTrue(true); | ||||
} | } | ||||
public function testPrePasswordReset() { | |||||
$params = ['uid' => 'user1']; | |||||
$expected = ['user1' => true]; | |||||
$this->instance->prePasswordReset($params); | |||||
$passwordResetUsers = $this->invokePrivate($this->instance, 'passwordResetUsers'); | |||||
$this->assertSame($expected, $passwordResetUsers); | |||||
} | |||||
public function testPostPasswordReset() { | |||||
$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 | * @dataProvider dataTestPreSetPassphrase | ||||
*/ | */ | ||||
$this->assertNull($this->instance->setPassphrase($this->params)); | $this->assertNull($this->instance->setPassphrase($this->params)); | ||||
} | } | ||||
public function testSetPassphraseResetUserMode() { | |||||
$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 testSetPasswordNoUser() { | public function testSetPasswordNoUser() { | ||||
$this->sessionMock->expects($this->once()) | $this->sessionMock->expects($this->once()) | ||||
->method('getPrivateKey') | ->method('getPrivateKey') | ||||
$this->assertNull($userHooks->setPassphrase($this->params)); | $this->assertNull($userHooks->setPassphrase($this->params)); | ||||
} | } | ||||
public function testPostPasswordReset() { | |||||
$this->keyManagerMock->expects($this->once()) | |||||
->method('deleteUserKeys') | |||||
->with('testUser'); | |||||
$this->userSetupMock->expects($this->once()) | |||||
->method('setupUser') | |||||
->with('testUser', 'password'); | |||||
$this->instance->postPasswordReset($this->params); | |||||
$this->assertTrue(true); | |||||
} | |||||
protected function setUp() { | protected function setUp() { | ||||
parent::setUp(); | parent::setUp(); | ||||
$this->loggerMock = $this->createMock(ILogger::class); | $this->loggerMock = $this->createMock(ILogger::class); |
$this->instance->setVersion('/admin/files/myfile.txt', 5, $view); | $this->instance->setVersion('/admin/files/myfile.txt', 5, $view); | ||||
} | } | ||||
public function testBackupUserKeys() { | |||||
$this->keyStorageMock->expects($this->once())->method('backupUserKeys') | |||||
->with('OC_DEFAULT_MODULE', 'test', 'user1'); | |||||
$this->instance->backupUserKeys('test', 'user1'); | |||||
} | |||||
} | } |
$this->checkPasswordResetToken($token, $userId); | $this->checkPasswordResetToken($token, $userId); | ||||
$user = $this->userManager->get($userId); | $user = $this->userManager->get($userId); | ||||
\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'pre_passwordReset', array('uid' => $userId, 'password' => $password)); | |||||
if (!$user->setPassword($password)) { | if (!$user->setPassword($password)) { | ||||
throw new \Exception(); | throw new \Exception(); | ||||
} | } | ||||
$this->config->deleteUserValue($userId, 'core', 'lostpassword'); | $this->config->deleteUserValue($userId, 'core', 'lostpassword'); | ||||
@\OC_User::unsetMagicInCookie(); | @\OC_User::unsetMagicInCookie(); | ||||
} catch (PrivateKeyMissingException $e) { | |||||
// in this case it is OK if we couldn't reset the users private key | |||||
// They chose explicitely to continue at the password reset dialog | |||||
// (see $proceed flag) | |||||
return $this->success(); | |||||
} catch (\Exception $e){ | } catch (\Exception $e){ | ||||
return $this->error($e->getMessage()); | return $this->error($e->getMessage()); | ||||
} | } |
sendSuccessMsg : t('core', 'The link to reset your password has been sent to your email. If you do not receive it within a reasonable amount of time, check your spam/junk folders.<br>If it is not there ask your local administrator.'), | sendSuccessMsg : t('core', 'The link to reset your password has been sent to your email. If you do not receive it within a reasonable amount of time, check your spam/junk folders.<br>If it is not there ask your local administrator.'), | ||||
encryptedMsg : t('core', "Your files are encrypted. If you haven't enabled the recovery key, there will be no way to get your data back after your password is reset.<br />If you are not sure what to do, please contact your administrator before you continue. <br />Do you really want to continue?") | |||||
encryptedMsg : t('core', "Your files are encrypted. There will be no way to get your data back after your password is reset.<br />If you are not sure what to do, please contact your administrator before you continue. <br />Do you really want to continue?") | |||||
+ ('<br /><input type="checkbox" id="encrypted-continue" value="Yes" />') | + ('<br /><input type="checkbox" id="encrypted-continue" value="Yes" />') | ||||
+ '<label for="encrypted-continue">' | + '<label for="encrypted-continue">' | ||||
+ t('core', 'I know what I\'m doing') | + t('core', 'I know what I\'m doing') |
/** @var string */ | /** @var string */ | ||||
private $encryption_base_dir; | private $encryption_base_dir; | ||||
/** @var string */ | |||||
private $backup_base_dir; | |||||
/** @var array */ | /** @var array */ | ||||
private $keyCache = []; | private $keyCache = []; | ||||
$this->encryption_base_dir = '/files_encryption'; | $this->encryption_base_dir = '/files_encryption'; | ||||
$this->keys_base_dir = $this->encryption_base_dir .'/keys'; | $this->keys_base_dir = $this->encryption_base_dir .'/keys'; | ||||
$this->backup_base_dir = $this->encryption_base_dir .'/backup'; | |||||
$this->root_dir = $this->util->getKeyStorageRoot(); | $this->root_dir = $this->util->getKeyStorageRoot(); | ||||
} | } | ||||
return false; | return false; | ||||
} | } | ||||
/** | |||||
* backup keys of a given encryption module | |||||
* | |||||
* @param string $encryptionModuleId | |||||
* @param string $purpose | |||||
* @param string $uid | |||||
* @return bool | |||||
* @since 12.0.0 | |||||
*/ | |||||
public function backupUserKeys($encryptionModuleId, $purpose, $uid) { | |||||
$source = $uid . $this->encryption_base_dir . '/' . $encryptionModuleId; | |||||
$backupDir = $uid . $this->backup_base_dir; | |||||
if (!$this->view->file_exists($backupDir)) { | |||||
$this->view->mkdir($backupDir); | |||||
} | |||||
$backupDir = $backupDir . '/' . $purpose . '.' . $encryptionModuleId . '.' . $this->getTimestamp(); | |||||
$this->view->mkdir($backupDir); | |||||
return $this->view->copy($source, $backupDir); | |||||
} | |||||
/** | |||||
* get the current timestamp | |||||
* | |||||
* @return int | |||||
*/ | |||||
protected function getTimestamp() { | |||||
return time(); | |||||
} | |||||
/** | /** | ||||
* get system wide path and detect mount points | * get system wide path and detect mount points | ||||
* | * |
*/ | */ | ||||
public function copyKeys($source, $target); | public function copyKeys($source, $target); | ||||
/** | |||||
* backup keys of a given encryption module | |||||
* | |||||
* @param string $encryptionModuleId | |||||
* @param string $purpose | |||||
* @param string $uid | |||||
* @return bool | |||||
* @since 12.0.0 | |||||
*/ | |||||
public function backupUserKeys($encryptionModuleId, $purpose, $uid); | |||||
} | } |
$this->assertSame($expectedResponse, $response); | $this->assertSame($expectedResponse, $response); | ||||
} | } | ||||
public function testSetPasswordEncryptionProceed() { | |||||
/** @var LostController | PHPUnit_Framework_MockObject_MockObject $lostController */ | |||||
$lostController = $this->getMockBuilder(LostController::class) | |||||
->setConstructorArgs( | |||||
[ | |||||
'Core', | |||||
$this->request, | |||||
$this->urlGenerator, | |||||
$this->userManager, | |||||
$this->defaults, | |||||
$this->l10n, | |||||
$this->config, | |||||
$this->secureRandom, | |||||
'lostpassword-noreply@localhost', | |||||
$this->encryptionManager, | |||||
$this->mailer, | |||||
$this->timeFactory, | |||||
$this->crypto | |||||
] | |||||
)->setMethods(['checkPasswordResetToken'])->getMock(); | |||||
$lostController->expects($this->once())->method('checkPasswordResetToken')->willReturn(true); | |||||
$user = $this->createMock(IUser::class); | |||||
$user->method('setPassword')->willReturnCallback( | |||||
function() { | |||||
throw new PrivateKeyMissingException('user'); | |||||
} | |||||
); | |||||
$this->userManager->method('get')->with('user')->willReturn($user); | |||||
$response = $lostController->setPassword('myToken', 'user', 'newpass', true); | |||||
$expectedResponse = ['status' => 'success']; | |||||
$this->assertSame($expectedResponse, $response); | |||||
} | |||||
} | } |
]; | ]; | ||||
} | } | ||||
/** | |||||
* @dataProvider dataTestBackupUserKeys | |||||
* @param bool $createBackupDir | |||||
*/ | |||||
public function testBackupUserKeys($createBackupDir) { | |||||
$storage = $this->getMockBuilder('OC\Encryption\Keys\Storage') | |||||
->setConstructorArgs([$this->view, $this->util]) | |||||
->setMethods(['getTimestamp']) | |||||
->getMock(); | |||||
$storage->expects($this->any())->method('getTimestamp')->willReturn('1234567'); | |||||
$this->view->expects($this->once())->method('file_exists') | |||||
->with('user1/files_encryption/backup')->willReturn(!$createBackupDir); | |||||
if ($createBackupDir) { | |||||
$this->view->expects($this->at(1))->method('mkdir') | |||||
->with('user1/files_encryption/backup'); | |||||
$this->view->expects($this->at(2))->method('mkdir') | |||||
->with('user1/files_encryption/backup/test.encryptionModule.1234567'); | |||||
} else { | |||||
$this->view->expects($this->once())->method('mkdir') | |||||
->with('user1/files_encryption/backup/test.encryptionModule.1234567'); | |||||
} | |||||
$this->view->expects($this->once())->method('copy') | |||||
->with( | |||||
'user1/files_encryption/encryptionModule', | |||||
'user1/files_encryption/backup/test.encryptionModule.1234567' | |||||
)->willReturn(true); | |||||
$this->assertTrue($storage->backupUserKeys('encryptionModule', 'test', 'user1')); | |||||
} | |||||
public function dataTestBackupUserKeys() { | |||||
return [ | |||||
[true], [false] | |||||
]; | |||||
} | |||||
} | } |