Merge pull request #10718 from nextcloud/occ-recover-encryption
add occ command to recover encrypted files in case of password lost
This commit is contained in:
commit
3a44cce5cc
|
@ -14,7 +14,7 @@
|
||||||
Please read the documentation to know all implications before you decide
|
Please read the documentation to know all implications before you decide
|
||||||
to enable server-side encryption.
|
to enable server-side encryption.
|
||||||
</description>
|
</description>
|
||||||
<version>2.1.0</version>
|
<version>2.2.0</version>
|
||||||
<licence>agpl</licence>
|
<licence>agpl</licence>
|
||||||
<author>Bjoern Schiessle</author>
|
<author>Bjoern Schiessle</author>
|
||||||
<author>Clark Tomlinson</author>
|
<author>Clark Tomlinson</author>
|
||||||
|
@ -43,6 +43,7 @@
|
||||||
<commands>
|
<commands>
|
||||||
<command>OCA\Encryption\Command\EnableMasterKey</command>
|
<command>OCA\Encryption\Command\EnableMasterKey</command>
|
||||||
<command>OCA\Encryption\Command\DisableMasterKey</command>
|
<command>OCA\Encryption\Command\DisableMasterKey</command>
|
||||||
|
<command>OCA\Encryption\Command\RecoverUser</command>
|
||||||
</commands>
|
</commands>
|
||||||
|
|
||||||
<settings>
|
<settings>
|
||||||
|
|
|
@ -9,6 +9,7 @@ return array(
|
||||||
'OCA\\Encryption\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
|
'OCA\\Encryption\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
|
||||||
'OCA\\Encryption\\Command\\DisableMasterKey' => $baseDir . '/../lib/Command/DisableMasterKey.php',
|
'OCA\\Encryption\\Command\\DisableMasterKey' => $baseDir . '/../lib/Command/DisableMasterKey.php',
|
||||||
'OCA\\Encryption\\Command\\EnableMasterKey' => $baseDir . '/../lib/Command/EnableMasterKey.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\\RecoveryController' => $baseDir . '/../lib/Controller/RecoveryController.php',
|
||||||
'OCA\\Encryption\\Controller\\SettingsController' => $baseDir . '/../lib/Controller/SettingsController.php',
|
'OCA\\Encryption\\Controller\\SettingsController' => $baseDir . '/../lib/Controller/SettingsController.php',
|
||||||
'OCA\\Encryption\\Controller\\StatusController' => $baseDir . '/../lib/Controller/StatusController.php',
|
'OCA\\Encryption\\Controller\\StatusController' => $baseDir . '/../lib/Controller/StatusController.php',
|
||||||
|
|
|
@ -24,6 +24,7 @@ class ComposerStaticInitEncryption
|
||||||
'OCA\\Encryption\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
|
'OCA\\Encryption\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
|
||||||
'OCA\\Encryption\\Command\\DisableMasterKey' => __DIR__ . '/..' . '/../lib/Command/DisableMasterKey.php',
|
'OCA\\Encryption\\Command\\DisableMasterKey' => __DIR__ . '/..' . '/../lib/Command/DisableMasterKey.php',
|
||||||
'OCA\\Encryption\\Command\\EnableMasterKey' => __DIR__ . '/..' . '/../lib/Command/EnableMasterKey.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\\RecoveryController' => __DIR__ . '/..' . '/../lib/Controller/RecoveryController.php',
|
||||||
'OCA\\Encryption\\Controller\\SettingsController' => __DIR__ . '/..' . '/../lib/Controller/SettingsController.php',
|
'OCA\\Encryption\\Controller\\SettingsController' => __DIR__ . '/..' . '/../lib/Controller/SettingsController.php',
|
||||||
'OCA\\Encryption\\Controller\\StatusController' => __DIR__ . '/..' . '/../lib/Controller/StatusController.php',
|
'OCA\\Encryption\\Controller\\StatusController' => __DIR__ . '/..' . '/../lib/Controller/StatusController.php',
|
||||||
|
|
|
@ -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.');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ namespace OCA\Encryption\Hooks;
|
||||||
|
|
||||||
|
|
||||||
use OC\Files\Filesystem;
|
use OC\Files\Filesystem;
|
||||||
|
use OCP\Encryption\Exceptions\GenericEncryptionException;
|
||||||
use OCP\IUserManager;
|
use OCP\IUserManager;
|
||||||
use OCP\Util as OCUtil;
|
use OCP\Util as OCUtil;
|
||||||
use OCA\Encryption\Hooks\Contracts\IHook;
|
use OCA\Encryption\Hooks\Contracts\IHook;
|
||||||
|
@ -252,11 +253,12 @@ class UserHooks implements IHook {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get existing decrypted private key
|
// Get existing decrypted private key
|
||||||
$privateKey = $this->session->getPrivateKey();
|
|
||||||
$user = $this->user->getUser();
|
$user = $this->user->getUser();
|
||||||
|
|
||||||
// current logged in user changes his own password
|
// 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
|
// Encrypt private key with new user pwd as passphrase
|
||||||
$encryptedPrivateKey = $this->crypt->encryptPrivateKey($privateKey, $params['password'], $params['uid']);
|
$encryptedPrivateKey = $this->crypt->encryptPrivateKey($privateKey, $params['password'], $params['uid']);
|
||||||
|
@ -277,6 +279,18 @@ class UserHooks implements IHook {
|
||||||
$this->initMountPoints($user);
|
$this->initMountPoints($user);
|
||||||
$recoveryPassword = isset($params['recoveryPassword']) ? $params['recoveryPassword'] : null;
|
$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 generate new keys if...
|
||||||
// ...we have a recovery password and the user enabled the recovery key
|
// ...we have a recovery password and the user enabled the recovery key
|
||||||
// ...encryption was activated for the first time (no keys exists)
|
// ...encryption was activated for the first time (no keys exists)
|
||||||
|
|
|
@ -210,9 +210,9 @@ class UserHooksTest extends TestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetPassphrase() {
|
public function testSetPassphrase() {
|
||||||
$this->sessionMock->expects($this->exactly(4))
|
$this->sessionMock->expects($this->once())
|
||||||
->method('getPrivateKey')
|
->method('getPrivateKey')
|
||||||
->willReturnOnConsecutiveCalls(true, false);
|
->willReturn(true);
|
||||||
|
|
||||||
$this->cryptMock->expects($this->exactly(4))
|
$this->cryptMock->expects($this->exactly(4))
|
||||||
->method('encryptPrivateKey')
|
->method('encryptPrivateKey')
|
||||||
|
@ -236,7 +236,7 @@ class UserHooksTest extends TestCase {
|
||||||
|
|
||||||
$this->recoveryMock->expects($this->exactly(3))
|
$this->recoveryMock->expects($this->exactly(3))
|
||||||
->method('isRecoveryEnabledForUser')
|
->method('isRecoveryEnabledForUser')
|
||||||
->with('testUser')
|
->with('testUser1')
|
||||||
->willReturnOnConsecutiveCalls(true, false);
|
->willReturnOnConsecutiveCalls(true, false);
|
||||||
|
|
||||||
|
|
||||||
|
@ -257,13 +257,15 @@ class UserHooksTest extends TestCase {
|
||||||
|
|
||||||
$this->instance->expects($this->exactly(3))->method('initMountPoints');
|
$this->instance->expects($this->exactly(3))->method('initMountPoints');
|
||||||
|
|
||||||
|
$this->params['uid'] = 'testUser1';
|
||||||
|
|
||||||
// Test first if statement
|
// Test first if statement
|
||||||
$this->assertNull($this->instance->setPassphrase($this->params));
|
$this->assertNull($this->instance->setPassphrase($this->params));
|
||||||
|
|
||||||
// Test Second if conditional
|
// Test Second if conditional
|
||||||
$this->keyManagerMock->expects($this->exactly(2))
|
$this->keyManagerMock->expects($this->exactly(2))
|
||||||
->method('userHasKeys')
|
->method('userHasKeys')
|
||||||
->with('testUser')
|
->with('testUser1')
|
||||||
->willReturn(true);
|
->willReturn(true);
|
||||||
|
|
||||||
$this->assertNull($this->instance->setPassphrase($this->params));
|
$this->assertNull($this->instance->setPassphrase($this->params));
|
||||||
|
@ -271,7 +273,7 @@ class UserHooksTest extends TestCase {
|
||||||
// Test third and final if condition
|
// Test third and final if condition
|
||||||
$this->utilMock->expects($this->once())
|
$this->utilMock->expects($this->once())
|
||||||
->method('userHasFiles')
|
->method('userHasFiles')
|
||||||
->with('testUser')
|
->with('testUser1')
|
||||||
->willReturn(false);
|
->willReturn(false);
|
||||||
|
|
||||||
$this->cryptMock->expects($this->once())
|
$this->cryptMock->expects($this->once())
|
||||||
|
@ -282,7 +284,7 @@ class UserHooksTest extends TestCase {
|
||||||
|
|
||||||
$this->recoveryMock->expects($this->once())
|
$this->recoveryMock->expects($this->once())
|
||||||
->method('recoverUsersFiles')
|
->method('recoverUsersFiles')
|
||||||
->with('password', 'testUser');
|
->with('password', 'testUser1');
|
||||||
|
|
||||||
$this->assertNull($this->instance->setPassphrase($this->params));
|
$this->assertNull($this->instance->setPassphrase($this->params));
|
||||||
}
|
}
|
||||||
|
@ -297,9 +299,6 @@ class UserHooksTest extends TestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSetPasswordNoUser() {
|
public function testSetPasswordNoUser() {
|
||||||
$this->sessionMock->expects($this->once())
|
|
||||||
->method('getPrivateKey')
|
|
||||||
->willReturn(true);
|
|
||||||
|
|
||||||
$userSessionMock = $this->getMockBuilder(IUserSession::class)
|
$userSessionMock = $this->getMockBuilder(IUserSession::class)
|
||||||
->disableOriginalConstructor()
|
->disableOriginalConstructor()
|
||||||
|
|
Loading…
Reference in New Issue