add occ command to recover encrypted files in case of password lost

Signed-off-by: Bjoern Schiessle <bjoern@schiessle.org>
This commit is contained in:
Bjoern Schiessle 2018-08-16 15:06:23 +02:00
parent f7ae4771c8
commit 3adc2aca53
No known key found for this signature in database
GPG Key ID: 2378A753E2BF04F6
6 changed files with 146 additions and 12 deletions

View File

@ -14,7 +14,7 @@
Please read the documentation to know all implications before you decide
to enable server-side encryption.
</description>
<version>2.1.0</version>
<version>2.2.0</version>
<licence>agpl</licence>
<author>Bjoern Schiessle</author>
<author>Clark Tomlinson</author>
@ -43,6 +43,7 @@
<commands>
<command>OCA\Encryption\Command\EnableMasterKey</command>
<command>OCA\Encryption\Command\DisableMasterKey</command>
<command>OCA\Encryption\Command\RecoverUser</command>
</commands>
<settings>

View File

@ -9,6 +9,7 @@ return array(
'OCA\\Encryption\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
'OCA\\Encryption\\Command\\DisableMasterKey' => $baseDir . '/../lib/Command/DisableMasterKey.php',
'OCA\\Encryption\\Command\\EnableMasterKey' => $baseDir . '/../lib/Command/EnableMasterKey.php',
'OCA\\Encryption\\Command\\RecoverUser' => $baseDir . '/../lib/Command/RecoverUser.php',
'OCA\\Encryption\\Controller\\RecoveryController' => $baseDir . '/../lib/Controller/RecoveryController.php',
'OCA\\Encryption\\Controller\\SettingsController' => $baseDir . '/../lib/Controller/SettingsController.php',
'OCA\\Encryption\\Controller\\StatusController' => $baseDir . '/../lib/Controller/StatusController.php',

View File

@ -24,6 +24,7 @@ class ComposerStaticInitEncryption
'OCA\\Encryption\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
'OCA\\Encryption\\Command\\DisableMasterKey' => __DIR__ . '/..' . '/../lib/Command/DisableMasterKey.php',
'OCA\\Encryption\\Command\\EnableMasterKey' => __DIR__ . '/..' . '/../lib/Command/EnableMasterKey.php',
'OCA\\Encryption\\Command\\RecoverUser' => __DIR__ . '/..' . '/../lib/Command/RecoverUser.php',
'OCA\\Encryption\\Controller\\RecoveryController' => __DIR__ . '/..' . '/../lib/Controller/RecoveryController.php',
'OCA\\Encryption\\Controller\\SettingsController' => __DIR__ . '/..' . '/../lib/Controller/SettingsController.php',
'OCA\\Encryption\\Controller\\StatusController' => __DIR__ . '/..' . '/../lib/Controller/StatusController.php',

View File

@ -0,0 +1,118 @@
<?php
/**
* @copyright Copyright (c) 2018 Bjoern Schiessle <bjoern@schiessle.org>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Encryption\Command;
use OC\Files\Filesystem;
use OC\User\NoUserException;
use OCA\Encryption\Crypto\Crypt;
use OCA\Encryption\KeyManager;
use OCA\Encryption\Recovery;
use OCA\Encryption\Util;
use OCP\IConfig;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
class RecoverUser extends Command {
/** @var Util */
protected $util;
/** @var IUserManager */
protected $userManager;
/** @var QuestionHelper */
protected $questionHelper;
/**
* @param Util $util
* @param IConfig $config
* @param IUserManager $userManager
* @param QuestionHelper $questionHelper
*/
public function __construct(Util $util,
IConfig $config,
IUserManager $userManager,
QuestionHelper $questionHelper) {
$this->util = $util;
$this->questionHelper = $questionHelper;
$this->userManager = $userManager;
parent::__construct();
}
protected function configure() {
$this
->setName('encryption:recover-user')
->setDescription('Recover user data in case of password lost. This only works if the user enabled the recovery key.');
$this->addArgument(
'user',
InputArgument::REQUIRED,
'user which should be recovered'
);
}
protected function execute(InputInterface $input, OutputInterface $output) {
$isMasterKeyEnabled = $this->util->isMasterKeyEnabled();
if($isMasterKeyEnabled) {
$output->writeln('You use the master key, no individual user recovery needed.');
return;
}
$uid = $input->getArgument('user');
$userExists = $this->userManager->userExists($uid);
if ($userExists === false) {
$output->writeln('User "' . $uid . '" unknown.');
return;
}
$recoveryKeyEnabled = $this->util->isRecoveryEnabledForUser($uid);
if($recoveryKeyEnabled === false) {
$output->writeln('Recovery key is not enabled for: ' . $uid);
return;
}
$question = new Question('Please enter the recovery key password: ');
$question->setHidden(true);
$question->setHiddenFallback(false);
$recoveryPassword = $this->questionHelper->ask($input, $output, $question);
$question = new Question('Please enter the new login password for the user: ');
$question->setHidden(true);
$question->setHiddenFallback(false);
$newLoginPassword = $this->questionHelper->ask($input, $output, $question);
$output->write('Start to recover users files... This can take some time...');
$this->userManager->get($uid)->setPassword($newLoginPassword, $recoveryPassword);
$output->writeln('Done.');
}
}

View File

@ -28,6 +28,7 @@ namespace OCA\Encryption\Hooks;
use OC\Files\Filesystem;
use OCP\Encryption\Exceptions\GenericEncryptionException;
use OCP\IUserManager;
use OCP\Util as OCUtil;
use OCA\Encryption\Hooks\Contracts\IHook;
@ -252,11 +253,12 @@ class UserHooks implements IHook {
}
// Get existing decrypted private key
$privateKey = $this->session->getPrivateKey();
$user = $this->user->getUser();
// current logged in user changes his own password
if ($user && $params['uid'] === $user->getUID() && $privateKey) {
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']);
@ -277,6 +279,18 @@ class UserHooks implements IHook {
$this->initMountPoints($user);
$recoveryPassword = isset($params['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)

View File

@ -210,9 +210,9 @@ class UserHooksTest extends TestCase {
}
public function testSetPassphrase() {
$this->sessionMock->expects($this->exactly(4))
$this->sessionMock->expects($this->once())
->method('getPrivateKey')
->willReturnOnConsecutiveCalls(true, false);
->willReturn(true);
$this->cryptMock->expects($this->exactly(4))
->method('encryptPrivateKey')
@ -236,7 +236,7 @@ class UserHooksTest extends TestCase {
$this->recoveryMock->expects($this->exactly(3))
->method('isRecoveryEnabledForUser')
->with('testUser')
->with('testUser1')
->willReturnOnConsecutiveCalls(true, false);
@ -257,13 +257,15 @@ class UserHooksTest extends TestCase {
$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('testUser')
->with('testUser1')
->willReturn(true);
$this->assertNull($this->instance->setPassphrase($this->params));
@ -271,7 +273,7 @@ class UserHooksTest extends TestCase {
// Test third and final if condition
$this->utilMock->expects($this->once())
->method('userHasFiles')
->with('testUser')
->with('testUser1')
->willReturn(false);
$this->cryptMock->expects($this->once())
@ -282,7 +284,7 @@ class UserHooksTest extends TestCase {
$this->recoveryMock->expects($this->once())
->method('recoverUsersFiles')
->with('password', 'testUser');
->with('password', 'testUser1');
$this->assertNull($this->instance->setPassphrase($this->params));
}
@ -297,9 +299,6 @@ class UserHooksTest extends TestCase {
}
public function testSetPasswordNoUser() {
$this->sessionMock->expects($this->once())
->method('getPrivateKey')
->willReturn(true);
$userSessionMock = $this->getMockBuilder(IUserSession::class)
->disableOriginalConstructor()