Files
gravity/บริหารจัดการคิวนวดแผนไทย/app/Services/TwoFactorService.php
T
2026-09-16 23:20:08 +07:00

108 lines
3.2 KiB
PHP

<?php
namespace App\Services;
/**
* Class TwoFactorService
* Enterprise 2FA TOTP RFC 6238 Service (Google Authenticator Compatible)
*
* @package App\Services
*/
class TwoFactorService
{
private static array $base32Chars = [
'A'=>0,'B'=>1,'C'=>2,'D'=>3,'E'=>4,'F'=>5,'G'=>6,'H'=>7,
'I'=>8,'J'=>9,'K'=>10,'L'=>11,'M'=>12,'N'=>13,'O'=>14,'P'=>15,
'Q'=>16,'R'=>17,'S'=>18,'T'=>19,'U'=>20,'V'=>21,'W'=>22,'X'=>23,
'Y'=>24,'Z'=>25,'2'=>26,'3'=>27,'4'=>28,'5'=>29,'6'=>30,'7'=>31
];
/**
* Generate 16-character Base32 secret key
*/
public static function generateSecret(): string
{
$chars = array_keys(self::$base32Chars);
$secret = '';
for ($i = 0; $i < 16; $i++) {
$secret .= $chars[random_int(0, 31)];
}
return $secret;
}
/**
* Generate otpauth:// URI for Google Authenticator QR Code
*/
public static function getQrCodeUrl(string $issuer, string $accountName, string $secret): string
{
$encodedIssuer = urlencode($issuer);
$encodedAccount = urlencode($accountName);
return "otpauth://totp/{$encodedIssuer}:{$encodedAccount}?secret={$secret}&issuer={$encodedIssuer}&algorithm=SHA1&digits=6&period=30";
}
/**
* Verify 6-digit TOTP Code against Secret (RFC 6238)
*/
public static function verifyCode(string $secret, string $code, int $window = 1): bool
{
if (strlen($code) !== 6 || !is_numeric($code)) return false;
$timestamp = time();
$timeSlice = (int)floor($timestamp / 30);
for ($i = -$window; $i <= $window; $i++) {
$calculated = self::calculateCode($secret, $timeSlice + $i);
if (hash_equals($calculated, str_pad($code, 6, '0', STR_PAD_LEFT))) {
return true;
}
}
return false;
}
/**
* Calculate 6-digit TOTP for a specific time slice
*/
private static function calculateCode(string $secret, int $timeSlice): string
{
$secretKey = self::base32Decode(strtoupper($secret));
// Pack time slice into 64-bit big-endian binary string
$timeBytes = pack('N2', 0, $timeSlice);
$hash = hash_hmac('sha1', $timeBytes, $secretKey, true);
$offset = ord($hash[19]) & 0xf;
$otp = (
((ord($hash[$offset+0]) & 0x7f) << 24 ) |
((ord($hash[$offset+1]) & 0xff) << 16 ) |
((ord($hash[$offset+2]) & 0xff) << 8 ) |
(ord($hash[$offset+3]) & 0xff)
) % 1000000;
return str_pad((string)$otp, 6, '0', STR_PAD_LEFT);
}
private static function base32Decode(string $secret): string
{
$secret = strtoupper($secret);
$buffer = 0;
$bitsLeft = 0;
$result = '';
for ($i = 0; $i < strlen($secret); $i++) {
$char = $secret[$i];
if (!isset(self::$base32Chars[$char])) continue;
$buffer <<= 5;
$buffer |= self::$base32Chars[$char];
$bitsLeft += 5;
if ($bitsLeft >= 8) {
$bitsLeft -= 8;
$result .= chr(($buffer >> $bitsLeft) & 0xFF);
}
}
return $result;
}
}