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); + } +}