Some TOTP touch-ups.

This commit is contained in:
flash 2023-07-29 20:18:41 +00:00
parent 0158333c90
commit e813f2a90e
7 changed files with 66 additions and 67 deletions

View file

@ -118,7 +118,7 @@ while(!empty($_POST['login']) && is_array($_POST['login'])) {
break; break;
} }
if($userInfo->hasTOTP()) { if($userInfo->hasTOTPKey()) {
$tfaToken = $msz->getTFASessions()->createToken($userInfo); $tfaToken = $msz->getTFASessions()->createToken($userInfo);
url_redirect('auth-two-factor', [ url_redirect('auth-two-factor', [
'token' => $tfaToken, 'token' => $tfaToken,

View file

@ -35,7 +35,7 @@ $userInfo = User::byId((int)$tokenUserId);
// checking user_totp_key specifically because there's a fringe chance that // checking user_totp_key specifically because there's a fringe chance that
// there's a token present, but totp is actually disabled // there's a token present, but totp is actually disabled
if(!$userInfo->hasTOTP()) { if(!$userInfo->hasTOTPKey()) {
url_redirect('auth-login'); url_redirect('auth-login');
return; return;
} }
@ -60,8 +60,9 @@ while(!empty($twofactor)) {
} }
$clientInfo = ClientInfo::fromRequest(); $clientInfo = ClientInfo::fromRequest();
$totp = $userInfo->createTOTPGenerator();
if(!in_array($twofactor['code'], $userInfo->getValidTOTPTokens())) { if(!in_array($twofactor['code'], $totp->generateRange())) {
$notices[] = sprintf( $notices[] = sprintf(
"Invalid two factor code, %d attempt%s remaining", "Invalid two factor code, %d attempt%s remaining",
$remainingAttempts - 1, $remainingAttempts - 1,

View file

@ -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']) { if((bool)$_POST['tfa']['enable']) {
$tfaKey = TOTP::generateKey(); $tfaKey = TOTPGenerator::generateKey();
$tfaIssuer = $cfg->getString('site.name', 'Misuzu'); $tfaIssuer = $cfg->getString('site.name', 'Misuzu');
$tfaQrcode = (new QRCode(new QROptions([ $tfaQrcode = (new QRCode(new QROptions([
'version' => 5, 'version' => 5,

View file

@ -1,39 +0,0 @@
<?php
namespace Misuzu;
use Index\Serialisation\Base32;
class TOTP {
public const DIGITS = 6;
public const INTERVAL = 30;
public const HASH_ALGO = 'sha1';
private $secretKey;
public function __construct(string $secretKey) {
$this->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);
}
}

52
src/TOTPGenerator.php Normal file
View file

@ -0,0 +1,52 @@
<?php
namespace Misuzu;
use InvalidArgumentException;
use Index\Serialisation\Base32;
class TOTPGenerator {
public const DIGITS = 6;
public const INTERVAL = 30000;
public function __construct(private string $secretKey) {}
public static function generateKey(): string {
return Base32::encode(random_bytes(16));
}
public static function timecode(?int $timestamp = null): int {
$timestamp ??= time();
return (int)(($timestamp * 1000) / self::INTERVAL);
}
public function generate(?int $timecode = null): string {
$timecode ??= self::timecode();
$hash = hash_hmac('sha1', pack('J', $timecode), 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);
}
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;
}
}

View file

@ -11,7 +11,7 @@ use Misuzu\DateCheck;
use Misuzu\DB; use Misuzu\DB;
use Misuzu\Memoizer; use Misuzu\Memoizer;
use Misuzu\Pagination; use Misuzu\Pagination;
use Misuzu\TOTP; use Misuzu\TOTPGenerator;
use Misuzu\Parsers\Parser; use Misuzu\Parsers\Parser;
use Misuzu\Users\Assets\UserAvatarAsset; use Misuzu\Users\Assets\UserAvatarAsset;
use Misuzu\Users\Assets\UserBackgroundAsset; use Misuzu\Users\Assets\UserBackgroundAsset;
@ -67,8 +67,6 @@ class User {
private static $localUser = null; private static $localUser = null;
private $totp = null;
private const QUERY_SELECT = 'SELECT %1$s FROM `msz_users`'; private const QUERY_SELECT = 'SELECT %1$s FROM `msz_users`';
private const SELECT = '`user_id`, `username`, `password`, `email`, `user_super`, `user_title`' private const SELECT = '`user_id`, `username`, `password`, `email`, `user_super`, `user_title`'
. ', `user_country`, `user_colour`, `display_role`, `user_totp_key`' . ', `user_country`, `user_colour`, `display_role`, `user_totp_key`'
@ -177,37 +175,23 @@ class User {
return $this->display_role < 1 ? -1 : $this->display_role; return $this->display_role < 1 ? -1 : $this->display_role;
} }
public function hasTOTP(): bool { public function createTOTPGenerator(): TOTPGenerator {
return !empty($this->user_totp_key); return new TOTPGenerator($this->user_totp_key);
} }
public function getTOTP(): TOTP { public function hasTOTPKey(): bool {
if($this->totp === null) return !empty($this->user_totp_key);
$this->totp = new TOTP($this->user_totp_key);
return $this->totp;
} }
public function getTOTPKey(): string { public function getTOTPKey(): string {
return $this->user_totp_key ?? ''; return $this->user_totp_key ?? '';
} }
public function setTOTPKey(string $key): self { public function setTOTPKey(string $key): self {
$this->totp = null;
$this->user_totp_key = $key; $this->user_totp_key = $key;
return $this; return $this;
} }
public function removeTOTPKey(): self { public function removeTOTPKey(): self {
$this->totp = null;
$this->user_totp_key = null; $this->user_totp_key = null;
return $this; 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 { public function hasProfileAbout(): bool {
return !empty($this->user_about_content); return !empty($this->user_about_content);

View file

@ -113,7 +113,8 @@
{{ input_csrf() }} {{ input_csrf() }}
<div class="settings__description"> <div class="settings__description">
<p>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.</p> <p>Secure your account by requiring a second step during log in in the form of a time based code.</p>
<p>You can use <a href="https://authy.com/" target="_blank" rel="noopener" class="link">Authy</a> (<a href="https://apps.apple.com/us/app/authy/id494168017" target="_blank" rel="noopener" class="link">iOS</a> / <a href="https://play.google.com/store/apps/details?id=com.authy.authy" target="_blank" rel="noopener" class="link">Android</a>), <a href="https://keepassxc.org/" target="_blank" rel="noopener" class="link">KeePassXC</a> paired with something like <a href="https://keepassium.com/" target="_blank" rel="noopener" class="link">KeePassium</a>, Google Authenticator (<a href="https://apps.apple.com/us/app/google-authenticator/id388497605" target="_blank" rel="noopener" class="link">iOS</a> / <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank" rel="noopener" class="link">Android</a>) or any other application with the ability to generate time-based codes.</p>
</div> </div>
<div class="settings__two-factor"> <div class="settings__two-factor">
@ -127,7 +128,7 @@
{% endif %} {% endif %}
<div class="settings__two-factor__settings"> <div class="settings__two-factor__settings">
{% if settings_user.hasTOTP %} {% if settings_user.hasTOTPKey %}
<div class="settings__two-factor__settings__status"> <div class="settings__two-factor__settings__status">
<i class="fas fa-lock fa-fw"></i> Two Factor Authentication is enabled! <i class="fas fa-lock fa-fw"></i> Two Factor Authentication is enabled!
</div> </div>