From e813f2a90e53dc834e48835b212c53a4be2bf356 Mon Sep 17 00:00:00 2001 From: flashwave Date: Sat, 29 Jul 2023 20:18:41 +0000 Subject: [PATCH] Some TOTP touch-ups. --- public-legacy/auth/login.php | 2 +- public-legacy/auth/twofactor.php | 5 +-- public-legacy/settings/account.php | 4 +-- src/TOTP.php | 39 ---------------------- src/TOTPGenerator.php | 52 ++++++++++++++++++++++++++++++ src/Users/User.php | 26 +++------------ templates/settings/account.twig | 5 +-- 7 files changed, 66 insertions(+), 67 deletions(-) delete mode 100644 src/TOTP.php create mode 100644 src/TOTPGenerator.php diff --git a/public-legacy/auth/login.php b/public-legacy/auth/login.php index b801533..89dea63 100644 --- a/public-legacy/auth/login.php +++ b/public-legacy/auth/login.php @@ -118,7 +118,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) { break; } - if($userInfo->hasTOTP()) { + if($userInfo->hasTOTPKey()) { $tfaToken = $msz->getTFASessions()->createToken($userInfo); url_redirect('auth-two-factor', [ 'token' => $tfaToken, diff --git a/public-legacy/auth/twofactor.php b/public-legacy/auth/twofactor.php index f45e72c..1e8e476 100644 --- a/public-legacy/auth/twofactor.php +++ b/public-legacy/auth/twofactor.php @@ -35,7 +35,7 @@ $userInfo = User::byId((int)$tokenUserId); // checking user_totp_key specifically because there's a fringe chance that // there's a token present, but totp is actually disabled -if(!$userInfo->hasTOTP()) { +if(!$userInfo->hasTOTPKey()) { url_redirect('auth-login'); return; } @@ -60,8 +60,9 @@ while(!empty($twofactor)) { } $clientInfo = ClientInfo::fromRequest(); + $totp = $userInfo->createTOTPGenerator(); - if(!in_array($twofactor['code'], $userInfo->getValidTOTPTokens())) { + if(!in_array($twofactor['code'], $totp->generateRange())) { $notices[] = sprintf( "Invalid two factor code, %d attempt%s remaining", $remainingAttempts - 1, diff --git a/public-legacy/settings/account.php b/public-legacy/settings/account.php index c54f6f8..8fc9834 100644 --- a/public-legacy/settings/account.php +++ b/public-legacy/settings/account.php @@ -45,9 +45,9 @@ if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) { } } -if($isVerifiedRequest && isset($_POST['tfa']['enable']) && $currentUser->hasTOTP() !== (bool)$_POST['tfa']['enable']) { +if($isVerifiedRequest && isset($_POST['tfa']['enable']) && $currentUser->hasTOTPKey() !== (bool)$_POST['tfa']['enable']) { if((bool)$_POST['tfa']['enable']) { - $tfaKey = TOTP::generateKey(); + $tfaKey = TOTPGenerator::generateKey(); $tfaIssuer = $cfg->getString('site.name', 'Misuzu'); $tfaQrcode = (new QRCode(new QROptions([ 'version' => 5, diff --git a/src/TOTP.php b/src/TOTP.php deleted file mode 100644 index 5ad08bf..0000000 --- a/src/TOTP.php +++ /dev/null @@ -1,39 +0,0 @@ -secretKey = $secretKey; - } - - public static function generateKey(): string { - return Base32::encode(random_bytes(16)); - } - - public static function timecode(?int $timestamp = null, int $interval = self::INTERVAL): int { - $timestamp = $timestamp ?? time(); - return (int)(($timestamp * 1000) / ($interval * 1000)); - } - - public function generate(?int $timestamp = null): string { - $hash = hash_hmac(self::HASH_ALGO, pack('J', self::timecode($timestamp)), Base32::decode($this->secretKey), true); - $offset = ord($hash[strlen($hash) - 1]) & 0x0F; - - $bin = 0; - $bin |= (ord($hash[$offset]) & 0x7F) << 24; - $bin |= (ord($hash[$offset + 1]) & 0xFF) << 16; - $bin |= (ord($hash[$offset + 2]) & 0xFF) << 8; - $bin |= (ord($hash[$offset + 3]) & 0xFF); - $otp = $bin % pow(10, self::DIGITS); - - return str_pad((string)$otp, self::DIGITS, '0', STR_PAD_LEFT); - } -} diff --git a/src/TOTPGenerator.php b/src/TOTPGenerator.php new file mode 100644 index 0000000..393f30c --- /dev/null +++ b/src/TOTPGenerator.php @@ -0,0 +1,52 @@ +secretKey), true); + $offset = ord($hash[strlen($hash) - 1]) & 0x0F; + + $bin = 0; + $bin |= (ord($hash[$offset]) & 0x7F) << 24; + $bin |= (ord($hash[$offset + 1]) & 0xFF) << 16; + $bin |= (ord($hash[$offset + 2]) & 0xFF) << 8; + $bin |= (ord($hash[$offset + 3]) & 0xFF); + $otp = $bin % pow(10, self::DIGITS); + + return str_pad((string)$otp, self::DIGITS, '0', STR_PAD_LEFT); + } + + public function generateRange(int $range = 1, ?int $timecode = null): array { + if($range < 1) + throw new InvalidArgumentException('$range must be greater than 0.'); + + $timecode ??= self::timecode(); + $tokens = [$this->generate($timecode)]; + + for($i = 1; $i <= $range; ++$i) + $tokens[] = $this->generate($timecode - $i); + for($i = 1; $i <= $range; ++$i) + $tokens[] = $this->generate($timecode + $i); + + return $tokens; + } +} diff --git a/src/Users/User.php b/src/Users/User.php index 4dd3127..1542b7a 100644 --- a/src/Users/User.php +++ b/src/Users/User.php @@ -11,7 +11,7 @@ use Misuzu\DateCheck; use Misuzu\DB; use Misuzu\Memoizer; use Misuzu\Pagination; -use Misuzu\TOTP; +use Misuzu\TOTPGenerator; use Misuzu\Parsers\Parser; use Misuzu\Users\Assets\UserAvatarAsset; use Misuzu\Users\Assets\UserBackgroundAsset; @@ -67,8 +67,6 @@ class User { private static $localUser = null; - private $totp = null; - private const QUERY_SELECT = 'SELECT %1$s FROM `msz_users`'; private const SELECT = '`user_id`, `username`, `password`, `email`, `user_super`, `user_title`' . ', `user_country`, `user_colour`, `display_role`, `user_totp_key`' @@ -177,37 +175,23 @@ class User { return $this->display_role < 1 ? -1 : $this->display_role; } - public function hasTOTP(): bool { - return !empty($this->user_totp_key); + public function createTOTPGenerator(): TOTPGenerator { + return new TOTPGenerator($this->user_totp_key); } - public function getTOTP(): TOTP { - if($this->totp === null) - $this->totp = new TOTP($this->user_totp_key); - return $this->totp; + public function hasTOTPKey(): bool { + return !empty($this->user_totp_key); } public function getTOTPKey(): string { return $this->user_totp_key ?? ''; } public function setTOTPKey(string $key): self { - $this->totp = null; $this->user_totp_key = $key; return $this; } public function removeTOTPKey(): self { - $this->totp = null; $this->user_totp_key = null; return $this; } - public function getValidTOTPTokens(): array { - if(!$this->hasTOTP()) - return []; - $totp = $this->getTOTP(); - return [ - $totp->generate(time()), - $totp->generate(time() - 30), - $totp->generate(time() + 30), - ]; - } public function hasProfileAbout(): bool { return !empty($this->user_about_content); diff --git a/templates/settings/account.twig b/templates/settings/account.twig index 5f33be8..1800a36 100644 --- a/templates/settings/account.twig +++ b/templates/settings/account.twig @@ -113,7 +113,8 @@ {{ input_csrf() }}
-

Secure your account by requiring a second step during log in in the form of a time based code. You can use applications like Authy, Google or Microsoft Authenticator or other compliant TOTP applications.

+

Secure your account by requiring a second step during log in in the form of a time based code.

+

You can use Authy (iOS / Android), KeePassXC paired with something like KeePassium, Google Authenticator (iOS / Android) or any other application with the ability to generate time-based codes.

@@ -127,7 +128,7 @@ {% endif %}
- {% if settings_user.hasTOTP %} + {% if settings_user.hasTOTPKey %}
Two Factor Authentication is enabled!