Some TOTP touch-ups.
This commit is contained in:
parent
0158333c90
commit
e813f2a90e
7 changed files with 66 additions and 67 deletions
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
39
src/TOTP.php
39
src/TOTP.php
|
@ -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
52
src/TOTPGenerator.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -113,7 +113,8 @@
|
|||
{{ input_csrf() }}
|
||||
|
||||
<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 class="settings__two-factor">
|
||||
|
@ -127,7 +128,7 @@
|
|||
{% endif %}
|
||||
|
||||
<div class="settings__two-factor__settings">
|
||||
{% if settings_user.hasTOTP %}
|
||||
{% if settings_user.hasTOTPKey %}
|
||||
<div class="settings__two-factor__settings__status">
|
||||
<i class="fas fa-lock fa-fw"></i> Two Factor Authentication is enabled!
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue