Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<command>OCA\Encryption\Command\SelectEncryptionType</command>
<command>OCA\Encryption\Command\RecreateMasterKey</command>
<command>OCA\Encryption\Command\MigrateKeys</command>
<command>OCA\Encryption\Command\HSMDaemon</command>
</commands>
<settings>
<admin>OCA\Encryption\Panels\Admin</admin>
Expand Down
25 changes: 20 additions & 5 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down
119 changes: 119 additions & 0 deletions lib/Command/HSMDaemon.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php
/**
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
*
* @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 <http://www.gnu.org/licenses/>
*
*/

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("<error>hsm.url not set</error>");
}
}
}
4 changes: 2 additions & 2 deletions lib/Crypto/Crypt.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
215 changes: 215 additions & 0 deletions lib/Crypto/CryptHSM.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
<?php
/**
* @author Björn Schießle <bjoern@schiessle.org>
* @author Clark Tomlinson <fallen013@gmail.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @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 <http://www.gnu.org/licenses/>
*
*/

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
];
}
}
Loading