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.php99
-rw-r--r--lib/private/Security/Bruteforce/Backend/IBackend.php65
-rw-r--r--lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php144
-rw-r--r--lib/private/Security/Bruteforce/Capabilities.php56
-rw-r--r--lib/private/Security/Bruteforce/CleanupJob.php39
-rw-r--r--lib/private/Security/Bruteforce/Throttler.php335
-rw-r--r--lib/private/Security/CSP/ContentSecurityPolicy.php147
-rw-r--r--lib/private/Security/CSP/ContentSecurityPolicyManager.php54
-rw-r--r--lib/private/Security/CSP/ContentSecurityPolicyNonceManager.php69
-rw-r--r--lib/private/Security/CSRF/CsrfToken.php39
-rw-r--r--lib/private/Security/CSRF/CsrfTokenGenerator.php35
-rw-r--r--lib/private/Security/CSRF/CsrfTokenManager.php52
-rw-r--r--lib/private/Security/CSRF/TokenStorage/SessionStorage.php51
-rw-r--r--lib/private/Security/Certificate.php83
-rw-r--r--lib/private/Security/CertificateManager.php139
-rw-r--r--lib/private/Security/CredentialsManager.php56
-rw-r--r--lib/private/Security/Crypto.php88
-rw-r--r--lib/private/Security/FeaturePolicy/FeaturePolicy.php21
-rw-r--r--lib/private/Security/FeaturePolicy/FeaturePolicyManager.php38
-rw-r--r--lib/private/Security/Hasher.php81
-rw-r--r--lib/private/Security/IdentityProof/Key.php38
-rw-r--r--lib/private/Security/IdentityProof/Manager.php130
-rw-r--r--lib/private/Security/IdentityProof/Signer.php58
-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.php135
-rw-r--r--lib/private/Security/RateLimiting/Backend/DatabaseBackend.php84
-rw-r--r--lib/private/Security/RateLimiting/Backend/IBackend.php37
-rw-r--r--lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php78
-rw-r--r--lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php24
-rw-r--r--lib/private/Security/RateLimiting/Limiter.php82
-rw-r--r--lib/private/Security/RemoteHostValidator.php48
-rw-r--r--lib/private/Security/SecureRandom.php36
-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.php41
-rw-r--r--lib/private/Security/VerificationToken/CleanUpJob.php44
-rw-r--r--lib/private/Security/VerificationToken/VerificationToken.php75
44 files changed, 2505 insertions, 1589 deletions
diff --git a/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php b/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php
new file mode 100644
index 00000000000..33c2a3aae62
--- /dev/null
+++ b/lib/private/Security/Bruteforce/Backend/DatabaseBackend.php
@@ -0,0 +1,99 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\Bruteforce\Backend;
+
+use OCP\IDBConnection;
+
+class DatabaseBackend implements IBackend {
+ private const TABLE_NAME = 'bruteforce_attempts';
+
+ public function __construct(
+ private IDBConnection $db,
+ ) {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getAttempts(
+ string $ipSubnet,
+ int $maxAgeTimestamp,
+ ?string $action = null,
+ ?array $metadata = null,
+ ): int {
+ $query = $this->db->getQueryBuilder();
+ $query->select($query->func()->count('*', 'attempts'))
+ ->from(self::TABLE_NAME)
+ ->where($query->expr()->gt('occurred', $query->createNamedParameter($maxAgeTimestamp)))
+ ->andWhere($query->expr()->eq('subnet', $query->createNamedParameter($ipSubnet)));
+
+ if ($action !== null) {
+ $query->andWhere($query->expr()->eq('action', $query->createNamedParameter($action)));
+
+ if ($metadata !== null) {
+ $query->andWhere($query->expr()->eq('metadata', $query->createNamedParameter(json_encode($metadata))));
+ }
+ }
+
+ $result = $query->executeQuery();
+ $row = $result->fetch();
+ $result->closeCursor();
+
+ return (int)$row['attempts'];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function resetAttempts(
+ string $ipSubnet,
+ ?string $action = null,
+ ?array $metadata = null,
+ ): void {
+ $query = $this->db->getQueryBuilder();
+ $query->delete(self::TABLE_NAME)
+ ->where($query->expr()->eq('subnet', $query->createNamedParameter($ipSubnet)));
+
+ if ($action !== null) {
+ $query->andWhere($query->expr()->eq('action', $query->createNamedParameter($action)));
+
+ if ($metadata !== null) {
+ $query->andWhere($query->expr()->eq('metadata', $query->createNamedParameter(json_encode($metadata))));
+ }
+ }
+
+ $query->executeStatement();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function registerAttempt(
+ string $ip,
+ string $ipSubnet,
+ int $timestamp,
+ string $action,
+ array $metadata = [],
+ ): void {
+ $values = [
+ 'ip' => $ip,
+ 'subnet' => $ipSubnet,
+ 'action' => $action,
+ 'metadata' => json_encode($metadata),
+ 'occurred' => $timestamp,
+ ];
+
+ $qb = $this->db->getQueryBuilder();
+ $qb->insert(self::TABLE_NAME);
+ foreach ($values as $column => $value) {
+ $qb->setValue($column, $qb->createNamedParameter($value));
+ }
+ $qb->executeStatement();
+ }
+}
diff --git a/lib/private/Security/Bruteforce/Backend/IBackend.php b/lib/private/Security/Bruteforce/Backend/IBackend.php
new file mode 100644
index 00000000000..7118123cbb5
--- /dev/null
+++ b/lib/private/Security/Bruteforce/Backend/IBackend.php
@@ -0,0 +1,65 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\Bruteforce\Backend;
+
+/**
+ * Interface IBackend defines a storage backend for the bruteforce data. It
+ * should be noted that writing and reading brute force data is an expensive
+ * operation and one should thus make sure to only use sufficient fast backends.
+ */
+interface IBackend {
+ /**
+ * Gets the number of attempts for the specified subnet (and further filters)
+ *
+ * @param string $ipSubnet
+ * @param int $maxAgeTimestamp
+ * @param ?string $action Optional action to further limit attempts
+ * @param ?array $metadata Optional metadata stored to further limit attempts (Only considered when $action is set)
+ * @return int
+ * @since 28.0.0
+ */
+ public function getAttempts(
+ string $ipSubnet,
+ int $maxAgeTimestamp,
+ ?string $action = null,
+ ?array $metadata = null,
+ ): int;
+
+ /**
+ * Reset the attempts for the specified subnet (and further filters)
+ *
+ * @param string $ipSubnet
+ * @param ?string $action Optional action to further limit attempts
+ * @param ?array $metadata Optional metadata stored to further limit attempts(Only considered when $action is set)
+ * @since 28.0.0
+ */
+ public function resetAttempts(
+ string $ipSubnet,
+ ?string $action = null,
+ ?array $metadata = null,
+ ): void;
+
+ /**
+ * Register a failed attempt to bruteforce a security control
+ *
+ * @param string $ip
+ * @param string $ipSubnet
+ * @param int $timestamp
+ * @param string $action
+ * @param array $metadata Optional metadata stored to further limit attempts when getting
+ * @since 28.0.0
+ */
+ public function registerAttempt(
+ string $ip,
+ string $ipSubnet,
+ int $timestamp,
+ string $action,
+ array $metadata = [],
+ ): void;
+}
diff --git a/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php b/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php
new file mode 100644
index 00000000000..9a0723db47e
--- /dev/null
+++ b/lib/private/Security/Bruteforce/Backend/MemoryCacheBackend.php
@@ -0,0 +1,144 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\Bruteforce\Backend;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\ICache;
+use OCP\ICacheFactory;
+
+class MemoryCacheBackend implements IBackend {
+ private ICache $cache;
+
+ public function __construct(
+ ICacheFactory $cacheFactory,
+ private ITimeFactory $timeFactory,
+ ) {
+ $this->cache = $cacheFactory->createDistributed(self::class);
+ }
+
+ private function hash(
+ null|string|array $data,
+ ): ?string {
+ if ($data === null) {
+ return null;
+ }
+ if (!is_string($data)) {
+ $data = json_encode($data);
+ }
+ return hash('sha1', $data);
+ }
+
+ private function getExistingAttempts(string $identifier): array {
+ $cachedAttempts = $this->cache->get($identifier);
+ if ($cachedAttempts === null) {
+ return [];
+ }
+
+ $cachedAttempts = json_decode($cachedAttempts, true);
+ if (\is_array($cachedAttempts)) {
+ return $cachedAttempts;
+ }
+
+ return [];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getAttempts(
+ string $ipSubnet,
+ int $maxAgeTimestamp,
+ ?string $action = null,
+ ?array $metadata = null,
+ ): int {
+ $identifier = $this->hash($ipSubnet);
+ $actionHash = $this->hash($action);
+ $metadataHash = $this->hash($metadata);
+ $existingAttempts = $this->getExistingAttempts($identifier);
+
+ $count = 0;
+ foreach ($existingAttempts as $info) {
+ [$occurredTime, $attemptAction, $attemptMetadata] = explode('#', $info, 3);
+ if ($action === null || $attemptAction === $actionHash) {
+ if ($metadata === null || $attemptMetadata === $metadataHash) {
+ if ($occurredTime > $maxAgeTimestamp) {
+ $count++;
+ }
+ }
+ }
+ }
+
+ return $count;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function resetAttempts(
+ string $ipSubnet,
+ ?string $action = null,
+ ?array $metadata = null,
+ ): void {
+ $identifier = $this->hash($ipSubnet);
+ if ($action === null) {
+ $this->cache->remove($identifier);
+ } else {
+ $actionHash = $this->hash($action);
+ $metadataHash = $this->hash($metadata);
+ $existingAttempts = $this->getExistingAttempts($identifier);
+ $maxAgeTimestamp = $this->timeFactory->getTime() - 12 * 3600;
+
+ foreach ($existingAttempts as $key => $info) {
+ [$occurredTime, $attemptAction, $attemptMetadata] = explode('#', $info, 3);
+ if ($attemptAction === $actionHash) {
+ if ($metadata === null || $attemptMetadata === $metadataHash) {
+ unset($existingAttempts[$key]);
+ } elseif ($occurredTime < $maxAgeTimestamp) {
+ unset($existingAttempts[$key]);
+ }
+ }
+ }
+
+ if (!empty($existingAttempts)) {
+ $this->cache->set($identifier, json_encode($existingAttempts), 12 * 3600);
+ } else {
+ $this->cache->remove($identifier);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function registerAttempt(
+ string $ip,
+ string $ipSubnet,
+ int $timestamp,
+ string $action,
+ array $metadata = [],
+ ): void {
+ $identifier = $this->hash($ipSubnet);
+ $existingAttempts = $this->getExistingAttempts($identifier);
+ $maxAgeTimestamp = $this->timeFactory->getTime() - 12 * 3600;
+
+ // Unset all attempts that are already expired
+ foreach ($existingAttempts as $key => $info) {
+ [$occurredTime,] = explode('#', $info, 3);
+ if ($occurredTime < $maxAgeTimestamp) {
+ unset($existingAttempts[$key]);
+ }
+ }
+ $existingAttempts = array_values($existingAttempts);
+
+ // Store the new attempt
+ $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 5de4f35f24e..581b4480a27 100644
--- a/lib/private/Security/Bruteforce/Capabilities.php
+++ b/lib/private/Security/Bruteforce/Capabilities.php
@@ -3,62 +3,32 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017 Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author J0WI <J0WI@users.noreply.github.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;
-use OCP\Capabilities\IPublicCapability;
use OCP\Capabilities\IInitialStateExcludedCapability;
+use OCP\Capabilities\IPublicCapability;
use OCP\IRequest;
+use OCP\Security\Bruteforce\IThrottler;
class Capabilities implements IPublicCapability, IInitialStateExcludedCapability {
- /** @var IRequest */
- private $request;
-
- /** @var Throttler */
- private $throttler;
+ public function __construct(
+ private IRequest $request,
+ private IThrottler $throttler,
+ ) {
+ }
/**
- * Capabilities constructor.
- *
- * @param IRequest $request
- * @param Throttler $throttler
+ * @return array{bruteforce: array{delay: int, allow-listed: bool}}
*/
- public function __construct(IRequest $request,
- Throttler $throttler) {
- $this->request = $request;
- $this->throttler = $throttler;
- }
-
public function getCapabilities(): array {
- if (version_compare(\OC::$server->getConfig()->getSystemValue('version', '0.0.0.0'), '12.0.0.0', '<')) {
- return [];
- }
-
return [
'bruteforce' => [
- 'delay' => $this->throttler->getDelay($this->request->getRemoteAddress())
- ]
+ 'delay' => $this->throttler->getDelay($this->request->getRemoteAddress()),
+ 'allow-listed' => $this->throttler->isBypassListed($this->request->getRemoteAddress()),
+ ],
];
}
}
diff --git a/lib/private/Security/Bruteforce/CleanupJob.php b/lib/private/Security/Bruteforce/CleanupJob.php
index 45cfe572acb..f07e4dbacbd 100644
--- a/lib/private/Security/Bruteforce/CleanupJob.php
+++ b/lib/private/Security/Bruteforce/CleanupJob.php
@@ -3,54 +3,35 @@
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;
class CleanupJob extends TimedJob {
- /** @var IDBConnection */
- private $connection;
-
- public function __construct(ITimeFactory $time, IDBConnection $connection) {
+ public function __construct(
+ ITimeFactory $time,
+ private IDBConnection $connection,
+ ) {
parent::__construct($time);
- $this->connection = $connection;
// 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) {
+ protected function run($argument): void {
// Delete all entries more than 48 hours old
$time = $this->time->getTime() - (48 * 3600);
$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 299cab93eb3..574f6c80c3f 100644
--- a/lib/private/Security/Bruteforce/Throttler.php
+++ b/lib/private/Security/Bruteforce/Throttler.php
@@ -3,39 +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 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;
-use OCP\IDBConnection;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\Bruteforce\MaxDelayReached;
use Psr\Log\LoggerInterface;
@@ -54,76 +31,33 @@ use Psr\Log\LoggerInterface;
* @package OC\Security\Bruteforce
*/
class Throttler implements IThrottler {
- public const LOGIN_ACTION = 'login';
-
- /** @var IDBConnection */
- private $db;
- /** @var ITimeFactory */
- private $timeFactory;
- private LoggerInterface $logger;
- /** @var IConfig */
- private $config;
/** @var bool[] */
- private $hasAttemptsDeleted = [];
-
- public function __construct(IDBConnection $db,
- ITimeFactory $timeFactory,
- LoggerInterface $logger,
- IConfig $config) {
- $this->db = $db;
- $this->timeFactory = $timeFactory;
- $this->logger = $logger;
- $this->config = $config;
- }
-
- /**
- * Convert a number of seconds into the appropriate DateInterval
- *
- * @param int $expire
- * @return \DateInterval
- */
- private function getCutoff(int $expire): \DateInterval {
- $d1 = new \DateTime();
- $d2 = clone $d1;
- $d2->sub(new \DateInterval('PT' . $expire . 'S'));
- return $d2->diff($d1);
- }
-
- /**
- * Calculate the cut off timestamp
- *
- * @param float $maxAgeHours
- * @return int
- */
- private function getCutoffTimestamp(float $maxAgeHours = 12.0): int {
- return (new \DateTime())
- ->sub($this->getCutoff((int) ($maxAgeHours * 3600)))
- ->getTimestamp();
+ private array $hasAttemptsDeleted = [];
+
+ public function __construct(
+ private ITimeFactory $timeFactory,
+ private LoggerInterface $logger,
+ private IConfig $config,
+ private IBackend $backend,
+ private BruteforceAllowList $allowList,
+ ) {
}
/**
- * Register a failed attempt to bruteforce a security control
- *
- * @param string $action
- * @param string $ip
- * @param array $metadata Optional metadata logged to the database
+ * {@inheritDoc}
*/
public function registerAttempt(string $action,
- string $ip,
- array $metadata = []): void {
+ string $ip,
+ array $metadata = []): void {
// No need to log if the bruteforce protection is disabled
- if ($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) {
+ if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
return;
}
$ipAddress = new IpAddress($ip);
- $values = [
- 'action' => $action,
- 'occurred' => $this->timeFactory->getTime(),
- 'ip' => (string)$ipAddress,
- 'subnet' => $ipAddress->getSubnet(),
- 'metadata' => json_encode($metadata),
- ];
+ if ($this->isBypassListed((string)$ipAddress)) {
+ return;
+ }
$this->logger->notice(
sprintf(
@@ -136,86 +70,33 @@ class Throttler implements IThrottler {
]
);
- $qb = $this->db->getQueryBuilder();
- $qb->insert('bruteforce_attempts');
- foreach ($values as $column => $value) {
- $qb->setValue($column, $qb->createNamedParameter($value));
- }
- $qb->execute();
+ $this->backend->registerAttempt(
+ (string)$ipAddress,
+ $ipAddress->getSubnet(),
+ $this->timeFactory->getTime(),
+ $action,
+ $metadata
+ );
}
/**
* Check if the IP is whitelisted
- *
- * @param string $ip
- * @return bool
*/
- private function isIPWhitelisted(string $ip): bool {
- if ($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) {
- return true;
- }
-
- $keys = $this->config->getAppKeys('bruteForce');
- $keys = array_filter($keys, function ($key) {
- return 0 === strpos($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 {
- 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) {
- return true;
- }
- }
+ public function isBypassListed(string $ip): bool {
+ return $this->allowList->isBypassListed($ip);
+ }
- return false;
+ /**
+ * {@inheritDoc}
+ */
+ public function showBruteforceWarning(string $ip, string $action = ''): bool {
+ $attempts = $this->getAttempts($ip, $action);
+ // 4 failed attempts is the last delay below 5 seconds
+ return $attempts >= 4;
}
/**
- * Get the throttling delay (in milliseconds)
- *
- * @param string $ip
- * @param string $action optionally filter by action
- * @param float $maxAgeHours
- * @return int
+ * {@inheritDoc}
*/
public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int {
if ($maxAgeHours > 48) {
@@ -228,44 +109,37 @@ class Throttler implements IThrottler {
}
$ipAddress = new IpAddress($ip);
- if ($this->isIPWhitelisted((string)$ipAddress)) {
+ if ($this->isBypassListed((string)$ipAddress)) {
return 0;
}
- $cutoffTime = $this->getCutoffTimestamp($maxAgeHours);
+ $maxAgeTimestamp = (int)($this->timeFactory->getTime() - 3600 * $maxAgeHours);
- $qb = $this->db->getQueryBuilder();
- $qb->select($qb->func()->count('*', 'attempts'))
- ->from('bruteforce_attempts')
- ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
- ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())));
-
- if ($action !== '') {
- $qb->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)));
- }
-
- $result = $qb->execute();
- $row = $result->fetch();
- $result->closeCursor();
-
- return (int) $row['attempts'];
+ return $this->backend->getAttempts(
+ $ipAddress->getSubnet(),
+ $maxAgeTimestamp,
+ $action !== '' ? $action : null,
+ );
}
/**
- * Get the throttling delay (in milliseconds)
- *
- * @param string $ip
- * @param string $action optionally filter by action
- * @return int
+ * {@inheritDoc}
*/
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;
}
@@ -274,92 +148,89 @@ class Throttler implements IThrottler {
if ($delay > self::MAX_DELAY) {
return self::MAX_DELAY_MS;
}
- return (int) \ceil($delay * 1000);
+ return (int)\ceil($delay * 1000);
}
/**
- * Reset the throttling delay for an IP address, action and metadata
- *
- * @param string $ip
- * @param string $action
- * @param array $metadata
+ * {@inheritDoc}
*/
public function resetDelay(string $ip, string $action, array $metadata): void {
- $ipAddress = new IpAddress($ip);
- if ($this->isIPWhitelisted((string)$ipAddress)) {
+ // No need to log if the bruteforce protection is disabled
+ if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
return;
}
- $cutoffTime = $this->getCutoffTimestamp();
-
- $qb = $this->db->getQueryBuilder();
- $qb->delete('bruteforce_attempts')
- ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
- ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($ipAddress->getSubnet())))
- ->andWhere($qb->expr()->eq('action', $qb->createNamedParameter($action)))
- ->andWhere($qb->expr()->eq('metadata', $qb->createNamedParameter(json_encode($metadata))));
+ $ipAddress = new IpAddress($ip);
+ if ($this->isBypassListed((string)$ipAddress)) {
+ return;
+ }
- $qb->executeStatement();
+ $this->backend->resetAttempts(
+ $ipAddress->getSubnet(),
+ $action,
+ $metadata,
+ );
$this->hasAttemptsDeleted[$action] = true;
}
/**
- * Reset the throttling delay for an IP address
- *
- * @param string $ip
+ * {@inheritDoc}
*/
public function resetDelayForIP(string $ip): void {
- $cutoffTime = $this->getCutoffTimestamp();
+ // No need to log if the bruteforce protection is disabled
+ if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
+ return;
+ }
- $qb = $this->db->getQueryBuilder();
- $qb->delete('bruteforce_attempts')
- ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
- ->andWhere($qb->expr()->eq('ip', $qb->createNamedParameter($ip)));
+ $ipAddress = new IpAddress($ip);
+ if ($this->isBypassListed((string)$ipAddress)) {
+ return;
+ }
- $qb->execute();
+ $this->backend->resetAttempts($ipAddress->getSubnet());
}
/**
- * Will sleep for the defined amount of time
- *
- * @param string $ip
- * @param string $action optionally filter by action
- * @return int the time spent sleeping
+ * {@inheritDoc}
*/
public function sleepDelay(string $ip, string $action = ''): int {
$delay = $this->getDelay($ip, $action);
- usleep($delay * 1000);
+ if (!$this->config->getSystemValueBool('auth.bruteforce.protection.testing')) {
+ usleep($delay * 1000);
+ }
return $delay;
}
/**
- * Will sleep for the defined amount of time unless maximum was reached in the last 30 minutes
- * In this case a "429 Too Many Request" exception is thrown
- *
- * @param string $ip
- * @param string $action optionally filter by action
- * @return int the time spent sleeping
- * @throws MaxDelayReached when reached the maximum
+ * {@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}]', [
+ $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,
+ 'attempts' => $attempts,
]);
- // 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}]', [
- 'action' => $action,
- 'ip' => $ip,
- 'delay' => $delay,
- ]);
+
+ if ($attempts > 0) {
+ return $this->calculateDelay($attempts);
}
- usleep($delay * 1000);
- return $delay;
+
+ return 0;
}
}
diff --git a/lib/private/Security/CSP/ContentSecurityPolicy.php b/lib/private/Security/CSP/ContentSecurityPolicy.php
index 8d9551c8978..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;
@@ -34,164 +16,106 @@ namespace OC\Security\CSP;
* @package OC\Security\CSP
*/
class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy {
- /**
- * @return boolean
- */
public function isInlineScriptAllowed(): bool {
return $this->inlineScriptAllowed;
}
- /**
- * @param boolean $inlineScriptAllowed
- */
- public function setInlineScriptAllowed(bool $inlineScriptAllowed) {
+ public function setInlineScriptAllowed(bool $inlineScriptAllowed): void {
$this->inlineScriptAllowed = $inlineScriptAllowed;
}
- /**
- * @return boolean
- */
public function isEvalScriptAllowed(): bool {
return $this->evalScriptAllowed;
}
/**
- * @param boolean $evalScriptAllowed
- *
* @deprecated 17.0.0 Unsafe eval should not be used anymore.
*/
- public function setEvalScriptAllowed(bool $evalScriptAllowed) {
+ public function setEvalScriptAllowed(bool $evalScriptAllowed): void {
$this->evalScriptAllowed = $evalScriptAllowed;
}
- /**
- * @return array
- */
+ public function isEvalWasmAllowed(): ?bool {
+ return $this->evalWasmAllowed;
+ }
+
+ public function setEvalWasmAllowed(bool $evalWasmAllowed): void {
+ $this->evalWasmAllowed = $evalWasmAllowed;
+ }
+
public function getAllowedScriptDomains(): array {
return $this->allowedScriptDomains;
}
- /**
- * @param array $allowedScriptDomains
- */
- public function setAllowedScriptDomains(array $allowedScriptDomains) {
+ public function setAllowedScriptDomains(array $allowedScriptDomains): void {
$this->allowedScriptDomains = $allowedScriptDomains;
}
- /**
- * @return boolean
- */
public function isInlineStyleAllowed(): bool {
return $this->inlineStyleAllowed;
}
- /**
- * @param boolean $inlineStyleAllowed
- */
- public function setInlineStyleAllowed(bool $inlineStyleAllowed) {
+ public function setInlineStyleAllowed(bool $inlineStyleAllowed): void {
$this->inlineStyleAllowed = $inlineStyleAllowed;
}
- /**
- * @return array
- */
public function getAllowedStyleDomains(): array {
return $this->allowedStyleDomains;
}
- /**
- * @param array $allowedStyleDomains
- */
- public function setAllowedStyleDomains(array $allowedStyleDomains) {
+ public function setAllowedStyleDomains(array $allowedStyleDomains): void {
$this->allowedStyleDomains = $allowedStyleDomains;
}
- /**
- * @return array
- */
public function getAllowedImageDomains(): array {
return $this->allowedImageDomains;
}
- /**
- * @param array $allowedImageDomains
- */
- public function setAllowedImageDomains(array $allowedImageDomains) {
+ public function setAllowedImageDomains(array $allowedImageDomains): void {
$this->allowedImageDomains = $allowedImageDomains;
}
- /**
- * @return array
- */
public function getAllowedConnectDomains(): array {
return $this->allowedConnectDomains;
}
- /**
- * @param array $allowedConnectDomains
- */
- public function setAllowedConnectDomains(array $allowedConnectDomains) {
+ public function setAllowedConnectDomains(array $allowedConnectDomains): void {
$this->allowedConnectDomains = $allowedConnectDomains;
}
- /**
- * @return array
- */
public function getAllowedMediaDomains(): array {
return $this->allowedMediaDomains;
}
- /**
- * @param array $allowedMediaDomains
- */
- public function setAllowedMediaDomains(array $allowedMediaDomains) {
+ public function setAllowedMediaDomains(array $allowedMediaDomains): void {
$this->allowedMediaDomains = $allowedMediaDomains;
}
- /**
- * @return array
- */
public function getAllowedObjectDomains(): array {
return $this->allowedObjectDomains;
}
- /**
- * @param array $allowedObjectDomains
- */
- public function setAllowedObjectDomains(array $allowedObjectDomains) {
+ public function setAllowedObjectDomains(array $allowedObjectDomains): void {
$this->allowedObjectDomains = $allowedObjectDomains;
}
- /**
- * @return array
- */
public function getAllowedFrameDomains(): array {
return $this->allowedFrameDomains;
}
- /**
- * @param array $allowedFrameDomains
- */
- public function setAllowedFrameDomains(array $allowedFrameDomains) {
+ public function setAllowedFrameDomains(array $allowedFrameDomains): void {
$this->allowedFrameDomains = $allowedFrameDomains;
}
- /**
- * @return array
- */
public function getAllowedFontDomains(): array {
return $this->allowedFontDomains;
}
- /**
- * @param array $allowedFontDomains
- */
- public function setAllowedFontDomains($allowedFontDomains) {
+ public function setAllowedFontDomains($allowedFontDomains): void {
$this->allowedFontDomains = $allowedFontDomains;
}
/**
- * @return array
* @deprecated 15.0.0 use FrameDomains and WorkerSrcDomains
*/
public function getAllowedChildSrcDomains(): array {
@@ -202,13 +126,10 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
* @param array $allowedChildSrcDomains
* @deprecated 15.0.0 use FrameDomains and WorkerSrcDomains
*/
- public function setAllowedChildSrcDomains($allowedChildSrcDomains) {
+ public function setAllowedChildSrcDomains($allowedChildSrcDomains): void {
$this->allowedChildSrcDomains = $allowedChildSrcDomains;
}
- /**
- * @return array
- */
public function getAllowedFrameAncestors(): array {
return $this->allowedFrameAncestors;
}
@@ -216,7 +137,7 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
/**
* @param array $allowedFrameAncestors
*/
- public function setAllowedFrameAncestors($allowedFrameAncestors) {
+ public function setAllowedFrameAncestors($allowedFrameAncestors): void {
$this->allowedFrameAncestors = $allowedFrameAncestors;
}
@@ -224,7 +145,7 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
return $this->allowedWorkerSrcDomains;
}
- public function setAllowedWorkerSrcDomains(array $allowedWorkerSrcDomains) {
+ public function setAllowedWorkerSrcDomains(array $allowedWorkerSrcDomains): void {
$this->allowedWorkerSrcDomains = $allowedWorkerSrcDomains;
}
@@ -241,21 +162,23 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
return $this->reportTo;
}
- public function setReportTo(array $reportTo) {
+ public function setReportTo(array $reportTo): void {
$this->reportTo = $reportTo;
}
- /**
- * @return boolean
- */
public function isStrictDynamicAllowed(): bool {
return $this->strictDynamicAllowed;
}
- /**
- * @param boolean $strictDynamicAllowed
- */
- public function setStrictDynamicAllowed(bool $strictDynamicAllowed) {
+ public function setStrictDynamicAllowed(bool $strictDynamicAllowed): void {
$this->strictDynamicAllowed = $strictDynamicAllowed;
}
+
+ public function isStrictDynamicAllowedOnScripts(): bool {
+ return $this->strictDynamicAllowedOnScripts;
+ }
+
+ public function setStrictDynamicAllowedOnScripts(bool $strictDynamicAllowedOnScripts): void {
+ $this->strictDynamicAllowedOnScripts = $strictDynamicAllowedOnScripts;
+ }
}
diff --git a/lib/private/Security/CSP/ContentSecurityPolicyManager.php b/lib/private/Security/CSP/ContentSecurityPolicyManager.php
index 4930dcb759c..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;
@@ -35,25 +17,21 @@ use OCP\Security\IContentSecurityPolicyManager;
class ContentSecurityPolicyManager implements IContentSecurityPolicyManager {
/** @var ContentSecurityPolicy[] */
- private $policies = [];
+ private array $policies = [];
- /** @var IEventDispatcher */
- private $dispatcher;
-
- public function __construct(IEventDispatcher $dispatcher) {
- $this->dispatcher = $dispatcher;
+ public function __construct(
+ private IEventDispatcher $dispatcher,
+ ) {
}
/** {@inheritdoc} */
- public function addDefaultPolicy(EmptyContentSecurityPolicy $policy) {
+ public function addDefaultPolicy(EmptyContentSecurityPolicy $policy): void {
$this->policies[] = $policy;
}
/**
* Get the configured default policy. This is not in the public namespace
* as it is only supposed to be used by core itself.
- *
- * @return ContentSecurityPolicy
*/
public function getDefaultPolicy(): ContentSecurityPolicy {
$event = new AddContentSecurityPolicyEvent($this);
@@ -68,21 +46,19 @@ class ContentSecurityPolicyManager implements IContentSecurityPolicyManager {
/**
* Merges the first given policy with the second one
- *
- * @param ContentSecurityPolicy $defaultPolicy
- * @param EmptyContentSecurityPolicy $originalPolicy
- * @return ContentSecurityPolicy
*/
- public function mergePolicies(ContentSecurityPolicy $defaultPolicy,
- EmptyContentSecurityPolicy $originalPolicy): ContentSecurityPolicy {
+ public function mergePolicies(
+ ContentSecurityPolicy $defaultPolicy,
+ 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 1167b3358d2..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;
@@ -38,32 +16,25 @@ use OCP\IRequest;
* @package OC\Security\CSP
*/
class ContentSecurityPolicyNonceManager {
- /** @var CsrfTokenManager */
- private $csrfTokenManager;
- /** @var IRequest */
- private $request;
- /** @var string */
- private $nonce = '';
+ private string $nonce = '';
- /**
- * @param CsrfTokenManager $csrfTokenManager
- * @param IRequest $request
- */
- public function __construct(CsrfTokenManager $csrfTokenManager,
- IRequest $request) {
- $this->csrfTokenManager = $csrfTokenManager;
- $this->request = $request;
+ public function __construct(
+ private CsrfTokenManager $csrfTokenManager,
+ private IRequest $request,
+ ) {
}
/**
- * Returns the current CSP nounce
- *
- * @return string
+ * Returns the current CSP nonce
*/
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'];
}
@@ -74,20 +45,16 @@ class ContentSecurityPolicyNonceManager {
/**
* Check if the browser supports CSP v3
- *
- * @return bool
*/
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 a76e169e5b9..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;
@@ -36,23 +17,19 @@ namespace OC\Security\CSRF;
* @package OC\Security\CSRF
*/
class CsrfToken {
- /** @var string */
- private $value;
- /** @var string */
- private $encryptedValue = '';
+ private string $encryptedValue = '';
/**
* @param string $value Value of the token. Can be encrypted or not encrypted.
*/
- public function __construct(string $value) {
- $this->value = $value;
+ public function __construct(
+ private string $value,
+ ) {
}
/**
* Encrypted value of the token. This is used to mitigate BREACH alike
* vulnerabilities. For display measures do use this functionality.
- *
- * @return string
*/
public function getEncryptedValue(): string {
if ($this->encryptedValue === '') {
@@ -66,8 +43,6 @@ class CsrfToken {
/**
* The unencrypted value of the token. Used for decrypting an already
* encrypted token.
- *
- * @return string
*/
public function getDecryptedValue(): string {
$token = explode(':', $this->value);
diff --git a/lib/private/Security/CSRF/CsrfTokenGenerator.php b/lib/private/Security/CSRF/CsrfTokenGenerator.php
index 0576fda9e06..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;
@@ -34,21 +17,15 @@ use OCP\Security\ISecureRandom;
* @package OC\Security\CSRF
*/
class CsrfTokenGenerator {
- /** @var ISecureRandom */
- private $random;
-
- /**
- * @param ISecureRandom $random
- */
- public function __construct(ISecureRandom $random) {
- $this->random = $random;
+ public function __construct(
+ private ISecureRandom $random,
+ ) {
}
/**
* Generate a new CSRF token.
*
* @param int $length Length of the token in characters.
- * @return string
*/
public function generateToken(int $length = 32): string {
return $this->random->generate($length);
diff --git a/lib/private/Security/CSRF/CsrfTokenManager.php b/lib/private/Security/CSRF/CsrfTokenManager.php
index 2c6dd45866d..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;
@@ -34,27 +16,18 @@ use OC\Security\CSRF\TokenStorage\SessionStorage;
* @package OC\Security\CSRF
*/
class CsrfTokenManager {
- /** @var CsrfTokenGenerator */
- private $tokenGenerator;
- /** @var SessionStorage */
- private $sessionStorage;
- /** @var CsrfToken|null */
- private $csrfToken = null;
+ private SessionStorage $sessionStorage;
+ private ?CsrfToken $csrfToken = null;
- /**
- * @param CsrfTokenGenerator $tokenGenerator
- * @param SessionStorage $storageInterface
- */
- public function __construct(CsrfTokenGenerator $tokenGenerator,
- SessionStorage $storageInterface) {
- $this->tokenGenerator = $tokenGenerator;
+ public function __construct(
+ private CsrfTokenGenerator $tokenGenerator,
+ SessionStorage $storageInterface,
+ ) {
$this->sessionStorage = $storageInterface;
}
/**
* Returns the current CSRF token, if none set it will create a new one.
- *
- * @return CsrfToken
*/
public function getToken(): CsrfToken {
if (!\is_null($this->csrfToken)) {
@@ -74,8 +47,6 @@ class CsrfTokenManager {
/**
* Invalidates any current token and sets a new one.
- *
- * @return CsrfToken
*/
public function refreshToken(): CsrfToken {
$value = $this->tokenGenerator->generateToken();
@@ -87,16 +58,13 @@ class CsrfTokenManager {
/**
* Remove the current token from the storage.
*/
- public function removeToken() {
+ public function removeToken(): void {
$this->csrfToken = null;
$this->sessionStorage->removeToken();
}
/**
* Verifies whether the provided token is valid.
- *
- * @param CsrfToken $token
- * @return bool
*/
public function isTokenValid(CsrfToken $token): bool {
if (!$this->sessionStorage->hasToken()) {
diff --git a/lib/private/Security/CSRF/TokenStorage/SessionStorage.php b/lib/private/Security/CSRF/TokenStorage/SessionStorage.php
index ab05d5b1493..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;
@@ -35,27 +16,18 @@ use OCP\ISession;
* @package OC\Security\CSRF\TokenStorage
*/
class SessionStorage {
- /** @var ISession */
- private $session;
-
- /**
- * @param ISession $session
- */
- public function __construct(ISession $session) {
- $this->session = $session;
+ public function __construct(
+ private ISession $session,
+ ) {
}
- /**
- * @param ISession $session
- */
- public function setSession(ISession $session) {
+ public function setSession(ISession $session): void {
$this->session = $session;
}
/**
* Returns the current token or throws an exception if none is found.
*
- * @return string
* @throws \Exception
*/
public function getToken(): string {
@@ -69,23 +41,20 @@ class SessionStorage {
/**
* Set the valid current token to $value.
- *
- * @param string $value
*/
- public function setToken(string $value) {
+ public function setToken(string $value): void {
$this->session->set('requesttoken', $value);
}
/**
* Removes the current token.
*/
- public function removeToken() {
+ public function removeToken(): void {
$this->session->remove('requesttoken');
}
+
/**
* Whether the storage has a storage.
- *
- * @return bool
*/
public function hasToken(): bool {
return $this->session->exists('requesttoken');
diff --git a/lib/private/Security/Certificate.php b/lib/private/Security/Certificate.php
index fb5b9aa8a93..1551694c21f 100644
--- a/lib/private/Security/Certificate.php
+++ b/lib/private/Security/Certificate.php
@@ -1,54 +1,33 @@
<?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;
use OCP\ICertificate;
class Certificate implements ICertificate {
- protected $name;
+ protected string $name;
- protected $commonName;
+ protected ?string $commonName;
- protected $organization;
+ protected ?string $organization;
- protected $serial;
- protected $issueDate;
+ protected \DateTime $issueDate;
- protected $expireDate;
+ protected \DateTime $expireDate;
- protected $issuerName;
+ protected ?string $issuerName;
- protected $issuerOrganization;
+ protected ?string $issuerOrganization;
/**
* @param string $data base64 encoded certificate
- * @param string $name
* @throws \Exception If the certificate could not get parsed
*/
public function __construct(string $data, string $name) {
@@ -63,70 +42,56 @@ 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.');
}
- $this->commonName = isset($info['subject']['CN']) ? $info['subject']['CN'] : null;
- $this->organization = isset($info['subject']['O']) ? $info['subject']['O'] : null;
+ $this->commonName = $info['subject']['CN'] ?? null;
+ $this->organization = $info['subject']['O'] ?? null;
$this->issueDate = new \DateTime('@' . $info['validFrom_time_t'], $gmt);
$this->expireDate = new \DateTime('@' . $info['validTo_time_t'], $gmt);
- $this->issuerName = isset($info['issuer']['CN']) ? $info['issuer']['CN'] : null;
- $this->issuerOrganization = isset($info['issuer']['O']) ? $info['issuer']['O'] : null;
+ $this->issuerName = $info['issuer']['CN'] ?? null;
+ $this->issuerOrganization = $info['issuer']['O'] ?? null;
}
- /**
- * @return string
- */
public function getName(): string {
return $this->name;
}
- /**
- * @return string|null
- */
public function getCommonName(): ?string {
return $this->commonName;
}
- /**
- * @return string|null
- */
public function getOrganization(): ?string {
return $this->organization;
}
- /**
- * @return \DateTime
- */
public function getIssueDate(): \DateTime {
return $this->issueDate;
}
- /**
- * @return \DateTime
- */
public function getExpireDate(): \DateTime {
return $this->expireDate;
}
- /**
- * @return bool
- */
public function isExpired(): bool {
$now = new \DateTime();
return $this->issueDate > $now or $now > $this->expireDate;
}
- /**
- * @return string|null
- */
public function getIssuerName(): ?string {
return $this->issuerName;
}
- /**
- * @return string|null
- */
public function getIssuerOrganization(): ?string {
return $this->issuerOrganization;
}
diff --git a/lib/private/Security/CertificateManager.php b/lib/private/Security/CertificateManager.php
index fa26c19ceae..00babff735f 100644
--- a/lib/private/Security/CertificateManager.php
+++ b/lib/private/Security/CertificateManager.php
@@ -1,38 +1,14 @@
<?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;
use OCP\IConfig;
@@ -43,31 +19,14 @@ use Psr\Log\LoggerInterface;
* Manage trusted certificates for users
*/
class CertificateManager implements ICertificateManager {
- /**
- * @var \OC\Files\View
- */
- protected $view;
-
- /**
- * @var IConfig
- */
- protected $config;
-
- protected LoggerInterface $logger;
-
- /** @var ISecureRandom */
- protected $random;
-
private ?string $bundlePath = null;
- public function __construct(\OC\Files\View $view,
- IConfig $config,
- LoggerInterface $logger,
- ISecureRandom $random) {
- $this->view = $view;
- $this->config = $config;
- $this->logger = $logger;
- $this->random = $random;
+ public function __construct(
+ protected View $view,
+ protected IConfig $config,
+ protected LoggerInterface $logger,
+ protected ISecureRandom $random,
+ ) {
}
/**
@@ -76,7 +35,7 @@ class CertificateManager implements ICertificateManager {
* @return \OCP\ICertificate[]
*/
public function listCertificates(): array {
- if (!$this->config->getSystemValue('installed', false)) {
+ if (!$this->config->getSystemValueBool('installed', false)) {
return [];
}
@@ -92,8 +51,14 @@ class CertificateManager implements ICertificateManager {
while (false !== ($file = readdir($handle))) {
if ($file != '.' && $file != '..') {
try {
- $result[] = new Certificate($this->view->file_get_contents($path . $file), $file);
+ $content = $this->view->file_get_contents($path . $file);
+ if ($content !== false) {
+ $result[] = new Certificate($content, $file);
+ } else {
+ $this->logger->error("Failed to read certificate from $path");
+ }
} catch (\Exception $e) {
+ $this->logger->error("Failed to read certificate from $path", ['exception' => $e]);
}
}
}
@@ -102,7 +67,7 @@ class CertificateManager implements ICertificateManager {
}
private function hasCertificates(): bool {
- if (!$this->config->getSystemValue('installed', false)) {
+ if (!$this->config->getSystemValueBool('installed', false)) {
return false;
}
@@ -147,6 +112,10 @@ class CertificateManager implements ICertificateManager {
$tmpPath = $certPath . '.tmp' . $this->random->generate(10, ISecureRandom::CHAR_DIGITS);
$fhCerts = $this->view->fopen($tmpPath, 'w');
+ if (!is_resource($fhCerts)) {
+ throw new \RuntimeException('Unable to open file handler to create certificate bundle "' . $tmpPath . '".');
+ }
+
// Write user certificates
foreach ($certs as $cert) {
$file = $path . '/uploads/' . $cert->getName();
@@ -177,24 +146,22 @@ class CertificateManager implements ICertificateManager {
*
* @param string $certificate the certificate data
* @param string $name the filename for the certificate
- * @return \OCP\ICertificate
* @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) {
@@ -204,19 +171,19 @@ class CertificateManager implements ICertificateManager {
/**
* Remove the certificate and re-generate the certificate bundle
- *
- * @param string $name
- * @return bool
*/
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;
@@ -224,8 +191,6 @@ class CertificateManager implements ICertificateManager {
/**
* Get the path to the certificate bundle
- *
- * @return string
*/
public function getCertificateBundle(): string {
return $this->getPathToCertificates() . 'rootcerts.crt';
@@ -233,39 +198,39 @@ class CertificateManager implements ICertificateManager {
/**
* Get the full local path to the certificate bundle
- *
- * @return string
+ * @throws \Exception when getting bundle path fails
*/
public function getAbsoluteBundlePath(): string {
try {
- if (!$this->bundlePath) {
+ if ($this->bundlePath === null) {
if (!$this->hasCertificates()) {
$this->bundlePath = \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
- }
+ } else {
+ if ($this->needsRebundling()) {
+ $this->createCertificateBundle();
+ }
- if ($this->needsRebundling()) {
- $this->createCertificateBundle();
- }
+ $certificateBundle = $this->getCertificateBundle();
+ $this->bundlePath = $this->view->getLocalFile($certificateBundle) ?: null;
- $this->bundlePath = $this->view->getLocalFile($this->getCertificateBundle());
+ if ($this->bundlePath === null) {
+ throw new \RuntimeException('Unable to get certificate bundle "' . $certificateBundle . '".');
+ }
+ }
}
return $this->bundlePath;
} catch (\Exception $e) {
+ $this->logger->error('Failed to get absolute bundle path. Fallback to default ca-bundle.crt', ['exception' => $e]);
return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
}
}
- /**
- * @return string
- */
private function getPathToCertificates(): string {
return '/files_external/';
}
/**
* Check if we need to re-bundle the certificates because one of the sources has updated
- *
- * @return bool
*/
private function needsRebundling(): bool {
$targetBundle = $this->getCertificateBundle();
@@ -279,8 +244,6 @@ class CertificateManager implements ICertificateManager {
/**
* get mtime of ca-bundle shipped by Nextcloud
- *
- * @return int
*/
protected function getFilemtimeOfCaBundle(): int {
return filemtime(\OC::$SERVERROOT . '/resources/config/ca-bundle.crt');
diff --git a/lib/private/Security/CredentialsManager.php b/lib/private/Security/CredentialsManager.php
index 0bddaeda1b0..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;
@@ -40,26 +20,16 @@ use OCP\Security\ICrypto;
class CredentialsManager implements ICredentialsManager {
public const DB_TABLE = 'storages_credentials';
- /** @var ICrypto */
- protected $crypto;
-
- /** @var IDBConnection */
- protected $dbConnection;
-
- /**
- * @param ICrypto $crypto
- * @param IDBConnection $dbConnection
- */
- public function __construct(ICrypto $crypto, IDBConnection $dbConnection) {
- $this->crypto = $crypto;
- $this->dbConnection = $dbConnection;
+ public function __construct(
+ protected ICrypto $crypto,
+ protected IDBConnection $dbConnection,
+ ) {
}
/**
* Store a set of credentials
*
* @param string $userId empty string for system-wide credentials
- * @param string $identifier
* @param mixed $credentials
*/
public function store(string $userId, string $identifier, $credentials): void {
@@ -77,10 +47,8 @@ class CredentialsManager implements ICredentialsManager {
* Retrieve a set of credentials
*
* @param string $userId empty string for system-wide credentials
- * @param string $identifier
- * @return mixed
*/
- public function retrieve(string $userId, string $identifier) {
+ public function retrieve(string $userId, string $identifier): mixed {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('credentials')
->from(self::DB_TABLE)
@@ -92,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();
@@ -108,7 +76,6 @@ class CredentialsManager implements ICredentialsManager {
* Delete a set of credentials
*
* @param string $userId empty string for system-wide credentials
- * @param string $identifier
* @return int rows removed
*/
public function delete(string $userId, string $identifier): int {
@@ -122,13 +89,12 @@ class CredentialsManager implements ICredentialsManager {
$qb->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($userId)));
}
- return $qb->execute();
+ return $qb->executeStatement();
}
/**
* Erase all credentials stored for a user
*
- * @param string $userId
* @return int rows removed
*/
public function erase(string $userId): int {
@@ -136,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 aeeafcc271c..39ce5e89aeb 100644
--- a/lib/private/Security/Crypto.php
+++ b/lib/private/Security/Crypto.php
@@ -1,38 +1,16 @@
<?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;
use Exception;
use OCP\IConfig;
use OCP\Security\ICrypto;
-use OCP\Security\ISecureRandom;
use phpseclib\Crypt\AES;
use phpseclib\Crypt\Hash;
@@ -47,20 +25,13 @@ use phpseclib\Crypt\Hash;
* @package OC\Security
*/
class Crypto implements ICrypto {
- /** @var AES $cipher */
- private $cipher;
- /** @var int */
- private $ivLength = 16;
- /** @var IConfig */
- private $config;
+ private AES $cipher;
+ private int $ivLength = 16;
- /**
- * @param IConfig $config
- * @param ISecureRandom $random
- */
- public function __construct(IConfig $config) {
+ public function __construct(
+ private IConfig $config,
+ ) {
$this->cipher = new AES();
- $this->config = $config;
}
/**
@@ -70,7 +41,7 @@ class Crypto implements ICrypto {
*/
public function calculateHMAC(string $message, string $password = ''): string {
if ($password === '') {
- $password = $this->config->getSystemValue('secret');
+ $password = $this->config->getSystemValueString('secret');
}
// Append an "a" behind the password and hash it to prevent reusing the same password as for encryption
@@ -84,7 +55,6 @@ class Crypto implements ICrypto {
/**
* Encrypts a value and adds an HMAC (Encrypt-Then-MAC)
*
- * @param string $plaintext
* @param string $password Password to encrypt, if not specified the secret from config.php will be taken
* @return string Authenticated ciphertext
* @throws Exception if it was not possible to gather sufficient entropy
@@ -92,7 +62,7 @@ class Crypto implements ICrypto {
*/
public function encrypt(string $plaintext, string $password = ''): string {
if ($password === '') {
- $password = $this->config->getSystemValue('secret');
+ $password = $this->config->getSystemValueString('secret');
}
$keyMaterial = hash_hkdf('sha512', $password);
$this->cipher->setPassword(substr($keyMaterial, 0, 32));
@@ -108,16 +78,14 @@ 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';
}
/**
* Decrypts a value and verifies the HMAC (Encrypt-Then-Mac)
- * @param string $authenticatedCiphertext
* @param string $password Password to encrypt, if not specified the secret from config.php will be taken
- * @return string plaintext
* @throws Exception If the HMAC does not match
* @throws Exception If the decryption failed
*/
@@ -146,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]);
@@ -156,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);
@@ -165,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 3aa93ac3da4..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;
@@ -32,13 +14,11 @@ use OCP\Security\FeaturePolicy\AddFeaturePolicyEvent;
class FeaturePolicyManager {
/** @var EmptyFeaturePolicy[] */
- private $policies = [];
+ private array $policies = [];
- /** @var IEventDispatcher */
- private $dispatcher;
-
- public function __construct(IEventDispatcher $dispatcher) {
- $this->dispatcher = $dispatcher;
+ public function __construct(
+ private IEventDispatcher $dispatcher,
+ ) {
}
public function addDefaultPolicy(EmptyFeaturePolicy $policy): void {
@@ -60,8 +40,10 @@ class FeaturePolicyManager {
* Merges the first given policy with the second one
*
*/
- public function mergePolicies(FeaturePolicy $defaultPolicy,
- EmptyFeaturePolicy $originalPolicy): FeaturePolicy {
+ public function mergePolicies(
+ FeaturePolicy $defaultPolicy,
+ EmptyFeaturePolicy $originalPolicy,
+ ): FeaturePolicy {
foreach ((object)(array)$originalPolicy as $name => $value) {
$setter = 'set' . ucfirst($name);
if (\is_array($value)) {
diff --git a/lib/private/Security/Hasher.php b/lib/private/Security/Hasher.php
index 4731ba96bd3..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,28 +22,23 @@ 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
*/
class Hasher implements IHasher {
- /** @var IConfig */
- private $config;
- /** @var array Options passed to password_hash and password_needs_rehash */
- private $options = [];
- /** @var string Salt used for legacy passwords */
- private $legacySalt = null;
-
- /**
- * @param IConfig $config
- */
- public function __construct(IConfig $config) {
- $this->config = $config;
-
+ /** Options passed to password_hash and password_needs_rehash */
+ private array $options = [];
+ /** Salt used for legacy passwords */
+ private ?string $legacySalt = null;
+
+ public function __construct(
+ private IConfig $config,
+ ) {
if (\defined('PASSWORD_ARGON2ID') || \defined('PASSWORD_ARGON2I')) {
// password_hash fails, when the minimum values are undershot.
// In this case, apply minimum.
@@ -104,9 +79,9 @@ 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) {
+ protected function splitHash(string $prefixedHash): ?array {
$explodedString = explode('|', $prefixedHash, 2);
if (\count($explodedString) === 2) {
if ((int)$explodedString[0] > 0) {
@@ -131,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;
}
@@ -140,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;
}
@@ -198,7 +173,7 @@ class Hasher implements IHasher {
return password_needs_rehash($hash, $algorithm, $this->options);
}
- private function getPrefferedAlgorithm() {
+ private function getPrefferedAlgorithm(): string {
$default = PASSWORD_BCRYPT;
if (\defined('PASSWORD_ARGON2I')) {
$default = PASSWORD_ARGON2I;
@@ -209,10 +184,24 @@ class Hasher implements IHasher {
}
// Check if we should use PASSWORD_DEFAULT
- if ($this->config->getSystemValue('hashing_default_password', false) === true) {
+ if ($this->config->getSystemValueBool('hashing_default_password', false)) {
$default = PASSWORD_DEFAULT;
}
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 349ffd3c15a..0bfcd6bf9ed 100644
--- a/lib/private/Security/IdentityProof/Key.php
+++ b/lib/private/Security/IdentityProof/Key.php
@@ -3,42 +3,16 @@
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;
class Key {
- /** @var string */
- private $publicKey;
- /** @var string */
- private $privateKey;
-
- /**
- * @param string $publicKey
- * @param string $privateKey
- */
- public function __construct(string $publicKey, string $privateKey) {
- $this->publicKey = $publicKey;
- $this->privateKey = $privateKey;
+ public function __construct(
+ private string $publicKey,
+ private string $privateKey,
+ ) {
}
public function getPrivate(): string {
diff --git a/lib/private/Security/IdentityProof/Manager.php b/lib/private/Security/IdentityProof/Manager.php
index c92d7390969..c16b8314beb 100644
--- a/lib/private/Security/IdentityProof/Manager.php
+++ b/lib/private/Security/IdentityProof/Manager.php
@@ -3,75 +3,55 @@
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;
use Psr\Log\LoggerInterface;
class Manager {
- /** @var IAppData */
- private $appData;
- /** @var ICrypto */
- private $crypto;
- /** @var IConfig */
- private $config;
- private LoggerInterface $logger;
-
- public function __construct(Factory $appDataFactory,
- ICrypto $crypto,
- IConfig $config,
- LoggerInterface $logger
+ 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->crypto = $crypto;
- $this->config = $config;
- $this->logger = $logger;
+ $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');
@@ -94,16 +74,17 @@ class Manager {
* Note: If a key already exists it will be overwritten
*
* @param string $id key id
- * @return Key
+ * @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')
@@ -117,18 +98,28 @@ class Manager {
/**
* Get key for a specific id
*
- * @param string $id
- * @return Key
* @throws \RuntimeException
*/
protected function retrieveKey(string $id): Key {
try {
+ $cachedPublicKey = $this->cache->get($id . '-public');
+ $cachedPrivateKey = $this->cache->get($id . '-private');
+
+ if ($cachedPublicKey !== null && $cachedPrivateKey !== null) {
+ $decryptedPrivateKey = $this->crypto->decrypt($cachedPrivateKey);
+
+ return new Key($cachedPublicKey, $decryptedPrivateKey);
+ }
+
$folder = $this->appData->getFolder($id);
- $privateKey = $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);
}
@@ -137,8 +128,6 @@ class Manager {
/**
* Get public and private key for $user
*
- * @param IUser $user
- * @return Key
* @throws \RuntimeException
*/
public function getKey(IUser $user): Key {
@@ -149,7 +138,6 @@ class Manager {
/**
* Get instance wide public and private key
*
- * @return Key
* @throws \RuntimeException
*/
public function getSystemKey(): Key {
@@ -160,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 7431bfe815f..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;
@@ -32,32 +13,16 @@ use OCP\IUser;
use OCP\IUserManager;
class Signer {
- /** @var Manager */
- private $keyManager;
- /** @var ITimeFactory */
- private $timeFactory;
- /** @var IUserManager */
- private $userManager;
-
- /**
- * @param Manager $keyManager
- * @param ITimeFactory $timeFactory
- * @param IUserManager $userManager
- */
- public function __construct(Manager $keyManager,
- ITimeFactory $timeFactory,
- IUserManager $userManager) {
- $this->keyManager = $keyManager;
- $this->timeFactory = $timeFactory;
- $this->userManager = $userManager;
+ public function __construct(
+ private Manager $keyManager,
+ private ITimeFactory $timeFactory,
+ private IUserManager $userManager,
+ ) {
}
/**
* Returns a signed blob for $data
*
- * @param string $type
- * @param array $data
- * @param IUser $user
* @return array ['message', 'signature']
*/
public function sign(string $type, array $data, IUser $user): array {
@@ -79,13 +44,10 @@ class Signer {
/**
* Whether the data is signed properly
*
- * @param array $data
- * @return bool
*/
public function verify(array $data): bool {
- if (isset($data['message'])
+ if (isset($data['message']['signer'])
&& isset($data['signature'])
- && isset($data['message']['signer'])
) {
$location = strrpos($data['message']['signer'], '@');
$userId = substr($data['message']['signer'], 0, $location);
@@ -93,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 98d85ce07a1..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.
@@ -37,43 +17,19 @@ namespace OC\Security\Normalizer;
* @package OC\Security\Normalizer
*/
class IpAddress {
- /** @var string */
- private $ip;
-
/**
- * @param string $ip IP to normalized
+ * @param string $ip IP to normalize
*/
- public function __construct(string $ip) {
- $this->ip = $ip;
+ public function __construct(
+ private string $ip,
+ ) {
}
/**
- * Return the given subnet for an IPv4 address and mask bits
- *
- * @param string $ip
- * @param int $maskBits
- * @return string
+ * Return the given subnet for an IPv6 address
+ * Rely on security.ipv6_normalized_subnet_size, defaults to 56
*/
- private function getIPv4Subnet(string $ip, int $maskBits = 32): string {
- $binary = \inet_pton($ip);
- for ($i = 32; $i > $maskBits; $i -= 8) {
- $j = \intdiv($i, 8) - 1;
- $k = \min(8, $i - $maskBits);
- $mask = (0xff - ((2 ** $k) - 1));
- $int = \unpack('C', $binary[$j]);
- $binary[$j] = \pack('C', $int[1] & $mask);
- }
- return \inet_ntop($binary).'/'.$maskBits;
- }
-
- /**
- * Return the given subnet for an IPv6 address and mask bits
- *
- * @param string $ip
- * @param int $maskBits
- * @return string
- */
- private function getIPv6Subnet(string $ip, int $maskBits = 48): string {
+ private function getIPv6Subnet(string $ip): string {
if ($ip[0] === '[' && $ip[-1] === ']') { // If IP is with brackets, for example [::1]
$ip = substr($ip, 1, strlen($ip) - 2);
}
@@ -81,15 +37,26 @@ class IpAddress {
if ($pos !== false) {
$ip = substr($ip, 0, $pos - 1);
}
- $binary = \inet_pton($ip);
- for ($i = 128; $i > $maskBits; $i -= 8) {
- $j = \intdiv($i, 8) - 1;
- $k = \min(8, $i - $maskBits);
- $mask = (0xff - ((2 ** $k) - 1));
- $int = \unpack('C', $binary[$j]);
- $binary[$j] = \pack('C', $int[1] & $mask);
+
+ $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).'/'.$maskBits;
+
+ $binary = \inet_pton($ip);
+ return inet_ntop($binary & $mask) . '/' . $maskSize;
}
/**
@@ -103,58 +70,34 @@ class IpAddress {
if (!$binary) {
return null;
}
- for ($i = 0; $i <= 9; $i++) {
- if (unpack('C', $binary[$i])[1] !== 0) {
- return null;
- }
- }
-
- for ($i = 10; $i <= 11; $i++) {
- if (unpack('C', $binary[$i])[1] !== 255) {
- return null;
- }
- }
- $binary4 = '';
- for ($i = 12; $i < 16; $i++) {
- $binary4 .= $binary[$i];
+ $mask = inet_pton('::FFFF:FFFF');
+ if (($binary & ~$mask) !== inet_pton('::FFFF:0.0.0.0')) {
+ return null;
}
- return inet_ntop($binary4);
+ return inet_ntop(substr($binary, -4));
}
/**
- * Gets either the /32 (IPv4) or the /64 (IPv6) subnet of an IP address
- *
- * @return string
+ * Gets either the /32 (IPv4) or the /56 (default for IPv6) subnet of an IP address
*/
public function getSubnet(): string {
- if (\preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $this->ip)) {
- return $this->getIPv4Subnet(
- $this->ip,
- 32
- );
+ if (filter_var($this->ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
+ return $this->ip . '/32';
}
$ipv4 = $this->getEmbeddedIpv4($this->ip);
if ($ipv4 !== null) {
- return $this->getIPv4Subnet(
- $ipv4,
- 32
- );
+ return $ipv4 . '/32';
}
- return $this->getIPv6Subnet(
- $this->ip,
- 64
- );
+ return $this->getIPv6Subnet($this->ip);
}
/**
* Returns the specified IP address
- *
- * @return string
*/
public function __toString(): string {
return $this->ip;
diff --git a/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php b/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php
index ea4bd87d6cd..9fb237f2f72 100644
--- a/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php
+++ b/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php
@@ -3,77 +3,46 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2021 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: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\RateLimiting\Backend;
use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
use OCP\IDBConnection;
class DatabaseBackend implements IBackend {
private const TABLE_NAME = 'ratelimit_entries';
- /** @var IDBConnection */
- private $dbConnection;
- /** @var ITimeFactory */
- private $timeFactory;
-
- /**
- * @param IDBConnection $dbConnection
- * @param ITimeFactory $timeFactory
- */
public function __construct(
- IDBConnection $dbConnection,
- ITimeFactory $timeFactory
+ private IConfig $config,
+ private IDBConnection $dbConnection,
+ private ITimeFactory $timeFactory,
) {
- $this->dbConnection = $dbConnection;
- $this->timeFactory = $timeFactory;
}
- /**
- * @param string $methodIdentifier
- * @param string $userIdentifier
- * @return string
- */
- private function hash(string $methodIdentifier,
- string $userIdentifier): string {
+ private function hash(
+ string $methodIdentifier,
+ string $userIdentifier,
+ ): string {
return hash('sha512', $methodIdentifier . $userIdentifier);
}
/**
- * @param string $identifier
- * @param int $seconds
- * @return int
- * @throws \OCP\DB\Exception
+ * @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();
@@ -94,8 +63,10 @@ class DatabaseBackend implements IBackend {
/**
* {@inheritDoc}
*/
- public function getAttempts(string $methodIdentifier,
- string $userIdentifier): int {
+ public function getAttempts(
+ string $methodIdentifier,
+ string $userIdentifier,
+ ): int {
$identifier = $this->hash($methodIdentifier, $userIdentifier);
return $this->getExistingAttemptCount($identifier);
}
@@ -103,9 +74,11 @@ class DatabaseBackend implements IBackend {
/**
* {@inheritDoc}
*/
- public function registerAttempt(string $methodIdentifier,
- string $userIdentifier,
- int $period) {
+ public function registerAttempt(
+ string $methodIdentifier,
+ string $userIdentifier,
+ int $period,
+ ): void {
$identifier = $this->hash($methodIdentifier, $userIdentifier);
$deleteAfter = $this->timeFactory->getDateTime()->add(new \DateInterval("PT{$period}S"));
@@ -114,8 +87,13 @@ 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),
- ])
- ->executeStatement();
+ 'delete_after' => $qb->createNamedParameter($deleteAfter, IQueryBuilder::PARAM_DATETIME_MUTABLE),
+ ]);
+
+ if (!$this->config->getSystemValueBool('ratelimit.protection.enabled', true)) {
+ return;
+ }
+
+ $qb->executeStatement();
}
}
diff --git a/lib/private/Security/RateLimiting/Backend/IBackend.php b/lib/private/Security/RateLimiting/Backend/IBackend.php
index 960bfd2d159..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;
@@ -39,10 +21,11 @@ interface IBackend {
*
* @param string $methodIdentifier Identifier for the method
* @param string $userIdentifier Identifier for the user
- * @return int
*/
- public function getAttempts(string $methodIdentifier,
- string $userIdentifier): int;
+ public function getAttempts(
+ string $methodIdentifier,
+ string $userIdentifier,
+ ): int;
/**
* Registers an attempt
@@ -51,7 +34,9 @@ interface IBackend {
* @param string $userIdentifier Identifier for the user
* @param int $period Period in seconds how long this attempt should be stored
*/
- public function registerAttempt(string $methodIdentifier,
- string $userIdentifier,
- int $period);
+ public function registerAttempt(
+ string $methodIdentifier,
+ string $userIdentifier,
+ int $period,
+ );
}
diff --git a/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php b/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php
index f4880fb239c..4c33b49d05e 100644
--- a/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php
+++ b/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php
@@ -3,34 +3,15 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
- *
- * @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 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;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\ICache;
use OCP\ICacheFactory;
+use OCP\IConfig;
/**
* Class MemoryCacheBackend uses the configured distributed memory cache for storing
@@ -39,35 +20,23 @@ use OCP\ICacheFactory;
* @package OC\Security\RateLimiting\Backend
*/
class MemoryCacheBackend implements IBackend {
- /** @var ICache */
- private $cache;
- /** @var ITimeFactory */
- private $timeFactory;
+ private ICache $cache;
- /**
- * @param ICacheFactory $cacheFactory
- * @param ITimeFactory $timeFactory
- */
- public function __construct(ICacheFactory $cacheFactory,
- ITimeFactory $timeFactory) {
- $this->cache = $cacheFactory->createDistributed(__CLASS__);
- $this->timeFactory = $timeFactory;
+ public function __construct(
+ private IConfig $config,
+ ICacheFactory $cacheFactory,
+ private ITimeFactory $timeFactory,
+ ) {
+ $this->cache = $cacheFactory->createDistributed(self::class);
}
- /**
- * @param string $methodIdentifier
- * @param string $userIdentifier
- * @return string
- */
- private function hash(string $methodIdentifier,
- string $userIdentifier): string {
+ private function hash(
+ string $methodIdentifier,
+ string $userIdentifier,
+ ): string {
return hash('sha512', $methodIdentifier . $userIdentifier);
}
- /**
- * @param string $identifier
- * @return array
- */
private function getExistingAttempts(string $identifier): array {
$cachedAttempts = $this->cache->get($identifier);
if ($cachedAttempts === null) {
@@ -85,8 +54,10 @@ class MemoryCacheBackend implements IBackend {
/**
* {@inheritDoc}
*/
- public function getAttempts(string $methodIdentifier,
- string $userIdentifier): int {
+ public function getAttempts(
+ string $methodIdentifier,
+ string $userIdentifier,
+ ): int {
$identifier = $this->hash($methodIdentifier, $userIdentifier);
$existingAttempts = $this->getExistingAttempts($identifier);
@@ -104,9 +75,11 @@ class MemoryCacheBackend implements IBackend {
/**
* {@inheritDoc}
*/
- public function registerAttempt(string $methodIdentifier,
- string $userIdentifier,
- int $period) {
+ public function registerAttempt(
+ string $methodIdentifier,
+ string $userIdentifier,
+ int $period,
+ ): void {
$identifier = $this->hash($methodIdentifier, $userIdentifier);
$existingAttempts = $this->getExistingAttempts($identifier);
$currentTime = $this->timeFactory->getTime();
@@ -121,6 +94,11 @@ class MemoryCacheBackend implements IBackend {
// Store the new attempt
$existingAttempts[] = (string)($currentTime + $period);
+
+ if (!$this->config->getSystemValueBool('ratelimit.protection.enabled', true)) {
+ return;
+ }
+
$this->cache->set($identifier, json_encode($existingAttempts));
}
}
diff --git a/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php b/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php
index 08091e997ca..19defc2d896 100644
--- a/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php
+++ b/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php
@@ -3,32 +3,16 @@
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;
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
use OCP\AppFramework\Http;
+use OCP\Security\RateLimiting\IRateLimitExceededException;
-class RateLimitExceededException extends SecurityException {
+class RateLimitExceededException extends SecurityException implements IRateLimitExceededException {
public function __construct() {
parent::__construct('Rate limit exceeded', Http::STATUS_TOO_MANY_REQUESTS);
}
diff --git a/lib/private/Security/RateLimiting/Limiter.php b/lib/private/Security/RateLimiting/Limiter.php
index 7848a5b75a7..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;
@@ -30,31 +12,33 @@ use OC\Security\Normalizer\IpAddress;
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 {
- /** @var IBackend */
- private $backend;
-
- /**
- * @param IBackend $backend
- */
- public function __construct(IBackend $backend) {
- $this->backend = $backend;
+class Limiter implements ILimiter {
+ public function __construct(
+ private IBackend $backend,
+ private LoggerInterface $logger,
+ ) {
}
/**
- * @param string $methodIdentifier
- * @param string $userIdentifier
* @param int $period in seconds
- * @param int $limit
* @throws RateLimitExceededException
*/
- private function register(string $methodIdentifier,
- string $userIdentifier,
- int $period,
- int $limit): void {
+ private function register(
+ string $methodIdentifier,
+ string $userIdentifier,
+ int $period,
+ int $limit,
+ ): 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();
}
@@ -64,16 +48,15 @@ class Limiter {
/**
* Registers attempt for an anonymous request
*
- * @param string $identifier
- * @param int $anonLimit
* @param int $anonPeriod in seconds
- * @param string $ip
* @throws RateLimitExceededException
*/
- public function registerAnonRequest(string $identifier,
- int $anonLimit,
- int $anonPeriod,
- string $ip): void {
+ public function registerAnonRequest(
+ string $identifier,
+ int $anonLimit,
+ int $anonPeriod,
+ string $ip,
+ ): void {
$ipSubnet = (new IpAddress($ip))->getSubnet();
$anonHashIdentifier = hash('sha512', 'anon::' . $identifier . $ipSubnet);
@@ -83,16 +66,15 @@ class Limiter {
/**
* Registers attempt for an authenticated request
*
- * @param string $identifier
- * @param int $userLimit
* @param int $userPeriod in seconds
- * @param IUser $user
* @throws RateLimitExceededException
*/
- public function registerUserRequest(string $identifier,
- int $userLimit,
- int $userPeriod,
- IUser $user): void {
+ public function registerUserRequest(
+ string $identifier,
+ int $userLimit,
+ int $userPeriod,
+ IUser $user,
+ ): void {
$userHashIdentifier = hash('sha512', 'user::' . $identifier . $user->getUID());
$this->register($identifier, $userHashIdentifier, $userPeriod, $userLimit);
}
diff --git a/lib/private/Security/RemoteHostValidator.php b/lib/private/Security/RemoteHostValidator.php
index e48bd862472..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;
@@ -30,7 +14,6 @@ use OC\Net\IpAddressClassifier;
use OCP\IConfig;
use OCP\Security\IRemoteHostValidator;
use Psr\Log\LoggerInterface;
-use function strpos;
use function strtolower;
use function substr;
use function urldecode;
@@ -39,19 +22,12 @@ use function urldecode;
* @internal
*/
final class RemoteHostValidator implements IRemoteHostValidator {
- private IConfig $config;
- private HostnameClassifier $hostnameClassifier;
- private IpAddressClassifier $ipAddressClassifier;
- private LoggerInterface $logger;
-
- public function __construct(IConfig $config,
- HostnameClassifier $hostnameClassifier,
- IpAddressClassifier $ipAddressClassifier,
- LoggerInterface $logger) {
- $this->config = $config;
- $this->hostnameClassifier = $hostnameClassifier;
- $this->ipAddressClassifier = $ipAddressClassifier;
- $this->logger = $logger;
+ public function __construct(
+ private IConfig $config,
+ private HostnameClassifier $hostnameClassifier,
+ private IpAddressClassifier $ipAddressClassifier,
+ private LoggerInterface $logger,
+ ) {
}
public function isValid(string $host): bool {
@@ -60,8 +36,12 @@ final class RemoteHostValidator implements IRemoteHostValidator {
}
$host = idn_to_utf8(strtolower(urldecode($host)));
+ if ($host === false) {
+ return false;
+ }
+
// Remove brackets from IPv6 addresses
- if (strpos($host, '[') === 0 && substr($host, -1) === ']') {
+ if (str_starts_with($host, '[') && str_ends_with($host, ']')) {
$host = substr($host, 1, -1);
}
diff --git a/lib/private/Security/SecureRandom.php b/lib/private/Security/SecureRandom.php
index cbd1dc8db6d..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,12 +24,13 @@ 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.
- * @return string
+ * specified all valid base64 characters are used.
* @throws \LengthException if an invalid length is requested
*/
- public function generate(int $length,
- string $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'): string {
+ public function generate(
+ int $length,
+ string $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
+ ): string {
if ($length <= 0) {
throw new \LengthException('Invalid length specified: ' . $length . ' must be bigger than 0');
}
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 1927af9cb1d..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;
@@ -34,19 +13,13 @@ use OCP\IConfig;
use OCP\Security\ITrustedDomainHelper;
class TrustedDomainHelper implements ITrustedDomainHelper {
- /** @var IConfig */
- private $config;
-
- /**
- * @param IConfig $config
- */
- public function __construct(IConfig $config) {
- $this->config = $config;
+ public function __construct(
+ private IConfig $config,
+ ) {
}
/**
* Strips a potential port from a domain (in format domain:port)
- * @param string $host
* @return string $host without appended port
*/
private function getDomainWithoutPort(string $host): string {
@@ -98,7 +71,7 @@ class TrustedDomainHelper implements ITrustedDomainHelper {
return true;
}
// Reject malformed domains in any case
- if (strpos($domain, '-') === 0 || strpos($domain, '..') !== false) {
+ if (str_starts_with($domain, '-') || str_contains($domain, '..')) {
return false;
}
// Match, allowing for * wildcards
diff --git a/lib/private/Security/VerificationToken/CleanUpJob.php b/lib/private/Security/VerificationToken/CleanUpJob.php
index 4510dcffe0a..ba8f4352f80 100644
--- a/lib/private/Security/VerificationToken/CleanUpJob.php
+++ b/lib/private/Security/VerificationToken/CleanUpJob.php
@@ -1,36 +1,17 @@
<?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;
-use OCP\IConfig;
-use OCP\IUserManager;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\Job;
+use OCP\IConfig;
+use OCP\IUserManager;
use OCP\Security\VerificationToken\InvalidTokenException;
use OCP\Security\VerificationToken\IVerificationToken;
@@ -39,18 +20,17 @@ class CleanUpJob extends Job {
protected ?string $userId = null;
protected ?string $subject = null;
protected ?string $pwdPrefix = null;
- private IConfig $config;
- private IVerificationToken $verificationToken;
- private IUserManager $userManager;
- public function __construct(ITimeFactory $time, IConfig $config, IVerificationToken $verificationToken, IUserManager $userManager) {
+ public function __construct(
+ ITimeFactory $time,
+ private IConfig $config,
+ private IVerificationToken $verificationToken,
+ private IUserManager $userManager,
+ ) {
parent::__construct($time);
- $this->config = $config;
- $this->verificationToken = $verificationToken;
- $this->userManager = $userManager;
}
- public function setArgument($argument) {
+ public function setArgument($argument): void {
parent::setArgument($argument);
$args = \json_decode($argument, true);
$this->userId = (string)$args['userId'];
@@ -59,7 +39,7 @@ class CleanUpJob extends Job {
$this->runNotBefore = (int)$args['notBefore'];
}
- protected function run($argument) {
+ protected function run($argument): void {
try {
$user = $this->userManager->get($this->userId);
if ($user === null) {
diff --git a/lib/private/Security/VerificationToken/VerificationToken.php b/lib/private/Security/VerificationToken/VerificationToken.php
index 2d3f902b622..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;
@@ -39,29 +20,13 @@ use function json_encode;
class VerificationToken implements IVerificationToken {
protected const TOKEN_LIFETIME = 60 * 60 * 24 * 7;
- /** @var IConfig */
- private $config;
- /** @var ICrypto */
- private $crypto;
- /** @var ITimeFactory */
- private $timeFactory;
- /** @var ISecureRandom */
- private $secureRandom;
- /** @var IJobList */
- private $jobList;
-
public function __construct(
- IConfig $config,
- ICrypto $crypto,
- ITimeFactory $timeFactory,
- ISecureRandom $secureRandom,
- IJobList $jobList
+ private IConfig $config,
+ private ICrypto $crypto,
+ private ITimeFactory $timeFactory,
+ private ISecureRandom $secureRandom,
+ private IJobList $jobList,
) {
- $this->config = $config;
- $this->crypto = $crypto;
- $this->timeFactory = $timeFactory;
- $this->secureRandom = $secureRandom;
- $this->jobList = $jobList;
}
/**
@@ -71,7 +36,13 @@ class VerificationToken implements IVerificationToken {
throw new InvalidTokenException($code);
}
- public function check(string $token, ?IUser $user, string $subject, string $passwordPrefix = '', bool $expiresWithLogin = false): void {
+ public function check(
+ string $token,
+ ?IUser $user,
+ string $subject,
+ string $passwordPrefix = '',
+ bool $expiresWithLogin = false,
+ ): void {
if ($user === null || !$user->isEnabled()) {
$this->throwInvalidTokenException(InvalidTokenException::USER_UNKNOWN);
}
@@ -82,7 +53,7 @@ class VerificationToken implements IVerificationToken {
}
try {
- $decryptedToken = $this->crypto->decrypt($encryptedToken, $passwordPrefix.$this->config->getSystemValue('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 {
@@ -107,15 +78,19 @@ class VerificationToken implements IVerificationToken {
}
}
- public function create(IUser $user, string $subject, string $passwordPrefix = ''): string {
+ public function create(
+ IUser $user,
+ string $subject,
+ string $passwordPrefix = '',
+ ): 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;
- $encryptedValue = $this->crypto->encrypt($tokenValue, $passwordPrefix . $this->config->getSystemValue('secret'));
+ $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([
'userId' => $user->getUID(),