@@ -84,6 +84,9 @@ class Encryption implements IEncryptionModule { | |||
/** @var EncryptAll */ | |||
private $encryptAll; | |||
/** @var bool */ | |||
private $useMasterPassword; | |||
/** | |||
* | |||
* @param Crypt $crypt | |||
@@ -105,6 +108,7 @@ class Encryption implements IEncryptionModule { | |||
$this->encryptAll = $encryptAll; | |||
$this->logger = $logger; | |||
$this->l = $il10n; | |||
$this->useMasterPassword = $util->isMasterKeyEnabled(); | |||
} | |||
/** | |||
@@ -193,23 +197,26 @@ class Encryption implements IEncryptionModule { | |||
$this->writeCache = ''; | |||
} | |||
$publicKeys = array(); | |||
foreach ($this->accessList['users'] as $uid) { | |||
try { | |||
$publicKeys[$uid] = $this->keyManager->getPublicKey($uid); | |||
} catch (PublicKeyMissingException $e) { | |||
$this->logger->warning( | |||
'no public key found for user "{uid}", user will not be able to read the file', | |||
['app' => 'encryption', 'uid' => $uid] | |||
); | |||
// if the public key of the owner is missing we should fail | |||
if ($uid === $this->user) { | |||
throw $e; | |||
if ($this->useMasterPassword === true) { | |||
$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey(); | |||
} else { | |||
foreach ($this->accessList['users'] as $uid) { | |||
try { | |||
$publicKeys[$uid] = $this->keyManager->getPublicKey($uid); | |||
} catch (PublicKeyMissingException $e) { | |||
$this->logger->warning( | |||
'no public key found for user "{uid}", user will not be able to read the file', | |||
['app' => 'encryption', 'uid' => $uid] | |||
); | |||
// if the public key of the owner is missing we should fail | |||
if ($uid === $this->user) { | |||
throw $e; | |||
} | |||
} | |||
} | |||
} | |||
$publicKeys = $this->keyManager->addSystemKeys($this->accessList, $publicKeys, $this->user); | |||
$encryptedKeyfiles = $this->crypt->multiKeyEncrypt($this->fileKey, $publicKeys); | |||
$this->keyManager->setAllFileKeys($this->path, $encryptedKeyfiles); | |||
} | |||
@@ -318,8 +325,12 @@ class Encryption implements IEncryptionModule { | |||
if (!empty($fileKey)) { | |||
$publicKeys = array(); | |||
foreach ($accessList['users'] as $user) { | |||
$publicKeys[$user] = $this->keyManager->getPublicKey($user); | |||
if ($this->useMasterPassword === true) { | |||
$publicKeys[$this->keyManager->getMasterKeyId()] = $this->keyManager->getPublicMasterKey(); | |||
} else { | |||
foreach ($accessList['users'] as $user) { | |||
$publicKeys[$user] = $this->keyManager->getPublicKey($user); | |||
} | |||
} | |||
$publicKeys = $this->keyManager->addSystemKeys($accessList, $publicKeys, $uid); |
@@ -54,6 +54,10 @@ class KeyManager { | |||
* @var string | |||
*/ | |||
private $publicShareKeyId; | |||
/** | |||
* @var string | |||
*/ | |||
private $masterKeyId; | |||
/** | |||
* @var string UserID | |||
*/ | |||
@@ -131,10 +135,20 @@ class KeyManager { | |||
$this->config->setAppValue('encryption', 'publicShareKeyId', $this->publicShareKeyId); | |||
} | |||
$this->masterKeyId = $this->config->getAppValue('encryption', | |||
'masterKeyId'); | |||
if (empty($this->masterKeyId)) { | |||
$this->masterKeyId = 'master_' . substr(md5(time()), 0, 8); | |||
$this->config->setAppValue('encryption', 'masterKeyId', $this->masterKeyId); | |||
} | |||
$this->keyId = $userSession && $userSession->isLoggedIn() ? $userSession->getUser()->getUID() : false; | |||
$this->log = $log; | |||
} | |||
/** | |||
* check if key pair for public link shares exists, if not we create one | |||
*/ | |||
public function validateShareKey() { | |||
$shareKey = $this->getPublicShareKey(); | |||
if (empty($shareKey)) { | |||
@@ -152,6 +166,26 @@ class KeyManager { | |||
} | |||
} | |||
/** | |||
* check if a key pair for the master key exists, if not we create one | |||
*/ | |||
public function validateMasterKey() { | |||
$masterKey = $this->getPublicMasterKey(); | |||
if (empty($masterKey)) { | |||
$keyPair = $this->crypt->createKeyPair(); | |||
// Save public key | |||
$this->keyStorage->setSystemUserKey( | |||
$this->masterKeyId . '.publicKey', $keyPair['publicKey'], | |||
Encryption::ID); | |||
// Encrypt private key with system password | |||
$encryptedKey = $this->crypt->encryptPrivateKey($keyPair['privateKey'], $this->getMasterKeyPassword(), $this->masterKeyId); | |||
$header = $this->crypt->generateHeader(); | |||
$this->setSystemPrivateKey($this->masterKeyId, $header . $encryptedKey); | |||
} | |||
} | |||
/** | |||
* @return bool | |||
*/ | |||
@@ -304,8 +338,15 @@ class KeyManager { | |||
$this->session->setStatus(Session::INIT_EXECUTED); | |||
try { | |||
$privateKey = $this->getPrivateKey($uid); | |||
if($this->util->isMasterKeyEnabled()) { | |||
$uid = $this->getMasterKeyId(); | |||
$passPhrase = $this->getMasterKeyPassword(); | |||
$privateKey = $this->getSystemPrivateKey($uid); | |||
} else { | |||
$privateKey = $this->getPrivateKey($uid); | |||
} | |||
$privateKey = $this->crypt->decryptPrivateKey($privateKey, $passPhrase, $uid); | |||
} catch (PrivateKeyMissingException $e) { | |||
return false; | |||
@@ -345,6 +386,10 @@ class KeyManager { | |||
public function getFileKey($path, $uid) { | |||
$encryptedFileKey = $this->keyStorage->getFileKey($path, $this->fileKeyId, Encryption::ID); | |||
if ($this->util->isMasterKeyEnabled()) { | |||
$uid = $this->getMasterKeyId(); | |||
} | |||
if (is_null($uid)) { | |||
$uid = $this->getPublicShareKeyId(); | |||
$shareKey = $this->getShareKey($path, $uid); | |||
@@ -566,4 +611,37 @@ class KeyManager { | |||
return $publicKeys; | |||
} | |||
/** | |||
* get master key password | |||
* | |||
* @return string | |||
* @throws \Exception | |||
*/ | |||
protected function getMasterKeyPassword() { | |||
$password = $this->config->getSystemValue('secret'); | |||
if (empty($password)){ | |||
throw new \Exception('Can not get secret from ownCloud instance'); | |||
} | |||
return $password; | |||
} | |||
/** | |||
* return master key id | |||
* | |||
* @return string | |||
*/ | |||
public function getMasterKeyId() { | |||
return $this->masterKeyId; | |||
} | |||
/** | |||
* get public master key | |||
* | |||
* @return string | |||
*/ | |||
public function getPublicMasterKey() { | |||
return $this->keyStorage->getSystemUserKey($this->masterKeyId . '.publicKey', Encryption::ID); | |||
} | |||
} |
@@ -84,6 +84,7 @@ class Setup { | |||
*/ | |||
public function setupServerSide($uid, $password) { | |||
$this->keyManager->validateShareKey(); | |||
$this->keyManager->validateMasterKey(); | |||
// Check if user already has keys | |||
if (!$this->keyManager->userHasKeys($uid)) { | |||
return $this->keyManager->storeKeyPair($uid, $password, |
@@ -101,6 +101,16 @@ class Util { | |||
return ($recoveryMode === '1'); | |||
} | |||
/** | |||
* check if master key is enabled | |||
* | |||
* @return bool | |||
*/ | |||
public function isMasterKeyEnabled() { | |||
$userMasterKey = $this->config->getAppValue('encryption', 'useMasterKey', '0'); | |||
return ($userMasterKey === '1'); | |||
} | |||
/** | |||
* @param $enabled | |||
* @return bool |
@@ -27,6 +27,7 @@ namespace OCA\Encryption\Tests; | |||
use OCA\Encryption\KeyManager; | |||
use OCA\Encryption\Session; | |||
use Test\TestCase; | |||
class KeyManagerTest extends TestCase { | |||
@@ -237,24 +238,62 @@ class KeyManagerTest extends TestCase { | |||
} | |||
/** | |||
* @dataProvider dataTestInit | |||
* | |||
* @param bool $useMasterKey | |||
*/ | |||
public function testInit($useMasterKey) { | |||
$instance = $this->getMockBuilder('OCA\Encryption\KeyManager') | |||
->setConstructorArgs( | |||
[ | |||
$this->keyStorageMock, | |||
$this->cryptMock, | |||
$this->configMock, | |||
$this->userMock, | |||
$this->sessionMock, | |||
$this->logMock, | |||
$this->utilMock | |||
] | |||
)->setMethods(['getMasterKeyId', 'getMasterKeyPassword', 'getSystemPrivateKey', 'getPrivateKey']) | |||
->getMock(); | |||
public function testInit() { | |||
$this->keyStorageMock->expects($this->any()) | |||
->method('getUserKey') | |||
->with($this->equalTo($this->userId), $this->equalTo('privateKey')) | |||
->willReturn('privateKey'); | |||
$this->cryptMock->expects($this->any()) | |||
->method('decryptPrivateKey') | |||
->with($this->equalTo('privateKey'), $this->equalTo('pass')) | |||
->willReturn('decryptedPrivateKey'); | |||
$this->utilMock->expects($this->once())->method('isMasterKeyEnabled') | |||
->willReturn($useMasterKey); | |||
$this->sessionMock->expects($this->at(0))->method('setStatus') | |||
->with(Session::INIT_EXECUTED); | |||
$instance->expects($this->any())->method('getMasterKeyId')->willReturn('masterKeyId'); | |||
$instance->expects($this->any())->method('getMasterKeyPassword')->willReturn('masterKeyPassword'); | |||
$instance->expects($this->any())->method('getSystemPrivateKey')->with('masterKeyId')->willReturn('privateMasterKey'); | |||
$instance->expects($this->any())->method('getPrivateKey')->with($this->userId)->willReturn('privateUserKey'); | |||
if($useMasterKey) { | |||
$this->cryptMock->expects($this->once())->method('decryptPrivateKey') | |||
->with('privateMasterKey', 'masterKeyPassword', 'masterKeyId') | |||
->willReturn('key'); | |||
} else { | |||
$this->cryptMock->expects($this->once())->method('decryptPrivateKey') | |||
->with('privateUserKey', 'pass', $this->userId) | |||
->willReturn('key'); | |||
} | |||
$this->sessionMock->expects($this->once())->method('setPrivateKey') | |||
->with('key'); | |||
$this->assertTrue( | |||
$this->instance->init($this->userId, 'pass') | |||
); | |||
$this->assertTrue($instance->init($this->userId, 'pass')); | |||
} | |||
public function dataTestInit() { | |||
return [ | |||
[true], | |||
[false] | |||
]; | |||
} | |||
public function testSetRecoveryKey() { | |||
$this->keyStorageMock->expects($this->exactly(2)) | |||
->method('setSystemUserKey') | |||
@@ -401,5 +440,92 @@ class KeyManagerTest extends TestCase { | |||
); | |||
} | |||
public function testGetMasterKeyId() { | |||
$this->assertSame('systemKeyId', $this->instance->getMasterKeyId()); | |||
} | |||
public function testGetPublicMasterKey() { | |||
$this->keyStorageMock->expects($this->once())->method('getSystemUserKey') | |||
->with('systemKeyId.publicKey', \OCA\Encryption\Crypto\Encryption::ID) | |||
->willReturn(true); | |||
$this->assertTrue( | |||
$this->instance->getPublicMasterKey() | |||
); | |||
} | |||
public function testGetMasterKeyPassword() { | |||
$this->configMock->expects($this->once())->method('getSystemValue')->with('secret') | |||
->willReturn('password'); | |||
$this->assertSame('password', | |||
$this->invokePrivate($this->instance, 'getMasterKeyPassword', []) | |||
); | |||
} | |||
/** | |||
* @expectedException \Exception | |||
*/ | |||
public function testGetMasterKeyPasswordException() { | |||
$this->configMock->expects($this->once())->method('getSystemValue')->with('secret') | |||
->willReturn(''); | |||
$this->invokePrivate($this->instance, 'getMasterKeyPassword', []); | |||
} | |||
/** | |||
* @dataProvider dataTestValidateMasterKey | |||
* | |||
* @param $masterKey | |||
*/ | |||
public function testValidateMasterKey($masterKey) { | |||
/** @var \OCA\Encryption\KeyManager | \PHPUnit_Framework_MockObject_MockObject $instance */ | |||
$instance = $this->getMockBuilder('OCA\Encryption\KeyManager') | |||
->setConstructorArgs( | |||
[ | |||
$this->keyStorageMock, | |||
$this->cryptMock, | |||
$this->configMock, | |||
$this->userMock, | |||
$this->sessionMock, | |||
$this->logMock, | |||
$this->utilMock | |||
] | |||
)->setMethods(['getPublicMasterKey', 'setSystemPrivateKey', 'getMasterKeyPassword']) | |||
->getMock(); | |||
$instance->expects($this->once())->method('getPublicMasterKey') | |||
->willReturn($masterKey); | |||
$instance->expects($this->any())->method('getMasterKeyPassword')->willReturn('masterKeyPassword'); | |||
$this->cryptMock->expects($this->any())->method('generateHeader')->willReturn('header'); | |||
if(empty($masterKey)) { | |||
$this->cryptMock->expects($this->once())->method('createKeyPair') | |||
->willReturn(['publicKey' => 'public', 'privateKey' => 'private']); | |||
$this->keyStorageMock->expects($this->once())->method('setSystemUserKey') | |||
->with('systemKeyId.publicKey', 'public', \OCA\Encryption\Crypto\Encryption::ID); | |||
$this->cryptMock->expects($this->once())->method('encryptPrivateKey') | |||
->with('private', 'masterKeyPassword', 'systemKeyId') | |||
->willReturn('EncryptedKey'); | |||
$instance->expects($this->once())->method('setSystemPrivateKey') | |||
->with('systemKeyId', 'headerEncryptedKey'); | |||
} else { | |||
$this->cryptMock->expects($this->never())->method('createKeyPair'); | |||
$this->keyStorageMock->expects($this->never())->method('setSystemUserKey'); | |||
$this->cryptMock->expects($this->never())->method('encryptPrivateKey'); | |||
$instance->expects($this->never())->method('setSystemPrivateKey'); | |||
} | |||
$instance->validateMasterKey(); | |||
} | |||
public function dataTestValidateMasterKey() { | |||
return [ | |||
['masterKey'], | |||
[''] | |||
]; | |||
} | |||
} |
@@ -132,4 +132,25 @@ class UtilTest extends TestCase { | |||
return $default ?: null; | |||
} | |||
/** | |||
* @dataProvider dataTestIsMasterKeyEnabled | |||
* | |||
* @param string $value | |||
* @param bool $expect | |||
*/ | |||
public function testIsMasterKeyEnabled($value, $expect) { | |||
$this->configMock->expects($this->once())->method('getAppValue') | |||
->with('encryption', 'useMasterKey', '0')->willReturn($value); | |||
$this->assertSame($expect, | |||
$this->instance->isMasterKeyEnabled() | |||
); | |||
} | |||
public function dataTestIsMasterKeyEnabled() { | |||
return [ | |||
['0', false], | |||
['1', true] | |||
]; | |||
} | |||
} |
@@ -43,6 +43,8 @@ class SetupTest extends TestCase { | |||
private $instance; | |||
public function testSetupServerSide() { | |||
$this->keyManagerMock->expects($this->exactly(2))->method('validateShareKey'); | |||
$this->keyManagerMock->expects($this->exactly(2))->method('validateMasterKey'); | |||
$this->keyManagerMock->expects($this->exactly(2)) | |||
->method('userHasKeys') | |||
->with('admin') |