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,67 @@
<?php
declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Activity;
use InvalidArgumentException;
use OCP\Activity\IEvent;
use OCP\Activity\IProvider;
use OCP\IURLGenerator;
use OCP\L10N\IFactory as L10nFactory;
class Provider implements IProvider {
/** @var L10nFactory */
private $l10n;
/** @var IURLGenerator */
private $urlGenerator;
public function __construct(L10nFactory $l10n, IURLGenerator $urlGenerator) {
$this->urlGenerator = $urlGenerator;
$this->l10n = $l10n;
}
public function parse($language, IEvent $event, IEvent $previousEvent = null): IEvent {
if ($event->getApp() !== 'twofactor_totp') {
throw new InvalidArgumentException();
}
$l = $this->l10n->get('twofactor_totp', $language);
$event->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'actions/password.svg')));
switch ($event->getSubject()) {
case 'totp_enabled_subject':
$event->setSubject($l->t('You enabled TOTP two-factor authentication for your account'));
break;
case 'totp_disabled_subject':
$event->setSubject($l->t('You disabled TOTP two-factor authentication for your account'));
break;
case 'totp_disabled_by_admin':
$event->setSubject($l->t('TOTP two-factor authentication disabled by an admin'));
break;
}
return $event;
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Activity;
use OCP\Activity\ISetting;
use OCP\IL10N;
class Setting implements ISetting {
/** @var IL10N */
private $l10n;
public function __construct(IL10N $l10n) {
$this->l10n = $l10n;
}
public function canChangeMail(): bool {
return false;
}
public function canChangeStream(): bool {
return false;
}
public function getIdentifier(): string {
return 'twofactor_totp';
}
public function getName(): string {
return $this->l10n->t('TOTP (Authenticator app)');
}
public function getPriority(): int {
return 10;
}
public function isDefaultEnabledMail(): bool {
return true;
}
public function isDefaultEnabledStream(): bool {
return true;
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\AppInfo;
use OCA\TwoFactorTOTP\Event\DisabledByAdmin;
use OCA\TwoFactorTOTP\Event\StateChanged;
use OCA\TwoFactorTOTP\Listener\StateChangeActivity;
use OCA\TwoFactorTOTP\Listener\StateChangeRegistryUpdater;
use OCA\TwoFactorTOTP\Listener\UserDeleted;
use OCA\TwoFactorTOTP\Service\ITotp;
use OCA\TwoFactorTOTP\Service\Totp;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\User\Events\UserDeletedEvent;
class Application extends App implements IBootstrap {
public const APP_ID = 'twofactor_totp';
public function __construct() {
parent::__construct(self::APP_ID);
}
public function register(IRegistrationContext $context): void {
include_once __DIR__ . '/../../vendor/autoload.php';
$context->registerServiceAlias(ITotp::class, Totp::class);
$context->registerEventListener(StateChanged::class, StateChangeActivity::class);
$context->registerEventListener(StateChanged::class, StateChangeRegistryUpdater::class);
$context->registerEventListener(DisabledByAdmin::class, StateChangeActivity::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeleted::class);
}
public function boot(IBootContext $context): void {
}
}
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Daniel Kesselberg <mail@danielkesselberg.de>
*
* @author Daniel Kesselberg <mail@danielkesselberg.de>
*
* @license AGPL-3.0-or-later
*
* 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 OCA\TwoFactorTOTP\Command;
use OCA\TwoFactorTOTP\Db\TotpSecretMapper;
use OCP\DB\Exception;
use OCP\IDBConnection;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class CleanUp extends Command {
/** @var IDBConnection */
private $db;
/** @var IUserManager */
private $userManager;
/** @var TotpSecretMapper */
private $totpSecretMapper;
public function __construct(
IDBConnection $db,
IUserManager $userManager,
TotpSecretMapper $totpSecretMapper
) {
parent::__construct();
$this->db = $db;
$this->userManager = $userManager;
$this->totpSecretMapper = $totpSecretMapper;
}
protected function configure(): void {
$this
->setName('twofactor_totp:cleanup')
->setDescription('Remove orphaned totp secrets');
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$io = new SymfonyStyle($input, $output);
$io->title('Remove totp secrets for deleted users');
foreach ($this->findUserIds() as $userId) {
if ($this->userManager->userExists($userId) === false) {
try {
$io->text('Delete secret for uid "' . $userId . '"');
$this->totpSecretMapper->deleteSecretByUserId($userId);
} catch (Exception $e) {
$io->caution('Error deleting secret: ' . $e->getMessage());
}
}
}
$io->success('Orphaned totp secrets removed.');
$io->text('Thank you for using Two-Factor TOTP!');
return 0;
}
/**
* @throws Exception
*/
private function findUserIds(): array {
$userIds = [];
$qb = $this->db->getQueryBuilder()
->selectDistinct('user_id')
->from($this->totpSecretMapper->getTableName());
$result = $qb->executeQuery();
while ($row = $result->fetch()) {
$userIds[] = $row['user_id'];
}
$result->closeCursor();
return $userIds;
}
}
@@ -0,0 +1,134 @@
<?php
declare(strict_types = 1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Controller;
use InvalidArgumentException;
use OCA\TwoFactorTOTP\Service\ITotp;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Authentication\TwoFactorAuth\ALoginSetupController;
use OCP\Defaults;
use OCP\IRequest;
use OCP\IUserSession;
use RuntimeException;
use function is_null;
class SettingsController extends ALoginSetupController {
/** @var ITotp */
private $totp;
/** @var IUserSession */
private $userSession;
/** @var Defaults */
private $defaults;
public function __construct(string $appName, IRequest $request, IUserSession $userSession, ITotp $totp, Defaults $defaults) {
parent::__construct($appName, $request);
$this->userSession = $userSession;
$this->totp = $totp;
$this->defaults = $defaults;
}
/**
* @NoAdminRequired
* @return JSONResponse
*/
public function state(): JSONResponse {
$user = $this->userSession->getUser();
if (is_null($user)) {
throw new \Exception('user not available');
}
return new JSONResponse([
'state' => $this->totp->hasSecret($user) ? ITotp::STATE_ENABLED : ITotp::STATE_DISABLED,
]);
}
/**
* @NoAdminRequired
* @PasswordConfirmationRequired
*
* @param int $state
* @param string|null $code for verification
*/
public function enable(int $state, string $code = null): JSONResponse {
$user = $this->userSession->getUser();
if (is_null($user)) {
throw new \Exception('user not available');
}
switch ($state) {
case ITotp::STATE_DISABLED:
$this->totp->deleteSecret($user);
return new JSONResponse([
'state' => ITotp::STATE_DISABLED,
]);
case ITotp::STATE_CREATED:
$secret = $this->totp->createSecret($user);
$secretName = $this->getSecretName();
$issuer = $this->getSecretIssuer();
$qrUrl = "otpauth://totp/$secretName?secret=$secret&issuer=$issuer";
return new JSONResponse([
'state' => ITotp::STATE_CREATED,
'secret' => $secret,
'qrUrl' => $qrUrl,
]);
case ITotp::STATE_ENABLED:
if ($code === null) {
throw new InvalidArgumentException("code is missing");
}
$success = $this->totp->enable($user, $code);
return new JSONResponse([
'state' => $success ? ITotp::STATE_ENABLED : ITotp::STATE_CREATED,
]);
default:
throw new InvalidArgumentException('Invalid TOTP state');
}
}
/**
* The user's cloud id, e.g. "christina@university.domain/owncloud"
*
* @return string
*/
private function getSecretName(): string {
$productName = $this->defaults->getName();
$user = $this->userSession->getUser();
if ($user === null) {
throw new RuntimeException("No user in this context");
}
$userName = $user->getCloudId();
return rawurlencode("$productName:$userName");
}
/**
* The issuer, e.g. "Nextcloud"
*
* @return string
*/
private function getSecretIssuer(): string {
$productName = $this->defaults->getName();
return rawurlencode($productName);
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types = 1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Db;
use OCP\AppFramework\Db\Entity;
/**
* @method string getUserId()
* @method void setUserId(string $userId)
* @method string getSecret()
* @method void setSecret(string $secret)
* @method int getState()
* @method void setState(int $state)
* @method int getLastCounter();
* @method void setLastCounter(int $counter)
*/
class TotpSecret extends Entity {
/** @var string */
protected $userId;
/** @var string */
protected $secret;
/** @var int */
protected $state;
/** @var int */
protected $lastCounter;
public function __construct() {
$this->addType('userId', 'string');
$this->addType('secret', 'string');
$this->addType('state', 'int');
$this->addType('lastCounter', 'int');
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types = 1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Db;
use Doctrine\DBAL\Statement;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
/**
* @template-extends QBMapper<TotpSecret>
*/
class TotpSecretMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'twofactor_totp_secrets');
}
/**
* @param IUser $user
* @throws DoesNotExistException
* @return TotpSecret
*/
public function getSecret(IUser $user): TotpSecret {
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'user_id', 'secret', 'state', 'last_counter')
->from($this->getTableName())
->from('twofactor_totp_secrets')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID())));
/** @var Statement $result */
$result = $qb->execute();
$row = $result->fetch();
$result->closeCursor();
if ($row === false) {
throw new DoesNotExistException('Secret does not exist');
}
return TotpSecret::fromRow($row);
}
/**
* @param string $uid
* @throws Exception
*/
public function deleteSecretByUserId(string $uid): void {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->getTableName())
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($uid)));
$qb->executeStatement();
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Copyright (c) 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Event;
use OCP\IUser;
class DisabledByAdmin extends StateChanged {
public function __construct(IUser $user) {
parent::__construct($user, false);
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Copyright (c) 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Event;
use OCP\EventDispatcher\Event;
use OCP\IUser;
class StateChanged extends Event {
/** @var IUser */
private $user;
/** @var bool */
private $enabled;
public function __construct(IUser $user, bool $enabled) {
parent::__construct();
$this->user = $user;
$this->enabled = $enabled;
}
/**
* @return IUser
*/
public function getUser(): IUser {
return $this->user;
}
/**
* @return bool
*/
public function isEnabled(): bool {
return $this->enabled;
}
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types = 1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Exception;
use Exception;
class NoTotpSecretFoundException extends Exception {
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types = 1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Exception;
use Exception;
class TotpSecretAlreadySet extends Exception {
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Copyright (c) 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Listener;
use OCA\TwoFactorTOTP\Event\DisabledByAdmin;
use OCA\TwoFactorTOTP\Event\StateChanged;
use OCP\Activity\IManager as ActivityManager;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
/**
* @template-implements IEventListener<StateChanged>
*/
class StateChangeActivity implements IEventListener {
/** @var ActivityManager */
private $activityManager;
public function __construct(ActivityManager $activityManager) {
$this->activityManager = $activityManager;
}
public function handle(Event $event): void {
if ($event instanceof StateChanged) {
if ($event instanceof DisabledByAdmin) {
$subject = 'totp_disabled_by_admin';
} else {
$subject = $event->isEnabled() ? 'totp_enabled_subject' : 'totp_disabled_subject';
}
$user = $event->getUser();
$activity = $this->activityManager->generateEvent();
$activity->setApp('twofactor_totp')
->setType('security')
->setAuthor($user->getUID())
->setAffectedUser($user->getUID())
->setSubject($subject);
$this->activityManager->publish($activity);
}
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Copyright (c) 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Listener;
use OCA\TwoFactorTOTP\Event\StateChanged;
use OCA\TwoFactorTOTP\Provider\TotpProvider;
use OCP\Authentication\TwoFactorAuth\IRegistry;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
/**
* @template-implements IEventListener<StateChanged>
*/
class StateChangeRegistryUpdater implements IEventListener {
/** @var IRegistry */
private $registry;
/** @var TotpProvider */
private $provider;
public function __construct(IRegistry $registry, TotpProvider $provider) {
$this->registry = $registry;
$this->provider = $provider;
}
public function handle(Event $event): void {
if ($event instanceof StateChanged) {
if ($event->isEnabled()) {
$this->registry->enableProviderFor($this->provider, $event->getUser());
} else {
$this->registry->disableProviderFor($this->provider, $event->getUser());
}
}
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Daniel Kesselberg <mail@danielkesselberg.de>
*
* @author Daniel Kesselberg <mail@danielkesselberg.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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\TwoFactorTOTP\Listener;
use OCA\TwoFactorTOTP\Db\TotpSecretMapper;
use OCP\DB\Exception;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\User\Events\UserDeletedEvent;
use Psr\Log\LoggerInterface;
/**
* @template-implements IEventListener<UserDeletedEvent>
*/
class UserDeleted implements IEventListener {
/** @var TotpSecretMapper */
private $totpSecretMapper;
/** @var LoggerInterface */
private $logger;
public function __construct(TotpSecretMapper $totpSecretMapper, LoggerInterface $logger) {
$this->totpSecretMapper = $totpSecretMapper;
$this->logger = $logger;
}
public function handle(Event $event): void {
if ($event instanceof UserDeletedEvent) {
try {
$this->totpSecretMapper->deleteSecretByUserId($event->getUser()->getUID());
} catch (Exception $e) {
$this->logger->warning($e->getMessage(), ['uid' => $event->getUser()->getUID()]);
}
}
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 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 OCA\TwoFactorTOTP\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version010501Date20181018124436 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('twofactor_totp_secrets')) {
$table = $schema->createTable('twofactor_totp_secrets');
// TODO: use \OCP\DB\Types::INT
$table->addColumn('id', 'integer', [
'autoincrement' => true,
'notnull' => true,
'length' => 4,
]);
// TODO: use \OCP\DB\Types::STRING
$table->addColumn('user_id', 'string', [
'notnull' => true,
'length' => 64,
'default' => '',
]);
// TODO: use \OCP\DB\Types::TEXT
$table->addColumn('secret', 'text', [
'notnull' => true,
]);
// TODO: \OCP\DB\Types::INT
$table->addColumn('state', 'integer', [
'notnull' => true,
'default' => 2,
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['user_id'], 'totp_secrets_user_id');
}
return $schema;
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace OCA\TwoFactorTOTP\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version020102Date20190304124405 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*
* @return ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('twofactor_totp_secrets');
if (!$table->hasColumn('state')) {
// TODO: use \OCP\DB\Types::INT
$table->addColumn('state', 'integer', [
'notnull' => true,
'default' => 2,
]);
}
if (!$table->hasPrimaryKey()) {
$table->setPrimaryKey(['id']);
}
if (!$table->hasIndex('totp_secrets_user_id')) {
$table->addUniqueIndex(['user_id'], 'totp_secrets_user_id');
}
return $schema;
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace OCA\TwoFactorTOTP\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version030000Date20190305114917 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('twofactor_totp_secrets');
// TODO: use \OCP\DB\Types::BIGINT
$table->addColumn('last_counter', 'bigint', [
'notnull' => true,
'default' => -1,
]);
return $schema;
}
}
@@ -0,0 +1,47 @@
<?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 OCA\TwoFactorTOTP\Provider;
use OCA\TwoFactorTOTP\AppInfo\Application;
use OCP\Authentication\TwoFactorAuth\ILoginSetupProvider;
use OCP\IURLGenerator;
use OCP\Template;
class AtLoginProvider implements ILoginSetupProvider {
/** @var IURLGenerator */
private $urlGenerator;
public function __construct(IURLGenerator $urlGenerator) {
$this->urlGenerator = $urlGenerator;
}
public function getBody(): Template {
$template = new Template(Application::APP_ID, 'loginsetup');
$template->assign('urlGenerator', $this->urlGenerator);
return $template;
}
}
@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Provider;
use OCA\TwoFactorTOTP\AppInfo\Application;
use OCA\TwoFactorTOTP\Service\ITotp;
use OCA\TwoFactorTOTP\Settings\Personal;
use OCP\AppFramework\IAppContainer;
use OCP\AppFramework\Services\IInitialState;
use OCP\Authentication\TwoFactorAuth\IActivatableAtLogin;
use OCP\Authentication\TwoFactorAuth\IDeactivatableByAdmin;
use OCP\Authentication\TwoFactorAuth\ILoginSetupProvider;
use OCP\Authentication\TwoFactorAuth\IPersonalProviderSettings;
use OCP\Authentication\TwoFactorAuth\IProvider;
use OCP\Authentication\TwoFactorAuth\IProvidesIcons;
use OCP\Authentication\TwoFactorAuth\IProvidesPersonalSettings;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Template;
class TotpProvider implements IProvider, IProvidesIcons, IProvidesPersonalSettings, IDeactivatableByAdmin, IActivatableAtLogin {
/** @var ITotp */
private $totp;
/** @var IL10N */
private $l10n;
/** @var IAppContainer */
private $container;
/** @var IInitialState */
private $initialState;
/** @var IURLGenerator */
private $urlGenerator;
public function __construct(ITotp $totp,
IL10N $l10n,
IAppContainer $container,
IInitialState $initialStateService,
IURLGenerator $urlGenerator) {
$this->totp = $totp;
$this->l10n = $l10n;
$this->container = $container;
$this->initialState = $initialStateService;
$this->urlGenerator = $urlGenerator;
}
/**
* Get unique identifier of this 2FA provider
*/
public function getId(): string {
return 'totp';
}
/**
* Get the display name for selecting the 2FA provider
*/
public function getDisplayName(): string {
return 'TOTP (Authenticator app)';
}
/**
* Get the description for selecting the 2FA provider
*/
public function getDescription(): string {
return $this->l10n->t('Authenticate with a TOTP app');
}
/**
* Get the template for rending the 2FA provider view
*/
public function getTemplate(IUser $user): Template {
return new Template('twofactor_totp', 'challenge');
}
/**
* Verify the given challenge
*/
public function verifyChallenge(IUser $user, string $challenge): bool {
$challenge = preg_replace('/[^0-9]/', '', $challenge);
return $this->totp->validateSecret($user, $challenge);
}
/**
* Decides whether 2FA is enabled for the given user
*/
public function isTwoFactorAuthEnabledForUser(IUser $user): bool {
return $this->totp->hasSecret($user);
}
public function getLightIcon(): String {
return $this->urlGenerator->imagePath(Application::APP_ID, 'app.svg');
}
public function getDarkIcon(): String {
return $this->urlGenerator->imagePath(Application::APP_ID, 'app-dark.svg');
}
public function getPersonalSettings(IUser $user): IPersonalProviderSettings {
$this->initialState->provideInitialState('state', $this->totp->hasSecret($user) ? ITotp::STATE_ENABLED : ITotp::STATE_DISABLED);
return new Personal();
}
/**
* Disable this provider for the given user.
*
* @param IUser $user the user to deactivate this provider for
*/
public function disableFor(IUser $user) {
$this->totp->deleteSecret($user, true);
}
public function getLoginSetup(IUser $user): ILoginSetupProvider {
return $this->container->query(AtLoginProvider::class);
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types = 1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Service;
use OCA\TwoFactorTOTP\Exception\NoTotpSecretFoundException;
use OCA\TwoFactorTOTP\Exception\TotpSecretAlreadySet;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IUser;
interface ITotp {
public const STATE_DISABLED = 0;
public const STATE_CREATED = 1;
public const STATE_ENABLED = 2;
public function hasSecret(IUser $user): bool;
/**
* Create a new secret
*
* Note: the newly generated secret is disabled by default, because
* the user should once confirm that the OTP app was set up successfully.
*
* @param IUser $user
* @return string the newly created secret
* @throws TotpSecretAlreadySet
*/
public function createSecret(IUser $user): string;
/**
* Enable OTP for the given user. The secret has to be generated
* beforehand, using ITotp::createSecret
*
* @param IUser $user
* @param string $key for verification
* @return bool whether the key is valid and the secret has been enabled
* @throws DoesNotExistException
* @throws NoTotpSecretFoundException
*/
public function enable(IUser $user, $key): bool;
public function deleteSecret(IUser $user, bool $byAdmin = false): void;
public function validateSecret(IUser $user, string $key): bool;
}
@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Copyright (c) 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Service;
use Base32\Base32;
use EasyTOTP\Factory;
use EasyTOTP\TOTPValidResultInterface;
use OCA\TwoFactorTOTP\Db\TotpSecret;
use OCA\TwoFactorTOTP\Db\TotpSecretMapper;
use OCA\TwoFactorTOTP\Event\DisabledByAdmin;
use OCA\TwoFactorTOTP\Event\StateChanged;
use OCA\TwoFactorTOTP\Exception\NoTotpSecretFoundException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IUser;
use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;
class Totp implements ITotp {
/** @var TotpSecretMapper */
private $secretMapper;
/** @var ICrypto */
private $crypto;
/** @var IEventDispatcher */
private $eventDispatcher;
/** @var ISecureRandom */
private $random;
public function __construct(TotpSecretMapper $secretMapper,
ICrypto $crypto,
IEventDispatcher $eventDispatcher,
ISecureRandom $random) {
$this->secretMapper = $secretMapper;
$this->crypto = $crypto;
$this->eventDispatcher = $eventDispatcher;
$this->random = $random;
}
public function hasSecret(IUser $user): bool {
try {
$secret = $this->secretMapper->getSecret($user);
return ITotp::STATE_ENABLED === (int)$secret->getState();
} catch (DoesNotExistException $ex) {
return false;
}
}
private function generateSecret(): string {
return $this->random->generate(16, ISecureRandom::CHAR_UPPER.'234567');
}
/**
* @param IUser $user
*/
public function createSecret(IUser $user): string {
try {
// Delete existing one
$oldSecret = $this->secretMapper->getSecret($user);
$this->secretMapper->delete($oldSecret);
} catch (DoesNotExistException $ex) {
// Ignore
}
// Create new one
$secret = $this->generateSecret();
$dbSecret = new TotpSecret();
$dbSecret->setUserId($user->getUID());
$dbSecret->setSecret($this->crypto->encrypt($secret));
$dbSecret->setState(ITotp::STATE_CREATED);
$this->secretMapper->insert($dbSecret);
return $secret;
}
public function enable(IUser $user, $key): bool {
if (!$this->validateSecret($user, $key)) {
return false;
}
$dbSecret = $this->secretMapper->getSecret($user);
$dbSecret->setState(ITotp::STATE_ENABLED);
$this->secretMapper->update($dbSecret);
$this->eventDispatcher->dispatch(StateChanged::class, new StateChanged($user, true));
return true;
}
public function deleteSecret(IUser $user, bool $byAdmin = false): void {
try {
// TODO: execute DELETE sql in mapper instead
$dbSecret = $this->secretMapper->getSecret($user);
$this->secretMapper->delete($dbSecret);
} catch (DoesNotExistException $ex) {
// Ignore
}
if ($byAdmin) {
$this->eventDispatcher->dispatch(DisabledByAdmin::class, new DisabledByAdmin($user));
} else {
$this->eventDispatcher->dispatch(StateChanged::class, new StateChanged($user, false));
}
}
public function validateSecret(IUser $user, string $key): bool {
try {
$dbSecret = $this->secretMapper->getSecret($user);
} catch (DoesNotExistException $ex) {
throw new NoTotpSecretFoundException();
}
$secret = $this->crypto->decrypt($dbSecret->getSecret());
$otp = Factory::getTOTP(Base32::decode($secret), 30, 6);
$counter = null;
$lastCounter = $dbSecret->getLastCounter();
if ($lastCounter !== -1) {
$counter = $lastCounter;
}
$result = $otp->verify($key, 3, $counter);
if ($result instanceof TOTPValidResultInterface) {
$dbSecret->setLastCounter($result->getCounter());
$this->secretMapper->update($dbSecret);
return true;
}
return false;
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* Two-factor TOTP
*
* 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 OCA\TwoFactorTOTP\Settings;
use OCP\Authentication\TwoFactorAuth\IPersonalProviderSettings;
use OCP\Template;
class Personal implements IPersonalProviderSettings {
public function getBody(): Template {
return new Template('twofactor_totp', 'personal');
}
}