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;
|
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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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\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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue