*/
namespace OCA\CloudFederationAPI;
-use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairException;
+use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
use NCU\Security\Signature\Exceptions\SignatoryException;
use OC\OCM\OCMSignatoryManager;
use OCP\Capabilities\ICapability;
} else {
$this->logger->debug('ocm public key feature disabled');
}
- } catch (SignatoryException|KeyPairException $e) {
+ } catch (SignatoryException|IdentityNotFoundException $e) {
$this->logger->warning('cannot generate local signatory', ['exception' => $e]);
}
*/
namespace OCA\CloudFederationAPI\Controller;
+use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
use NCU\Security\Signature\Exceptions\IncomingRequestException;
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
use NCU\Security\Signature\Exceptions\SignatureException;
use OC\OCM\OCMSignatoryManager;
use OCA\CloudFederationAPI\Config;
use OCA\CloudFederationAPI\ResponseDefinitions;
+use OCA\FederatedFileSharing\AddressHandler;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\BruteForceProtection;
private IURLGenerator $urlGenerator,
private ICloudFederationProviderManager $cloudFederationProviderManager,
private Config $config,
+ private readonly AddressHandler $addressHandler,
private readonly IAppConfig $appConfig,
private ICloudFederationFactory $factory,
private ICloudIdManager $cloudIdManager,
$response->throttle();
return $response;
} catch (\Exception $e) {
+ $this->logger->warning('incoming notification exception', ['exception' => $e]);
return new JSONResponse(
[
'message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl(),
$body = json_decode($signedRequest->getBody(), true) ?? [];
$entry = trim($body[$key] ?? '', '@');
if ($this->getHostFromFederationId($entry) !== $signedRequest->getOrigin()) {
- throw new IncomingRequestException('share initiation from different instance');
+ throw new IncomingRequestException('share initiation (' . $signedRequest->getOrigin() . ') from different instance (' . $entry . ') [key=' . $key . ']');
}
}
* @param IIncomingSignedRequest|null $signedRequest
* @param string $token
*
- * @return void
* @throws IncomingRequestException
*/
private function confirmShareOrigin(?IIncomingSignedRequest $signedRequest, string $token): void {
$provider = $this->shareProviderFactory->getProviderForType(IShare::TYPE_REMOTE);
$share = $provider->getShareByToken($token);
- $entry = $share->getSharedWith();
+ try {
+ $this->confirmShareEntry($signedRequest, $share->getSharedWith());
+ } catch (IncomingRequestException) {
+ // notification might come from the instance that owns the share
+ $this->logger->debug('could not confirm origin on sharedWith (' . $share->getSharedWIth() . '); going with shareOwner (' . $share->getShareOwner() . ')');
+ $this->confirmShareEntry($signedRequest, $share->getShareOwner());
+ }
+ }
+ /**
+ * @param IIncomingSignedRequest|null $signedRequest
+ * @param string $entry
+ *
+ * @return void
+ * @throws IncomingRequestException
+ */
+ private function confirmShareEntry(?IIncomingSignedRequest $signedRequest, string $entry): void {
$instance = $this->getHostFromFederationId($entry);
if ($signedRequest === null) {
try {
return;
}
} elseif ($instance !== $signedRequest->getOrigin()) {
- throw new IncomingRequestException('token sharedWith from different instance');
+ throw new IncomingRequestException('token sharedWith (' . $instance . ') not linked to origin (' . $signedRequest->getOrigin() . ')');
}
}
*/
private function getHostFromFederationId(string $entry): string {
if (!str_contains($entry, '@')) {
- throw new IncomingRequestException('entry does not contains @');
+ throw new IncomingRequestException('entry ' . $entry . ' does not contains @');
}
- [, $rightPart] = explode('@', $entry, 2);
+ $rightPart = substr($entry, strrpos($entry, '@') + 1);
- $host = parse_url($rightPart, PHP_URL_HOST);
- $port = parse_url($rightPart, PHP_URL_PORT);
- if ($port !== null && $port !== false) {
- $host .= ':' . $port;
- }
-
- if (is_string($host) && $host !== '') {
- return $host;
+ // in case the full scheme is sent; getting rid of it
+ $rightPart = $this->addressHandler->removeProtocolFromUrl($rightPart);
+ try {
+ return $this->signatureManager->extractIdentityFromUri('https://' . $rightPart);
+ } catch (IdentityNotFoundException) {
+ throw new IncomingRequestException('invalid host within federation id: ' . $entry);
}
-
- throw new IncomingRequestException('host is empty');
}
}
$remote,
$shareWith,
$share->getPermissions(),
- $share->getNode()->getName()
+ $share->getNode()->getName(),
+ $share->getShareType(),
);
return [$token, $remoteId];
* @throws HintException
* @throws \OC\ServerNotAvailableException
*/
- public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission, $filename) {
+ public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission, $filename, $shareType) {
$fields = [
'shareWith' => $shareWith,
'token' => $token,
'permission' => $permission,
'remoteId' => $shareId,
+ 'shareType' => $shareType,
];
$ocmFields = $fields;
$port = getenv('PORT_FED');
- self::$phpFederatedServerPid = exec('php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!');
+ self::$phpFederatedServerPid = exec('PHP_CLI_SERVER_WORKERS=2 php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!');
}
/**
Background:
Given using api version "1"
+ Scenario: cleanup remote storage with no storage
+ Given Using server "LOCAL"
+ And user "user0" exists
+ Given Using server "REMOTE"
+ And user "user1" exists
+ # Rename file so it has a unique name in the target server (as the target
+ # server may have its own /textfile0.txt" file)
+ And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
+ And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
+ And As an "user1"
+ And Deleting last share
+ And the OCS status code should be "100"
+ And the HTTP status code should be "200"
+ And Deleting last share
+ And Using server "LOCAL"
+ When invoking occ with "sharing:cleanup-remote-storage"
+ Then the command was successful
+ And the command output contains the text "0 remote storage(s) need(s) to be checked"
+ And the command output contains the text "0 remote share(s) exist"
+ And the command output contains the text "no storages deleted"
+
Scenario: cleanup remote storage with active storages
Given Using server "LOCAL"
And user "user0" exists
# server may have its own /textfile0.txt" file)
And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
- And As an "user1"
- And sending "GET" to "/apps/files_sharing/api/v1/shares"
- And the list of returned shares has 1 shares
And Using server "LOCAL"
# Accept and download the file to ensure that a storage is created for the
# federated share
'OC\\Security\\Ip\\Range' => $baseDir . '/lib/private/Security/Ip/Range.php',
'OC\\Security\\Ip\\RemoteAddress' => $baseDir . '/lib/private/Security/Ip/RemoteAddress.php',
'OC\\Security\\Normalizer\\IpAddress' => $baseDir . '/lib/private/Security/Normalizer/IpAddress.php',
- 'OC\\Security\\PublicPrivateKeyPairs\\KeyPairManager' => $baseDir . '/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php',
- 'OC\\Security\\PublicPrivateKeyPairs\\Model\\KeyPair' => $baseDir . '/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php',
'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php',
'OC\\Security\\RateLimiting\\Backend\\IBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/IBackend.php',
'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php',
'OC\\Security\\Ip\\Range' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/Range.php',
'OC\\Security\\Ip\\RemoteAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/RemoteAddress.php',
'OC\\Security\\Normalizer\\IpAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Normalizer/IpAddress.php',
- 'OC\\Security\\PublicPrivateKeyPairs\\KeyPairManager' => __DIR__ . '/../../..' . '/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php',
- 'OC\\Security\\PublicPrivateKeyPairs\\Model\\KeyPair' => __DIR__ . '/../../..' . '/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php',
'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php',
'OC\\Security\\RateLimiting\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/IBackend.php',
'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php',
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Federation\ICloudFederationShare;
use OCP\Federation\ICloudIdManager;
+use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
use OCP\IAppConfig;
public function sendShare(ICloudFederationShare $share) {
$cloudID = $this->cloudIdManager->resolveCloudId($share->getShareWith());
try {
- $ocmProvider = $this->discoveryService->discover($cloudID->getRemote());
- } catch (OCMProviderException $e) {
- return false;
- }
-
- $client = $this->httpClientService->newClient();
- try {
- // signing the payload using OCMSignatoryManager before initializing the request
- $uri = $ocmProvider->getEndPoint() . '/shares';
- $payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($share->getShare())]);
- if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
- $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
- $this->signatoryManager,
- $payload,
- 'post', $uri
- );
+ try {
+ $response = $this->postOcmPayload($cloudID->getRemote(), '/shares', json_encode($share->getShare()));
+ } catch (OCMProviderException) {
+ return false;
}
- $response = $client->post($uri, $signedPayload ?? $payload);
-
if ($response->getStatusCode() === Http::STATUS_CREATED) {
$result = json_decode($response->getBody(), true);
return (is_array($result)) ? $result : [];
*/
public function sendCloudShare(ICloudFederationShare $share): IResponse {
$cloudID = $this->cloudIdManager->resolveCloudId($share->getShareWith());
- $ocmProvider = $this->discoveryService->discover($cloudID->getRemote());
-
$client = $this->httpClientService->newClient();
try {
- // signing the payload using OCMSignatoryManager before initializing the request
- $uri = $ocmProvider->getEndPoint() . '/shares';
- $payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($share->getShare())]);
- if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
- $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
- $this->signatoryManager,
- $payload,
- 'post', $uri
- );
- }
-
- return $client->post($uri, $signedPayload ?? $payload);
+ return $this->postOcmPayload($cloudID->getRemote(), '/shares', json_encode($share->getShare()), $client);
} catch (\Throwable $e) {
$this->logger->error('Error while sending share to federation server: ' . $e->getMessage(), ['exception' => $e]);
try {
*/
public function sendNotification($url, ICloudFederationNotification $notification) {
try {
- $ocmProvider = $this->discoveryService->discover($url);
- } catch (OCMProviderException $e) {
- return false;
- }
-
- $client = $this->httpClientService->newClient();
- try {
-
- // signing the payload using OCMSignatoryManager before initializing the request
- $uri = $ocmProvider->getEndPoint() . '/notifications';
- $payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($notification->getMessage())]);
- if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
- $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
- $this->signatoryManager,
- $payload,
- 'post', $uri
- );
+ try {
+ $response = $this->postOcmPayload($url, '/notifications', json_encode($notification->getMessage()));
+ } catch (OCMProviderException) {
+ return false;
}
- $response = $client->post($uri, $signedPayload ?? $payload);
-
if ($response->getStatusCode() === Http::STATUS_CREATED) {
$result = json_decode($response->getBody(), true);
return (is_array($result)) ? $result : [];
* @throws OCMProviderException
*/
public function sendCloudNotification(string $url, ICloudFederationNotification $notification): IResponse {
- $ocmProvider = $this->discoveryService->discover($url);
-
$client = $this->httpClientService->newClient();
try {
- // signing the payload using OCMSignatoryManager before initializing the request
- $uri = $ocmProvider->getEndPoint() . '/notifications';
- $payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($notification->getMessage())]);
- if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
- $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
- $this->signatoryManager,
- $payload,
- 'post', $uri
- );
- }
- return $client->post($uri, $signedPayload ?? $payload);
+ return $this->postOcmPayload($url, '/notifications', json_encode($notification->getMessage()), $client);
} catch (\Throwable $e) {
$this->logger->error('Error while sending notification to federation server: ' . $e->getMessage(), ['exception' => $e]);
try {
return $this->appManager->isEnabledForUser('cloud_federation_api');
}
+ /**
+ * @param string $cloudId
+ * @param string $uri
+ * @param string $payload
+ *
+ * @return IResponse
+ * @throws OCMProviderException
+ */
+ private function postOcmPayload(string $cloudId, string $uri, string $payload, ?IClient $client = null): IResponse {
+ $ocmProvider = $this->discoveryService->discover($cloudId);
+ $uri = $ocmProvider->getEndPoint() . '/' . ltrim($uri, '/');
+ $client = $client ?? $this->httpClientService->newClient();
+ return $client->post($uri, $this->prepareOcmPayload($uri, $payload));
+ }
+
+ /**
+ * @param string $uri
+ * @param string $payload
+ *
+ * @return array
+ */
+ private function prepareOcmPayload(string $uri, string $payload): array {
+ $payload = array_merge($this->getDefaultRequestOptions(), ['body' => $payload]);
+ if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
+ $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
+ $this->signatoryManager,
+ $payload,
+ 'post', $uri
+ );
+ }
+
+ return $signedPayload ?? $payload;
+ }
+
private function getDefaultRequestOptions(): array {
return [
'headers' => ['content-type' => 'application/json'],
protected $httpClientService;
/** @var ICertificateManager */
protected $certManager;
- protected bool $verify = true;
protected LoggerInterface $logger;
protected IEventLogger $eventLogger;
protected IMimeTypeDetector $mimeTypeDetector;
if (isset($parameters['authType'])) {
$this->authType = $parameters['authType'];
}
- $this->verify = (($parameters['verify'] ?? true) !== false);
if (isset($parameters['secure'])) {
if (is_string($parameters['secure'])) {
$this->secure = ($parameters['secure'] === 'true');
}
}
- if (!$this->verify) {
- $this->client->addCurlSetting(CURLOPT_SSL_VERIFYHOST, 0);
- $this->client->addCurlSetting(CURLOPT_SSL_VERIFYPEER, false);
- }
-
$lastRequestStart = 0;
$this->client->on('beforeRequest', function (RequestInterface $request) use (&$lastRequestStart) {
$this->logger->debug('sending dav ' . $request->getMethod() . ' request to external storage: ' . $request->getAbsoluteUrl(), ['app' => 'dav']);
* apiVersion: '1.0-proposal1',
* endPoint: string,
* publicKey: ISignatory|null,
- * resourceTypes: array{
+ * resourceTypes: list<array{
* name: string,
* shareTypes: list<string>,
* protocols: array<string, string>
- * }[],
+ * }>,
* version: string
* }
*/
*/
public function discover(string $remote, bool $skipCache = false): IOCMProvider {
$remote = rtrim($remote, '/');
+ if (!str_starts_with($remote, 'http://') && !str_starts_with($remote, 'https://')) {
+ // if scheme not specified, we test both;
+ try {
+ return $this->discover('https://' . $remote, $skipCache);
+ } catch (OCMProviderException) {
+ return $this->discover('http://' . $remote, $skipCache);
+ }
+ }
if (!$skipCache) {
try {
if ($this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates') === true) {
$options['verify'] = false;
}
- $response = $client->get(
- $remote . '/ocm-provider/',
- $options,
- );
+ $response = $client->get($remote . '/ocm-provider/', $options);
if ($response->getStatusCode() === Http::STATUS_OK) {
$body = $response->getBody();
*/
namespace OC\OCM;
-use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairConflictException;
-use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairNotFoundException;
-use NCU\Security\PublicPrivateKeyPairs\IKeyPairManager;
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
use NCU\Security\Signature\ISignatoryManager;
use NCU\Security\Signature\ISignatureManager;
use NCU\Security\Signature\Model\IIncomingSignedRequest;
use NCU\Security\Signature\Model\ISignatory;
use NCU\Security\Signature\Model\SignatoryType;
+use OC\Security\IdentityProof\Manager;
use OC\Security\Signature\Model\Signatory;
use OCP\IAppConfig;
use OCP\IURLGenerator;
private readonly IAppConfig $appConfig,
private readonly ISignatureManager $signatureManager,
private readonly IURLGenerator $urlGenerator,
- private readonly IKeyPairManager $keyPairManager,
+ private readonly Manager $identityProofManager,
private readonly OCMDiscoveryService $ocmDiscoveryService,
) {
}
* @inheritDoc
*
* @return ISignatory
- * @throws KeyPairConflictException
* @throws IdentityNotFoundException
* @since 31.0.0
*/
$keyId = $this->generateKeyId();
}
- try {
- $keyPair = $this->keyPairManager->getKeyPair('core', 'ocm_external');
- } catch (KeyPairNotFoundException) {
- $keyPair = $this->keyPairManager->generateKeyPair('core', 'ocm_external');
+ if (!$this->identityProofManager->hasAppKey('core', 'ocm_external')) {
+ $this->identityProofManager->generateAppKey('core', 'ocm_external', [
+ 'algorithm' => 'rsa',
+ 'private_key_bits' => 2048,
+ 'private_key_type' => OPENSSL_KEYTYPE_RSA,
+ ]);
}
+ $keyPair = $this->identityProofManager->getAppKey('core', 'ocm_external');
- return new Signatory($keyId, $keyPair->getPublicKey(), $keyPair->getPrivateKey(), local: true);
+ return new Signatory($keyId, $keyPair->getPublic(), $keyPair->getPrivate(), local: true);
}
/**
use OC\Files\AppData\Factory;
use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\IUser;
use OCP\Security\ICrypto;
* 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 {
+ protected function generateKeyPair(array $options = []): array {
$config = [
- 'digest_alg' => 'sha512',
- 'private_key_bits' => 2048,
+ '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');
* 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): Key {
- [$publicKey, $privateKey] = $this->generateKeyPair();
+ 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 $e) {
+ } catch (\Exception) {
}
$folder = $this->appData->getFolder($id);
$folder->newFile('private')
return $this->retrieveKey('system-' . $instanceId);
}
+ public function hasAppKey(string $app, string $name): bool {
+ $id = $this->generateAppKeyId($app, $name);
+ try {
+ $this->appData->getFolder($id);
+ return true;
+ } 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));
+ } catch (NotFoundException) {
+ return false;
+ }
+ $folder->delete();
+ return true;
+ }
+
+ private function generateAppKeyId(string $app, string $name): string {
+ return 'app-' . $app . '-' . $name;
+ }
+
private function logOpensslError(): void {
$errors = [];
while ($error = openssl_error_string()) {
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-namespace OC\Security\PublicPrivateKeyPairs;
-
-use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairConflictException;
-use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairNotFoundException;
-use NCU\Security\PublicPrivateKeyPairs\IKeyPairManager;
-use NCU\Security\PublicPrivateKeyPairs\Model\IKeyPair;
-use OC\Security\PublicPrivateKeyPairs\Model\KeyPair;
-use OCP\IAppConfig;
-
-/**
- * @inheritDoc
- *
- * KeyPairManager store internal public/private key pair using AppConfig, taking advantage of the encryption
- * and lazy loading.
- *
- * @since 31.0.0
- */
-class KeyPairManager implements IKeyPairManager {
- private const CONFIG_PREFIX = 'security.keypair.';
-
- public function __construct(
- private readonly IAppConfig $appConfig,
- ) {
- }
-
- /**
- * @inheritDoc
- *
- * @param string $app appId
- * @param string $name key name
- * @param array $options algorithms, metadata
- *
- * @return IKeyPair
- * @throws KeyPairConflictException if a key already exist
- * @since 31.0.0
- */
- public function generateKeyPair(string $app, string $name, array $options = []): IKeyPair {
- if ($this->hasKeyPair($app, $name)) {
- throw new KeyPairConflictException('key pair already exist');
- }
-
- $keyPair = new KeyPair($app, $name);
-
- [$publicKey, $privateKey] = $this->generateKeys($options);
- $keyPair->setPublicKey($publicKey)
- ->setPrivateKey($privateKey)
- ->setOptions($options);
-
- $this->appConfig->setValueArray(
- $app, $this->generateAppConfigKey($name),
- [
- 'public' => $keyPair->getPublicKey(),
- 'private' => $keyPair->getPrivateKey(),
- 'options' => $keyPair->getOptions()
- ],
- lazy: true,
- sensitive: true
- );
-
- return $keyPair;
- }
-
- /**
- * @inheritDoc
- *
- * @param string $app appId
- * @param string $name key name
- *
- * @return bool TRUE if key pair exists in database
- * @since 31.0.0
- */
- public function hasKeyPair(string $app, string $name): bool {
- $key = $this->generateAppConfigKey($name);
- return $this->appConfig->hasKey($app, $key, lazy: true);
- }
-
- /**
- * @inheritDoc
- *
- * @param string $app appId
- * @param string $name key name
- *
- * @return IKeyPair
- * @throws KeyPairNotFoundException if key pair is not known
- * @since 31.0.0
- */
- public function getKeyPair(string $app, string $name): IKeyPair {
- if (!$this->hasKeyPair($app, $name)) {
- throw new KeyPairNotFoundException('unknown key pair');
- }
-
- $key = $this->generateAppConfigKey($name);
- $stored = $this->appConfig->getValueArray($app, $key, lazy: true);
- if (!array_key_exists('public', $stored) ||
- !array_key_exists('private', $stored)) {
- throw new KeyPairNotFoundException('corrupted key pair');
- }
-
- $keyPair = new KeyPair($app, $name);
- return $keyPair->setPublicKey($stored['public'])
- ->setPrivateKey($stored['private'])
- ->setOptions($stored['options'] ?? []);
- }
-
- /**
- * @inheritDoc
- *
- * @param string $app appid
- * @param string $name key name
- *
- * @since 31.0.0
- */
- public function deleteKeyPair(string $app, string $name): void {
- $this->appConfig->deleteKey('core', $this->generateAppConfigKey($name));
- }
-
- /**
- * @inheritDoc
- *
- * @param IKeyPair $keyPair keypair to test
- *
- * @return bool
- * @since 31.0.0
- */
- public function testKeyPair(IKeyPair $keyPair): bool {
- $clear = md5((string)time());
-
- // signing with private key
- openssl_sign($clear, $signed, $keyPair->getPrivateKey(), OPENSSL_ALGO_SHA256);
- $encoded = base64_encode($signed);
-
- // verify with public key
- $signed = base64_decode($encoded);
- return (openssl_verify($clear, $signed, $keyPair->getPublicKey(), 'sha256') === 1);
- }
-
- /**
- * return appconfig key based on name of the key pair
- *
- * @param string $name
- *
- * @return string
- */
- private function generateAppConfigKey(string $name): string {
- return self::CONFIG_PREFIX . $name;
- }
-
- /**
- * generate the key pair, based on $options with the following default values:
- * [
- * 'algorithm' => 'rsa',
- * 'bits' => 2048,
- * 'type' => OPENSSL_KEYTYPE_RSA
- * ]
- *
- * @param array $options
- *
- * @return array
- */
- private function generateKeys(array $options = []): array {
- $res = openssl_pkey_new(
- [
- 'digest_alg' => $options['algorithm'] ?? 'rsa',
- 'private_key_bits' => $options['bits'] ?? 2048,
- 'private_key_type' => $options['type'] ?? OPENSSL_KEYTYPE_RSA,
- ]
- );
-
- openssl_pkey_export($res, $privateKey);
- $publicKey = openssl_pkey_get_details($res)['key'];
-
- return [$publicKey, $privateKey];
- }
-}
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-namespace OC\Security\PublicPrivateKeyPairs\Model;
-
-use NCU\Security\PublicPrivateKeyPairs\Model\IKeyPair;
-
-/**
- * @inheritDoc
- *
- * @since 31.0.0
- */
-class KeyPair implements IKeyPair {
- private string $publicKey = '';
- private string $privateKey = '';
- private array $options = [];
-
- public function __construct(
- private readonly string $app,
- private readonly string $name,
- ) {
- }
-
- /**
- * @inheritDoc
- *
- * @return string
- * @since 31.0.0
- */
- public function getApp(): string {
- return $this->app;
- }
-
- /**
- * @inheritDoc
- *
- * @return string
- * @since 31.0.0
- */
- public function getName(): string {
- return $this->name;
- }
-
- /**
- * @inheritDoc
- *
- * @param string $publicKey
- * @return IKeyPair
- * @since 31.0.0
- */
- public function setPublicKey(string $publicKey): IKeyPair {
- $this->publicKey = $publicKey;
- return $this;
- }
-
- /**
- * @inheritDoc
- *
- * @return string
- * @since 31.0.0
- */
- public function getPublicKey(): string {
- return $this->publicKey;
- }
-
- /**
- * @inheritDoc
- *
- * @param string $privateKey
- * @return IKeyPair
- * @since 31.0.0
- */
- public function setPrivateKey(string $privateKey): IKeyPair {
- $this->privateKey = $privateKey;
- return $this;
- }
-
- /**
- * @inheritDoc
- *
- * @return string
- * @since 31.0.0
- */
- public function getPrivateKey(): string {
- return $this->privateKey;
- }
-
- /**
- * @inheritDoc
- *
- * @param array $options
- * @return IKeyPair
- * @since 31.0.0
- */
- public function setOptions(array $options): IKeyPair {
- $this->options = $options;
- return $this;
- }
-
- /**
- * @inheritDoc
- *
- * @return array
- * @since 31.0.0
- */
- public function getOptions(): array {
- return $this->options;
- }
-}
$this->prepIncomingSignatureHeader($signedRequest);
$this->verifyIncomingSignatureHeader($signedRequest);
$this->prepEstimatedSignature($signedRequest, $options['extraSignatureHeaders'] ?? []);
- $this->verifyIncomingRequestSignature(
- $signedRequest, $signatoryManager, $options['ttlSignatory'] ?? self::SIGNATORY_TTL
- );
+ $this->verifyIncomingRequestSignature($signedRequest, $signatoryManager, $options['ttlSignatory'] ?? self::SIGNATORY_TTL);
} catch (SignatureException $e) {
$this->logger->warning(
'signature could not be verified', [
case SignatoryType::FORGIVABLE:
$this->deleteSignatory($knownSignatory->getKeyId());
$this->insertSignatory($signatory);
-
return;
case SignatoryType::REFRESHABLE:
case SignatoryType::TRUSTED:
// TODO: send notice to admin
throw new SignatoryConflictException();
- break;
case SignatoryType::STATIC:
// TODO: send warning to admin
throw new SignatoryConflictException();
- break;
}
}
use OC\Security\CSRF\TokenStorage\SessionStorage;
use OC\Security\Hasher;
use OC\Security\Ip\RemoteAddress;
-use OC\Security\PublicPrivateKeyPairs\KeyPairManager;
use OC\Security\RateLimiting\Limiter;
use OC\Security\SecureRandom;
use OC\Security\Signature\SignatureManager;
$this->registerAlias(IRichTextFormatter::class, \OC\RichObjectStrings\RichTextFormatter::class);
- $this->registerAlias(IKeyPairManager::class, KeyPairManager::class);
$this->registerAlias(ISignatureManager::class, SignatureManager::class);
$this->connectDispatcher();
* apiVersion: '1.0-proposal1',
* endPoint: string,
* publicKey: ISignatory|null,
- * resourceTypes: array{
+ * resourceTypes: list<array{
* name: string,
* shareTypes: list<string>,
* protocols: array<string, string>
- * }[],
+ * }>,
* version: string
* }
* @since 28.0.0
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-namespace NCU\Security\PublicPrivateKeyPairs\Exceptions;
-
-/**
- * conflict between public and private key pair
- *
- * @experimental 31.0.0
- * @since 31.0.0
- */
-class KeyPairConflictException extends KeyPairException {
-}
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-namespace NCU\Security\PublicPrivateKeyPairs\Exceptions;
-
-use Exception;
-
-/**
- * global exception related to key pairs
- *
- * @experimental 31.0.0
- * @since 31.0.0
- */
-class KeyPairException extends Exception {
-}
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-namespace NCU\Security\PublicPrivateKeyPairs\Exceptions;
-
-/**
- * @experimental 31.0.0
- * @since 31.0.0
- */
-class KeyPairNotFoundException extends KeyPairException {
-}
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-namespace NCU\Security\PublicPrivateKeyPairs;
-
-use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairConflictException;
-use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairNotFoundException;
-use NCU\Security\PublicPrivateKeyPairs\Model\IKeyPair;
-
-/**
- * IKeyPairManager contains a group of method to create/manage/store internal public/private key pair.
- *
- * @experimental 31.0.0
- * @since 31.0.0
- */
-interface IKeyPairManager {
-
- /**
- * generate and store public/private key pair.
- * throws exception if key pair already exist
- *
- * @param string $app appId
- * @param string $name key name
- * @param array $options algorithms, metadata
- *
- * @return IKeyPair
- * @throws KeyPairConflictException if a key already exist
- * @since 31.0.0
- */
- public function generateKeyPair(string $app, string $name, array $options = []): IKeyPair;
-
- /**
- * returns if key pair is known.
- *
- * @param string $app appId
- * @param string $name key name
- *
- * @return bool TRUE if key pair exists in database
- * @since 31.0.0
- */
- public function hasKeyPair(string $app, string $name): bool;
-
- /**
- * return key pair from database based on $app and $name.
- * throws exception if key pair does not exist
- *
- * @param string $app appId
- * @param string $name key name
- *
- * @return IKeyPair
- * @throws KeyPairNotFoundException if key pair is not known
- * @since 31.0.0
- */
- public function getKeyPair(string $app, string $name): IKeyPair;
-
- /**
- * delete key pair from database
- *
- * @param string $app appid
- * @param string $name key name
- *
- * @since 31.0.0
- */
- public function deleteKeyPair(string $app, string $name): void;
-
- /**
- * test key pair by encrypting/decrypting a string
- *
- * @param IKeyPair $keyPair keypair to test
- *
- * @return bool
- * @since 31.0.0
- */
- public function testKeyPair(IKeyPair $keyPair): bool;
-}
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-/**
- * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-namespace NCU\Security\PublicPrivateKeyPairs\Model;
-
-/**
- * simple model that store key pair, its name, its origin (app)
- * and the options used during its creation
- *
- * @experimental 31.0.0
- * @since 31.0.0
- */
-interface IKeyPair {
- /**
- * returns id of the app owning the key pair
- *
- * @return string
- * @since 31.0.0
- */
- public function getApp(): string;
-
- /**
- * returns name of the key pair
- *
- * @return string
- * @since 31.0.0
- */
- public function getName(): string;
-
- /**
- * set public key
- *
- * @param string $publicKey
- * @return IKeyPair
- * @since 31.0.0
- */
- public function setPublicKey(string $publicKey): IKeyPair;
-
- /**
- * returns public key
- *
- * @return string
- * @since 31.0.0
- */
- public function getPublicKey(): string;
-
- /**
- * set private key
- *
- * @param string $privateKey
- * @return IKeyPair
- * @since 31.0.0
- */
- public function setPrivateKey(string $privateKey): IKeyPair;
-
- /**
- * returns private key
- *
- * @return string
- * @since 31.0.0
- */
- public function getPrivateKey(): string;
-
- /**
- * set options
- *
- * @param array $options
- * @return IKeyPair
- * @since 31.0.0
- */
- public function setOptions(array $options): IKeyPair;
-
- /**
- * returns options
- *
- * @return array
- * @since 31.0.0
- */
- public function getOptions(): array;
-}