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