migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,116 @@
<?php
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/>.
*
*/
namespace OC\Security\Bruteforce\Backend;
use OCP\IDBConnection;
class DatabaseBackend implements IBackend {
private const TABLE_NAME = 'bruteforce_attempts';
public function __construct(
private IDBConnection $db,
) {
}
/**
* {@inheritDoc}
*/
public function getAttempts(
string $ipSubnet,
int $maxAgeTimestamp,
?string $action = null,
?array $metadata = null,
): int {
$query = $this->db->getQueryBuilder();
$query->select($query->func()->count('*', 'attempts'))
->from(self::TABLE_NAME)
->where($query->expr()->gt('occurred', $query->createNamedParameter($maxAgeTimestamp)))
->andWhere($query->expr()->eq('subnet', $query->createNamedParameter($ipSubnet)));
if ($action !== null) {
$query->andWhere($query->expr()->eq('action', $query->createNamedParameter($action)));
if ($metadata !== null) {
$query->andWhere($query->expr()->eq('metadata', $query->createNamedParameter(json_encode($metadata))));
}
}
$result = $query->executeQuery();
$row = $result->fetch();
$result->closeCursor();
return (int) $row['attempts'];
}
/**
* {@inheritDoc}
*/
public function resetAttempts(
string $ipSubnet,
?string $action = null,
?array $metadata = null,
): void {
$query = $this->db->getQueryBuilder();
$query->delete(self::TABLE_NAME)
->where($query->expr()->eq('subnet', $query->createNamedParameter($ipSubnet)));
if ($action !== null) {
$query->andWhere($query->expr()->eq('action', $query->createNamedParameter($action)));
if ($metadata !== null) {
$query->andWhere($query->expr()->eq('metadata', $query->createNamedParameter(json_encode($metadata))));
}
}
$query->executeStatement();
}
/**
* {@inheritDoc}
*/
public function registerAttempt(
string $ip,
string $ipSubnet,
int $timestamp,
string $action,
array $metadata = [],
): void {
$values = [
'ip' => $ip,
'subnet' => $ipSubnet,
'action' => $action,
'metadata' => json_encode($metadata),
'occurred' => $timestamp,
];
$qb = $this->db->getQueryBuilder();
$qb->insert(self::TABLE_NAME);
foreach ($values as $column => $value) {
$qb->setValue($column, $qb->createNamedParameter($value));
}
$qb->executeStatement();
}
}
@@ -0,0 +1,82 @@
<?php
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/>.
*
*/
namespace OC\Security\Bruteforce\Backend;
/**
* Interface IBackend defines a storage backend for the bruteforce data. It
* should be noted that writing and reading brute force data is an expensive
* operation and one should thus make sure to only use sufficient fast backends.
*/
interface IBackend {
/**
* Gets the number of attempts for the specified subnet (and further filters)
*
* @param string $ipSubnet
* @param int $maxAgeTimestamp
* @param ?string $action Optional action to further limit attempts
* @param ?array $metadata Optional metadata stored to further limit attempts (Only considered when $action is set)
* @return int
* @since 28.0.0
*/
public function getAttempts(
string $ipSubnet,
int $maxAgeTimestamp,
?string $action = null,
?array $metadata = null,
): int;
/**
* Reset the attempts for the specified subnet (and further filters)
*
* @param string $ipSubnet
* @param ?string $action Optional action to further limit attempts
* @param ?array $metadata Optional metadata stored to further limit attempts(Only considered when $action is set)
* @since 28.0.0
*/
public function resetAttempts(
string $ipSubnet,
?string $action = null,
?array $metadata = null,
): void;
/**
* Register a failed attempt to bruteforce a security control
*
* @param string $ip
* @param string $ipSubnet
* @param int $timestamp
* @param string $action
* @param array $metadata Optional metadata stored to further limit attempts when getting
* @since 28.0.0
*/
public function registerAttempt(
string $ip,
string $ipSubnet,
int $timestamp,
string $action,
array $metadata = [],
): void;
}
@@ -0,0 +1,161 @@
<?php
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/>.
*
*/
namespace OC\Security\Bruteforce\Backend;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\ICache;
use OCP\ICacheFactory;
class MemoryCacheBackend implements IBackend {
private ICache $cache;
public function __construct(
ICacheFactory $cacheFactory,
private ITimeFactory $timeFactory,
) {
$this->cache = $cacheFactory->createDistributed(__CLASS__);
}
private function hash(
null|string|array $data,
): ?string {
if ($data === null) {
return null;
}
if (!is_string($data)) {
$data = json_encode($data);
}
return hash('sha1', $data);
}
private function getExistingAttempts(string $identifier): array {
$cachedAttempts = $this->cache->get($identifier);
if ($cachedAttempts === null) {
return [];
}
$cachedAttempts = json_decode($cachedAttempts, true);
if (\is_array($cachedAttempts)) {
return $cachedAttempts;
}
return [];
}
/**
* {@inheritDoc}
*/
public function getAttempts(
string $ipSubnet,
int $maxAgeTimestamp,
?string $action = null,
?array $metadata = null,
): int {
$identifier = $this->hash($ipSubnet);
$actionHash = $this->hash($action);
$metadataHash = $this->hash($metadata);
$existingAttempts = $this->getExistingAttempts($identifier);
$count = 0;
foreach ($existingAttempts as $info) {
[$occurredTime, $attemptAction, $attemptMetadata] = explode('#', $info, 3);
if ($action === null || $attemptAction === $actionHash) {
if ($metadata === null || $attemptMetadata === $metadataHash) {
if ($occurredTime > $maxAgeTimestamp) {
$count++;
}
}
}
}
return $count;
}
/**
* {@inheritDoc}
*/
public function resetAttempts(
string $ipSubnet,
?string $action = null,
?array $metadata = null,
): void {
$identifier = $this->hash($ipSubnet);
if ($action === null) {
$this->cache->remove($identifier);
} else {
$actionHash = $this->hash($action);
$metadataHash = $this->hash($metadata);
$existingAttempts = $this->getExistingAttempts($identifier);
$maxAgeTimestamp = $this->timeFactory->getTime() - 12 * 3600;
foreach ($existingAttempts as $key => $info) {
[$occurredTime, $attemptAction, $attemptMetadata] = explode('#', $info, 3);
if ($attemptAction === $actionHash) {
if ($metadata === null || $attemptMetadata === $metadataHash) {
unset($existingAttempts[$key]);
} elseif ($occurredTime < $maxAgeTimestamp) {
unset($existingAttempts[$key]);
}
}
}
if (!empty($existingAttempts)) {
$this->cache->set($identifier, json_encode($existingAttempts), 12 * 3600);
} else {
$this->cache->remove($identifier);
}
}
}
/**
* {@inheritDoc}
*/
public function registerAttempt(
string $ip,
string $ipSubnet,
int $timestamp,
string $action,
array $metadata = [],
): void {
$identifier = $this->hash($ipSubnet);
$existingAttempts = $this->getExistingAttempts($identifier);
$maxAgeTimestamp = $this->timeFactory->getTime() - 12 * 3600;
// Unset all attempts that are already expired
foreach ($existingAttempts as $key => $info) {
[$occurredTime,] = explode('#', $info, 3);
if ($occurredTime < $maxAgeTimestamp) {
unset($existingAttempts[$key]);
}
}
$existingAttempts = array_values($existingAttempts);
// Store the new attempt
$existingAttempts[] = $timestamp . '#' . $this->hash($action) . '#' . $this->hash($metadata);
$this->cache->set($identifier, json_encode($existingAttempts), 12 * 3600);
}
}
@@ -0,0 +1,55 @@
<?php
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/>.
*
*/
namespace OC\Security\Bruteforce;
use OCP\Capabilities\IInitialStateExcludedCapability;
use OCP\Capabilities\IPublicCapability;
use OCP\IRequest;
use OCP\Security\Bruteforce\IThrottler;
class Capabilities implements IPublicCapability, IInitialStateExcludedCapability {
public function __construct(
private IRequest $request,
private IThrottler $throttler,
) {
}
/**
* @return array{bruteforce: array{delay: int, allow-listed: bool}}
*/
public function getCapabilities(): array {
return [
'bruteforce' => [
'delay' => $this->throttler->getDelay($this->request->getRemoteAddress()),
'allow-listed' => $this->throttler->isBypassListed($this->request->getRemoteAddress()),
],
];
}
}
@@ -0,0 +1,55 @@
<?php
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/>.
*
*/
namespace OC\Security\Bruteforce;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\TimedJob;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
class CleanupJob extends TimedJob {
public function __construct(
ITimeFactory $time,
private IDBConnection $connection,
) {
parent::__construct($time);
// Run once a day
$this->setInterval(3600 * 24);
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
}
protected function run($argument): void {
// Delete all entries more than 48 hours old
$time = $this->time->getTime() - (48 * 3600);
$qb = $this->connection->getQueryBuilder();
$qb->delete('bruteforce_attempts')
->where($qb->expr()->lt('occurred', $qb->createNamedParameter($time), IQueryBuilder::PARAM_INT));
$qb->execute();
}
}
@@ -0,0 +1,311 @@
<?php
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/>.
*
*/
namespace OC\Security\Bruteforce;
use OC\Security\Bruteforce\Backend\IBackend;
use OC\Security\Normalizer\IpAddress;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\Bruteforce\MaxDelayReached;
use Psr\Log\LoggerInterface;
/**
* Class Throttler implements the bruteforce protection for security actions in
* Nextcloud.
*
* It is working by logging invalid login attempts to the database and slowing
* down all login attempts from the same subnet. The max delay is 30 seconds and
* the starting delay are 200 milliseconds. (after the first failed login)
*
* This is based on Paragonie's AirBrake for Airship CMS. You can find the original
* code at https://github.com/paragonie/airship/blob/7e5bad7e3c0fbbf324c11f963fd1f80e59762606/src/Engine/Security/AirBrake.php
*
* @package OC\Security\Bruteforce
*/
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,
) {
}
/**
* {@inheritDoc}
*/
public function registerAttempt(string $action,
string $ip,
array $metadata = []): void {
// No need to log if the bruteforce protection is disabled
if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
return;
}
$ipAddress = new IpAddress($ip);
if ($this->isBypassListed((string)$ipAddress)) {
return;
}
$this->logger->notice(
sprintf(
'Bruteforce attempt from "%s" detected for action "%s".',
$ip,
$action
),
[
'app' => 'core',
]
);
$this->backend->registerAttempt(
(string)$ipAddress,
$ipAddress->getSubnet(),
$this->timeFactory->getTime(),
$action,
$metadata
);
}
/**
* 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;
}
/**
* {@inheritDoc}
*/
public function showBruteforceWarning(string $ip, string $action = ''): bool {
$attempts = $this->getAttempts($ip, $action);
// 4 failed attempts is the last delay below 5 seconds
return $attempts >= 4;
}
/**
* {@inheritDoc}
*/
public function getAttempts(string $ip, string $action = '', float $maxAgeHours = 12): int {
if ($maxAgeHours > 48) {
$this->logger->error('Bruteforce has to use less than 48 hours');
$maxAgeHours = 48;
}
if ($ip === '' || isset($this->hasAttemptsDeleted[$action])) {
return 0;
}
$ipAddress = new IpAddress($ip);
if ($this->isBypassListed((string)$ipAddress)) {
return 0;
}
$maxAgeTimestamp = (int) ($this->timeFactory->getTime() - 3600 * $maxAgeHours);
return $this->backend->getAttempts(
$ipAddress->getSubnet(),
$maxAgeTimestamp,
$action !== '' ? $action : null,
);
}
/**
* {@inheritDoc}
*/
public function getDelay(string $ip, string $action = ''): int {
$attempts = $this->getAttempts($ip, $action);
if ($attempts === 0) {
return 0;
}
$firstDelay = 0.1;
if ($attempts > self::MAX_ATTEMPTS) {
// Don't ever overflow. Just assume the maxDelay time:s
return self::MAX_DELAY_MS;
}
$delay = $firstDelay * 2 ** $attempts;
if ($delay > self::MAX_DELAY) {
return self::MAX_DELAY_MS;
}
return (int) \ceil($delay * 1000);
}
/**
* {@inheritDoc}
*/
public function resetDelay(string $ip, string $action, array $metadata): void {
// No need to log if the bruteforce protection is disabled
if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
return;
}
$ipAddress = new IpAddress($ip);
if ($this->isBypassListed((string)$ipAddress)) {
return;
}
$this->backend->resetAttempts(
$ipAddress->getSubnet(),
$action,
$metadata,
);
$this->hasAttemptsDeleted[$action] = true;
}
/**
* {@inheritDoc}
*/
public function resetDelayForIP(string $ip): void {
// No need to log if the bruteforce protection is disabled
if (!$this->config->getSystemValueBool('auth.bruteforce.protection.enabled', true)) {
return;
}
$ipAddress = new IpAddress($ip);
if ($this->isBypassListed((string)$ipAddress)) {
return;
}
$this->backend->resetAttempts($ipAddress->getSubnet());
}
/**
* {@inheritDoc}
*/
public function sleepDelay(string $ip, string $action = ''): int {
$delay = $this->getDelay($ip, $action);
if (!$this->config->getSystemValueBool('auth.bruteforce.protection.testing')) {
usleep($delay * 1000);
}
return $delay;
}
/**
* {@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}]', [
'action' => $action,
'ip' => $ip,
'delay' => $delay,
]);
}
if (!$this->config->getSystemValueBool('auth.bruteforce.protection.testing')) {
usleep($delay * 1000);
}
return $delay;
}
}
@@ -0,0 +1,202 @@
<?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/>
*
*/
namespace OC\Security\CSP;
/**
* Class ContentSecurityPolicy extends the public class and adds getter and setters.
* This is necessary since we don't want to expose the setters and getters to the
* public API.
*
* @package OC\Security\CSP
*/
class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy {
public function isInlineScriptAllowed(): bool {
return $this->inlineScriptAllowed;
}
public function setInlineScriptAllowed(bool $inlineScriptAllowed): void {
$this->inlineScriptAllowed = $inlineScriptAllowed;
}
public function isEvalScriptAllowed(): bool {
return $this->evalScriptAllowed;
}
/**
* @deprecated 17.0.0 Unsafe eval should not be used anymore.
*/
public function setEvalScriptAllowed(bool $evalScriptAllowed): void {
$this->evalScriptAllowed = $evalScriptAllowed;
}
public function isEvalWasmAllowed(): ?bool {
return $this->evalWasmAllowed;
}
public function setEvalWasmAllowed(bool $evalWasmAllowed): void {
$this->evalWasmAllowed = $evalWasmAllowed;
}
public function getAllowedScriptDomains(): array {
return $this->allowedScriptDomains;
}
public function setAllowedScriptDomains(array $allowedScriptDomains): void {
$this->allowedScriptDomains = $allowedScriptDomains;
}
public function isInlineStyleAllowed(): bool {
return $this->inlineStyleAllowed;
}
public function setInlineStyleAllowed(bool $inlineStyleAllowed): void {
$this->inlineStyleAllowed = $inlineStyleAllowed;
}
public function getAllowedStyleDomains(): array {
return $this->allowedStyleDomains;
}
public function setAllowedStyleDomains(array $allowedStyleDomains): void {
$this->allowedStyleDomains = $allowedStyleDomains;
}
public function getAllowedImageDomains(): array {
return $this->allowedImageDomains;
}
public function setAllowedImageDomains(array $allowedImageDomains): void {
$this->allowedImageDomains = $allowedImageDomains;
}
public function getAllowedConnectDomains(): array {
return $this->allowedConnectDomains;
}
public function setAllowedConnectDomains(array $allowedConnectDomains): void {
$this->allowedConnectDomains = $allowedConnectDomains;
}
public function getAllowedMediaDomains(): array {
return $this->allowedMediaDomains;
}
public function setAllowedMediaDomains(array $allowedMediaDomains): void {
$this->allowedMediaDomains = $allowedMediaDomains;
}
public function getAllowedObjectDomains(): array {
return $this->allowedObjectDomains;
}
public function setAllowedObjectDomains(array $allowedObjectDomains): void {
$this->allowedObjectDomains = $allowedObjectDomains;
}
public function getAllowedFrameDomains(): array {
return $this->allowedFrameDomains;
}
public function setAllowedFrameDomains(array $allowedFrameDomains): void {
$this->allowedFrameDomains = $allowedFrameDomains;
}
public function getAllowedFontDomains(): array {
return $this->allowedFontDomains;
}
public function setAllowedFontDomains($allowedFontDomains): void {
$this->allowedFontDomains = $allowedFontDomains;
}
/**
* @deprecated 15.0.0 use FrameDomains and WorkerSrcDomains
*/
public function getAllowedChildSrcDomains(): array {
return $this->allowedChildSrcDomains;
}
/**
* @param array $allowedChildSrcDomains
* @deprecated 15.0.0 use FrameDomains and WorkerSrcDomains
*/
public function setAllowedChildSrcDomains($allowedChildSrcDomains): void {
$this->allowedChildSrcDomains = $allowedChildSrcDomains;
}
public function getAllowedFrameAncestors(): array {
return $this->allowedFrameAncestors;
}
/**
* @param array $allowedFrameAncestors
*/
public function setAllowedFrameAncestors($allowedFrameAncestors): void {
$this->allowedFrameAncestors = $allowedFrameAncestors;
}
public function getAllowedWorkerSrcDomains(): array {
return $this->allowedWorkerSrcDomains;
}
public function setAllowedWorkerSrcDomains(array $allowedWorkerSrcDomains): void {
$this->allowedWorkerSrcDomains = $allowedWorkerSrcDomains;
}
public function getAllowedFormActionDomains(): array {
return $this->allowedFormActionDomains;
}
public function setAllowedFormActionDomains(array $allowedFormActionDomains): void {
$this->allowedFormActionDomains = $allowedFormActionDomains;
}
public function getReportTo(): array {
return $this->reportTo;
}
public function setReportTo(array $reportTo): void {
$this->reportTo = $reportTo;
}
public function isStrictDynamicAllowed(): bool {
return $this->strictDynamicAllowed;
}
public function setStrictDynamicAllowed(bool $strictDynamicAllowed): void {
$this->strictDynamicAllowed = $strictDynamicAllowed;
}
public function isStrictDynamicAllowedOnScripts(): bool {
return $this->strictDynamicAllowedOnScripts;
}
public function setStrictDynamicAllowedOnScripts(bool $strictDynamicAllowedOnScripts): void {
$this->strictDynamicAllowedOnScripts = $strictDynamicAllowedOnScripts;
}
}
@@ -0,0 +1,90 @@
<?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/>
*
*/
namespace OC\Security\CSP;
use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\EmptyContentSecurityPolicy;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Security\CSP\AddContentSecurityPolicyEvent;
use OCP\Security\IContentSecurityPolicyManager;
class ContentSecurityPolicyManager implements IContentSecurityPolicyManager {
/** @var ContentSecurityPolicy[] */
private array $policies = [];
public function __construct(
private IEventDispatcher $dispatcher,
) {
}
/** {@inheritdoc} */
public function addDefaultPolicy(EmptyContentSecurityPolicy $policy): void {
$this->policies[] = $policy;
}
/**
* Get the configured default policy. This is not in the public namespace
* as it is only supposed to be used by core itself.
*/
public function getDefaultPolicy(): ContentSecurityPolicy {
$event = new AddContentSecurityPolicyEvent($this);
$this->dispatcher->dispatchTyped($event);
$defaultPolicy = new \OC\Security\CSP\ContentSecurityPolicy();
foreach ($this->policies as $policy) {
$defaultPolicy = $this->mergePolicies($defaultPolicy, $policy);
}
return $defaultPolicy;
}
/**
* Merges the first given policy with the second one
*/
public function mergePolicies(
ContentSecurityPolicy $defaultPolicy,
EmptyContentSecurityPolicy $originalPolicy,
): ContentSecurityPolicy {
foreach ((object)(array)$originalPolicy as $name => $value) {
$setter = 'set'.ucfirst($name);
if (\is_array($value)) {
$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);
$currentValue = $defaultPolicy->$getter();
// true wins over false
if ($value > $currentValue) {
$defaultPolicy->$setter($value);
}
}
}
return $defaultPolicy;
}
}
@@ -0,0 +1,80 @@
<?php
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/>.
*
*/
namespace OC\Security\CSP;
use OC\AppFramework\Http\Request;
use OC\Security\CSRF\CsrfTokenManager;
use OCP\IRequest;
/**
* @package OC\Security\CSP
*/
class ContentSecurityPolicyNonceManager {
private string $nonce = '';
public function __construct(
private CsrfTokenManager $csrfTokenManager,
private IRequest $request,
) {
}
/**
* Returns the current CSP nonce
*/
public function getNonce(): string {
if ($this->nonce === '') {
if (empty($this->request->server['CSP_NONCE'])) {
$this->nonce = base64_encode($this->csrfTokenManager->getToken()->getEncryptedValue());
} else {
$this->nonce = $this->request->server['CSP_NONCE'];
}
}
return $this->nonce;
}
/**
* Check if the browser supports CSP v3
*/
public function browserSupportsCspV3(): bool {
$browserWhitelist = [
Request::USER_AGENT_CHROME,
Request::USER_AGENT_FIREFOX,
Request::USER_AGENT_SAFARI,
];
if ($this->request->isUserAgent($browserWhitelist)) {
return true;
}
return false;
}
}
@@ -0,0 +1,75 @@
<?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/>
*
*/
namespace OC\Security\CSRF;
/**
* Class CsrfToken represents the stored or provided CSRF token. To mitigate
* BREACH alike vulnerabilities the token is returned in an encrypted value as
* well in an unencrypted value. For display measures to the user always the
* unencrypted one should be chosen.
*
* @package OC\Security\CSRF
*/
class CsrfToken {
private string $encryptedValue = '';
/**
* @param string $value Value of the token. Can be encrypted or not encrypted.
*/
public function __construct(
private string $value,
) {
}
/**
* Encrypted value of the token. This is used to mitigate BREACH alike
* vulnerabilities. For display measures do use this functionality.
*/
public function getEncryptedValue(): string {
if ($this->encryptedValue === '') {
$sharedSecret = random_bytes(\strlen($this->value));
$this->encryptedValue = base64_encode($this->value ^ $sharedSecret) . ':' . base64_encode($sharedSecret);
}
return $this->encryptedValue;
}
/**
* The unencrypted value of the token. Used for decrypting an already
* encrypted token.
*/
public function getDecryptedValue(): string {
$token = explode(':', $this->value);
if (\count($token) !== 2) {
return '';
}
$obfuscatedToken = $token[0];
$secret = $token[1];
return base64_decode($obfuscatedToken) ^ base64_decode($secret);
}
}
@@ -0,0 +1,50 @@
<?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/>
*
*/
namespace OC\Security\CSRF;
use OCP\Security\ISecureRandom;
/**
* Class CsrfTokenGenerator is used to generate a cryptographically secure
* pseudo-random number for the token.
*
* @package OC\Security\CSRF
*/
class CsrfTokenGenerator {
public function __construct(
private ISecureRandom $random,
) {
}
/**
* Generate a new CSRF token.
*
* @param int $length Length of the token in characters.
*/
public function generateToken(int $length = 32): string {
return $this->random->generate($length);
}
}
@@ -0,0 +1,97 @@
<?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/>
*
*/
namespace OC\Security\CSRF;
use OC\Security\CSRF\TokenStorage\SessionStorage;
/**
* Class CsrfTokenManager is the manager for all CSRF token related activities.
*
* @package OC\Security\CSRF
*/
class CsrfTokenManager {
private SessionStorage $sessionStorage;
private ?CsrfToken $csrfToken = null;
public function __construct(
private CsrfTokenGenerator $tokenGenerator,
SessionStorage $storageInterface,
) {
$this->sessionStorage = $storageInterface;
}
/**
* Returns the current CSRF token, if none set it will create a new one.
*/
public function getToken(): CsrfToken {
if (!\is_null($this->csrfToken)) {
return $this->csrfToken;
}
if ($this->sessionStorage->hasToken()) {
$value = $this->sessionStorage->getToken();
} else {
$value = $this->tokenGenerator->generateToken();
$this->sessionStorage->setToken($value);
}
$this->csrfToken = new CsrfToken($value);
return $this->csrfToken;
}
/**
* Invalidates any current token and sets a new one.
*/
public function refreshToken(): CsrfToken {
$value = $this->tokenGenerator->generateToken();
$this->sessionStorage->setToken($value);
$this->csrfToken = new CsrfToken($value);
return $this->csrfToken;
}
/**
* Remove the current token from the storage.
*/
public function removeToken(): void {
$this->csrfToken = null;
$this->sessionStorage->removeToken();
}
/**
* Verifies whether the provided token is valid.
*/
public function isTokenValid(CsrfToken $token): bool {
if (!$this->sessionStorage->hasToken()) {
return false;
}
return hash_equals(
$this->sessionStorage->getToken(),
$token->getDecryptedValue()
);
}
}
@@ -0,0 +1,81 @@
<?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/>
*
*/
namespace OC\Security\CSRF\TokenStorage;
use OCP\ISession;
/**
* Class SessionStorage provides the session storage
*
* @package OC\Security\CSRF\TokenStorage
*/
class SessionStorage {
public function __construct(
private ISession $session,
) {
}
public function setSession(ISession $session): void {
$this->session = $session;
}
/**
* Returns the current token or throws an exception if none is found.
*
* @throws \Exception
*/
public function getToken(): string {
$token = $this->session->get('requesttoken');
if (empty($token)) {
throw new \Exception('Session does not contain a requesttoken');
}
return $token;
}
/**
* Set the valid current token to $value.
*/
public function setToken(string $value): void {
$this->session->set('requesttoken', $value);
}
/**
* Removes the current token.
*/
public function removeToken(): void {
$this->session->remove('requesttoken');
}
/**
* Whether the storage has a storage.
*/
public function hasToken(): bool {
return $this->session->exists('requesttoken');
}
}
@@ -0,0 +1,107 @@
<?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/>
*
*/
namespace OC\Security;
use OCP\ICertificate;
class Certificate implements ICertificate {
protected string $name;
protected ?string $commonName;
protected ?string $organization;
protected \DateTime $issueDate;
protected \DateTime $expireDate;
protected ?string $issuerName;
protected ?string $issuerOrganization;
/**
* @param string $data base64 encoded certificate
* @throws \Exception If the certificate could not get parsed
*/
public function __construct(string $data, string $name) {
$this->name = $name;
$gmt = new \DateTimeZone('GMT');
// If string starts with "file://" ignore the certificate
$query = 'file://';
if (strtolower(substr($data, 0, strlen($query))) === $query) {
throw new \Exception('Certificate could not get parsed.');
}
$info = openssl_x509_parse($data);
if (!is_array($info)) {
throw new \Exception('Certificate could not get parsed.');
}
$this->commonName = $info['subject']['CN'] ?? null;
$this->organization = $info['subject']['O'] ?? null;
$this->issueDate = new \DateTime('@' . $info['validFrom_time_t'], $gmt);
$this->expireDate = new \DateTime('@' . $info['validTo_time_t'], $gmt);
$this->issuerName = $info['issuer']['CN'] ?? null;
$this->issuerOrganization = $info['issuer']['O'] ?? null;
}
public function getName(): string {
return $this->name;
}
public function getCommonName(): ?string {
return $this->commonName;
}
public function getOrganization(): ?string {
return $this->organization;
}
public function getIssueDate(): \DateTime {
return $this->issueDate;
}
public function getExpireDate(): \DateTime {
return $this->expireDate;
}
public function isExpired(): bool {
$now = new \DateTime();
return $this->issueDate > $now or $now > $this->expireDate;
}
public function getIssuerName(): ?string {
return $this->issuerName;
}
public function getIssuerOrganization(): ?string {
return $this->issuerOrganization;
}
}
@@ -0,0 +1,274 @@
<?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/>
*
*/
namespace OC\Security;
use OC\Files\Filesystem;
use OC\Files\View;
use OCP\ICertificate;
use OCP\ICertificateManager;
use OCP\IConfig;
use OCP\Security\ISecureRandom;
use Psr\Log\LoggerInterface;
/**
* Manage trusted certificates for users
*/
class CertificateManager implements ICertificateManager {
private ?string $bundlePath = null;
public function __construct(
protected View $view,
protected IConfig $config,
protected LoggerInterface $logger,
protected ISecureRandom $random,
) {
}
/**
* Returns all certificates trusted by the user
*
* @return \OCP\ICertificate[]
*/
public function listCertificates(): array {
if (!$this->config->getSystemValueBool('installed', false)) {
return [];
}
$path = $this->getPathToCertificates() . 'uploads/';
if (!$this->view->is_dir($path)) {
return [];
}
$result = [];
$handle = $this->view->opendir($path);
if (!is_resource($handle)) {
return [];
}
while (false !== ($file = readdir($handle))) {
if ($file != '.' && $file != '..') {
try {
$content = $this->view->file_get_contents($path . $file);
if ($content !== false) {
$result[] = new Certificate($content, $file);
} else {
$this->logger->error("Failed to read certificate from $path");
}
} catch (\Exception $e) {
$this->logger->error("Failed to read certificate from $path", ['exception' => $e]);
}
}
}
closedir($handle);
return $result;
}
private function hasCertificates(): bool {
if (!$this->config->getSystemValueBool('installed', false)) {
return false;
}
$path = $this->getPathToCertificates() . 'uploads/';
if (!$this->view->is_dir($path)) {
return false;
}
$result = [];
$handle = $this->view->opendir($path);
if (!is_resource($handle)) {
return false;
}
while (false !== ($file = readdir($handle))) {
if ($file !== '.' && $file !== '..') {
return true;
}
}
closedir($handle);
return false;
}
/**
* create the certificate bundle of all trusted certificated
*/
public function createCertificateBundle(): void {
$path = $this->getPathToCertificates();
$certs = $this->listCertificates();
if (!$this->view->file_exists($path)) {
$this->view->mkdir($path);
}
$defaultCertificates = file_get_contents(\OC::$SERVERROOT . '/resources/config/ca-bundle.crt');
if (strlen($defaultCertificates) < 1024) { // sanity check to verify that we have some content for our bundle
// log as exception so we have a stacktrace
$e = new \Exception('Shipped ca-bundle is empty, refusing to create certificate bundle');
$this->logger->error($e->getMessage(), ['exception' => $e]);
return;
}
$certPath = $path . 'rootcerts.crt';
$tmpPath = $certPath . '.tmp' . $this->random->generate(10, ISecureRandom::CHAR_DIGITS);
$fhCerts = $this->view->fopen($tmpPath, 'w');
if (!is_resource($fhCerts)) {
throw new \RuntimeException('Unable to open file handler to create certificate bundle "' . $tmpPath . '".');
}
// Write user certificates
foreach ($certs as $cert) {
$file = $path . '/uploads/' . $cert->getName();
$data = $this->view->file_get_contents($file);
if (strpos($data, 'BEGIN CERTIFICATE')) {
fwrite($fhCerts, $data);
fwrite($fhCerts, "\r\n");
}
}
// Append the default certificates
fwrite($fhCerts, $defaultCertificates);
// Append the system certificate bundle
$systemBundle = $this->getCertificateBundle();
if ($systemBundle !== $certPath && $this->view->file_exists($systemBundle)) {
$systemCertificates = $this->view->file_get_contents($systemBundle);
fwrite($fhCerts, $systemCertificates);
}
fclose($fhCerts);
$this->view->rename($tmpPath, $certPath);
}
/**
* Save the certificate and re-generate the certificate bundle
*
* @param string $certificate the certificate data
* @param string $name the filename for the certificate
* @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');
}
$this->bundlePath = null;
$dir = $this->getPathToCertificates() . 'uploads/';
if (!$this->view->file_exists($dir)) {
$this->view->mkdir($dir);
}
try {
$file = $dir . $name;
$certificateObject = new Certificate($certificate, $name);
$this->view->file_put_contents($file, $certificate);
$this->createCertificateBundle();
return $certificateObject;
} catch (\Exception $e) {
throw $e;
}
}
/**
* Remove the certificate and re-generate the certificate bundle
*/
public function removeCertificate(string $name): bool {
if (!Filesystem::isValidPath($name)) {
return false;
}
$this->bundlePath = null;
$path = $this->getPathToCertificates() . 'uploads/';
if ($this->view->file_exists($path . $name)) {
$this->view->unlink($path . $name);
$this->createCertificateBundle();
}
return true;
}
/**
* Get the path to the certificate bundle
*/
public function getCertificateBundle(): string {
return $this->getPathToCertificates() . 'rootcerts.crt';
}
/**
* Get the full local path to the certificate bundle
* @throws \Exception when getting bundle path fails
*/
public function getAbsoluteBundlePath(): string {
try {
if ($this->bundlePath === null) {
if (!$this->hasCertificates()) {
$this->bundlePath = \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
}
if ($this->needsRebundling()) {
$this->createCertificateBundle();
}
$certificateBundle = $this->getCertificateBundle();
$this->bundlePath = $this->view->getLocalFile($certificateBundle) ?: null;
if ($this->bundlePath === null) {
throw new \RuntimeException('Unable to get certificate bundle "' . $certificateBundle . '".');
}
}
return $this->bundlePath;
} catch (\Exception $e) {
$this->logger->error('Failed to get absolute bundle path. Fallback to default ca-bundle.crt', ['exception' => $e]);
return \OC::$SERVERROOT . '/resources/config/ca-bundle.crt';
}
}
private function getPathToCertificates(): string {
return '/files_external/';
}
/**
* Check if we need to re-bundle the certificates because one of the sources has updated
*/
private function needsRebundling(): bool {
$targetBundle = $this->getCertificateBundle();
if (!$this->view->file_exists($targetBundle)) {
return true;
}
$sourceMTime = $this->getFilemtimeOfCaBundle();
return $sourceMTime > $this->view->filemtime($targetBundle);
}
/**
* get mtime of ca-bundle shipped by Nextcloud
*/
protected function getFilemtimeOfCaBundle(): int {
return filemtime(\OC::$SERVERROOT . '/resources/config/ca-bundle.crt');
}
}
@@ -0,0 +1,127 @@
<?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/>
*
*/
namespace OC\Security;
use OCP\IDBConnection;
use OCP\Security\ICredentialsManager;
use OCP\Security\ICrypto;
/**
* Store and retrieve credentials for external services
*
* @package OC\Security
*/
class CredentialsManager implements ICredentialsManager {
public const DB_TABLE = 'storages_credentials';
public function __construct(
protected ICrypto $crypto,
protected IDBConnection $dbConnection,
) {
}
/**
* Store a set of credentials
*
* @param string $userId empty string for system-wide credentials
* @param mixed $credentials
*/
public function store(string $userId, string $identifier, $credentials): void {
$value = $this->crypto->encrypt(json_encode($credentials));
$this->dbConnection->setValues(self::DB_TABLE, [
'user' => $userId,
'identifier' => $identifier,
], [
'credentials' => $value,
]);
}
/**
* Retrieve a set of credentials
*
* @param string $userId empty string for system-wide credentials
*/
public function retrieve(string $userId, string $identifier): mixed {
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('credentials')
->from(self::DB_TABLE)
->where($qb->expr()->eq('identifier', $qb->createNamedParameter($identifier)));
if ($userId === '') {
$qb->andWhere($qb->expr()->emptyString('user'));
} else {
$qb->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($userId)));
}
$qResult = $qb->execute();
$result = $qResult->fetch();
$qResult->closeCursor();
if (!$result) {
return null;
}
$value = $result['credentials'];
return json_decode($this->crypto->decrypt($value), true);
}
/**
* Delete a set of credentials
*
* @param string $userId empty string for system-wide credentials
* @return int rows removed
*/
public function delete(string $userId, string $identifier): int {
$qb = $this->dbConnection->getQueryBuilder();
$qb->delete(self::DB_TABLE)
->where($qb->expr()->eq('identifier', $qb->createNamedParameter($identifier)));
if ($userId === '') {
$qb->andWhere($qb->expr()->emptyString('user'));
} else {
$qb->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($userId)));
}
return $qb->execute();
}
/**
* Erase all credentials stored for a user
*
* @return int rows removed
*/
public function erase(string $userId): int {
$qb = $this->dbConnection->getQueryBuilder();
$qb->delete(self::DB_TABLE)
->where($qb->expr()->eq('user', $qb->createNamedParameter($userId)))
;
return $qb->execute();
}
}
@@ -0,0 +1,184 @@
<?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/>
*
*/
namespace OC\Security;
use Exception;
use OCP\IConfig;
use OCP\Security\ICrypto;
use phpseclib\Crypt\AES;
use phpseclib\Crypt\Hash;
/**
* Class Crypto provides a high-level encryption layer using AES-CBC. If no key has been provided
* it will use the secret defined in config.php as key. Additionally the message will be HMAC'd.
*
* Usage:
* $encryptWithDefaultPassword = \OC::$server->getCrypto()->encrypt('EncryptedText');
* $encryptWithCustompassword = \OC::$server->getCrypto()->encrypt('EncryptedText', 'password');
*
* @package OC\Security
*/
class Crypto implements ICrypto {
private AES $cipher;
private int $ivLength = 16;
public function __construct(
private IConfig $config,
) {
$this->cipher = new AES();
}
/**
* @param string $message The message to authenticate
* @param string $password Password to use (defaults to `secret` in config.php)
* @return string Calculated HMAC
*/
public function calculateHMAC(string $message, string $password = ''): string {
if ($password === '') {
$password = $this->config->getSystemValueString('secret');
}
// Append an "a" behind the password and hash it to prevent reusing the same password as for encryption
$password = hash('sha512', $password . 'a');
$hash = new Hash('sha512');
$hash->setKey($password);
return $hash->hash($message);
}
/**
* Encrypts a value and adds an HMAC (Encrypt-Then-MAC)
*
* @param string $password Password to encrypt, if not specified the secret from config.php will be taken
* @return string Authenticated ciphertext
* @throws Exception if it was not possible to gather sufficient entropy
* @throws Exception if encrypting the data failed
*/
public function encrypt(string $plaintext, string $password = ''): string {
if ($password === '') {
$password = $this->config->getSystemValueString('secret');
}
$keyMaterial = hash_hkdf('sha512', $password);
$this->cipher->setPassword(substr($keyMaterial, 0, 32));
$iv = \random_bytes($this->ivLength);
$this->cipher->setIV($iv);
/** @var string|false $encrypted */
$encrypted = $this->cipher->encrypt($plaintext);
if ($encrypted === false) {
throw new Exception('Encrypting failed.');
}
$ciphertext = bin2hex($encrypted);
$iv = bin2hex($iv);
$hmac = bin2hex($this->calculateHMAC($ciphertext.$iv, substr($keyMaterial, 32)));
return $ciphertext.'|'.$iv.'|'.$hmac.'|3';
}
/**
* Decrypts a value and verifies the HMAC (Encrypt-Then-Mac)
* @param string $password Password to encrypt, if not specified the secret from config.php will be taken
* @throws Exception If the HMAC does not match
* @throws Exception If the decryption failed
*/
public function decrypt(string $authenticatedCiphertext, string $password = ''): string {
$secret = $this->config->getSystemValue('secret');
try {
if ($password === '') {
return $this->decryptWithoutSecret($authenticatedCiphertext, $secret);
}
return $this->decryptWithoutSecret($authenticatedCiphertext, $password);
} catch (Exception $e) {
if ($password === '') {
// Retry with empty secret as a fallback for instances where the secret might not have been set by accident
return $this->decryptWithoutSecret($authenticatedCiphertext, '');
}
throw $e;
}
}
private function decryptWithoutSecret(string $authenticatedCiphertext, string $password = ''): string {
$hmacKey = $encryptionKey = $password;
$parts = explode('|', $authenticatedCiphertext);
$partCount = \count($parts);
if ($partCount < 3 || $partCount > 4) {
throw new Exception('Authenticated ciphertext could not be decoded.');
}
$ciphertext = $this->hex2bin($parts[0]);
$iv = $parts[1];
$hmac = $this->hex2bin($parts[2]);
if ($partCount === 4) {
$version = $parts[3];
if ($version >= '2') {
$iv = $this->hex2bin($iv);
}
if ($version === '3') {
$keyMaterial = hash_hkdf('sha512', $password);
$encryptionKey = substr($keyMaterial, 0, 32);
$hmacKey = substr($keyMaterial, 32);
}
}
$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.');
}
$result = $this->cipher->decrypt($ciphertext);
if ($result === false) {
throw new Exception('Decryption failed');
}
return $result;
}
private function hex2bin(string $hex): string {
if (!ctype_xdigit($hex)) {
throw new \RuntimeException('String contains non hex chars: ' . $hex);
}
if (strlen($hex) % 2 !== 0) {
throw new \RuntimeException('Hex string is not of even length: ' . $hex);
}
$result = hex2bin($hex);
if ($result === false) {
throw new \RuntimeException('Hex to bin conversion failed: ' . $hex);
}
return $result;
}
}
@@ -0,0 +1,76 @@
<?php
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/>.
*
*/
namespace OC\Security\FeaturePolicy;
class FeaturePolicy extends \OCP\AppFramework\Http\FeaturePolicy {
public function getAutoplayDomains(): array {
return $this->autoplayDomains;
}
public function setAutoplayDomains(array $autoplayDomains): void {
$this->autoplayDomains = $autoplayDomains;
}
public function getCameraDomains(): array {
return $this->cameraDomains;
}
public function setCameraDomains(array $cameraDomains): void {
$this->cameraDomains = $cameraDomains;
}
public function getFullscreenDomains(): array {
return $this->fullscreenDomains;
}
public function setFullscreenDomains(array $fullscreenDomains): void {
$this->fullscreenDomains = $fullscreenDomains;
}
public function getGeolocationDomains(): array {
return $this->geolocationDomains;
}
public function setGeolocationDomains(array $geolocationDomains): void {
$this->geolocationDomains = $geolocationDomains;
}
public function getMicrophoneDomains(): array {
return $this->microphoneDomains;
}
public function setMicrophoneDomains(array $microphoneDomains): void {
$this->microphoneDomains = $microphoneDomains;
}
public function getPaymentDomains(): array {
return $this->paymentDomains;
}
public function setPaymentDomains(array $paymentDomains): void {
$this->paymentDomains = $paymentDomains;
}
}
@@ -0,0 +1,78 @@
<?php
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/>.
*
*/
namespace OC\Security\FeaturePolicy;
use OCP\AppFramework\Http\EmptyFeaturePolicy;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Security\FeaturePolicy\AddFeaturePolicyEvent;
class FeaturePolicyManager {
/** @var EmptyFeaturePolicy[] */
private array $policies = [];
public function __construct(
private IEventDispatcher $dispatcher,
) {
}
public function addDefaultPolicy(EmptyFeaturePolicy $policy): void {
$this->policies[] = $policy;
}
public function getDefaultPolicy(): FeaturePolicy {
$event = new AddFeaturePolicyEvent($this);
$this->dispatcher->dispatchTyped($event);
$defaultPolicy = new FeaturePolicy();
foreach ($this->policies as $policy) {
$defaultPolicy = $this->mergePolicies($defaultPolicy, $policy);
}
return $defaultPolicy;
}
/**
* Merges the first given policy with the second one
*
*/
public function mergePolicies(
FeaturePolicy $defaultPolicy,
EmptyFeaturePolicy $originalPolicy,
): FeaturePolicy {
foreach ((object)(array)$originalPolicy as $name => $value) {
$setter = 'set' . ucfirst($name);
if (\is_array($value)) {
$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)) {
$defaultPolicy->$setter($value);
}
}
return $defaultPolicy;
}
}
@@ -0,0 +1,213 @@
<?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/>
*
*/
namespace OC\Security;
use OCP\IConfig;
use OCP\Security\IHasher;
/**
* Class Hasher provides some basic hashing functions. Furthermore, it supports legacy hashes
* used by previous versions of ownCloud and helps migrating those hashes to newer ones.
*
* The hashes generated by this class are prefixed (version|hash) with a version parameter to allow possible
* updates in the future.
* Possible versions:
* - 1 (Initial version)
*
* Usage:
* // Hashing a message
* $hash = \OC::$server->getHasher()->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($newHash);
*
* @package OC\Security
*/
class Hasher implements IHasher {
/** Options passed to password_hash and password_needs_rehash */
private array $options = [];
/** Salt used for legacy passwords */
private ?string $legacySalt = null;
public function __construct(
private IConfig $config,
) {
if (\defined('PASSWORD_ARGON2ID') || \defined('PASSWORD_ARGON2I')) {
// password_hash fails, when the minimum values are undershot.
// In this case, apply minimum.
$this->options['threads'] = max($this->config->getSystemValueInt('hashingThreads', PASSWORD_ARGON2_DEFAULT_THREADS), 1);
// The minimum memory cost is 8 KiB per thread.
$this->options['memory_cost'] = max($this->config->getSystemValueInt('hashingMemoryCost', PASSWORD_ARGON2_DEFAULT_MEMORY_COST), $this->options['threads'] * 8);
$this->options['time_cost'] = max($this->config->getSystemValueInt('hashingTimeCost', PASSWORD_ARGON2_DEFAULT_TIME_COST), 1);
}
$hashingCost = $this->config->getSystemValue('hashingCost', null);
if (!\is_null($hashingCost)) {
$this->options['cost'] = $hashingCost;
}
}
/**
* Hashes a message using PHP's `password_hash` functionality.
* Please note that the size of the returned string is not guaranteed
* and can be up to 255 characters.
*
* @param string $message Message to generate hash from
* @return string Hash of the message with appended version parameter
*/
public function hash(string $message): string {
$alg = $this->getPrefferedAlgorithm();
if (\defined('PASSWORD_ARGON2ID') && $alg === PASSWORD_ARGON2ID) {
return 3 . '|' . password_hash($message, PASSWORD_ARGON2ID, $this->options);
}
if (\defined('PASSWORD_ARGON2I') && $alg === PASSWORD_ARGON2I) {
return 2 . '|' . password_hash($message, PASSWORD_ARGON2I, $this->options);
}
return 1 . '|' . password_hash($message, PASSWORD_BCRYPT, $this->options);
}
/**
* 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')
*/
protected function splitHash(string $prefixedHash): ?array {
$explodedString = explode('|', $prefixedHash, 2);
if (\count($explodedString) === 2) {
if ((int)$explodedString[0] > 0) {
return ['version' => (int)$explodedString[0], 'hash' => $explodedString[1]];
}
}
return null;
}
/**
* Verify legacy hashes
* @param string $message Message to verify
* @param string $hash Assumed hash of the message
* @param null|string &$newHash Reference will contain the updated hash
* @return bool Whether $hash is a valid hash of $message
*/
protected function legacyHashVerify($message, $hash, &$newHash = null): bool {
if (empty($this->legacySalt)) {
$this->legacySalt = $this->config->getSystemValue('passwordsalt', '');
}
// 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)))) {
$newHash = $this->hash($message);
return true;
}
// 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)))) {
$newHash = $this->hash($message);
return true;
}
return false;
}
/**
* Verify V1 (blowfish) hashes
* Verify V2 (argon2i) hashes
* Verify V3 (argon2id) hashes
* @param string $message Message to verify
* @param string $hash Assumed hash of the message
* @param null|string &$newHash Reference will contain the updated hash if necessary. Update the existing hash with this one.
* @return bool Whether $hash is a valid hash of $message
*/
protected function verifyHash(string $message, string $hash, &$newHash = null): bool {
if (password_verify($message, $hash)) {
if ($this->needsRehash($hash)) {
$newHash = $this->hash($message);
}
return true;
}
return false;
}
/**
* @param string $message Message to verify
* @param string $hash Assumed hash of the message
* @param null|string &$newHash Reference will contain the updated hash if necessary. Update the existing hash with this one.
* @return bool Whether $hash is a valid hash of $message
*/
public function verify(string $message, string $hash, &$newHash = null): bool {
$splittedHash = $this->splitHash($hash);
if (isset($splittedHash['version'])) {
switch ($splittedHash['version']) {
case 3:
case 2:
case 1:
return $this->verifyHash($message, $splittedHash['hash'], $newHash);
}
} else {
return $this->legacyHashVerify($message, $hash, $newHash);
}
return false;
}
private function needsRehash(string $hash): bool {
$algorithm = $this->getPrefferedAlgorithm();
return password_needs_rehash($hash, $algorithm, $this->options);
}
private function getPrefferedAlgorithm(): string {
$default = PASSWORD_BCRYPT;
if (\defined('PASSWORD_ARGON2I')) {
$default = PASSWORD_ARGON2I;
}
if (\defined('PASSWORD_ARGON2ID')) {
$default = PASSWORD_ARGON2ID;
}
// Check if we should use PASSWORD_DEFAULT
if ($this->config->getSystemValueBool('hashing_default_password', false)) {
$default = PASSWORD_DEFAULT;
}
return $default;
}
}
@@ -0,0 +1,43 @@
<?php
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/>.
*
*/
namespace OC\Security\IdentityProof;
class Key {
public function __construct(
private string $publicKey,
private string $privateKey,
) {
}
public function getPrivate(): string {
return $this->privateKey;
}
public function getPublic(): string {
return $this->publicKey;
}
}
@@ -0,0 +1,156 @@
<?php
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/>.
*
*/
namespace OC\Security\IdentityProof;
use OC\Files\AppData\Factory;
use OCP\Files\IAppData;
use OCP\IConfig;
use OCP\IUser;
use OCP\Security\ICrypto;
use Psr\Log\LoggerInterface;
class Manager {
private IAppData $appData;
public function __construct(
Factory $appDataFactory,
private ICrypto $crypto,
private IConfig $config,
private LoggerInterface $logger,
) {
$this->appData = $appDataFactory->get('identityproof');
}
/**
* Calls the openssl functions to generate a public and private key.
* In a separate function for unit testing purposes.
*
* @return array [$publicKey, $privateKey]
* @throws \RuntimeException
*/
protected function generateKeyPair(): array {
$config = [
'digest_alg' => 'sha512',
'private_key_bits' => 2048,
];
// Generate new key
$res = openssl_pkey_new($config);
if ($res === false) {
$this->logOpensslError();
throw new \RuntimeException('OpenSSL reported a problem');
}
if (openssl_pkey_export($res, $privateKey, null, $config) === false) {
$this->logOpensslError();
throw new \RuntimeException('OpenSSL reported a problem');
}
// Extract the public key from $res to $pubKey
$publicKey = openssl_pkey_get_details($res);
$publicKey = $publicKey['key'];
return [$publicKey, $privateKey];
}
/**
* Generate a key for a given ID
* Note: If a key already exists it will be overwritten
*
* @param string $id key id
* @throws \RuntimeException
*/
protected function generateKey(string $id): Key {
[$publicKey, $privateKey] = $this->generateKeyPair();
// Write the private and public key to the disk
try {
$this->appData->newFolder($id);
} catch (\Exception $e) {
}
$folder = $this->appData->getFolder($id);
$folder->newFile('private')
->putContent($this->crypto->encrypt($privateKey));
$folder->newFile('public')
->putContent($publicKey);
return new Key($publicKey, $privateKey);
}
/**
* Get key for a specific id
*
* @throws \RuntimeException
*/
protected function retrieveKey(string $id): Key {
try {
$folder = $this->appData->getFolder($id);
$privateKey = $this->crypto->decrypt(
$folder->getFile('private')->getContent()
);
$publicKey = $folder->getFile('public')->getContent();
return new Key($publicKey, $privateKey);
} catch (\Exception $e) {
return $this->generateKey($id);
}
}
/**
* Get public and private key for $user
*
* @throws \RuntimeException
*/
public function getKey(IUser $user): Key {
$uid = $user->getUID();
return $this->retrieveKey('user-' . $uid);
}
/**
* Get instance wide public and private key
*
* @throws \RuntimeException
*/
public function getSystemKey(): Key {
$instanceId = $this->config->getSystemValue('instanceid', null);
if ($instanceId === null) {
throw new \RuntimeException('no instance id!');
}
return $this->retrieveKey('system-' . $instanceId);
}
private function logOpensslError(): void {
$errors = [];
while ($error = openssl_error_string()) {
$errors[] = $error;
}
$this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors));
}
}
@@ -0,0 +1,88 @@
<?php
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/>.
*
*/
namespace OC\Security\IdentityProof;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IUser;
use OCP\IUserManager;
class Signer {
public function __construct(
private Manager $keyManager,
private ITimeFactory $timeFactory,
private IUserManager $userManager,
) {
}
/**
* Returns a signed blob for $data
*
* @return array ['message', 'signature']
*/
public function sign(string $type, array $data, IUser $user): array {
$privateKey = $this->keyManager->getKey($user)->getPrivate();
$data = [
'data' => $data,
'type' => $type,
'signer' => $user->getCloudId(),
'timestamp' => $this->timeFactory->getTime(),
];
openssl_sign(json_encode($data), $signature, $privateKey, OPENSSL_ALGO_SHA512);
return [
'message' => $data,
'signature' => base64_encode($signature),
];
}
/**
* Whether the data is signed properly
*
*/
public function verify(array $data): bool {
if (isset($data['message']['signer'])
&& isset($data['signature'])
) {
$location = strrpos($data['message']['signer'], '@');
$userId = substr($data['message']['signer'], 0, $location);
$user = $this->userManager->get($userId);
if ($user !== null) {
$key = $this->keyManager->getKey($user);
return (bool)openssl_verify(
json_encode($data['message']),
base64_decode($data['signature']),
$key->getPublic(),
OPENSSL_ALGO_SHA512
);
}
}
return false;
}
}
@@ -0,0 +1,109 @@
<?php
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/>.
*
*/
namespace OC\Security\Normalizer;
/**
* Class IpAddress is used for normalizing IPv4 and IPv6 addresses in security
* relevant contexts in Nextcloud.
*
* @package OC\Security\Normalizer
*/
class IpAddress {
/**
* @param string $ip IP to normalize
*/
public function __construct(
private string $ip,
) {
}
/**
* Return the given subnet for an IPv6 address (64 first bits)
*/
private function getIPv6Subnet(string $ip): string {
if ($ip[0] === '[' && $ip[-1] === ']') { // If IP is with brackets, for example [::1]
$ip = substr($ip, 1, strlen($ip) - 2);
}
$pos = strpos($ip, '%'); // if there is an explicit interface added to the IP, e.g. fe80::ae2d:d1e7:fe1e:9a8d%enp2s0
if ($pos !== false) {
$ip = substr($ip, 0, $pos - 1);
}
$binary = \inet_pton($ip);
$mask = inet_pton('FFFF:FFFF:FFFF:FFFF::');
return inet_ntop($binary & $mask).'/64';
}
/**
* Returns the IPv4 address embedded in an IPv6 if applicable.
* The detected format is "::ffff:x.x.x.x" using the binary form.
*
* @return string|null embedded IPv4 string or null if none was found
*/
private function getEmbeddedIpv4(string $ipv6): ?string {
$binary = inet_pton($ipv6);
if (!$binary) {
return null;
}
$mask = inet_pton('::FFFF:FFFF');
if (($binary & ~$mask) !== inet_pton('::FFFF:0.0.0.0')) {
return null;
}
return inet_ntop(substr($binary, -4));
}
/**
* Gets either the /32 (IPv4) or the /64 (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';
}
$ipv4 = $this->getEmbeddedIpv4($this->ip);
if ($ipv4 !== null) {
return $ipv4.'/32';
}
return $this->getIPv6Subnet($this->ip);
}
/**
* Returns the specified IP address
*/
public function __toString(): string {
return $this->ip;
}
}
@@ -0,0 +1,118 @@
<?php
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/>.
*
*/
namespace OC\Security\RateLimiting\Backend;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IConfig;
use OCP\IDBConnection;
class DatabaseBackend implements IBackend {
private const TABLE_NAME = 'ratelimit_entries';
public function __construct(
private IConfig $config,
private IDBConnection $dbConnection,
private ITimeFactory $timeFactory
) {
}
private function hash(
string $methodIdentifier,
string $userIdentifier,
): string {
return hash('sha512', $methodIdentifier . $userIdentifier);
}
/**
* @throws Exception
*/
private function getExistingAttemptCount(
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))
)
->executeStatement();
$qb = $this->dbConnection->getQueryBuilder();
$qb->select($qb->func()->count())
->from(self::TABLE_NAME)
->where(
$qb->expr()->eq('hash', $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR))
);
$cursor = $qb->executeQuery();
$row = $cursor->fetchOne();
$cursor->closeCursor();
return (int)$row;
}
/**
* {@inheritDoc}
*/
public function getAttempts(
string $methodIdentifier,
string $userIdentifier,
): int {
$identifier = $this->hash($methodIdentifier, $userIdentifier);
return $this->getExistingAttemptCount($identifier);
}
/**
* {@inheritDoc}
*/
public function registerAttempt(
string $methodIdentifier,
string $userIdentifier,
int $period,
): void {
$identifier = $this->hash($methodIdentifier, $userIdentifier);
$deleteAfter = $this->timeFactory->getDateTime()->add(new \DateInterval("PT{$period}S"));
$qb = $this->dbConnection->getQueryBuilder();
$qb->insert(self::TABLE_NAME)
->values([
'hash' => $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR),
'delete_after' => $qb->createNamedParameter($deleteAfter, IQueryBuilder::PARAM_DATE),
]);
if (!$this->config->getSystemValueBool('ratelimit.protection.enabled', true)) {
return;
}
$qb->executeStatement();
}
}
@@ -0,0 +1,60 @@
<?php
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/>.
*
*/
namespace OC\Security\RateLimiting\Backend;
/**
* Interface IBackend defines a storage backend for the rate limiting data. It
* should be noted that writing and reading rate limiting data is an expensive
* operation and one should thus make sure to only use sufficient fast backends.
*
* @package OC\Security\RateLimiting\Backend
*/
interface IBackend {
/**
* Gets the number of attempts for the specified method
*
* @param string $methodIdentifier Identifier for the method
* @param string $userIdentifier Identifier for the user
*/
public function getAttempts(
string $methodIdentifier,
string $userIdentifier,
): int;
/**
* Registers an attempt
*
* @param string $methodIdentifier Identifier for the method
* @param string $userIdentifier Identifier for the user
* @param int $period Period in seconds how long this attempt should be stored
*/
public function registerAttempt(
string $methodIdentifier,
string $userIdentifier,
int $period,
);
}
@@ -0,0 +1,126 @@
<?php
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/>.
*
*/
namespace OC\Security\RateLimiting\Backend;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IConfig;
/**
* Class MemoryCacheBackend uses the configured distributed memory cache for storing
* rate limiting data.
*
* @package OC\Security\RateLimiting\Backend
*/
class MemoryCacheBackend implements IBackend {
private ICache $cache;
public function __construct(
private IConfig $config,
ICacheFactory $cacheFactory,
private ITimeFactory $timeFactory,
) {
$this->cache = $cacheFactory->createDistributed(__CLASS__);
}
private function hash(
string $methodIdentifier,
string $userIdentifier,
): string {
return hash('sha512', $methodIdentifier . $userIdentifier);
}
private function getExistingAttempts(string $identifier): array {
$cachedAttempts = $this->cache->get($identifier);
if ($cachedAttempts === null) {
return [];
}
$cachedAttempts = json_decode($cachedAttempts, true);
if (\is_array($cachedAttempts)) {
return $cachedAttempts;
}
return [];
}
/**
* {@inheritDoc}
*/
public function getAttempts(
string $methodIdentifier,
string $userIdentifier,
): int {
$identifier = $this->hash($methodIdentifier, $userIdentifier);
$existingAttempts = $this->getExistingAttempts($identifier);
$count = 0;
$currentTime = $this->timeFactory->getTime();
foreach ($existingAttempts as $expirationTime) {
if ($expirationTime > $currentTime) {
$count++;
}
}
return $count;
}
/**
* {@inheritDoc}
*/
public function registerAttempt(
string $methodIdentifier,
string $userIdentifier,
int $period,
): void {
$identifier = $this->hash($methodIdentifier, $userIdentifier);
$existingAttempts = $this->getExistingAttempts($identifier);
$currentTime = $this->timeFactory->getTime();
// Unset all attempts that are already expired
foreach ($existingAttempts as $key => $expirationTime) {
if ($expirationTime < $currentTime) {
unset($existingAttempts[$key]);
}
}
$existingAttempts = array_values($existingAttempts);
// Store the new attempt
$existingAttempts[] = (string)($currentTime + $period);
if (!$this->config->getSystemValueBool('ratelimit.protection.enabled', true)) {
return;
}
$this->cache->set($identifier, json_encode($existingAttempts));
}
}
@@ -0,0 +1,36 @@
<?php
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/>.
*
*/
namespace OC\Security\RateLimiting\Exception;
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
use OCP\AppFramework\Http;
use OCP\Security\RateLimiting\IRateLimitExceededException;
class RateLimitExceededException extends SecurityException implements IRateLimitExceededException {
public function __construct() {
parent::__construct('Rate limit exceeded', Http::STATUS_TOO_MANY_REQUESTS);
}
}
@@ -0,0 +1,92 @@
<?php
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/>.
*
*/
namespace OC\Security\RateLimiting;
use OC\Security\Normalizer\IpAddress;
use OC\Security\RateLimiting\Backend\IBackend;
use OC\Security\RateLimiting\Exception\RateLimitExceededException;
use OCP\IUser;
use OCP\Security\RateLimiting\ILimiter;
class Limiter implements ILimiter {
public function __construct(
private IBackend $backend,
) {
}
/**
* @param int $period in seconds
* @throws RateLimitExceededException
*/
private function register(
string $methodIdentifier,
string $userIdentifier,
int $period,
int $limit,
): void {
$existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier);
if ($existingAttempts >= $limit) {
throw new RateLimitExceededException();
}
$this->backend->registerAttempt($methodIdentifier, $userIdentifier, $period);
}
/**
* Registers attempt for an anonymous request
*
* @param int $anonPeriod in seconds
* @throws RateLimitExceededException
*/
public function registerAnonRequest(
string $identifier,
int $anonLimit,
int $anonPeriod,
string $ip,
): void {
$ipSubnet = (new IpAddress($ip))->getSubnet();
$anonHashIdentifier = hash('sha512', 'anon::' . $identifier . $ipSubnet);
$this->register($identifier, $anonHashIdentifier, $anonPeriod, $anonLimit);
}
/**
* Registers attempt for an authenticated request
*
* @param int $userPeriod in seconds
* @throws RateLimitExceededException
*/
public function registerUserRequest(
string $identifier,
int $userLimit,
int $userPeriod,
IUser $user,
): void {
$userHashIdentifier = hash('sha512', 'user::' . $identifier . $user->getUID());
$this->register($identifier, $userHashIdentifier, $userPeriod, $userLimit);
}
}
@@ -0,0 +1,72 @@
<?php
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/>.
*/
namespace OC\Security;
use OC\Net\HostnameClassifier;
use OC\Net\IpAddressClassifier;
use OCP\IConfig;
use OCP\Security\IRemoteHostValidator;
use Psr\Log\LoggerInterface;
use function strtolower;
use function substr;
use function urldecode;
/**
* @internal
*/
final class RemoteHostValidator implements IRemoteHostValidator {
public function __construct(
private IConfig $config,
private HostnameClassifier $hostnameClassifier,
private IpAddressClassifier $ipAddressClassifier,
private LoggerInterface $logger,
) {
}
public function isValid(string $host): bool {
if ($this->config->getSystemValueBool('allow_local_remote_servers', false)) {
return true;
}
$host = idn_to_utf8(strtolower(urldecode($host)));
if ($host === false) {
return false;
}
// Remove brackets from IPv6 addresses
if (str_starts_with($host, '[') && str_ends_with($host, ']')) {
$host = substr($host, 1, -1);
}
if ($this->hostnameClassifier->isLocalHostname($host)
|| $this->ipAddressClassifier->isLocalAddress($host)) {
$this->logger->warning("Host $host was not connected to because it violates local access rules");
return false;
}
return true;
}
}
@@ -0,0 +1,67 @@
<?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/>
*
*/
namespace OC\Security;
use OCP\Security\ISecureRandom;
/**
* Class SecureRandom provides a wrapper around the random_int function to generate
* secure random strings. For PHP 7 the native CSPRNG is used, older versions do
* use a fallback.
*
* Usage:
* \OC::$server->getSecureRandom()->generate(10);
* @package OC\Security
*/
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.
* @throws \LengthException if an invalid length is requested
*/
public function generate(
int $length,
string $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
): string {
if ($length <= 0) {
throw new \LengthException('Invalid length specified: ' . $length . ' must be bigger than 0');
}
$maxCharIndex = \strlen($characters) - 1;
$randomString = '';
while ($length > 0) {
$randomNumber = \random_int(0, $maxCharIndex);
$randomString .= $characters[$randomNumber];
$length--;
}
return $randomString;
}
}
@@ -0,0 +1,112 @@
<?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/>
*
*/
namespace OC\Security;
use OC\AppFramework\Http\Request;
use OCP\IConfig;
use OCP\Security\ITrustedDomainHelper;
class TrustedDomainHelper implements ITrustedDomainHelper {
public function __construct(
private IConfig $config,
) {
}
/**
* Strips a potential port from a domain (in format domain:port)
* @return string $host without appended port
*/
private function getDomainWithoutPort(string $host): string {
$pos = strrpos($host, ':');
if ($pos !== false) {
$port = substr($host, $pos + 1);
if (is_numeric($port)) {
$host = substr($host, 0, $pos);
}
}
return $host;
}
/**
* {@inheritDoc}
*/
public function isTrustedUrl(string $url): bool {
$parsedUrl = parse_url($url);
if (empty($parsedUrl['host'])) {
return false;
}
if (isset($parsedUrl['port']) && $parsedUrl['port']) {
return $this->isTrustedDomain($parsedUrl['host'] . ':' . $parsedUrl['port']);
}
return $this->isTrustedDomain($parsedUrl['host']);
}
/**
* {@inheritDoc}
*/
public function isTrustedDomain(string $domainWithPort): bool {
// overwritehost is always trusted
if ($this->config->getSystemValue('overwritehost') !== '') {
return true;
}
$domain = $this->getDomainWithoutPort($domainWithPort);
// Read trusted domains from config
$trustedList = $this->config->getSystemValue('trusted_domains', []);
if (!is_array($trustedList)) {
return false;
}
// Always allow access from localhost
if (preg_match(Request::REGEX_LOCALHOST, $domain) === 1) {
return true;
}
// Reject malformed domains in any case
if (str_starts_with($domain, '-') || str_contains($domain, '..')) {
return false;
}
// Match, allowing for * wildcards
foreach ($trustedList as $trusted) {
if (gettype($trusted) !== 'string') {
break;
}
$regex = '/^' . implode('[-\.a-zA-Z0-9]*', array_map(function ($v) {
return preg_quote($v, '/');
}, explode('*', $trusted))) . '$/i';
if (preg_match($regex, $domain) || preg_match($regex, $domainWithPort)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,82 @@
<?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/>.
*
*/
namespace OC\Security\VerificationToken;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\Job;
use OCP\IConfig;
use OCP\IUserManager;
use OCP\Security\VerificationToken\InvalidTokenException;
use OCP\Security\VerificationToken\IVerificationToken;
class CleanUpJob extends Job {
protected ?int $runNotBefore = null;
protected ?string $userId = null;
protected ?string $subject = null;
protected ?string $pwdPrefix = null;
public function __construct(
ITimeFactory $time,
private IConfig $config,
private IVerificationToken $verificationToken,
private IUserManager $userManager,
) {
parent::__construct($time);
}
public function setArgument($argument): void {
parent::setArgument($argument);
$args = \json_decode($argument, true);
$this->userId = (string)$args['userId'];
$this->subject = (string)$args['subject'];
$this->pwdPrefix = (string)$args['pp'];
$this->runNotBefore = (int)$args['notBefore'];
}
protected function run($argument): void {
try {
$user = $this->userManager->get($this->userId);
if ($user === null) {
return;
}
$this->verificationToken->check('irrelevant', $user, $this->subject, $this->pwdPrefix);
} catch (InvalidTokenException $e) {
if ($e->getCode() === InvalidTokenException::TOKEN_EXPIRED) {
// make sure to only remove expired tokens
$this->config->deleteUserValue($this->userId, 'core', $this->subject);
}
}
}
public function start(IJobList $jobList): void {
if ($this->time->getTime() >= $this->runNotBefore) {
$jobList->remove($this, $this->argument);
parent::start($jobList);
}
}
}
@@ -0,0 +1,128 @@
<?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/>.
*
*/
namespace OC\Security\VerificationToken;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\IConfig;
use OCP\IUser;
use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;
use OCP\Security\VerificationToken\InvalidTokenException;
use OCP\Security\VerificationToken\IVerificationToken;
use function json_encode;
class VerificationToken implements IVerificationToken {
protected const TOKEN_LIFETIME = 60 * 60 * 24 * 7;
public function __construct(
private IConfig $config,
private ICrypto $crypto,
private ITimeFactory $timeFactory,
private ISecureRandom $secureRandom,
private IJobList $jobList
) {
}
/**
* @throws InvalidTokenException
*/
protected function throwInvalidTokenException(int $code): void {
throw new InvalidTokenException($code);
}
public function check(
string $token,
?IUser $user,
string $subject,
string $passwordPrefix = '',
bool $expiresWithLogin = false,
): void {
if ($user === null || !$user->isEnabled()) {
$this->throwInvalidTokenException(InvalidTokenException::USER_UNKNOWN);
}
$encryptedToken = $this->config->getUserValue($user->getUID(), 'core', $subject, null);
if ($encryptedToken === null) {
$this->throwInvalidTokenException(InvalidTokenException::TOKEN_NOT_FOUND);
}
try {
$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 {
$decryptedToken = $this->crypto->decrypt($encryptedToken, $passwordPrefix);
} catch (\Exception $e2) {
$this->throwInvalidTokenException(InvalidTokenException::TOKEN_DECRYPTION_ERROR);
}
}
$splitToken = explode(':', $decryptedToken);
if (count($splitToken) !== 2) {
$this->throwInvalidTokenException(InvalidTokenException::TOKEN_INVALID_FORMAT);
}
if ($splitToken[0] < ($this->timeFactory->getTime() - self::TOKEN_LIFETIME)
|| ($expiresWithLogin && $user->getLastLogin() > $splitToken[0])) {
$this->throwInvalidTokenException(InvalidTokenException::TOKEN_EXPIRED);
}
if (!hash_equals($splitToken[1], $token)) {
$this->throwInvalidTokenException(InvalidTokenException::TOKEN_MISMATCH);
}
}
public function create(
IUser $user,
string $subject,
string $passwordPrefix = '',
): string {
$token = $this->secureRandom->generate(
21,
ISecureRandom::CHAR_DIGITS.
ISecureRandom::CHAR_LOWER.
ISecureRandom::CHAR_UPPER
);
$tokenValue = $this->timeFactory->getTime() .':'. $token;
$encryptedValue = $this->crypto->encrypt($tokenValue, $passwordPrefix . $this->config->getSystemValueString('secret'));
$this->config->setUserValue($user->getUID(), 'core', $subject, $encryptedValue);
$jobArgs = json_encode([
'userId' => $user->getUID(),
'subject' => $subject,
'pp' => $passwordPrefix,
'notBefore' => $this->timeFactory->getTime() + self::TOKEN_LIFETIME * 2, // multiply to provide a grace period
]);
$this->jobList->add(CleanUpJob::class, $jobArgs);
return $token;
}
public function delete(string $token, IUser $user, string $subject): void {
$this->config->deleteUserValue($user->getUID(), 'core', $subject);
}
}