Files

129 lines
3.7 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\common\library;
use ba\Random;
use app\common\facade\Token;
use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\Providers\Qr\BaconQrCodeProvider;
/**
* 管理员谷歌验证器TOTP
*/
class AdminTotp
{
public const TOKEN_TYPE_BIND = 'admin-totp-bind';
public const TOKEN_TYPE_VERIFY = 'admin-totp-verify';
public const TOKEN_EXPIRE = 300;
private static ?TwoFactorAuth $tfa = null;
public static function isEnabled(): bool
{
return (bool) config('buildadmin.admin_totp_enable', true);
}
private static function tfa(): TwoFactorAuth
{
if (self::$tfa === null) {
$issuer = (string) get_sys_config('site_name');
if ($issuer === '') {
$issuer = 'BuildAdmin';
}
// 使用 SVG 格式,避免依赖 imagick 扩展PNG 默认需要 ImagickImageBackEnd
self::$tfa = new TwoFactorAuth(new BaconQrCodeProvider(format: 'svg'), $issuer);
}
return self::$tfa;
}
public static function isBound(?string $encryptedSecret): bool
{
return is_string($encryptedSecret) && $encryptedSecret !== '';
}
public static function generateSecret(): string
{
return self::tfa()->createSecret();
}
public static function getQrDataUri(string $label, string $secret): string
{
return self::tfa()->getQRCodeImageAsDataUri($label, $secret);
}
public static function verifyCode(string $plainSecret, string $code): bool
{
$code = trim($code);
if (!preg_match('/^\d{6}$/', $code)) {
return false;
}
return self::tfa()->verifyCode($plainSecret, $code);
}
public static function encryptSecret(string $plainSecret): string
{
$key = substr(hash('sha256', (string) config('buildadmin.token.key')), 0, 32);
$iv = random_bytes(16);
$encrypted = openssl_encrypt($plainSecret, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
if ($encrypted === false) {
return '';
}
return base64_encode($iv . $encrypted);
}
public static function decryptSecret(string $encryptedSecret): string
{
if ($encryptedSecret === '') {
return '';
}
$raw = base64_decode($encryptedSecret, true);
if ($raw === false || strlen($raw) < 17) {
return '';
}
$iv = substr($raw, 0, 16);
$encrypted = substr($raw, 16);
$key = substr(hash('sha256', (string) config('buildadmin.token.key')), 0, 32);
$plain = openssl_decrypt($encrypted, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
return $plain === false ? '' : $plain;
}
public static function verifyStoredCode(string $encryptedSecret, string $code): bool
{
$plain = self::decryptSecret($encryptedSecret);
if ($plain === '') {
return false;
}
return self::verifyCode($plain, $code);
}
public static function createPendingToken(int $adminId, string $type): string
{
$token = Random::uuid();
Token::set($token, $type, $adminId, self::TOKEN_EXPIRE);
return $token;
}
public static function resolvePendingToken(string $token, string $type): int
{
$token = trim($token);
if ($token === '') {
return 0;
}
$data = Token::get($token);
if (!$data || ($data['type'] ?? '') !== $type) {
return 0;
}
Token::tokenExpirationCheck($data);
return (int) ($data['user_id'] ?? 0);
}
public static function deletePendingToken(string $token): void
{
if ($token !== '') {
Token::delete($token);
}
}
}