Browse Source

Merge pull request #2918 from nextcloud/encryption-recovery-improvements

create new encryption keys on password reset and backup the old one
tags/v12.0.0beta1
Morris Jobke 7 years ago
parent
commit
622101f2dd

+ 42
- 19
apps/encryption/lib/Hooks/UserHooks.php View File

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
* *

+ 3
- 5
apps/encryption/lib/KeyManager.php View File



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

+ 34
- 13
apps/encryption/tests/Hooks/UserHooksTest.php View File

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

+ 6
- 0
apps/encryption/tests/KeyManagerTest.php View File

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

} }

+ 2
- 5
core/Controller/LostController.php View File

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

+ 1
- 1
core/js/lostpassword.js View File



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')

+ 35
- 0
lib/private/Encryption/Keys/Storage.php View File

/** @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
* *

+ 10
- 0
lib/public/Encryption/Keys/IStorage.php View File

*/ */
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);
} }

+ 0
- 38
tests/Core/Controller/LostControllerTest.php View File

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

} }

+ 43
- 0
tests/lib/Encryption/Keys/StorageTest.php View File

]; ];
} }



/**
* @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]
];
}

} }

Loading…
Cancel
Save