diff options
Diffstat (limited to 'lib/private/Security/IdentityProof')
-rw-r--r-- | lib/private/Security/IdentityProof/Key.php | 25 | ||||
-rw-r--r-- | lib/private/Security/IdentityProof/Manager.php | 190 | ||||
-rw-r--r-- | lib/private/Security/IdentityProof/Signer.php | 69 |
3 files changed, 284 insertions, 0 deletions
diff --git a/lib/private/Security/IdentityProof/Key.php b/lib/private/Security/IdentityProof/Key.php new file mode 100644 index 00000000000..0bfcd6bf9ed --- /dev/null +++ b/lib/private/Security/IdentityProof/Key.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Security\IdentityProof; + +class Key { + public function __construct( + private string $publicKey, + private string $privateKey, + ) { + } + + public function getPrivate(): string { + return $this->privateKey; + } + + public function getPublic(): string { + return $this->publicKey; + } +} diff --git a/lib/private/Security/IdentityProof/Manager.php b/lib/private/Security/IdentityProof/Manager.php new file mode 100644 index 00000000000..c16b8314beb --- /dev/null +++ b/lib/private/Security/IdentityProof/Manager.php @@ -0,0 +1,190 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Security\IdentityProof; + +use OC\Files\AppData\Factory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IUser; +use OCP\Security\ICrypto; +use Psr\Log\LoggerInterface; + +class Manager { + private IAppData $appData; + + protected ICache $cache; + + public function __construct( + Factory $appDataFactory, + private ICrypto $crypto, + private IConfig $config, + private LoggerInterface $logger, + private ICacheFactory $cacheFactory, + ) { + $this->appData = $appDataFactory->get('identityproof'); + $this->cache = $this->cacheFactory->createDistributed('identityproof::'); + } + + /** + * Calls the openssl functions to generate a public and private key. + * In a separate function for unit testing purposes. + * + * @param array $options config options to generate key {@see openssl_csr_new} + * + * @return array [$publicKey, $privateKey] + * @throws \RuntimeException + */ + protected function generateKeyPair(array $options = []): array { + $config = [ + 'digest_alg' => $options['algorithm'] ?? 'sha512', + 'private_key_bits' => $options['bits'] ?? 2048, + 'private_key_type' => $options['type'] ?? OPENSSL_KEYTYPE_RSA, + ]; + + // Generate new key + $res = openssl_pkey_new($config); + if ($res === false) { + $this->logOpensslError(); + throw new \RuntimeException('OpenSSL reported a problem'); + } + + if (openssl_pkey_export($res, $privateKey, null, $config) === false) { + $this->logOpensslError(); + throw new \RuntimeException('OpenSSL reported a problem'); + } + + // Extract the public key from $res to $pubKey + $publicKey = openssl_pkey_get_details($res); + $publicKey = $publicKey['key']; + + return [$publicKey, $privateKey]; + } + + /** + * Generate a key for a given ID + * Note: If a key already exists it will be overwritten + * + * @param string $id key id + * @param array $options config options to generate key {@see openssl_csr_new} + * + * @throws \RuntimeException + */ + protected function generateKey(string $id, array $options = []): Key { + [$publicKey, $privateKey] = $this->generateKeyPair($options); + + // Write the private and public key to the disk + try { + $this->appData->newFolder($id); + } catch (\Exception) { + } + $folder = $this->appData->getFolder($id); + $folder->newFile('private') + ->putContent($this->crypto->encrypt($privateKey)); + $folder->newFile('public') + ->putContent($publicKey); + + return new Key($publicKey, $privateKey); + } + + /** + * Get key for a specific id + * + * @throws \RuntimeException + */ + protected function retrieveKey(string $id): Key { + try { + $cachedPublicKey = $this->cache->get($id . '-public'); + $cachedPrivateKey = $this->cache->get($id . '-private'); + + if ($cachedPublicKey !== null && $cachedPrivateKey !== null) { + $decryptedPrivateKey = $this->crypto->decrypt($cachedPrivateKey); + + return new Key($cachedPublicKey, $decryptedPrivateKey); + } + + $folder = $this->appData->getFolder($id); + $privateKey = $folder->getFile('private')->getContent(); + $publicKey = $folder->getFile('public')->getContent(); + + $this->cache->set($id . '-public', $publicKey); + $this->cache->set($id . '-private', $privateKey); + + $decryptedPrivateKey = $this->crypto->decrypt($privateKey); + return new Key($publicKey, $decryptedPrivateKey); + } catch (\Exception $e) { + return $this->generateKey($id); + } + } + + /** + * Get public and private key for $user + * + * @throws \RuntimeException + */ + public function getKey(IUser $user): Key { + $uid = $user->getUID(); + return $this->retrieveKey('user-' . $uid); + } + + /** + * Get instance wide public and private key + * + * @throws \RuntimeException + */ + public function getSystemKey(): Key { + $instanceId = $this->config->getSystemValue('instanceid', null); + if ($instanceId === null) { + throw new \RuntimeException('no instance id!'); + } + return $this->retrieveKey('system-' . $instanceId); + } + + public function hasAppKey(string $app, string $name): bool { + $id = $this->generateAppKeyId($app, $name); + try { + $folder = $this->appData->getFolder($id); + return ($folder->fileExists('public') && $folder->fileExists('private')); + } catch (NotFoundException) { + return false; + } + } + + public function getAppKey(string $app, string $name): Key { + return $this->retrieveKey($this->generateAppKeyId($app, $name)); + } + + public function generateAppKey(string $app, string $name, array $options = []): Key { + return $this->generateKey($this->generateAppKeyId($app, $name), $options); + } + + public function deleteAppKey(string $app, string $name): bool { + try { + $folder = $this->appData->getFolder($this->generateAppKeyId($app, $name)); + $folder->delete(); + return true; + } catch (NotFoundException) { + return false; + } + } + + private function generateAppKeyId(string $app, string $name): string { + return 'app-' . $app . '-' . $name; + } + + private function logOpensslError(): void { + $errors = []; + while ($error = openssl_error_string()) { + $errors[] = $error; + } + $this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors)); + } +} diff --git a/lib/private/Security/IdentityProof/Signer.php b/lib/private/Security/IdentityProof/Signer.php new file mode 100644 index 00000000000..6083cbb5c9b --- /dev/null +++ b/lib/private/Security/IdentityProof/Signer.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Security\IdentityProof; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IUser; +use OCP\IUserManager; + +class Signer { + public function __construct( + private Manager $keyManager, + private ITimeFactory $timeFactory, + private IUserManager $userManager, + ) { + } + + /** + * Returns a signed blob for $data + * + * @return array ['message', 'signature'] + */ + public function sign(string $type, array $data, IUser $user): array { + $privateKey = $this->keyManager->getKey($user)->getPrivate(); + $data = [ + 'data' => $data, + 'type' => $type, + 'signer' => $user->getCloudId(), + 'timestamp' => $this->timeFactory->getTime(), + ]; + openssl_sign(json_encode($data), $signature, $privateKey, OPENSSL_ALGO_SHA512); + + return [ + 'message' => $data, + 'signature' => base64_encode($signature), + ]; + } + + /** + * Whether the data is signed properly + * + */ + public function verify(array $data): bool { + if (isset($data['message']['signer']) + && isset($data['signature']) + ) { + $location = strrpos($data['message']['signer'], '@'); + $userId = substr($data['message']['signer'], 0, $location); + + $user = $this->userManager->get($userId); + if ($user !== null) { + $key = $this->keyManager->getKey($user); + return openssl_verify( + json_encode($data['message']), + base64_decode($data['signature']), + $key->getPublic(), + OPENSSL_ALGO_SHA512 + ) === 1; + } + } + + return false; + } +} |