<?php namespace Core; readonly class TOTP { private const BASE32CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // RFC 4648 Base32 public function __construct(private string $secret) {} public static function otpSecret($uniqueString, $length = 20): string { // Уникальная строка + текущее время в микросекундах $seed = $uniqueString . microtime(true); // Хэшируем сид для более случайного распределения $seed = hash('sha256', $seed); $secret = ''; $max = strlen(self::BASE32CHARS) - 1; // Генерируем случайную строку нужной длины for ($i = 0; $i < $length; $i++) { $index = hexdec(substr($seed, $i, 2)) % $max; // Получаем индекс символа из хэшированного сида $secret .= self::BASE32CHARS[$index]; } return $secret; } public function generate(int $digits = 6, int $timeStep = 30): string { $time = floor(time() / $timeStep); $secretKey = $this->base32_decode($this->secret); $binaryTime = pack('N*', 0) . pack('N*', $time); $hash = hash_hmac('sha1', $binaryTime, $secretKey, true); $offset = ord($hash[strlen($hash) - 1]) & 0x0F; $otp = ( ((ord($hash[$offset]) & 0x7F) << 24) | ((ord($hash[$offset + 1]) & 0xFF) << 16) | ((ord($hash[$offset + 2]) & 0xFF) << 8) | (ord($hash[$offset + 3]) & 0xFF) ) % 10 ** $digits; return str_pad((string)$otp, $digits, '0', STR_PAD_LEFT); } private function base32_decode(string $input): string { $base32charsFlipped = array_flip(str_split(self::BASE32CHARS)); $output = ''; $v = 0; $vbits = 0; for ($i = 0, $j = strlen($input); $i < $j; $i++) { $v <<= 5; if ($input[$i] == '=') continue; $v += $base32charsFlipped[$input[$i]]; $vbits += 5; if ($vbits >= 8) { $vbits -= 8; $output .= chr(($v & (0xFF << $vbits)) >> $vbits); } } return $output; } }