Skip to content

Commit

Permalink
Merge pull request #10718 from nextcloud/occ-recover-encryption
Browse files Browse the repository at this point in the history
add occ command to recover encrypted files in case of password lost
  • Loading branch information
schiessle authored Aug 17, 2018
2 parents bcc1a53 + 3adc2ac commit 3a44cce
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 12 deletions.
3 changes: 2 additions & 1 deletion apps/encryption/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions apps/encryption/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'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',
Expand Down
1 change: 1 addition & 0 deletions apps/encryption/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
118 changes: 118 additions & 0 deletions apps/encryption/lib/Command/RecoverUser.php
Original file line number Diff line number Diff line change
@@ -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.');

}

}
18 changes: 16 additions & 2 deletions apps/encryption/lib/Hooks/UserHooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@


use OC\Files\Filesystem;
use OCP\Encryption\Exceptions\GenericEncryptionException;
use OCP\IUserManager;
use OCP\Util as OCUtil;
use OCA\Encryption\Hooks\Contracts\IHook;
Expand Down Expand Up @@ -252,11 +253,12 @@ public function setPassphrase($params) {
}

// 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']);
Expand All @@ -277,6 +279,18 @@ public function setPassphrase($params) {
$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)
Expand Down
17 changes: 8 additions & 9 deletions apps/encryption/tests/Hooks/UserHooksTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ public function dataTestPreSetPassphrase() {
}

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')
Expand All @@ -236,7 +236,7 @@ public function testSetPassphrase() {

$this->recoveryMock->expects($this->exactly(3))
->method('isRecoveryEnabledForUser')
->with('testUser')
->with('testUser1')
->willReturnOnConsecutiveCalls(true, false);


Expand All @@ -257,21 +257,23 @@ public function testSetPassphrase() {

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

// Test third and final if condition
$this->utilMock->expects($this->once())
->method('userHasFiles')
->with('testUser')
->with('testUser1')
->willReturn(false);

$this->cryptMock->expects($this->once())
Expand All @@ -282,7 +284,7 @@ public function testSetPassphrase() {

$this->recoveryMock->expects($this->once())
->method('recoverUsersFiles')
->with('password', 'testUser');
->with('password', 'testUser1');

$this->assertNull($this->instance->setPassphrase($this->params));
}
Expand All @@ -297,9 +299,6 @@ public function testSetPassphraseResetUserMode() {
}

public function testSetPasswordNoUser() {
$this->sessionMock->expects($this->once())
->method('getPrivateKey')
->willReturn(true);

$userSessionMock = $this->getMockBuilder(IUserSession::class)
->disableOriginalConstructor()
Expand Down

0 comments on commit 3a44cce

Please sign in to comment.