diff --git a/appinfo/info.xml b/appinfo/info.xml
index 2a898c67..1aaa5234 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -33,6 +33,7 @@
OCA\Encryption\Command\SelectEncryptionType
OCA\Encryption\Command\RecreateMasterKey
OCA\Encryption\Command\MigrateKeys
+ OCA\Encryption\Command\HSMDaemon
OCA\Encryption\Panels\Admin
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index ba93d9f1..0598f3cd 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -38,7 +38,7 @@
use OCA\Encryption\Session;
use OCA\Encryption\Users\Setup;
use OCA\Encryption\Util;
-use OCP\App;
+use OCA\Encryption\Crypto\CryptHSM;
use OCP\AppFramework\IAppContainer;
use OCP\Encryption\IManager;
use OCP\IConfig;
@@ -128,10 +128,25 @@ public function registerServices() {
$container->registerService('Crypt',
function (IAppContainer $c) {
$server = $c->getServer();
- return new Crypt($server->getLogger(),
- $server->getUserSession(),
- $server->getConfig(),
- $server->getL10N($c->getAppName()));
+
+ if ($this->config->getAppValue('encryption', 'hsm.url', '') !== '') {
+ $this->config->setAppValue('crypto.engine', 'internal', 'hsm');
+ }
+
+ if ($this->config->getAppValue('crypto.engine', 'internal', '') === 'hsm') {
+ return new CryptHSM($server->getLogger(),
+ $server->getUserSession(),
+ $server->getConfig(),
+ $server->getL10N($c->getAppName()),
+ $server->getHTTPClientService(),
+ $server->getRequest(),
+ $server->getTimeFactory());
+ } else {
+ return new Crypt($server->getLogger(),
+ $server->getUserSession(),
+ $server->getConfig(),
+ $server->getL10N($c->getAppName()));
+ }
});
$container->registerService('Session',
diff --git a/lib/Command/HSMDaemon.php b/lib/Command/HSMDaemon.php
new file mode 100644
index 00000000..aedb2ccf
--- /dev/null
+++ b/lib/Command/HSMDaemon.php
@@ -0,0 +1,119 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCA\Encryption\Command;
+
+use OCA\Encryption\AppInfo\Application;
+use OCA\Encryption\JWT;
+use OCA\Encryption\KeyManager;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Encryption\IEncryptionModule;
+use OCP\Http\Client\IClient;
+use OCP\Http\Client\IClientService;
+use OCP\IConfig;
+use OCP\ILogger;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class HSMDaemon extends Command {
+
+ /** @var IClient */
+ private $httpClient;
+ /** @var IConfig */
+ private $config;
+ /** @var ILogger */
+ private $logger;
+ /** @var ITimeFactory */
+ private $timeFactory;
+
+ /**
+ * @param IEncryptionModule $encryption
+ * @param IClient $httpClient
+ * @param IConfig $config
+ * @param ILogger $logger
+ */
+ public function __construct(IClientService $httpClient,
+ IConfig $config,
+ ILogger $logger,
+ ITimeFactory $timeFactory) {
+ $this->httpClient = $httpClient->newClient();
+ $this->config = $config;
+ $this->logger = $logger;
+ $this->timeFactory = $timeFactory;
+ parent::__construct();
+ }
+
+ // TODO add route for hsmdaemon to post current secret
+ // TODO add encrypt masterkey command / as option
+ // TODO add decrypt option
+ protected function configure() {
+ $this
+ ->setName('encryption:hsmdaemon')
+ ->setDescription('hsmdaemon tool');
+ $this->addOption(
+ 'export-masterkey',
+ null,
+ InputOption::VALUE_NONE,
+ 'export the private master key in base64'
+ );
+ $this->addOption(
+ 'import-masterkey',
+ null,
+ InputOption::VALUE_REQUIRED,
+ 'import a base64 encoded private masterkey'
+ );
+ $this->addOption(
+ 'decrypt',
+ null,
+ InputOption::VALUE_REQUIRED,
+ 'decrypt a base64 encoded value with the hsm'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output) {
+ $hsmUrl = $this->config->getAppValue('encryption', 'hsm.url');
+ if (\is_string($hsmUrl) && $hsmUrl !== '') {
+ $decrypt = $input->getOption('decrypt');
+ if ($decrypt) {
+ $response = $this->httpClient->post($hsmUrl, [
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . JWT::token([
+ 'exp' => $this->timeFactory->getTime() + 120 // 2min for clock skew
+ ], 'secret')
+ ],
+ 'body' => \base64_decode($decrypt)
+ ]);
+
+ $output->writeln("received: '".$response->getBody()."'");
+ } elseif ($input->getOption('export-masterkey')) {
+ $manager = \OC::$server->getEncryptionKeyStorage();
+ $keyId = $this->config->getAppValue('encryption', 'masterKeyId').'.privateKey';
+ $key = $manager->getSystemUserKey($keyId, \OC::$server->getEncryptionManager()->getDefaultEncryptionModuleId());
+ // FIXME key might be too long to encrypt in one piece
+ $output->writeln("current masterkey (base64 encoded): '".\base64_encode($key)."'");
+ }
+ } else {
+ $output->writeln("hsm.url not set");
+ }
+ }
+}
diff --git a/lib/Crypto/Crypt.php b/lib/Crypto/Crypt.php
index daf7b0bd..36b12741 100644
--- a/lib/Crypto/Crypt.php
+++ b/lib/Crypto/Crypt.php
@@ -63,13 +63,13 @@ class Crypt {
const HEADER_END = 'HEND';
/** @var ILogger */
- private $logger;
+ protected $logger;
/** @var string */
private $user;
/** @var IConfig */
- private $config;
+ protected $config;
/** @var array */
private $supportedKeyFormats;
diff --git a/lib/Crypto/CryptHSM.php b/lib/Crypto/CryptHSM.php
new file mode 100644
index 00000000..0714c634
--- /dev/null
+++ b/lib/Crypto/CryptHSM.php
@@ -0,0 +1,215 @@
+
+ * @author Clark Tomlinson
+ * @author Joas Schilling
+ * @author Lukas Reschke
+ * @author Thomas Müller
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCA\Encryption\Crypto;
+
+use GuzzleHttp\Exception\ServerException;
+use OC\Encryption\Exceptions\DecryptionFailedException;
+use OCA\Encryption\Exceptions\MultiKeyDecryptException;
+use OCA\Encryption\Exceptions\MultiKeyEncryptException;
+use OCA\Encryption\JWT;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Http\Client\IClientService;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\ILogger;
+use OCP\IRequest;
+use OCP\IUserSession;
+
+/**
+ * Class CryptHSM moves the key generation and multiKeyDecrytion to an HSM.
+ * multiKeyEncrypt can be done locally because we have the public key
+ *
+ * @package OCA\Encryption\Crypto
+ */
+class CryptHSM extends Crypt {
+
+ /**
+ * @var IClientService
+ */
+ protected $clientService;
+
+ /**
+ * @var string
+ */
+ private $hsmUrl;
+
+ /**
+ * @var int
+ */
+ private $clockSkew;
+
+ /**
+ * @var
+ */
+ private $secret;
+
+ /**
+ * @var IRequest
+ */
+ private $request;
+
+ /**
+ * ITimeFactory
+ */
+ private $timeFactory;
+
+ const PATH_NEW_KEY = '/keys/new';
+ const PATH_DECRYPT = '/decrypt/'; // appended with keyid
+
+ /**
+ * @param ILogger $logger
+ * @param IUserSession $userSession
+ * @param IConfig $config
+ * @param IL10N $l
+ * @param IClientService $clientService
+ * @param ITimeFactory $timeFactory
+ */
+ public function __construct(ILogger $logger, IUserSession $userSession, IConfig $config, IL10N $l, IClientService $clientService, IRequest $request, ITimeFactory $timeFactory) {
+ parent::__construct($logger, $userSession, $config, $l);
+ $this->hsmUrl = \rtrim($this->config->getAppValue('encryption', 'hsm.url'), '/'); // no default, because Application DI only instantiates this if it is configured non empty
+ $this->secret = $this->config->getAppValue('encryption', 'hsm.jwt.secret', 'secret');
+ $this->clockSkew = (int)$this->config->getAppValue('encryption', 'hsm.jwt.clockskew', 120); // 2min
+ $this->clientService = $clientService;
+ $this->request = $request;
+ $this->timeFactory = $timeFactory;
+ }
+
+ /**
+ * create new private/public key-pair for user
+ * any key config happens in the service
+ *
+ * @param $label string human readable name
+ * @return array|bool
+ */
+ public function createKeyPair($label = null) {
+ $response = $this->clientService->newClient()->post(
+ $this->hsmUrl.$this::PATH_NEW_KEY, [
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . JWT::token([
+ 'iss' => $this->config->getSystemValue('instanceid'),
+ 'sub' => $label,
+ 'aud' => 'hsmdaemon',
+ 'exp' => $this->timeFactory->getTime() + $this->clockSkew,
+ 'rid' => $this->request->getId(),
+ ], $this->secret)
+ ],
+ ]);
+ $keyPair = \json_decode($response->getBody(), true);
+
+ return [
+ 'publicKey' => $keyPair['publicKey'],
+ 'privateKey' => $keyPair['privateKeyId'] // returns the key id in the hsm, not the actual private key
+ ];
+ }
+ /**
+ * check if it is a valid private key
+ *
+ * @param string $plainKey
+ * @return bool
+ */
+ protected function isValidPrivateKey($plainKey) { // unneded for HSM, may check if it is '*secret*'?
+ // TODO check if it is a uuid?
+ return true;
+ }
+
+ /**
+ * @param $encKeyFile
+ * @param $shareKey
+ * @param $privateKey string contains the key uuid in the hsm
+ * @return string
+ * @throws MultiKeyDecryptException
+ */
+ public function multiKeyDecrypt($encKeyFile, $shareKey, $privateKey) { // done with HSM, private key contains the key id in the hsm
+ if (!$encKeyFile) {
+ throw new MultiKeyDecryptException('Cannot multikey decrypt empty plain content');
+ }
+
+ // decrypt the shareKey
+ $keyId = $privateKey; // TODO check $privateKey is a uuid, should have been generated with genkey
+
+ try {
+ $response = $this->clientService->newClient()->post(
+ $this->hsmUrl.$this::PATH_DECRYPT.$keyId, [
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . JWT::token([
+ 'iss' => $this->config->getSystemValue('instanceid'),
+ // 'sub' => $keyId, does not add anything right now, use md5 of $shareKey?
+ 'aud' => 'hsmdaemon',
+ 'exp' => $this->timeFactory->getTime() + $this->clockSkew,
+ 'rid' => $this->request->getId(),
+ ], $this->secret)
+ ],
+ 'body' => $shareKey
+ ]);
+ $decryptedKey = $response->getBody();
+
+ // now decode the file.
+ // version and position are 0 because we always use fresh random data as passphrase
+ $decryptedContent = $this->symmetricDecryptFileContent($encKeyFile, $decryptedKey, self::DEFAULT_CIPHER, 0, 0);
+
+ return $decryptedContent;
+ } catch (ServerException $e) {
+ $body = $e->getResponse()->getBody();
+ $this->logger->logException($e, ['message' => $body, 'app' => __CLASS__]);
+ throw new MultiKeyDecryptException('Cannot multikey decrypt with HSM', '', 0, $e);
+ } catch (DecryptionFailedException $e) {
+ throw new MultiKeyDecryptException('Cannot multikey decrypt', '', 0, $e);
+ }
+ }
+
+ /**
+ * @param string $plainContent
+ * @param array $keyFiles
+ * @return array
+ * @throws MultiKeyEncryptException
+ */
+ public function multiKeyEncrypt($plainContent, array $keyFiles) { // done with HSM, needs to return the key ids from the hsm
+ $randomKey = $this->generateFileKey();
+
+ // encrypt $plainContent using a random key and iv.
+ // version and position are 0 because we use fresh random data as passphrase
+ $sealedContent = $this->symmetricEncryptFileContent($plainContent, $randomKey, 0, 0);
+
+ if ($sealedContent === false) {
+ throw new MultiKeyEncryptException('Could not create sealed content');
+ }
+
+ $encryptedKeys = [];
+ // encrypt $randomKey with all public keys
+ foreach ($keyFiles as $userId => $publicKey) {
+ // FIXME use OPENSSL_PKCS1_OAEP_PADDING, implemented in opensc on 2017-10-19 with https://github.com/OpenSC/OpenSC/pull/1169, see http://php.net/manual/de/function.openssl-public-encrypt.php#118466
+ // TODO make padding configurable?
+ // TODO add command to hsmdaemon to see supported paddings?
+ \openssl_public_encrypt($randomKey, $encryptedKey, $publicKey, OPENSSL_PKCS1_PADDING);
+ $encryptedKeys[$userId] = $encryptedKey;
+ }
+
+ return [
+ 'keys' => $encryptedKeys,
+ 'data' => $sealedContent
+ ];
+ }
+}
diff --git a/lib/Hooks/UserHooks.php b/lib/Hooks/UserHooks.php
index 8f312ec9..fd1ed1d7 100644
--- a/lib/Hooks/UserHooks.php
+++ b/lib/Hooks/UserHooks.php
@@ -284,7 +284,7 @@ public function setPassphrase($params) {
$newUserPassword = $params['password'];
- $keyPair = $this->crypt->createKeyPair();
+ $keyPair = $this->crypt->createKeyPair("oc:uid:$user");
// Save public key
$this->keyManager->setPublicKey($user, $keyPair['publicKey']);
diff --git a/lib/JWT.php b/lib/JWT.php
new file mode 100644
index 00000000..9d2e20aa
--- /dev/null
+++ b/lib/JWT.php
@@ -0,0 +1,57 @@
+
+ *
+ * @copyright Copyright (c) 2017, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see
+ *
+ */
+namespace OCA\Encryption;
+
+class JWT {
+ public static function base64UrlEncode($data) {
+ return \str_replace('=', '', \strtr(\base64_encode($data), '+/', '-_'));
+ }
+ /**
+ * @return string
+ */
+ public static function header() {
+ return self::base64UrlEncode(\json_encode([
+ 'typ' => 'JWT',
+ 'alg' => 'HS256'
+ ]));
+ }
+
+ /**
+ * @param array $header
+ * @return string
+ */
+ public static function payload($payload) {
+ $payload = \array_merge($payload, [
+ 'iat' => \time(),
+ 'jti' => \uniqid('', true)
+ ]);
+ return self::base64UrlEncode(\json_encode($payload));
+ }
+
+ public static function signature($data, $key) {
+ return self::base64UrlEncode(\hash_hmac('sha256', $data, $key, true));
+ }
+
+ public static function token($payload, $secret) {
+ $token = self::header().'.'.self::payload($payload);
+ return $token.'.'.self::signature($token, $secret);
+ }
+}
diff --git a/lib/KeyManager.php b/lib/KeyManager.php
index 4d341103..0a00a456 100644
--- a/lib/KeyManager.php
+++ b/lib/KeyManager.php
@@ -143,7 +143,7 @@ public function __construct(
public function validateShareKey() {
$shareKey = $this->getPublicShareKey();
if (empty($shareKey)) {
- $keyPair = $this->crypt->createKeyPair();
+ $keyPair = $this->crypt->createKeyPair("oc:".$this->publicShareKeyId);
// Save public key
$this->keyStorage->setSystemUserKey(
@@ -167,7 +167,7 @@ public function validateMasterKey() {
$masterKey = $this->getPublicMasterKey();
if (empty($masterKey)) {
- $keyPair = $this->crypt->createKeyPair();
+ $keyPair = $this->crypt->createKeyPair("oc:".$this->masterKeyId);
// Save public key
$this->keyStorage->setSystemUserKey(
diff --git a/lib/Recovery.php b/lib/Recovery.php
index 3d7cd641..99a810f7 100644
--- a/lib/Recovery.php
+++ b/lib/Recovery.php
@@ -107,7 +107,7 @@ public function enableAdminRecovery($password) {
$keyManager = $this->keyManager;
if (!$keyManager->recoveryKeyExists()) {
- $keyPair = $this->crypt->createKeyPair();
+ $keyPair = $this->crypt->createKeyPair("oc:".$this->keyManager->getRecoveryKeyId());
if (!\is_array($keyPair)) {
return false;
}
diff --git a/lib/Users/Setup.php b/lib/Users/Setup.php
index c831b263..5ae6af09 100644
--- a/lib/Users/Setup.php
+++ b/lib/Users/Setup.php
@@ -71,7 +71,7 @@ public function __construct(ILogger $logger,
public function setupUser($uid, $password) {
if (!$this->keyManager->userHasKeys($uid)) {
return $this->keyManager->storeKeyPair($uid, $password,
- $this->crypt->createKeyPair());
+ $this->crypt->createKeyPair("oc:uid:$uid"));
}
return true;
}
diff --git a/tests/unit/Command/HSMDaemonTest.php b/tests/unit/Command/HSMDaemonTest.php
new file mode 100644
index 00000000..26412110
--- /dev/null
+++ b/tests/unit/Command/HSMDaemonTest.php
@@ -0,0 +1,122 @@
+
+ *
+ * @copyright Copyright (c) 2019, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCA\Encryption\Tests\Command;
+
+use OCA\Encryption\Command\HSMDaemon;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Http\Client\IClientService;
+use GuzzleHttp\Client;
+use OCP\Http\Client\IResponse;
+use OCP\IConfig;
+use OCP\ILogger;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class HSMDaemonTest extends TestCase {
+ /** @var IClientService | \PHPUnit_Framework_MockObject_MockObject */
+ private $httpClient;
+ /** @var IConfig | \PHPUnit_Framework_MockObject_MockObject */
+ private $config;
+ /** @var ILogger | \PHPUnit_Framework_MockObject_MockObject */
+ private $logger;
+ /** @var ITimeFactory | \PHPUnit_Framework_MockObject_MockObject */
+ private $timeFactory;
+ /** @var HSMDaemon */
+ private $hsmDeamon;
+
+ public function setUp() {
+ parent::setUp();
+ $this->httpClient = $this->createMock(IClientService::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->logger = $this->createMock(ILogger::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+
+ $newClient = $this->createMock(Client::class);
+
+ $iResponse = $this->createMock(IResponse::class);
+ $iResponse->method('getBody')
+ ->willReturn(\base64_decode(\base64_encode("foo")));
+
+ $newClient->method('post')
+ ->willReturn($iResponse);
+
+ $this->httpClient->method('newClient')
+ ->willReturn($newClient);
+ $this->hsmDeamon = new HSMDaemon($this->httpClient, $this->config, $this->logger, $this->timeFactory);
+ }
+
+ public function testExecuteWithDecryptOption() {
+ $inputInterface = $this->createMock(InputInterface::class);
+ $inputInterface->method('getOption')
+ ->with('decrypt')
+ ->willReturn(true);
+
+ $outputInterface = $this->createMock(OutputInterface::class);
+ $outputInterface->expects($this->once())
+ ->method('writeln')
+ ->with("received: 'foo'");
+
+ $this->config->method('getAppValue')
+ ->willReturn('http://localhost:1234');
+
+ $iRespose = $this->createMock(IResponse::class);
+ $iRespose->method('getBody')
+ ->willReturn(\base64_decode(\base64_encode("foo")));
+
+ $this->invokePrivate($this->hsmDeamon, 'execute', [$inputInterface, $outputInterface]);
+ }
+
+ public function testExecuteWithExportMasterKey() {
+ $inputInterface = $this->createMock(InputInterface::class);
+ $inputInterface->method('getOption')
+ ->willReturnMap([
+ ['decrypt', false],
+ ['export-masterkey', true]
+ ]);
+
+ $outputInterface = $this->createMock(OutputInterface::class);
+ $outputInterface->expects($this->once())->method('writeln')
+ ->with("current masterkey (base64 encoded): ''");
+
+ $this->config->method('getAppValue')
+ ->willReturnMap([
+ ['encryption', 'hsm.url', '', 'http://localhost:1234'],
+ ['encryption', 'masterKeyId', '', 'abcd']
+ ]);
+
+ $this->invokePrivate($this->hsmDeamon, 'execute', [$inputInterface, $outputInterface]);
+ }
+
+ public function testExecuteFailsNoHSMURLSet() {
+ $inputInterface = $this->createMock(InputInterface::class);
+ $inputInterface->method('getOption')
+ ->willReturn(null);
+
+ $outputInterface = $this->createMock(OutputInterface::class);
+ $outputInterface->expects($this->once())
+ ->method('writeln')
+ ->with("hsm.url not set");
+
+ $this->invokePrivate($this->hsmDeamon, 'execute', [$inputInterface, $outputInterface]);
+ }
+}
diff --git a/tests/unit/Crypto/CryptHSMTest.php b/tests/unit/Crypto/CryptHSMTest.php
new file mode 100644
index 00000000..17e75baa
--- /dev/null
+++ b/tests/unit/Crypto/CryptHSMTest.php
@@ -0,0 +1,201 @@
+
+ *
+ * @copyright Copyright (c) 2019, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCA\Encryption\Tests\Crypto;
+
+use OCA\Encryption\Crypto\CryptHSM;
+use OCA\Encryption\Exceptions\MultiKeyDecryptException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Http\Client\IClientService;
+use GuzzleHttp\Client;
+use OCP\Http\Client\IResponse;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\ILogger;
+use OCP\IRequest;
+use OCP\IUserSession;
+use Test\TestCase;
+
+class CryptHSMTest extends TestCase {
+ /** @var IClientService | \PHPUnit_Framework_MockObject_MockObject */
+ private $clientService;
+ /** @var IRequest | \PHPUnit_Framework_MockObject_MockObject */
+ private $request;
+ /** @var ITimeFactory | \PHPUnit_Framework_MockObject_MockObject */
+ private $timeFactory;
+ /** @var ILogger | \PHPUnit_Framework_MockObject_MockObject */
+ private $logger;
+ /** @var IUserSession | \PHPUnit_Framework_MockObject_MockObject */
+ private $userSession;
+ /** @var IConfig | \PHPUnit_Framework_MockObject_MockObject */
+ private $config;
+ /** @var IL10N | \PHPUnit_Framework_MockObject_MockObject */
+ private $l10n;
+ /** @var CryptHSM */
+ private $cryptHSM;
+ public function setUp() {
+ parent::setUp();
+ $this->logger = $this->createMock(ILogger::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->clientService = $this->createMock(IClientService::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->cryptHSM = new CryptHSM($this->logger, $this->userSession, $this->config,
+ $this->l10n, $this->clientService, $this->request, $this->timeFactory);
+ }
+
+ public function testCreateKeyPair() {
+ $expectedResult = [
+ 'publicKey' => "-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxZlOnmM/+WJGyJ7hUaG1
+TycA8rOPtt1QDmmW7ML+MOWkKZ6TP9F8Q7buhlyt9O1uR1fy4czoM0uICU5LWQDh
+OgNgPwbfvXXuVgNbWtMZsYwaDbobyRbslemEM/hEOl/h1fNKlbv0TLc2+P8ycO9T
+ZeTcFCKCrYyNFmdekqufbHf2xMFWekarz8ikAfPwnsIX727TRMCDqUCVfVjBK6do
+dZo0ZQeZvutaQdCxYhg8muiQsd6puqv8p1Gp3BOZN5leyN+tAPjIwvyCC5RSc5IU
+kgVNdpawkWgQue4WcCmSt/4JGyQuLWcmq4lhEhJBNuFpIAnqlgAjEvwuPRaQLOmc
+lQIDAQAB
+-----END PUBLIC KEY-----",
+
+ "privateKeyId" => "50b959c0-1d6b-11e9-b127-18dbf216a375"
+ ];
+ $iResponse = $this->createMock(IResponse::class);
+ $iResponse->method('getBody')
+ ->willReturn(\json_encode($expectedResult));
+
+ $client = $this->createMock(Client::class);
+ $client->method('post')
+ ->willReturn($iResponse);
+
+ $this->clientService->method('newClient')
+ ->willReturn($client);
+
+ $result = $this->cryptHSM->createKeyPair('oc-auth-foo');
+ $expectedResult['privateKey'] = $expectedResult['privateKeyId'];
+ unset($expectedResult['privateKeyId']);
+ $this->assertEquals($expectedResult, $result);
+ }
+
+ public function testIsValidPrivateKey() {
+ $this->assertTrue($this->invokePrivate($this->cryptHSM, 'isValidPrivateKey', ['foo']));
+ }
+
+ public function providesMultiKeyDecrypt() {
+ return [
+ [false, 'foo', 'abcd'],
+ ['encKey', 'foo', 'abcd'],
+ ];
+ }
+ /**
+ * @dataProvider providesMultiKeyDecrypt
+ * @expectedException OCA\Encryption\Exceptions\MultiKeyDecryptException
+ */
+ public function testMultiKeyDecryptExceptions($encKeyFile, $shareKey, $privateKey) {
+ if (!$encKeyFile) {
+ $this->cryptHSM->multiKeyDecrypt($encKeyFile, $shareKey, $privateKey);
+ }
+
+ $expectedResult = [
+ "error"
+ ];
+
+ $iResponse = $this->createMock(IResponse::class);
+ if ($encKeyFile === 'encKey') {
+ $iResponse->method('getBody')
+ ->willThrowException(new MultiKeyDecryptException());
+ } else {
+ $iResponse->method('getBody')
+ ->willReturn($expectedResult);
+ }
+
+ $client = $this->createMock(Client::class);
+ $client->method('post')
+ ->willReturn($iResponse);
+
+ $this->clientService->method('newClient')
+ ->willReturn($client);
+
+ $this->cryptHSM->multiKeyDecrypt($encKeyFile, $shareKey, $privateKey);
+ }
+
+ public function testMultiKeyDecrypt() {
+ $cryptHSM = $this->getMockBuilder(CryptHSM::class)
+ ->setConstructorArgs([$this->logger, $this->userSession, $this->config,
+ $this->l10n, $this->clientService, $this->request, $this->timeFactory])
+ ->setMethods(['symmetricDecryptFileContent'])
+ ->getMock();
+
+ $iResponse = $this->createMock(IResponse::class);
+ $iResponse->method('getBody')
+ ->willReturn(\json_encode(['foo' => 'bar']));
+
+ $client = $this->createMock(Client::class);
+ $client->method('post')
+ ->willReturn($iResponse);
+
+ $this->clientService->method('newClient')
+ ->willReturn($client);
+ $cryptHSM->method('symmetricDecryptFileContent')
+ ->willReturn('key');
+ $result = $cryptHSM->multiKeyDecrypt('encKeyFile', 'foo', 'abcd');
+ $this->assertEquals('key', $result);
+ }
+
+ /**
+ * @expectedException OCA\Encryption\Exceptions\MultiKeyEncryptException
+ * @expectedExceptionMessage Could not create sealed content
+ */
+ public function testMultiKeyEncryptException() {
+ $cryptHSM = $this->getMockBuilder(CryptHSM::class)
+ ->setConstructorArgs([$this->logger, $this->userSession, $this->config,
+ $this->l10n, $this->clientService, $this->request, $this->timeFactory])
+ ->setMethods(['symmetricEncryptFileContent'])
+ ->getMock();
+
+ $cryptHSM->method('symmetricEncryptFileContent')
+ ->willReturn(false);
+ $cryptHSM->multiKeyEncrypt('foo bar', ["AAABBBCCC", "AABBCC"]);
+ }
+
+ public function testMultiKeyEncrypt() {
+ $cryptHSM = $this->getMockBuilder(CryptHSM::class)
+ ->setConstructorArgs([$this->logger, $this->userSession, $this->config,
+ $this->l10n, $this->clientService, $this->request, $this->timeFactory])
+ ->setMethods(['symmetricEncryptFileContent'])
+ ->getMock();
+
+ $cryptHSM->method('symmetricEncryptFileContent')
+ ->willReturn("sealed");
+ $result = $cryptHSM->multiKeyEncrypt('foo bar', ["foo" => "-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmHzD76i8DA25nC+Qsswi
+OM0lW+gViiQD4tEm7suxBc2BGibtdlrsprVIId92hSjQKx4x8+XVWU6k89T5vy8Y
+txpXN759OWdGkDi8uvZuYclMjW9Rao+oqSvbXH37R7oSY287I+6uOHclGhniQN3q
+RyoXBkbhDk0/FTI/i549q/gGk1UZYv449KLrDOqmtohRcIyAYVnvvWtD1kIzourq
+hMtEIrPqwoBqTaUA9kOIXw1jMovao2TN52j48KgOg9KjqtdwUwD9e6n7hJd/subF
+6woc8L7zjJFOHH5gacUC7vtiMpBpnSyLQpjFLepYYwftjsRmg4xLdh+Zvgw3xqi4
+lwIDAQAB
+-----END PUBLIC KEY-----"]);
+ $this->assertArrayHasKey('keys', $result);
+ $this->assertArrayHasKey('data', $result);
+ $this->assertEquals('sealed', $result['data']);
+ }
+}
diff --git a/tests/unit/JWTTest.php b/tests/unit/JWTTest.php
new file mode 100644
index 00000000..cec9294e
--- /dev/null
+++ b/tests/unit/JWTTest.php
@@ -0,0 +1,63 @@
+
+ *
+ * @copyright Copyright (c) 2019, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCA\Encryption\Tests;
+
+use OCA\Encryption\JWT;
+use Test\TestCase;
+
+class JWTTest extends TestCase {
+ /** @var JWT | \PHPUnit_Framework_MockObject_MockObject */
+ private $jwt;
+
+ public function setUp() {
+ parent::setUp();
+ $this->jwt = new JWT();
+ }
+
+ public function testBase64UrlEncode() {
+ $expectedResult = "Zm9v";
+ $result = $this->jwt->base64UrlEncode('foo');
+ $this->assertEquals($expectedResult, $result);
+ }
+
+ public function testHeader() {
+ $expectedResult = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9";
+ $result = $this->jwt->header();
+ $this->assertEquals($expectedResult, $result);
+ }
+
+ public function testPayload() {
+ $this->jwt->payload(['foo' => 'bar']);
+ $this->assertTrue(true);
+ }
+
+ public function testSignature() {
+ $expected = 'kFaZPruDYO6RIqibVuF8UqWucdn_Ig0PoCZ8Sq2r_X8';
+ $result = $this->jwt->signature('foo', 'abcd');
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testToken() {
+ $this->jwt->token(['foo' => 'bar'], 'secret');
+ $this->assertTrue(true);
+ }
+}