aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Security
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Security')
-rw-r--r--lib/private/Security/Bruteforce/Backend/DatabaseBackend.php23
-rw-r--r--lib/private/Security/Bruteforce/Backend/IBackend.php21
-rw-r--r--lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php25
-rw-r--r--lib/private/Security/Bruteforce/Capabilities.php25
-rw-r--r--lib/private/Security/Bruteforce/CleanupJob.php28
-rw-r--r--lib/private/Security/Bruteforce/Throttler.php147
-rw-r--r--lib/private/Security/CSP/ContentSecurityPolicy.php24
-rw-r--r--lib/private/Security/CSP/ContentSecurityPolicyManager.php30
-rw-r--r--lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php44
-rw-r--r--lib/private/Security/CSRF/CsrfToken.php25
-rw-r--r--lib/private/Security/CSRF/CsrfTokenGenerator.php23
-rw-r--r--lib/private/Security/CSRF/CsrfTokenManager.php24
-rw-r--r--lib/private/Security/CSRF/TokenStorage/SessionStorage.php25
-rw-r--r--lib/private/Security/Certificate.php35
-rw-r--r--lib/private/Security/CertificateManager.php77
-rw-r--r--lib/private/Security/CredentialsManager.php32
-rw-r--r--lib/private/Security/Crypto.php63
-rw-r--r--lib/private/Security/FeaturePolicy/FeaturePolicy.php21
-rw-r--r--lib/private/Security/FeaturePolicy/FeaturePolicyManager.php22
-rw-r--r--lib/private/Security/Hasher.php54
-rw-r--r--lib/private/Security/IdentityProof/Key.php22
-rw-r--r--lib/private/Security/IdentityProof/Manager.php102
-rw-r--r--lib/private/Security/IdentityProof/Signer.php27
-rw-r--r--lib/private/Security/Ip/Address.php49
-rw-r--r--lib/private/Security/Ip/BruteforceAllowList.php57
-rw-r--r--lib/private/Security/Ip/Factory.php23
-rw-r--r--lib/private/Security/Ip/Range.php40
-rw-r--r--lib/private/Security/Ip/RemoteAddress.php71
-rw-r--r--lib/private/Security/Normalizer/IpAddress.php58
-rw-r--r--lib/private/Security/RateLimiting/Backend/DatabaseBackend.php31
-rw-r--r--lib/private/Security/RateLimiting/Backend/IBackend.php22
-rw-r--r--lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php28
-rw-r--r--lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php21
-rw-r--r--lib/private/Security/RateLimiting/Limiter.php29
-rw-r--r--lib/private/Security/RemoteHostValidator.php22
-rw-r--r--lib/private/Security/SecureRandom.php29
-rw-r--r--lib/private/Security/Signature/Db/SignatoryMapper.php114
-rw-r--r--lib/private/Security/Signature/Model/IncomingSignedRequest.php268
-rw-r--r--lib/private/Security/Signature/Model/OutgoingSignedRequest.php229
-rw-r--r--lib/private/Security/Signature/Model/SignedRequest.php216
-rw-r--r--lib/private/Security/Signature/SignatureManager.php426
-rw-r--r--lib/private/Security/TrustedDomainHelper.php27
-rw-r--r--lib/private/Security/VerificationToken/CleanUpJob.php23
-rw-r--r--lib/private/Security/VerificationToken/VerificationToken.php35
44 files changed, 1830 insertions, 907 deletions
diff --git a/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php b/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php
index 04f2a7b6397..33c2a3aae62 100644
--- a/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php
+++ b/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Bruteforce\Backend;
@@ -62,7 +45,7 @@ class DatabaseBackend implements IBackend {
$row = $result->fetch();
$result->closeCursor();
- return (int) $row['attempts'];
+ return (int)$row['attempts'];
}
/**
diff --git a/lib/private/Security/Bruteforce/Backend/IBackend.php b/lib/private/Security/Bruteforce/Backend/IBackend.php
index 4b40262e645..7118123cbb5 100644
--- a/lib/private/Security/Bruteforce/Backend/IBackend.php
+++ b/lib/private/Security/Bruteforce/Backend/IBackend.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Bruteforce\Backend;
diff --git a/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php b/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php
index 432e99700fe..9a0723db47e 100644
--- a/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php
+++ b/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Bruteforce\Backend;
@@ -36,7 +19,7 @@ class MemoryCacheBackend implements IBackend {
ICacheFactory $cacheFactory,
private ITimeFactory $timeFactory,
) {
- $this->cache = $cacheFactory->createDistributed(__CLASS__);
+ $this->cache = $cacheFactory->createDistributed(self::class);
}
private function hash(
@@ -154,7 +137,7 @@ class MemoryCacheBackend implements IBackend {
$existingAttempts = array_values($existingAttempts);
// Store the new attempt
- $existingAttempts[] = $timestamp . '#' . $this->hash($action) . '#' . $this->hash($metadata);
+ $existingAttempts[] = $timestamp . '#' . $this->hash($action) . '#' . $this->hash($metadata);
$this->cache->set($identifier, json_encode($existingAttempts), 12 * 3600);
}
diff --git a/lib/private/Security/Bruteforce/Capabilities.php b/lib/private/Security/Bruteforce/Capabilities.php
index add2bb8d8b5..581b4480a27 100644
--- a/lib/private/Security/Bruteforce/Capabilities.php
+++ b/lib/private/Security/Bruteforce/Capabilities.php
@@ -3,29 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
- * @copyright Copyright (c) 2017 Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author J0WI <J0WI@users.noreply.github.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Bruteforce;
diff --git a/lib/private/Security/Bruteforce/CleanupJob.php b/lib/private/Security/Bruteforce/CleanupJob.php
index 13628dd300d..f07e4dbacbd 100644
--- a/lib/private/Security/Bruteforce/CleanupJob.php
+++ b/lib/private/Security/Bruteforce/CleanupJob.php
@@ -3,30 +3,12 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Bruteforce;
use OCP\AppFramework\Utility\ITimeFactory;
-use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\TimedJob;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
@@ -39,8 +21,8 @@ class CleanupJob extends TimedJob {
parent::__construct($time);
// Run once a day
- $this->setInterval(3600 * 24);
- $this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
+ $this->setInterval(60 * 60 * 24);
+ $this->setTimeSensitivity(self::TIME_INSENSITIVE);
}
protected function run($argument): void {
@@ -50,6 +32,6 @@ class CleanupJob extends TimedJob {
$qb = $this->connection->getQueryBuilder();
$qb->delete('bruteforce_attempts')
->where($qb->expr()->lt('occurred', $qb->createNamedParameter($time), IQueryBuilder::PARAM_INT));
- $qb->execute();
+ $qb->executeStatement();
}
}
diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php
index 7e5f1daa28c..574f6c80c3f 100644
--- a/lib/private/Security/Bruteforce/Throttler.php
+++ b/lib/private/Security/Bruteforce/Throttler.php
@@ -3,37 +3,13 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
- * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Johannes Riedel <joeried@users.noreply.github.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Bruteforce;
use OC\Security\Bruteforce\Backend\IBackend;
+use OC\Security\Ip\BruteforceAllowList;
use OC\Security\Normalizer\IpAddress;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
@@ -57,14 +33,13 @@ use Psr\Log\LoggerInterface;
class Throttler implements IThrottler {
/** @var bool[] */
private array $hasAttemptsDeleted = [];
- /** @var bool[] */
- private array $ipIsWhitelisted = [];
public function __construct(
private ITimeFactory $timeFactory,
private LoggerInterface $logger,
private IConfig $config,
private IBackend $backend,
+ private BruteforceAllowList $allowList,
) {
}
@@ -108,70 +83,7 @@ class Throttler implements IThrottler {
* Check if the IP is whitelisted
*/
public function isBypassListed(string $ip): bool {
- if (isset($this->ipIsWhitelisted[$ip])) {
- return $this->ipIsWhitelisted[$ip];
- }
-
- if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
- $this->ipIsWhitelisted[$ip] = true;
- return true;
- }
-
- $keys = $this->config->getAppKeys('bruteForce');
- $keys = array_filter($keys, function ($key) {
- return str_starts_with($key, 'whitelist_');
- });
-
- if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
- $type = 4;
- } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
- $type = 6;
- } else {
- $this->ipIsWhitelisted[$ip] = false;
- return false;
- }
-
- $ip = inet_pton($ip);
-
- foreach ($keys as $key) {
- $cidr = $this->config->getAppValue('bruteForce', $key, null);
-
- $cx = explode('/', $cidr);
- $addr = $cx[0];
- $mask = (int)$cx[1];
-
- // Do not compare ipv4 to ipv6
- if (($type === 4 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ||
- ($type === 6 && !filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6))) {
- continue;
- }
-
- $addr = inet_pton($addr);
-
- $valid = true;
- for ($i = 0; $i < $mask; $i++) {
- $part = ord($addr[(int)($i / 8)]);
- $orig = ord($ip[(int)($i / 8)]);
-
- $bitmask = 1 << (7 - ($i % 8));
-
- $part = $part & $bitmask;
- $orig = $orig & $bitmask;
-
- if ($part !== $orig) {
- $valid = false;
- break;
- }
- }
-
- if ($valid === true) {
- $this->ipIsWhitelisted[$ip] = true;
- return true;
- }
- }
-
- $this->ipIsWhitelisted[$ip] = false;
- return false;
+ return $this->allowList->isBypassListed($ip);
}
/**
@@ -201,7 +113,7 @@ class Throttler implements IThrottler {
return 0;
}
- $maxAgeTimestamp = (int) ($this->timeFactory->getTime() - 3600 * $maxAgeHours);
+ $maxAgeTimestamp = (int)($this->timeFactory->getTime() - 3600 * $maxAgeHours);
return $this->backend->getAttempts(
$ipAddress->getSubnet(),
@@ -215,12 +127,19 @@ class Throttler implements IThrottler {
*/
public function getDelay(string $ip, string $action = ''): int {
$attempts = $this->getAttempts($ip, $action);
+ return $this->calculateDelay($attempts);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function calculateDelay(int $attempts): int {
if ($attempts === 0) {
return 0;
}
$firstDelay = 0.1;
- if ($attempts > self::MAX_ATTEMPTS) {
+ if ($attempts > $this->config->getSystemValueInt('auth.bruteforce.max-attempts', self::MAX_ATTEMPTS)) {
// Don't ever overflow. Just assume the maxDelay time:s
return self::MAX_DELAY_MS;
}
@@ -229,7 +148,7 @@ class Throttler implements IThrottler {
if ($delay > self::MAX_DELAY) {
return self::MAX_DELAY_MS;
}
- return (int) \ceil($delay * 1000);
+ return (int)\ceil($delay * 1000);
}
/**
@@ -287,25 +206,31 @@ class Throttler implements IThrottler {
* {@inheritDoc}
*/
public function sleepDelayOrThrowOnMax(string $ip, string $action = ''): int {
- $delay = $this->getDelay($ip, $action);
- if (($delay === self::MAX_DELAY_MS) && $this->getAttempts($ip, $action, 0.5) > self::MAX_ATTEMPTS) {
- $this->logger->info('IP address blocked because it reached the maximum failed attempts in the last 30 minutes [action: {action}, ip: {ip}]', [
- 'action' => $action,
- 'ip' => $ip,
- ]);
- // If the ip made too many attempts within the last 30 mins we don't execute anymore
- throw new MaxDelayReached('Reached maximum delay');
- }
- if ($delay > 100) {
- $this->logger->info('IP address throttled because it reached the attempts limit in the last 30 minutes [action: {action}, delay: {delay}, ip: {ip}]', [
+ $maxAttempts = $this->config->getSystemValueInt('auth.bruteforce.max-attempts', self::MAX_ATTEMPTS);
+ $attempts = $this->getAttempts($ip, $action);
+ if ($attempts > $maxAttempts) {
+ $attempts30mins = $this->getAttempts($ip, $action, 0.5);
+ if ($attempts30mins > $maxAttempts) {
+ $this->logger->info('IP address blocked because it reached the maximum failed attempts in the last 30 minutes [action: {action}, attempts: {attempts}, ip: {ip}]', [
+ 'action' => $action,
+ 'ip' => $ip,
+ 'attempts' => $attempts30mins,
+ ]);
+ // If the ip made too many attempts within the last 30 mins we don't execute anymore
+ throw new MaxDelayReached('Reached maximum delay');
+ }
+
+ $this->logger->info('IP address throttled because it reached the attempts limit in the last 12 hours [action: {action}, attempts: {attempts}, ip: {ip}]', [
'action' => $action,
'ip' => $ip,
- 'delay' => $delay,
+ 'attempts' => $attempts,
]);
}
- if (!$this->config->getSystemValueBool('auth.bruteforce.protection.testing')) {
- usleep($delay * 1000);
+
+ if ($attempts > 0) {
+ return $this->calculateDelay($attempts);
}
- return $delay;
+
+ return 0;
}
}
diff --git a/lib/private/Security/CSP/ContentSecurityPolicy.php b/lib/private/Security/CSP/ContentSecurityPolicy.php
index ee525af4c2a..890251db040 100644
--- a/lib/private/Security/CSP/ContentSecurityPolicy.php
+++ b/lib/private/Security/CSP/ContentSecurityPolicy.php
@@ -1,28 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security\CSP;
diff --git a/lib/private/Security/CSP/ContentSecurityPolicyManager.php b/lib/private/Security/CSP/ContentSecurityPolicyManager.php
index 503933ef980..e9d6b2945a8 100644
--- a/lib/private/Security/CSP/ContentSecurityPolicyManager.php
+++ b/lib/private/Security/CSP/ContentSecurityPolicyManager.php
@@ -3,27 +3,9 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security\CSP;
@@ -70,13 +52,13 @@ class ContentSecurityPolicyManager implements IContentSecurityPolicyManager {
EmptyContentSecurityPolicy $originalPolicy,
): ContentSecurityPolicy {
foreach ((object)(array)$originalPolicy as $name => $value) {
- $setter = 'set'.ucfirst($name);
+ $setter = 'set' . ucfirst($name);
if (\is_array($value)) {
- $getter = 'get'.ucfirst($name);
+ $getter = 'get' . ucfirst($name);
$currentValues = \is_array($defaultPolicy->$getter()) ? $defaultPolicy->$getter() : [];
$defaultPolicy->$setter(array_values(array_unique(array_merge($currentValues, $value))));
} elseif (\is_bool($value)) {
- $getter = 'is'.ucfirst($name);
+ $getter = 'is' . ucfirst($name);
$currentValue = $defaultPolicy->$getter();
// true wins over false
if ($value > $currentValue) {
diff --git a/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php
index 6573007a459..993f74ae0e4 100644
--- a/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php
+++ b/lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php
@@ -3,30 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Pavel Krasikov <klonishe@gmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Sam Bull <aa6bs0@sambull.org>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\CSP;
@@ -52,7 +30,11 @@ class ContentSecurityPolicyNonceManager {
public function getNonce(): string {
if ($this->nonce === '') {
if (empty($this->request->server['CSP_NONCE'])) {
- $this->nonce = base64_encode($this->csrfTokenManager->getToken()->getEncryptedValue());
+ // Get the token from the CSRF token, we only use the "shared secret" part
+ // as the first part does not add any security / entropy to the token
+ // so it can be ignored to keep the nonce short while keeping the same randomness
+ $csrfSecret = explode(':', ($this->csrfTokenManager->getToken()->getEncryptedValue()));
+ $this->nonce = end($csrfSecret);
} else {
$this->nonce = $this->request->server['CSP_NONCE'];
}
@@ -65,16 +47,14 @@ class ContentSecurityPolicyNonceManager {
* Check if the browser supports CSP v3
*/
public function browserSupportsCspV3(): bool {
- $browserWhitelist = [
- Request::USER_AGENT_CHROME,
- Request::USER_AGENT_FIREFOX,
- Request::USER_AGENT_SAFARI,
+ $browserBlocklist = [
+ Request::USER_AGENT_IE,
];
- if ($this->request->isUserAgent($browserWhitelist)) {
- return true;
+ if ($this->request->isUserAgent($browserBlocklist)) {
+ return false;
}
- return false;
+ return true;
}
}
diff --git a/lib/private/Security/CSRF/CsrfToken.php b/lib/private/Security/CSRF/CsrfToken.php
index 45e628b3f3c..6aad0cd5944 100644
--- a/lib/private/Security/CSRF/CsrfToken.php
+++ b/lib/private/Security/CSRF/CsrfToken.php
@@ -1,29 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Leon Klingele <git@leonklingele.de>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security\CSRF;
diff --git a/lib/private/Security/CSRF/CsrfTokenGenerator.php b/lib/private/Security/CSRF/CsrfTokenGenerator.php
index c3d89247de1..9e3f15c63d2 100644
--- a/lib/private/Security/CSRF/CsrfTokenGenerator.php
+++ b/lib/private/Security/CSRF/CsrfTokenGenerator.php
@@ -1,27 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security\CSRF;
diff --git a/lib/private/Security/CSRF/CsrfTokenManager.php b/lib/private/Security/CSRF/CsrfTokenManager.php
index dceacf45e2a..00e1be5bedf 100644
--- a/lib/private/Security/CSRF/CsrfTokenManager.php
+++ b/lib/private/Security/CSRF/CsrfTokenManager.php
@@ -1,28 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security\CSRF;
diff --git a/lib/private/Security/CSRF/TokenStorage/SessionStorage.php b/lib/private/Security/CSRF/TokenStorage/SessionStorage.php
index 0ffe043e2f9..1f0f8bcaa0a 100644
--- a/lib/private/Security/CSRF/TokenStorage/SessionStorage.php
+++ b/lib/private/Security/CSRF/TokenStorage/SessionStorage.php
@@ -1,29 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security\CSRF\TokenStorage;
diff --git a/lib/private/Security/Certificate.php b/lib/private/Security/Certificate.php
index 759c71b2eec..1551694c21f 100644
--- a/lib/private/Security/Certificate.php
+++ b/lib/private/Security/Certificate.php
@@ -1,29 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author J0WI <J0WI@users.noreply.github.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security;
@@ -61,6 +42,16 @@ class Certificate implements ICertificate {
$info = openssl_x509_parse($data);
if (!is_array($info)) {
+ // There is a non-standardized certificate format only used by OpenSSL. Replace all
+ // separators and try again.
+ $data = str_replace(
+ ['-----BEGIN TRUSTED CERTIFICATE-----', '-----END TRUSTED CERTIFICATE-----'],
+ ['-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----'],
+ $data,
+ );
+ $info = openssl_x509_parse($data);
+ }
+ if (!is_array($info)) {
throw new \Exception('Certificate could not get parsed.');
}
diff --git a/lib/private/Security/CertificateManager.php b/lib/private/Security/CertificateManager.php
index cf5f0f41d56..00babff735f 100644
--- a/lib/private/Security/CertificateManager.php
+++ b/lib/private/Security/CertificateManager.php
@@ -1,38 +1,13 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author J0WI <J0WI@users.noreply.github.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security;
-use OC\Files\Filesystem;
use OC\Files\View;
use OCP\ICertificate;
use OCP\ICertificateManager;
@@ -174,20 +149,19 @@ class CertificateManager implements ICertificateManager {
* @throws \Exception If the certificate could not get added
*/
public function addCertificate(string $certificate, string $name): ICertificate {
- if (!Filesystem::isValidPath($name) or Filesystem::isFileBlacklisted($name)) {
- throw new \Exception('Filename is not valid');
- }
+ $path = $this->getPathToCertificates() . 'uploads/' . $name;
+ $directory = dirname($path);
+
+ $this->view->verifyPath($directory, basename($path));
$this->bundlePath = null;
- $dir = $this->getPathToCertificates() . 'uploads/';
- if (!$this->view->file_exists($dir)) {
- $this->view->mkdir($dir);
+ if (!$this->view->file_exists($directory)) {
+ $this->view->mkdir($directory);
}
try {
- $file = $dir . $name;
$certificateObject = new Certificate($certificate, $name);
- $this->view->file_put_contents($file, $certificate);
+ $this->view->file_put_contents($path, $certificate);
$this->createCertificateBundle();
return $certificateObject;
} catch (\Exception $e) {
@@ -199,14 +173,17 @@ class CertificateManager implements ICertificateManager {
* Remove the certificate and re-generate the certificate bundle
*/
public function removeCertificate(string $name): bool {
- if (!Filesystem::isValidPath($name)) {
+ $path = $this->getPathToCertificates() . 'uploads/' . $name;
+
+ try {
+ $this->view->verifyPath(dirname($path), basename($path));
+ } catch (\Exception) {
return false;
}
- $this->bundlePath = null;
- $path = $this->getPathToCertificates() . 'uploads/';
- if ($this->view->file_exists($path . $name)) {
- $this->view->unlink($path . $name);
+ $this->bundlePath = null;
+ if ($this->view->file_exists($path)) {
+ $this->view->unlink($path);
$this->createCertificateBundle();
}
return true;
@@ -228,17 +205,17 @@ class CertificateManager implements ICertificateManager {
if ($this->bundlePath === null) {
if (!$this->hasCertificates()) {
$this->bundlePath = \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
- }
-
- if ($this->needsRebundling()) {
- $this->createCertificateBundle();
- }
+ } else {
+ if ($this->needsRebundling()) {
+ $this->createCertificateBundle();
+ }
- $certificateBundle = $this->getCertificateBundle();
- $this->bundlePath = $this->view->getLocalFile($certificateBundle) ?: null;
+ $certificateBundle = $this->getCertificateBundle();
+ $this->bundlePath = $this->view->getLocalFile($certificateBundle) ?: null;
- if ($this->bundlePath === null) {
- throw new \RuntimeException('Unable to get certificate bundle "' . $certificateBundle . '".');
+ if ($this->bundlePath === null) {
+ throw new \RuntimeException('Unable to get certificate bundle "' . $certificateBundle . '".');
+ }
}
}
return $this->bundlePath;
diff --git a/lib/private/Security/CredentialsManager.php b/lib/private/Security/CredentialsManager.php
index ea59a24d646..254984261d2 100644
--- a/lib/private/Security/CredentialsManager.php
+++ b/lib/private/Security/CredentialsManager.php
@@ -1,30 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author J0WI <J0WI@users.noreply.github.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security;
@@ -80,7 +60,7 @@ class CredentialsManager implements ICredentialsManager {
$qb->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($userId)));
}
- $qResult = $qb->execute();
+ $qResult = $qb->executeQuery();
$result = $qResult->fetch();
$qResult->closeCursor();
@@ -109,7 +89,7 @@ class CredentialsManager implements ICredentialsManager {
$qb->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($userId)));
}
- return $qb->execute();
+ return $qb->executeStatement();
}
/**
@@ -122,6 +102,6 @@ class CredentialsManager implements ICredentialsManager {
$qb->delete(self::DB_TABLE)
->where($qb->expr()->eq('user', $qb->createNamedParameter($userId)))
;
- return $qb->execute();
+ return $qb->executeStatement();
}
}
diff --git a/lib/private/Security/Crypto.php b/lib/private/Security/Crypto.php
index 033456f3f2e..39ce5e89aeb 100644
--- a/lib/private/Security/Crypto.php
+++ b/lib/private/Security/Crypto.php
@@ -1,31 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Andreas Fischer <bantu@owncloud.com>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author lynn-stephenson <lynn.stephenson@protonmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security;
@@ -99,9 +78,9 @@ class Crypto implements ICrypto {
$ciphertext = bin2hex($encrypted);
$iv = bin2hex($iv);
- $hmac = bin2hex($this->calculateHMAC($ciphertext.$iv, substr($keyMaterial, 32)));
+ $hmac = bin2hex($this->calculateHMAC($ciphertext . $iv, substr($keyMaterial, 32)));
- return $ciphertext.'|'.$iv.'|'.$hmac.'|3';
+ return $ciphertext . '|' . $iv . '|' . $hmac . '|3';
}
/**
@@ -135,6 +114,25 @@ class Crypto implements ICrypto {
throw new Exception('Authenticated ciphertext could not be decoded.');
}
+ /*
+ * Rearrange arguments for legacy ownCloud migrations
+ *
+ * The original scheme consistent of three parts. Nextcloud added a
+ * fourth at the end as "2" or later "3", ownCloud added "v2" at the
+ * beginning.
+ */
+ $originalParts = $parts;
+ $isOwnCloudV2Migration = $partCount === 4 && $originalParts[0] === 'v2';
+ if ($isOwnCloudV2Migration) {
+ $parts = [
+ $parts[1],
+ $parts[2],
+ $parts[3],
+ '2'
+ ];
+ }
+
+ // Convert hex-encoded values to binary
$ciphertext = $this->hex2bin($parts[0]);
$iv = $parts[1];
$hmac = $this->hex2bin($parts[2]);
@@ -145,7 +143,7 @@ class Crypto implements ICrypto {
$iv = $this->hex2bin($iv);
}
- if ($version === '3') {
+ if ($version === '3' || $isOwnCloudV2Migration) {
$keyMaterial = hash_hkdf('sha512', $password);
$encryptionKey = substr($keyMaterial, 0, 32);
$hmacKey = substr($keyMaterial, 32);
@@ -154,8 +152,15 @@ class Crypto implements ICrypto {
$this->cipher->setPassword($encryptionKey);
$this->cipher->setIV($iv);
- if (!hash_equals($this->calculateHMAC($parts[0] . $parts[1], $hmacKey), $hmac)) {
- throw new Exception('HMAC does not match.');
+ if ($isOwnCloudV2Migration) {
+ // ownCloud uses the binary IV for HMAC calculation
+ if (!hash_equals($this->calculateHMAC($parts[0] . $iv, $hmacKey), $hmac)) {
+ throw new Exception('HMAC does not match.');
+ }
+ } else {
+ if (!hash_equals($this->calculateHMAC($parts[0] . $parts[1], $hmacKey), $hmac)) {
+ throw new Exception('HMAC does not match.');
+ }
}
$result = $this->cipher->decrypt($ciphertext);
diff --git a/lib/private/Security/FeaturePolicy/FeaturePolicy.php b/lib/private/Security/FeaturePolicy/FeaturePolicy.php
index a0ab2065ece..9b513b80813 100644
--- a/lib/private/Security/FeaturePolicy/FeaturePolicy.php
+++ b/lib/private/Security/FeaturePolicy/FeaturePolicy.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\FeaturePolicy;
diff --git a/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php b/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php
index bb9fc41332f..e50aaac0faf 100644
--- a/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php
+++ b/lib/private/Security/FeaturePolicy/FeaturePolicyManager.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\FeaturePolicy;
diff --git a/lib/private/Security/Hasher.php b/lib/private/Security/Hasher.php
index 23747751053..722fdab902f 100644
--- a/lib/private/Security/Hasher.php
+++ b/lib/private/Security/Hasher.php
@@ -1,30 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author MichaIng <micha@dietpi.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security;
@@ -42,10 +22,10 @@ use OCP\Security\IHasher;
*
* Usage:
* // Hashing a message
- * $hash = \OC::$server->getHasher()->hash('MessageToHash');
+ * $hash = \OC::$server->get(\OCP\Security\IHasher::class)->hash('MessageToHash');
* // Verifying a message - $newHash will contain the newly calculated hash
* $newHash = null;
- * var_dump(\OC::$server->getHasher()->verify('a', '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', $newHash));
+ * var_dump(\OC::$server->get(\OCP\Security\IHasher::class)->verify('a', '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', $newHash));
* var_dump($newHash);
*
* @package OC\Security
@@ -99,7 +79,7 @@ class Hasher implements IHasher {
/**
* Get the version and hash from a prefixedHash
* @param string $prefixedHash
- * @return null|array Null if the hash is not prefixed, otherwise array('version' => 1, 'hash' => 'foo')
+ * @return null|array{version: int, hash: string} Null if the hash is not prefixed, otherwise array('version' => 1, 'hash' => 'foo')
*/
protected function splitHash(string $prefixedHash): ?array {
$explodedString = explode('|', $prefixedHash, 2);
@@ -126,8 +106,8 @@ class Hasher implements IHasher {
// Verify whether it matches a legacy PHPass or SHA1 string
$hashLength = \strlen($hash);
- if (($hashLength === 60 && password_verify($message.$this->legacySalt, $hash)) ||
- ($hashLength === 40 && hash_equals($hash, sha1($message)))) {
+ if (($hashLength === 60 && password_verify($message . $this->legacySalt, $hash))
+ || ($hashLength === 40 && hash_equals($hash, sha1($message)))) {
$newHash = $this->hash($message);
return true;
}
@@ -135,8 +115,8 @@ class Hasher implements IHasher {
// Verify whether it matches a legacy PHPass or SHA1 string
// Retry with empty passwordsalt for cases where it was not set
$hashLength = \strlen($hash);
- if (($hashLength === 60 && password_verify($message, $hash)) ||
- ($hashLength === 40 && hash_equals($hash, sha1($message)))) {
+ if (($hashLength === 60 && password_verify($message, $hash))
+ || ($hashLength === 40 && hash_equals($hash, sha1($message)))) {
$newHash = $this->hash($message);
return true;
}
@@ -210,4 +190,18 @@ class Hasher implements IHasher {
return $default;
}
+
+ public function validate(string $prefixedHash): bool {
+ $splitHash = $this->splitHash($prefixedHash);
+ if (empty($splitHash)) {
+ return false;
+ }
+ $validVersions = [3, 2, 1];
+ $version = $splitHash['version'];
+ if (!in_array($version, $validVersions, true)) {
+ return false;
+ }
+ $algoName = password_get_info($splitHash['hash'])['algoName'];
+ return $algoName !== 'unknown';
+ }
}
diff --git a/lib/private/Security/IdentityProof/Key.php b/lib/private/Security/IdentityProof/Key.php
index bde828a3859..0bfcd6bf9ed 100644
--- a/lib/private/Security/IdentityProof/Key.php
+++ b/lib/private/Security/IdentityProof/Key.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\IdentityProof;
diff --git a/lib/private/Security/IdentityProof/Manager.php b/lib/private/Security/IdentityProof/Manager.php
index 49b9bb10c3e..c16b8314beb 100644
--- a/lib/private/Security/IdentityProof/Manager.php
+++ b/lib/private/Security/IdentityProof/Manager.php
@@ -3,34 +3,16 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * 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;
@@ -39,31 +21,37 @@ 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 {
+ 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');
@@ -86,15 +74,17 @@ class Manager {
* 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')
@@ -112,12 +102,24 @@ class Manager {
*/
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 = $this->crypto->decrypt(
- $folder->getFile('private')->getContent()
- );
+ $privateKey = $folder->getFile('private')->getContent();
$publicKey = $folder->getFile('public')->getContent();
- return new Key($publicKey, $privateKey);
+
+ $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);
}
@@ -146,6 +148,38 @@ class Manager {
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()) {
diff --git a/lib/private/Security/IdentityProof/Signer.php b/lib/private/Security/IdentityProof/Signer.php
index 1458390c327..6083cbb5c9b 100644
--- a/lib/private/Security/IdentityProof/Signer.php
+++ b/lib/private/Security/IdentityProof/Signer.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\IdentityProof;
@@ -74,12 +55,12 @@ class Signer {
$user = $this->userManager->get($userId);
if ($user !== null) {
$key = $this->keyManager->getKey($user);
- return (bool)openssl_verify(
+ return openssl_verify(
json_encode($data['message']),
base64_decode($data['signature']),
$key->getPublic(),
OPENSSL_ALGO_SHA512
- );
+ ) === 1;
}
}
diff --git a/lib/private/Security/Ip/Address.php b/lib/private/Security/Ip/Address.php
new file mode 100644
index 00000000000..0e94ec2d9ea
--- /dev/null
+++ b/lib/private/Security/Ip/Address.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Security\Ip;
+
+use InvalidArgumentException;
+use IPLib\Address\AddressInterface;
+use IPLib\Factory;
+use IPLib\ParseStringFlag;
+use OCP\Security\Ip\IAddress;
+use OCP\Security\Ip\IRange;
+
+/**
+ * @since 30.0.0
+ */
+class Address implements IAddress {
+ private readonly AddressInterface $ip;
+
+ public function __construct(string $ip) {
+ $ip = Factory::parseAddressString($ip, ParseStringFlag::MAY_INCLUDE_ZONEID);
+ if ($ip === null) {
+ throw new InvalidArgumentException('Given IP address can’t be parsed');
+ }
+ $this->ip = $ip;
+ }
+
+ public static function isValid(string $ip): bool {
+ return Factory::parseAddressString($ip, ParseStringFlag::MAY_INCLUDE_ZONEID) !== null;
+ }
+
+ public function matches(IRange ... $ranges): bool {
+ foreach ($ranges as $range) {
+ if ($range->contains($this)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function __toString(): string {
+ return $this->ip->toString();
+ }
+}
diff --git a/lib/private/Security/Ip/BruteforceAllowList.php b/lib/private/Security/Ip/BruteforceAllowList.php
new file mode 100644
index 00000000000..fb837690a7b
--- /dev/null
+++ b/lib/private/Security/Ip/BruteforceAllowList.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\Ip;
+
+use OCP\IAppConfig;
+use OCP\Security\Ip\IFactory;
+
+class BruteforceAllowList {
+ /** @var array<string, bool> */
+ protected array $ipIsAllowListed = [];
+
+ public function __construct(
+ private readonly IAppConfig $appConfig,
+ private readonly IFactory $factory,
+ ) {
+ }
+
+ /**
+ * Check if the IP is allowed to bypass bruteforce protection
+ */
+ public function isBypassListed(string $ip): bool {
+ if (isset($this->ipIsAllowListed[$ip])) {
+ return $this->ipIsAllowListed[$ip];
+ }
+
+ try {
+ $address = $this->factory->addressFromString($ip);
+ } catch (\InvalidArgumentException) {
+ $this->ipIsAllowListed[$ip] = false;
+ return false;
+ }
+
+ foreach ($this->appConfig->searchKeys('bruteForce', 'whitelist_') as $key) {
+ $rangeString = $this->appConfig->getValueString('bruteForce', $key);
+ try {
+ $range = $this->factory->rangeFromString($rangeString);
+ } catch (\InvalidArgumentException) {
+ continue;
+ }
+
+ $allowed = $range->contains($address);
+ if ($allowed) {
+ $this->ipIsAllowListed[$ip] = true;
+ return true;
+ }
+ }
+
+ $this->ipIsAllowListed[$ip] = false;
+ return false;
+ }
+}
diff --git a/lib/private/Security/Ip/Factory.php b/lib/private/Security/Ip/Factory.php
new file mode 100644
index 00000000000..1eedcf27a09
--- /dev/null
+++ b/lib/private/Security/Ip/Factory.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Security\Ip;
+
+use OCP\Security\Ip\IAddress;
+use OCP\Security\Ip\IFactory;
+use OCP\Security\Ip\IRange;
+
+class Factory implements IFactory {
+ public function rangeFromString(string $range): IRange {
+ return new Range($range);
+ }
+
+ public function addressFromString(string $ip): IAddress {
+ return new Address($ip);
+ }
+}
diff --git a/lib/private/Security/Ip/Range.php b/lib/private/Security/Ip/Range.php
new file mode 100644
index 00000000000..e32b7a5abc0
--- /dev/null
+++ b/lib/private/Security/Ip/Range.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Security\Ip;
+
+use InvalidArgumentException;
+use IPLib\Factory;
+use IPLib\ParseStringFlag;
+use IPLib\Range\RangeInterface;
+use OCP\Security\Ip\IAddress;
+use OCP\Security\Ip\IRange;
+
+class Range implements IRange {
+ private readonly RangeInterface $range;
+
+ public function __construct(string $range) {
+ $range = Factory::parseRangeString($range);
+ if ($range === null) {
+ throw new InvalidArgumentException('Given range can’t be parsed');
+ }
+ $this->range = $range;
+ }
+
+ public static function isValid(string $range): bool {
+ return Factory::parseRangeString($range) !== null;
+ }
+
+ public function contains(IAddress $address): bool {
+ return $this->range->contains(Factory::parseAddressString((string)$address, ParseStringFlag::MAY_INCLUDE_ZONEID));
+ }
+
+ public function __toString(): string {
+ return $this->range->toString();
+ }
+}
diff --git a/lib/private/Security/Ip/RemoteAddress.php b/lib/private/Security/Ip/RemoteAddress.php
new file mode 100644
index 00000000000..4eef8813898
--- /dev/null
+++ b/lib/private/Security/Ip/RemoteAddress.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Security\Ip;
+
+use OCP\IConfig;
+use OCP\IRequest;
+use OCP\Security\Ip\IAddress;
+use OCP\Security\Ip\IRange;
+use OCP\Security\Ip\IRemoteAddress;
+
+class RemoteAddress implements IRemoteAddress, IAddress {
+ public const SETTING_NAME = 'allowed_admin_ranges';
+
+ private readonly ?IAddress $ip;
+
+ public function __construct(
+ private IConfig $config,
+ IRequest $request,
+ ) {
+ $remoteAddress = $request->getRemoteAddress();
+ $this->ip = $remoteAddress === ''
+ ? null
+ : new Address($remoteAddress);
+ }
+
+ public static function isValid(string $ip): bool {
+ return Address::isValid($ip);
+ }
+
+ public function matches(IRange ... $ranges): bool {
+ return $this->ip === null
+ ? true
+ : $this->ip->matches(... $ranges);
+ }
+
+ public function allowsAdminActions(): bool {
+ if ($this->ip === null) {
+ return true;
+ }
+
+ $allowedAdminRanges = $this->config->getSystemValue(self::SETTING_NAME, false);
+
+ // Don't apply restrictions on empty or invalid configuration
+ if (
+ $allowedAdminRanges === false
+ || !is_array($allowedAdminRanges)
+ || empty($allowedAdminRanges)
+ ) {
+ return true;
+ }
+
+ foreach ($allowedAdminRanges as $allowedAdminRange) {
+ if ((new Range($allowedAdminRange))->contains($this->ip)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public function __toString(): string {
+ return (string)$this->ip;
+ }
+}
diff --git a/lib/private/Security/Normalizer/IpAddress.php b/lib/private/Security/Normalizer/IpAddress.php
index f8e55370da7..4d33a7bd632 100644
--- a/lib/private/Security/Normalizer/IpAddress.php
+++ b/lib/private/Security/Normalizer/IpAddress.php
@@ -3,33 +3,13 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Konrad Bucheli <kb@open.ch>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Normalizer;
+use OCP\IConfig;
+
/**
* Class IpAddress is used for normalizing IPv4 and IPv6 addresses in security
* relevant contexts in Nextcloud.
@@ -46,7 +26,8 @@ class IpAddress {
}
/**
- * Return the given subnet for an IPv6 address (64 first bits)
+ * Return the given subnet for an IPv6 address
+ * Rely on security.ipv6_normalized_subnet_size, defaults to 56
*/
private function getIPv6Subnet(string $ip): string {
if ($ip[0] === '[' && $ip[-1] === ']') { // If IP is with brackets, for example [::1]
@@ -57,10 +38,25 @@ class IpAddress {
$ip = substr($ip, 0, $pos - 1);
}
- $binary = \inet_pton($ip);
- $mask = inet_pton('FFFF:FFFF:FFFF:FFFF::');
+ $config = \OCP\Server::get(IConfig::class);
+ $maskSize = min(64, $config->getSystemValueInt('security.ipv6_normalized_subnet_size', 56));
+ $maskSize = max(32, $maskSize);
+ if (PHP_INT_SIZE === 4) {
+ if ($maskSize === 64) {
+ $value = -1;
+ } elseif ($maskSize === 63) {
+ $value = PHP_INT_MAX;
+ } else {
+ $value = (1 << $maskSize - 32) - 1;
+ }
+ // as long as we support 32bit PHP we cannot use the `P` pack formatter (and not overflow 32bit integer)
+ $mask = pack('VVVV', -1, $value, 0, 0);
+ } else {
+ $mask = pack('VVP', (1 << 32) - 1, (1 << $maskSize - 32) - 1, 0);
+ }
- return inet_ntop($binary & $mask).'/64';
+ $binary = \inet_pton($ip);
+ return inet_ntop($binary & $mask) . '/' . $maskSize;
}
/**
@@ -85,16 +81,16 @@ class IpAddress {
/**
- * Gets either the /32 (IPv4) or the /64 (IPv6) subnet of an IP address
+ * Gets either the /32 (IPv4) or the /56 (default for IPv6) subnet of an IP address
*/
public function getSubnet(): string {
if (filter_var($this->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
- return $this->ip.'/32';
+ return $this->ip . '/32';
}
$ipv4 = $this->getEmbeddedIpv4($this->ip);
if ($ipv4 !== null) {
- return $ipv4.'/32';
+ return $ipv4 . '/32';
}
return $this->getIPv6Subnet($this->ip);
diff --git a/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php b/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php
index 41f50a90b5c..9fb237f2f72 100644
--- a/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php
+++ b/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
- * @copyright Copyright (c) 2021 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\RateLimiting\Backend;
@@ -39,7 +20,7 @@ class DatabaseBackend implements IBackend {
public function __construct(
private IConfig $config,
private IDBConnection $dbConnection,
- private ITimeFactory $timeFactory
+ private ITimeFactory $timeFactory,
) {
}
@@ -54,14 +35,14 @@ class DatabaseBackend implements IBackend {
* @throws Exception
*/
private function getExistingAttemptCount(
- string $identifier
+ string $identifier,
): int {
$currentTime = $this->timeFactory->getDateTime();
$qb = $this->dbConnection->getQueryBuilder();
$qb->delete(self::TABLE_NAME)
->where(
- $qb->expr()->lte('delete_after', $qb->createNamedParameter($currentTime, IQueryBuilder::PARAM_DATE))
+ $qb->expr()->lte('delete_after', $qb->createNamedParameter($currentTime, IQueryBuilder::PARAM_DATETIME_MUTABLE))
)
->executeStatement();
@@ -106,7 +87,7 @@ class DatabaseBackend implements IBackend {
$qb->insert(self::TABLE_NAME)
->values([
'hash' => $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR),
- 'delete_after' => $qb->createNamedParameter($deleteAfter, IQueryBuilder::PARAM_DATE),
+ 'delete_after' => $qb->createNamedParameter($deleteAfter, IQueryBuilder::PARAM_DATETIME_MUTABLE),
]);
if (!$this->config->getSystemValueBool('ratelimit.protection.enabled', true)) {
diff --git a/lib/private/Security/RateLimiting/Backend/IBackend.php b/lib/private/Security/RateLimiting/Backend/IBackend.php
index 24715391a96..43eff5dcf02 100644
--- a/lib/private/Security/RateLimiting/Backend/IBackend.php
+++ b/lib/private/Security/RateLimiting/Backend/IBackend.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\RateLimiting\Backend;
diff --git a/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php b/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php
index b59178c7d7b..4c33b49d05e 100644
--- a/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php
+++ b/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php
@@ -3,30 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
- * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\RateLimiting\Backend;
@@ -49,7 +27,7 @@ class MemoryCacheBackend implements IBackend {
ICacheFactory $cacheFactory,
private ITimeFactory $timeFactory,
) {
- $this->cache = $cacheFactory->createDistributed(__CLASS__);
+ $this->cache = $cacheFactory->createDistributed(self::class);
}
private function hash(
diff --git a/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php b/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php
index baf74927886..19defc2d896 100644
--- a/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php
+++ b/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Lukas Reschke <lukas@statuscode.ch>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\RateLimiting\Exception;
diff --git a/lib/private/Security/RateLimiting/Limiter.php b/lib/private/Security/RateLimiting/Limiter.php
index 689e7b14558..316becfa009 100644
--- a/lib/private/Security/RateLimiting/Limiter.php
+++ b/lib/private/Security/RateLimiting/Limiter.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\RateLimiting;
@@ -31,10 +13,12 @@ use OC\Security\RateLimiting\Backend\IBackend;
use OC\Security\RateLimiting\Exception\RateLimitExceededException;
use OCP\IUser;
use OCP\Security\RateLimiting\ILimiter;
+use Psr\Log\LoggerInterface;
class Limiter implements ILimiter {
public function __construct(
private IBackend $backend,
+ private LoggerInterface $logger,
) {
}
@@ -50,6 +34,11 @@ class Limiter implements ILimiter {
): void {
$existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier);
if ($existingAttempts >= $limit) {
+ $this->logger->info('Request blocked because it exceeds the rate limit [method: {method}, limit: {limit}, period: {period}]', [
+ 'method' => $methodIdentifier,
+ 'limit' => $limit,
+ 'period' => $period,
+ ]);
throw new RateLimitExceededException();
}
diff --git a/lib/private/Security/RemoteHostValidator.php b/lib/private/Security/RemoteHostValidator.php
index 9cc69594c32..30bd59db2c1 100644
--- a/lib/private/Security/RemoteHostValidator.php
+++ b/lib/private/Security/RemoteHostValidator.php
@@ -2,25 +2,9 @@
declare(strict_types=1);
-/*
- * @copyright 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author 2022 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security;
diff --git a/lib/private/Security/SecureRandom.php b/lib/private/Security/SecureRandom.php
index f5bc5ddfb5e..b2a3d19ce74 100644
--- a/lib/private/Security/SecureRandom.php
+++ b/lib/private/Security/SecureRandom.php
@@ -1,29 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security;
@@ -35,7 +16,7 @@ use OCP\Security\ISecureRandom;
* use a fallback.
*
* Usage:
- * \OC::$server->getSecureRandom()->generate(10);
+ * \OC::$server->get(ISecureRandom::class)->generate(10);
* @package OC\Security
*/
class SecureRandom implements ISecureRandom {
@@ -43,7 +24,7 @@ class SecureRandom implements ISecureRandom {
* Generate a secure random string of specified length.
* @param int $length The length of the generated string
* @param string $characters An optional list of characters to use if no character list is
- * specified all valid base64 characters are used.
+ * specified all valid base64 characters are used.
* @throws \LengthException if an invalid length is requested
*/
public function generate(
diff --git a/lib/private/Security/Signature/Db/SignatoryMapper.php b/lib/private/Security/Signature/Db/SignatoryMapper.php
new file mode 100644
index 00000000000..47b79320548
--- /dev/null
+++ b/lib/private/Security/Signature/Db/SignatoryMapper.php
@@ -0,0 +1,114 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Security\Signature\Db;
+
+use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
+use NCU\Security\Signature\Model\Signatory;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Db\QBMapper;
+use OCP\DB\Exception;
+use OCP\IDBConnection;
+
+/**
+ * @template-extends QBMapper<Signatory>
+ */
+class SignatoryMapper extends QBMapper {
+ public const TABLE = 'sec_signatory';
+
+ public function __construct(
+ IDBConnection $db,
+ ) {
+ parent::__construct($db, self::TABLE, Signatory::class);
+ }
+
+ /**
+ *
+ */
+ public function getByHost(string $host, string $account = ''): Signatory {
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('host', $qb->createNamedParameter($host)))
+ ->andWhere($qb->expr()->eq('account', $qb->createNamedParameter($account)));
+
+ try {
+ return $this->findEntity($qb);
+ } catch (DoesNotExistException) {
+ throw new SignatoryNotFoundException('no signatory found');
+ }
+ }
+
+ /**
+ */
+ public function getByKeyId(string $keyId): Signatory {
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('*')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($keyId))));
+
+ try {
+ return $this->findEntity($qb);
+ } catch (DoesNotExistException) {
+ throw new SignatoryNotFoundException('no signatory found');
+ }
+ }
+
+ /**
+ * @param string $keyId
+ *
+ * @return int
+ * @throws Exception
+ */
+ public function deleteByKeyId(string $keyId): int {
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete($this->getTableName())
+ ->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($keyId))));
+
+ return $qb->executeStatement();
+ }
+
+ /**
+ * @param Signatory $signatory
+ *
+ * @return int
+ */
+ public function updateMetadata(Signatory $signatory): int {
+ $qb = $this->db->getQueryBuilder();
+ $qb->update($this->getTableName())
+ ->set('metadata', $qb->createNamedParameter(json_encode($signatory->getMetadata())))
+ ->set('last_updated', $qb->createNamedParameter(time()));
+ $qb->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId()))));
+
+ return $qb->executeStatement();
+ }
+
+ /**
+ * @param Signatory $signator
+ */
+ public function updatePublicKey(Signatory $signatory): int {
+ $qb = $this->db->getQueryBuilder();
+ $qb->update($this->getTableName())
+ ->set('signatory', $qb->createNamedParameter($signatory->getPublicKey()))
+ ->set('last_updated', $qb->createNamedParameter(time()));
+ $qb->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId()))));
+
+ return $qb->executeStatement();
+ }
+
+ /**
+ * returns a hash version for keyId for better index in the database
+ *
+ * @param string $keyId
+ *
+ * @return string
+ */
+ private function hashKeyId(string $keyId): string {
+ return hash('sha256', $keyId);
+ }
+}
diff --git a/lib/private/Security/Signature/Model/IncomingSignedRequest.php b/lib/private/Security/Signature/Model/IncomingSignedRequest.php
new file mode 100644
index 00000000000..0f7dc7cb771
--- /dev/null
+++ b/lib/private/Security/Signature/Model/IncomingSignedRequest.php
@@ -0,0 +1,268 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\Signature\Model;
+
+use JsonSerializable;
+use NCU\Security\Signature\Enum\DigestAlgorithm;
+use NCU\Security\Signature\Enum\SignatureAlgorithm;
+use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
+use NCU\Security\Signature\Exceptions\IncomingRequestException;
+use NCU\Security\Signature\Exceptions\InvalidSignatureException;
+use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
+use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException;
+use NCU\Security\Signature\Exceptions\SignatureException;
+use NCU\Security\Signature\Exceptions\SignatureNotFoundException;
+use NCU\Security\Signature\IIncomingSignedRequest;
+use NCU\Security\Signature\ISignatureManager;
+use NCU\Security\Signature\Model\Signatory;
+use OC\Security\Signature\SignatureManager;
+use OCP\IRequest;
+use ValueError;
+
+/**
+ * @inheritDoc
+ *
+ * @see ISignatureManager for details on signature
+ * @since 31.0.0
+ */
+class IncomingSignedRequest extends SignedRequest implements
+ IIncomingSignedRequest,
+ JsonSerializable {
+ private string $origin = '';
+
+ /**
+ * @param string $body
+ * @param IRequest $request
+ * @param array $options
+ *
+ * @throws IncomingRequestException if incoming request is wrongly signed
+ * @throws SignatureException if signature is faulty
+ * @throws SignatureNotFoundException if signature is not implemented
+ */
+ public function __construct(
+ string $body,
+ private readonly IRequest $request,
+ private readonly array $options = [],
+ ) {
+ parent::__construct($body);
+ $this->verifyHeaders();
+ $this->extractSignatureHeader();
+ $this->reconstructSignatureData();
+
+ try {
+ // we set origin based on the keyId defined in the Signature header of the request
+ $this->setOrigin(Signatory::extractIdentityFromUri($this->getSigningElement('keyId')));
+ } catch (IdentityNotFoundException $e) {
+ throw new IncomingRequestException($e->getMessage());
+ }
+ }
+
+ /**
+ * confirm that:
+ *
+ * - date is available in the header and its value is less than 5 minutes old
+ * - content-length is available and is the same as the payload size
+ * - digest is available and fit the checksum of the payload
+ *
+ * @throws IncomingRequestException
+ * @throws SignatureNotFoundException
+ */
+ private function verifyHeaders(): void {
+ if ($this->request->getHeader('Signature') === '') {
+ throw new SignatureNotFoundException('missing Signature in header');
+ }
+
+ // confirm presence of date, content-length, digest and Signature
+ $date = $this->request->getHeader('date');
+ if ($date === '') {
+ throw new IncomingRequestException('missing date in header');
+ }
+ $contentLength = $this->request->getHeader('content-length');
+ if ($contentLength === '') {
+ throw new IncomingRequestException('missing content-length in header');
+ }
+ $digest = $this->request->getHeader('digest');
+ if ($digest === '') {
+ throw new IncomingRequestException('missing digest in header');
+ }
+
+ // confirm date
+ try {
+ $dTime = new \DateTime($date);
+ $requestTime = $dTime->getTimestamp();
+ } catch (\Exception) {
+ throw new IncomingRequestException('datetime exception');
+ }
+ if ($requestTime < (time() - ($this->options['ttl'] ?? SignatureManager::DATE_TTL))) {
+ throw new IncomingRequestException('object is too old');
+ }
+
+ // confirm validity of content-length
+ if (strlen($this->getBody()) !== (int)$contentLength) {
+ throw new IncomingRequestException('inexact content-length in header');
+ }
+
+ // confirm digest value, based on body
+ [$algo, ] = explode('=', $digest);
+ try {
+ $this->setDigestAlgorithm(DigestAlgorithm::from($algo));
+ } catch (ValueError) {
+ throw new IncomingRequestException('unknown digest algorithm');
+ }
+ if ($digest !== $this->getDigest()) {
+ throw new IncomingRequestException('invalid value for digest in header');
+ }
+ }
+
+ /**
+ * extract data from the header entry 'Signature' and convert its content from string to an array
+ * also confirm that it contains the minimum mandatory information
+ *
+ * @throws IncomingRequestException
+ */
+ private function extractSignatureHeader(): void {
+ $details = [];
+ foreach (explode(',', $this->request->getHeader('Signature')) as $entry) {
+ if ($entry === '' || !strpos($entry, '=')) {
+ continue;
+ }
+
+ [$k, $v] = explode('=', $entry, 2);
+ preg_match('/^"([^"]+)"$/', $v, $var);
+ if ($var[0] !== '') {
+ $v = trim($var[0], '"');
+ }
+ $details[$k] = $v;
+ }
+
+ $this->setSigningElements($details);
+
+ try {
+ // confirm keys are in the Signature header
+ $this->getSigningElement('keyId');
+ $this->getSigningElement('headers');
+ $this->setSignature($this->getSigningElement('signature'));
+ } catch (SignatureElementNotFoundException $e) {
+ throw new IncomingRequestException($e->getMessage());
+ }
+ }
+
+ /**
+ * reconstruct signature data based on signature's metadata stored in the 'Signature' header
+ *
+ * @throws SignatureException
+ * @throws SignatureElementNotFoundException
+ */
+ private function reconstructSignatureData(): void {
+ $usedHeaders = explode(' ', $this->getSigningElement('headers'));
+ $neededHeaders = array_merge(['date', 'host', 'content-length', 'digest'],
+ array_keys($this->options['extraSignatureHeaders'] ?? []));
+
+ $missingHeaders = array_diff($neededHeaders, $usedHeaders);
+ if ($missingHeaders !== []) {
+ throw new SignatureException('missing entries in Signature.headers: ' . json_encode($missingHeaders));
+ }
+
+ $estimated = ['(request-target): ' . strtolower($this->request->getMethod()) . ' ' . $this->request->getRequestUri()];
+ foreach ($usedHeaders as $key) {
+ if ($key === '(request-target)') {
+ continue;
+ }
+ $value = (strtolower($key) === 'host') ? $this->request->getServerHost() : $this->request->getHeader($key);
+ if ($value === '') {
+ throw new SignatureException('missing header ' . $key . ' in request');
+ }
+
+ $estimated[] = $key . ': ' . $value;
+ }
+
+ $this->setSignatureData($estimated);
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return IRequest
+ * @since 31.0.0
+ */
+ public function getRequest(): IRequest {
+ return $this->request;
+ }
+
+ /**
+ * set the hostname at the source of the request,
+ * based on the keyId defined in the signature header.
+ *
+ * @param string $origin
+ * @since 31.0.0
+ */
+ private function setOrigin(string $origin): void {
+ $this->origin = $origin;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return string
+ * @throws IncomingRequestException
+ * @since 31.0.0
+ */
+ public function getOrigin(): string {
+ if ($this->origin === '') {
+ throw new IncomingRequestException('empty origin');
+ }
+ return $this->origin;
+ }
+
+ /**
+ * returns the keyId extracted from the signature headers.
+ * keyId is a mandatory entry in the headers of a signed request.
+ *
+ * @return string
+ * @throws SignatureElementNotFoundException
+ * @since 31.0.0
+ */
+ public function getKeyId(): string {
+ return $this->getSigningElement('keyId');
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @throws SignatureException
+ * @throws SignatoryNotFoundException
+ * @since 31.0.0
+ */
+ public function verify(): void {
+ $publicKey = $this->getSignatory()->getPublicKey();
+ if ($publicKey === '') {
+ throw new SignatoryNotFoundException('empty public key');
+ }
+
+ $algorithm = SignatureAlgorithm::tryFrom($this->getSigningElement('algorithm')) ?? SignatureAlgorithm::RSA_SHA256;
+ if (openssl_verify(
+ implode("\n", $this->getSignatureData()),
+ base64_decode($this->getSignature()),
+ $publicKey,
+ $algorithm->value
+ ) !== 1) {
+ throw new InvalidSignatureException('signature issue');
+ }
+ }
+
+ public function jsonSerialize(): array {
+ return array_merge(
+ parent::jsonSerialize(),
+ [
+ 'options' => $this->options,
+ 'origin' => $this->origin,
+ ]
+ );
+ }
+}
diff --git a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php
new file mode 100644
index 00000000000..dbfac3bfd34
--- /dev/null
+++ b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php
@@ -0,0 +1,229 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\Signature\Model;
+
+use JsonSerializable;
+use NCU\Security\Signature\Enum\DigestAlgorithm;
+use NCU\Security\Signature\Enum\SignatureAlgorithm;
+use NCU\Security\Signature\Exceptions\SignatoryException;
+use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
+use NCU\Security\Signature\IOutgoingSignedRequest;
+use NCU\Security\Signature\ISignatoryManager;
+use NCU\Security\Signature\ISignatureManager;
+use OC\Security\Signature\SignatureManager;
+
+/**
+ * extends ISignedRequest to add info requested at the generation of the signature
+ *
+ * @see ISignatureManager for details on signature
+ * @since 31.0.0
+ */
+class OutgoingSignedRequest extends SignedRequest implements
+ IOutgoingSignedRequest,
+ JsonSerializable {
+ private string $host = '';
+ private array $headers = [];
+ /** @var list<string> $headerList */
+ private array $headerList = [];
+ private SignatureAlgorithm $algorithm;
+ public function __construct(
+ string $body,
+ ISignatoryManager $signatoryManager,
+ private readonly string $identity,
+ private readonly string $method,
+ private readonly string $path,
+ ) {
+ parent::__construct($body);
+
+ $options = $signatoryManager->getOptions();
+ $this->setHost($identity)
+ ->setAlgorithm($options['algorithm'] ?? SignatureAlgorithm::RSA_SHA256)
+ ->setSignatory($signatoryManager->getLocalSignatory())
+ ->setDigestAlgorithm($options['digestAlgorithm'] ?? DigestAlgorithm::SHA256);
+
+ $headers = array_merge([
+ '(request-target)' => strtolower($method) . ' ' . $path,
+ 'content-length' => strlen($this->getBody()),
+ 'date' => gmdate($options['dateHeader'] ?? SignatureManager::DATE_HEADER),
+ 'digest' => $this->getDigest(),
+ 'host' => $this->getHost()
+ ], $options['extraSignatureHeaders'] ?? []);
+
+ $signing = $headerList = [];
+ foreach ($headers as $element => $value) {
+ $signing[] = $element . ': ' . $value;
+ $headerList[] = $element;
+ if ($element !== '(request-target)') {
+ $this->addHeader($element, $value);
+ }
+ }
+
+ $this->setHeaderList($headerList)
+ ->setSignatureData($signing);
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param string $host
+ * @return $this
+ * @since 31.0.0
+ */
+ public function setHost(string $host): self {
+ $this->host = $host;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return string
+ * @since 31.0.0
+ */
+ public function getHost(): string {
+ return $this->host;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param string $key
+ * @param string|int|float $value
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ public function addHeader(string $key, string|int|float $value): self {
+ $this->headers[$key] = $value;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return array
+ * @since 31.0.0
+ */
+ public function getHeaders(): array {
+ return $this->headers;
+ }
+
+ /**
+ * set the ordered list of used headers in the Signature
+ *
+ * @param list<string> $list
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ public function setHeaderList(array $list): self {
+ $this->headerList = $list;
+ return $this;
+ }
+
+ /**
+ * returns ordered list of used headers in the Signature
+ *
+ * @return list<string>
+ * @since 31.0.0
+ */
+ public function getHeaderList(): array {
+ return $this->headerList;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param SignatureAlgorithm $algorithm
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ public function setAlgorithm(SignatureAlgorithm $algorithm): self {
+ $this->algorithm = $algorithm;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return SignatureAlgorithm
+ * @since 31.0.0
+ */
+ public function getAlgorithm(): SignatureAlgorithm {
+ return $this->algorithm;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return self
+ * @throws SignatoryException
+ * @throws SignatoryNotFoundException
+ * @since 31.0.0
+ */
+ public function sign(): self {
+ $privateKey = $this->getSignatory()->getPrivateKey();
+ if ($privateKey === '') {
+ throw new SignatoryException('empty private key');
+ }
+
+ openssl_sign(
+ implode("\n", $this->getSignatureData()),
+ $signed,
+ $privateKey,
+ $this->getAlgorithm()->value
+ );
+
+ $this->setSignature(base64_encode($signed));
+ $this->setSigningElements(
+ [
+ 'keyId="' . $this->getSignatory()->getKeyId() . '"',
+ 'algorithm="' . $this->getAlgorithm()->value . '"',
+ 'headers="' . implode(' ', $this->getHeaderList()) . '"',
+ 'signature="' . $this->getSignature() . '"'
+ ]
+ );
+ $this->addHeader('Signature', implode(',', $this->getSigningElements()));
+
+ return $this;
+ }
+
+ /**
+ * @param string $clear
+ * @param string $privateKey
+ * @param SignatureAlgorithm $algorithm
+ *
+ * @return string
+ * @throws SignatoryException
+ */
+ private function signString(string $clear, string $privateKey, SignatureAlgorithm $algorithm): string {
+ if ($privateKey === '') {
+ throw new SignatoryException('empty private key');
+ }
+
+ openssl_sign($clear, $signed, $privateKey, $algorithm->value);
+
+ return base64_encode($signed);
+ }
+
+ public function jsonSerialize(): array {
+ return array_merge(
+ parent::jsonSerialize(),
+ [
+ 'host' => $this->host,
+ 'headers' => $this->headers,
+ 'algorithm' => $this->algorithm->value,
+ 'method' => $this->method,
+ 'identity' => $this->identity,
+ 'path' => $this->path,
+ ]
+ );
+ }
+}
diff --git a/lib/private/Security/Signature/Model/SignedRequest.php b/lib/private/Security/Signature/Model/SignedRequest.php
new file mode 100644
index 00000000000..12a43f32bcc
--- /dev/null
+++ b/lib/private/Security/Signature/Model/SignedRequest.php
@@ -0,0 +1,216 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\Signature\Model;
+
+use JsonSerializable;
+use NCU\Security\Signature\Enum\DigestAlgorithm;
+use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
+use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException;
+use NCU\Security\Signature\ISignedRequest;
+use NCU\Security\Signature\Model\Signatory;
+
+/**
+ * @inheritDoc
+ *
+ * @since 31.0.0
+ */
+class SignedRequest implements ISignedRequest, JsonSerializable {
+ private string $digest = '';
+ private DigestAlgorithm $digestAlgorithm = DigestAlgorithm::SHA256;
+ private array $signingElements = [];
+ private array $signatureData = [];
+ private string $signature = '';
+ private ?Signatory $signatory = null;
+
+ public function __construct(
+ private readonly string $body,
+ ) {
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return string
+ * @since 31.0.0
+ */
+ public function getBody(): string {
+ return $this->body;
+ }
+
+ /**
+ * set algorithm used to generate digest
+ *
+ * @param DigestAlgorithm $algorithm
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ protected function setDigestAlgorithm(DigestAlgorithm $algorithm): self {
+ $this->digestAlgorithm = $algorithm;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return DigestAlgorithm
+ * @since 31.0.0
+ */
+ public function getDigestAlgorithm(): DigestAlgorithm {
+ return $this->digestAlgorithm;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return string
+ * @since 31.0.0
+ */
+ public function getDigest(): string {
+ if ($this->digest === '') {
+ $this->digest = $this->digestAlgorithm->value . '='
+ . base64_encode(hash($this->digestAlgorithm->getHashingAlgorithm(), $this->body, true));
+ }
+ return $this->digest;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param array $elements
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ public function setSigningElements(array $elements): self {
+ $this->signingElements = $elements;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return array
+ * @since 31.0.0
+ */
+ public function getSigningElements(): array {
+ return $this->signingElements;
+ }
+
+ /**
+ * @param string $key
+ *
+ * @return string
+ * @throws SignatureElementNotFoundException
+ * @since 31.0.0
+ *
+ */
+ public function getSigningElement(string $key): string { // getSignatureDetail / getSignatureEntry() ?
+ if (!array_key_exists($key, $this->signingElements)) {
+ throw new SignatureElementNotFoundException('missing element ' . $key . ' in Signature header');
+ }
+
+ return $this->signingElements[$key];
+ }
+
+ /**
+ * store data used to generate signature
+ *
+ * @param array $data
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ protected function setSignatureData(array $data): self {
+ $this->signatureData = $data;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return array
+ * @since 31.0.0
+ */
+ public function getSignatureData(): array {
+ return $this->signatureData;
+ }
+
+ /**
+ * set the signed version of the signature
+ *
+ * @param string $signature
+ *
+ * @return self
+ * @since 31.0.0
+ */
+ protected function setSignature(string $signature): self {
+ $this->signature = $signature;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return string
+ * @since 31.0.0
+ */
+ public function getSignature(): string {
+ return $this->signature;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param Signatory $signatory
+ * @return self
+ * @since 31.0.0
+ */
+ public function setSignatory(Signatory $signatory): self {
+ $this->signatory = $signatory;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return Signatory
+ * @throws SignatoryNotFoundException
+ * @since 31.0.0
+ */
+ public function getSignatory(): Signatory {
+ if ($this->signatory === null) {
+ throw new SignatoryNotFoundException();
+ }
+
+ return $this->signatory;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return bool
+ * @since 31.0.0
+ */
+ public function hasSignatory(): bool {
+ return ($this->signatory !== null);
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'body' => $this->body,
+ 'digest' => $this->getDigest(),
+ 'digestAlgorithm' => $this->getDigestAlgorithm()->value,
+ 'signingElements' => $this->signingElements,
+ 'signatureData' => $this->signatureData,
+ 'signature' => $this->signature,
+ 'signatory' => $this->signatory ?? false,
+ ];
+ }
+}
diff --git a/lib/private/Security/Signature/SignatureManager.php b/lib/private/Security/Signature/SignatureManager.php
new file mode 100644
index 00000000000..91a06e29b4a
--- /dev/null
+++ b/lib/private/Security/Signature/SignatureManager.php
@@ -0,0 +1,426 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Security\Signature;
+
+use NCU\Security\Signature\Enum\SignatoryType;
+use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
+use NCU\Security\Signature\Exceptions\IncomingRequestException;
+use NCU\Security\Signature\Exceptions\InvalidKeyOriginException;
+use NCU\Security\Signature\Exceptions\InvalidSignatureException;
+use NCU\Security\Signature\Exceptions\SignatoryConflictException;
+use NCU\Security\Signature\Exceptions\SignatoryException;
+use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
+use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException;
+use NCU\Security\Signature\Exceptions\SignatureException;
+use NCU\Security\Signature\Exceptions\SignatureNotFoundException;
+use NCU\Security\Signature\IIncomingSignedRequest;
+use NCU\Security\Signature\IOutgoingSignedRequest;
+use NCU\Security\Signature\ISignatoryManager;
+use NCU\Security\Signature\ISignatureManager;
+use NCU\Security\Signature\Model\Signatory;
+use OC\Security\Signature\Db\SignatoryMapper;
+use OC\Security\Signature\Model\IncomingSignedRequest;
+use OC\Security\Signature\Model\OutgoingSignedRequest;
+use OCP\DB\Exception as DBException;
+use OCP\IAppConfig;
+use OCP\IRequest;
+use Psr\Log\LoggerInterface;
+
+/**
+ * ISignatureManager is a service integrated to core that provide tools
+ * to set/get authenticity of/from outgoing/incoming request.
+ *
+ * Quick description of the signature, added to the headers
+ * {
+ * "(request-target)": "post /path",
+ * "content-length": 385,
+ * "date": "Mon, 08 Jul 2024 14:16:20 GMT",
+ * "digest": "SHA-256=U7gNVUQiixe5BRbp4Tg0xCZMTcSWXXUZI2\\/xtHM40S0=",
+ * "host": "hostname.of.the.recipient",
+ * "Signature": "keyId=\"https://author.hostname/key\",algorithm=\"sha256\",headers=\"content-length
+ * date digest host\",signature=\"DzN12OCS1rsA[...]o0VmxjQooRo6HHabg==\""
+ * }
+ *
+ * 'content-length' is the total length of the data/content
+ * 'date' is the datetime the request have been initiated
+ * 'digest' is a checksum of the data/content
+ * 'host' is the hostname of the recipient of the request (remote when signing outgoing request, local on
+ * incoming request)
+ * 'Signature' contains the signature generated using the private key, and metadata:
+ * - 'keyId' is a unique id, formatted as an url. hostname is used to retrieve the public key via custom
+ * discovery
+ * - 'algorithm' define the algorithm used to generate signature
+ * - 'headers' contains a list of element used during the generation of the signature
+ * - 'signature' is the encrypted string, using local private key, of an array containing elements
+ * listed in 'headers' and their value. Some elements (content-length date digest host) are mandatory
+ * to ensure authenticity override protection.
+ *
+ * @since 31.0.0
+ */
+class SignatureManager implements ISignatureManager {
+ public const DATE_HEADER = 'D, d M Y H:i:s T';
+ public const DATE_TTL = 300;
+ public const SIGNATORY_TTL = 86400 * 3;
+ public const BODY_MAXSIZE = 50000; // max size of the payload of the request
+ public const APPCONFIG_IDENTITY = 'security.signature.identity';
+
+ public function __construct(
+ private readonly IRequest $request,
+ private readonly SignatoryMapper $mapper,
+ private readonly IAppConfig $appConfig,
+ private readonly LoggerInterface $logger,
+ ) {
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param ISignatoryManager $signatoryManager used to get details about remote instance
+ * @param string|null $body if NULL, body will be extracted from php://input
+ *
+ * @return IIncomingSignedRequest
+ * @throws IncomingRequestException if anything looks wrong with the incoming request
+ * @throws SignatureNotFoundException if incoming request is not signed
+ * @throws SignatureException if signature could not be confirmed
+ * @since 31.0.0
+ */
+ public function getIncomingSignedRequest(
+ ISignatoryManager $signatoryManager,
+ ?string $body = null,
+ ): IIncomingSignedRequest {
+ $body = $body ?? file_get_contents('php://input');
+ $options = $signatoryManager->getOptions();
+ if (strlen($body) > ($options['bodyMaxSize'] ?? self::BODY_MAXSIZE)) {
+ throw new IncomingRequestException('content of request is too big');
+ }
+
+ // generate IncomingSignedRequest based on body and request
+ $signedRequest = new IncomingSignedRequest($body, $this->request, $options);
+
+ try {
+ // confirm the validity of content and identity of the incoming request
+ $this->confirmIncomingRequestSignature($signedRequest, $signatoryManager, $options['ttlSignatory'] ?? self::SIGNATORY_TTL);
+ } catch (SignatureException $e) {
+ $this->logger->warning(
+ 'signature could not be verified', [
+ 'exception' => $e,
+ 'signedRequest' => $signedRequest,
+ 'signatoryManager' => get_class($signatoryManager)
+ ]
+ );
+ throw $e;
+ }
+
+ return $signedRequest;
+ }
+
+ /**
+ * confirm that the Signature is signed using the correct private key, using
+ * clear version of the Signature and the public key linked to the keyId
+ *
+ * @param IIncomingSignedRequest $signedRequest
+ * @param ISignatoryManager $signatoryManager
+ *
+ * @throws SignatoryNotFoundException
+ * @throws SignatureException
+ */
+ private function confirmIncomingRequestSignature(
+ IIncomingSignedRequest $signedRequest,
+ ISignatoryManager $signatoryManager,
+ int $ttlSignatory,
+ ): void {
+ $knownSignatory = null;
+ try {
+ $knownSignatory = $this->getStoredSignatory($signedRequest->getKeyId());
+ // refreshing ttl and compare with previous public key
+ if ($ttlSignatory > 0 && $knownSignatory->getLastUpdated() < (time() - $ttlSignatory)) {
+ $signatory = $this->getSaneRemoteSignatory($signatoryManager, $signedRequest);
+ $this->updateSignatoryMetadata($signatory);
+ $knownSignatory->setMetadata($signatory->getMetadata() ?? []);
+ }
+
+ $signedRequest->setSignatory($knownSignatory);
+ $signedRequest->verify();
+ } catch (InvalidKeyOriginException $e) {
+ throw $e; // issue while requesting remote instance also means there is no 2nd try
+ } catch (SignatoryNotFoundException) {
+ // if no signatory in cache, we retrieve the one from the remote instance (using
+ // $signatoryManager), check its validity with current signature and store it
+ $signatory = $this->getSaneRemoteSignatory($signatoryManager, $signedRequest);
+ $signedRequest->setSignatory($signatory);
+ $signedRequest->verify();
+ $this->storeSignatory($signatory);
+ } catch (SignatureException) {
+ // if public key (from cache) is not valid, we try to refresh it (based on SignatoryType)
+ try {
+ $signatory = $this->getSaneRemoteSignatory($signatoryManager, $signedRequest);
+ } catch (SignatoryNotFoundException $e) {
+ $this->manageDeprecatedSignatory($knownSignatory);
+ throw $e;
+ }
+
+ $signedRequest->setSignatory($signatory);
+ try {
+ $signedRequest->verify();
+ } catch (InvalidSignatureException $e) {
+ $this->logger->debug('signature issue', ['signed' => $signedRequest, 'exception' => $e]);
+ throw $e;
+ }
+
+ $this->storeSignatory($signatory);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param ISignatoryManager $signatoryManager
+ * @param string $content body to be signed
+ * @param string $method needed in the signature
+ * @param string $uri needed in the signature
+ *
+ * @return IOutgoingSignedRequest
+ * @throws IdentityNotFoundException
+ * @throws SignatoryException
+ * @throws SignatoryNotFoundException
+ * @since 31.0.0
+ */
+ public function getOutgoingSignedRequest(
+ ISignatoryManager $signatoryManager,
+ string $content,
+ string $method,
+ string $uri,
+ ): IOutgoingSignedRequest {
+ $signedRequest = new OutgoingSignedRequest(
+ $content,
+ $signatoryManager,
+ $this->extractIdentityFromUri($uri),
+ $method,
+ parse_url($uri, PHP_URL_PATH) ?? '/'
+ );
+
+ $signedRequest->sign();
+
+ return $signedRequest;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param ISignatoryManager $signatoryManager
+ * @param array $payload original payload, will be used to sign and completed with new headers with
+ * signature elements
+ * @param string $method needed in the signature
+ * @param string $uri needed in the signature
+ *
+ * @return array new payload to be sent, including original payload and signature elements in headers
+ * @since 31.0.0
+ */
+ public function signOutgoingRequestIClientPayload(
+ ISignatoryManager $signatoryManager,
+ array $payload,
+ string $method,
+ string $uri,
+ ): array {
+ $signedRequest = $this->getOutgoingSignedRequest($signatoryManager, $payload['body'], $method, $uri);
+ $payload['headers'] = array_merge($payload['headers'], $signedRequest->getHeaders());
+
+ return $payload;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param string $host remote host
+ * @param string $account linked account, should be used when multiple signature can exist for the same
+ * host
+ *
+ * @return Signatory
+ * @throws SignatoryNotFoundException if entry does not exist in local database
+ * @since 31.0.0
+ */
+ public function getSignatory(string $host, string $account = ''): Signatory {
+ return $this->mapper->getByHost($host, $account);
+ }
+
+
+ /**
+ * @inheritDoc
+ *
+ * keyId is set using app config 'core/security.signature.identity'
+ *
+ * @param string $path
+ *
+ * @return string
+ * @throws IdentityNotFoundException is identity is not set in app config
+ * @since 31.0.0
+ */
+ public function generateKeyIdFromConfig(string $path): string {
+ if (!$this->appConfig->hasKey('core', self::APPCONFIG_IDENTITY, true)) {
+ throw new IdentityNotFoundException(self::APPCONFIG_IDENTITY . ' not set');
+ }
+
+ $identity = trim($this->appConfig->getValueString('core', self::APPCONFIG_IDENTITY, lazy: true), '/');
+
+ return 'https://' . $identity . '/' . ltrim($path, '/');
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param string $uri
+ *
+ * @return string
+ * @throws IdentityNotFoundException if identity cannot be extracted
+ * @since 31.0.0
+ */
+ public function extractIdentityFromUri(string $uri): string {
+ return Signatory::extractIdentityFromUri($uri);
+ }
+
+ /**
+ * get remote signatory using the ISignatoryManager
+ * and confirm the validity of the keyId
+ *
+ * @param ISignatoryManager $signatoryManager
+ * @param IIncomingSignedRequest $signedRequest
+ *
+ * @return Signatory
+ * @throws InvalidKeyOriginException
+ * @throws SignatoryNotFoundException
+ * @see ISignatoryManager::getRemoteSignatory
+ */
+ private function getSaneRemoteSignatory(
+ ISignatoryManager $signatoryManager,
+ IIncomingSignedRequest $signedRequest,
+ ): Signatory {
+ $signatory = $signatoryManager->getRemoteSignatory($signedRequest->getOrigin());
+ if ($signatory === null) {
+ throw new SignatoryNotFoundException('empty result from getRemoteSignatory');
+ }
+ try {
+ if ($signatory->getKeyId() !== $signedRequest->getKeyId()) {
+ throw new InvalidKeyOriginException('keyId from signatory not related to the one from request');
+ }
+ } catch (SignatureElementNotFoundException) {
+ throw new InvalidKeyOriginException('missing keyId');
+ }
+ $signatory->setProviderId($signatoryManager->getProviderId());
+
+ return $signatory;
+ }
+
+ /**
+ * @param string $keyId
+ *
+ * @return Signatory
+ * @throws SignatoryNotFoundException
+ */
+ private function getStoredSignatory(string $keyId): Signatory {
+ return $this->mapper->getByKeyId($keyId);
+ }
+
+ /**
+ * @param Signatory $signatory
+ */
+ private function storeSignatory(Signatory $signatory): void {
+ try {
+ $this->insertSignatory($signatory);
+ } catch (DBException $e) {
+ if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ $this->logger->warning('exception while storing signature', ['exception' => $e]);
+ throw $e;
+ }
+
+ try {
+ $this->updateKnownSignatory($signatory);
+ } catch (SignatoryNotFoundException $e) {
+ $this->logger->warning('strange behavior, signatory not found ?', ['exception' => $e]);
+ }
+ }
+ }
+
+ /**
+ * @param Signatory $signatory
+ */
+ private function insertSignatory(Signatory $signatory): void {
+ $time = time();
+ $signatory->setCreation($time);
+ $signatory->setLastUpdated($time);
+ $signatory->setMetadata($signatory->getMetadata() ?? []); // trigger insert on field metadata using current or default value
+ $this->mapper->insert($signatory);
+ }
+
+ /**
+ * @param Signatory $signatory
+ *
+ * @throws SignatoryNotFoundException
+ * @throws SignatoryConflictException
+ */
+ private function updateKnownSignatory(Signatory $signatory): void {
+ $knownSignatory = $this->getStoredSignatory($signatory->getKeyId());
+ switch ($signatory->getType()) {
+ case SignatoryType::FORGIVABLE:
+ $this->deleteSignatory($knownSignatory->getKeyId());
+ $this->insertSignatory($signatory);
+ return;
+
+ case SignatoryType::REFRESHABLE:
+ $this->updateSignatoryPublicKey($signatory);
+ $this->updateSignatoryMetadata($signatory);
+ break;
+
+ case SignatoryType::TRUSTED:
+ // TODO: send notice to admin
+ throw new SignatoryConflictException();
+
+ case SignatoryType::STATIC:
+ // TODO: send warning to admin
+ throw new SignatoryConflictException();
+ }
+ }
+
+ /**
+ * This is called when a remote signatory does not exist anymore
+ *
+ * @param Signatory|null $knownSignatory NULL is not known
+ *
+ * @throws SignatoryConflictException
+ * @throws SignatoryNotFoundException
+ */
+ private function manageDeprecatedSignatory(?Signatory $knownSignatory): void {
+ switch ($knownSignatory?->getType()) {
+ case null: // unknown in local database
+ case SignatoryType::FORGIVABLE: // who cares ?
+ throw new SignatoryNotFoundException(); // meaning we just return the correct exception
+
+ case SignatoryType::REFRESHABLE:
+ // TODO: send notice to admin
+ throw new SignatoryConflictException(); // while it can be refreshed, it must exist
+
+ case SignatoryType::TRUSTED:
+ case SignatoryType::STATIC:
+ // TODO: send warning to admin
+ throw new SignatoryConflictException(); // no way.
+ }
+ }
+
+
+ private function updateSignatoryPublicKey(Signatory $signatory): void {
+ $this->mapper->updatePublicKey($signatory);
+ }
+
+ private function updateSignatoryMetadata(Signatory $signatory): void {
+ $this->mapper->updateMetadata($signatory);
+ }
+
+ private function deleteSignatory(string $keyId): void {
+ $this->mapper->deleteByKeyId($keyId);
+ }
+}
diff --git a/lib/private/Security/TrustedDomainHelper.php b/lib/private/Security/TrustedDomainHelper.php
index e91f230a9c9..a65779780e8 100644
--- a/lib/private/Security/TrustedDomainHelper.php
+++ b/lib/private/Security/TrustedDomainHelper.php
@@ -1,31 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author J0WI <J0WI@users.noreply.github.com>
- * @author Johannes Ernst <jernst@indiecomputing.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @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/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\Security;
diff --git a/lib/private/Security/VerificationToken/CleanUpJob.php b/lib/private/Security/VerificationToken/CleanUpJob.php
index 9c1b27d344d..ba8f4352f80 100644
--- a/lib/private/Security/VerificationToken/CleanUpJob.php
+++ b/lib/private/Security/VerificationToken/CleanUpJob.php
@@ -1,29 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OC\Security\VerificationToken;
use OCP\AppFramework\Utility\ITimeFactory;
diff --git a/lib/private/Security/VerificationToken/VerificationToken.php b/lib/private/Security/VerificationToken/VerificationToken.php
index 5f606d0e049..89f45180359 100644
--- a/lib/private/Security/VerificationToken/VerificationToken.php
+++ b/lib/private/Security/VerificationToken/VerificationToken.php
@@ -1,29 +1,10 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * 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
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OC\Security\VerificationToken;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -44,7 +25,7 @@ class VerificationToken implements IVerificationToken {
private ICrypto $crypto,
private ITimeFactory $timeFactory,
private ISecureRandom $secureRandom,
- private IJobList $jobList
+ private IJobList $jobList,
) {
}
@@ -72,7 +53,7 @@ class VerificationToken implements IVerificationToken {
}
try {
- $decryptedToken = $this->crypto->decrypt($encryptedToken, $passwordPrefix.$this->config->getSystemValueString('secret'));
+ $decryptedToken = $this->crypto->decrypt($encryptedToken, $passwordPrefix . $this->config->getSystemValueString('secret'));
} catch (\Exception $e) {
// Retry with empty secret as a fallback for instances where the secret might not have been set by accident
try {
@@ -104,11 +85,11 @@ class VerificationToken implements IVerificationToken {
): string {
$token = $this->secureRandom->generate(
21,
- ISecureRandom::CHAR_DIGITS.
- ISecureRandom::CHAR_LOWER.
- ISecureRandom::CHAR_UPPER
+ ISecureRandom::CHAR_DIGITS
+ . ISecureRandom::CHAR_LOWER
+ . ISecureRandom::CHAR_UPPER
);
- $tokenValue = $this->timeFactory->getTime() .':'. $token;
+ $tokenValue = $this->timeFactory->getTime() . ':' . $token;
$encryptedValue = $this->crypto->encrypt($tokenValue, $passwordPrefix . $this->config->getSystemValueString('secret'));
$this->config->setUserValue($user->getUID(), 'core', $subject, $encryptedValue);
$jobArgs = json_encode([