TOTP/TOTP.php
2024-05-09 14:02:54 +00:00

93 lines
4.2 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
namespace Core;
readonly class TOTP
{
private const BASE32CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // RFC 4648 Base32
/**
* Конструктор для инициализации объекта TOTP с секретным ключом.
*
* @param string $secret Секретный ключ в кодировке Base32.
*/
public function __construct(private string $secret) {}
/**
* Генерация случайного секретного ключа OTP заданной длины.
*
* @param string $uniqueString Уникальная строка для создания сида.
* @param int $length Длина секретного ключа (по умолчанию: 20).
* @return string Сгенерированный секретный ключ OTP.
*/
public static function generateSecret($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;
}
/**
* Генерация одноразового пароля (OTP) на основе времени с использованием секретного ключа.
*
* @param int $digits Количество цифр в OTP (по умолчанию: 6).
* @param int $timeStep Временной шаг в секундах (по умолчанию: 30).
* @return string Сгенерированный OTP.
*/
public function generate(int $digits = 6, int $timeStep = 30): string
{
$time = floor(time() / $timeStep); // Текущее время, разделенное на временной шаг
$secretKey = $this->base32decode($this->secret); // Декодируем секретный ключ из Base32
$binaryTime = pack('N*', 0) . pack('N*', $time); // Упаковываем время в бинарный формат (big endian)
$hash = hash_hmac('sha1', $binaryTime, $secretKey, true); // Вычисляем HMAC-SHA1 хеш
$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; // Вычисляем значение OTP
return str_pad((string)$otp, $digits, '0', STR_PAD_LEFT); // Форматируем OTP до указанной длины
}
/**
* Декодирование строки, закодированной в Base32.
*
* @param string $input Входная строка, закодированная в Base32.
* @return string Декодированная бинарная строка.
*/
private function base32decode(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]]; // Декодируем символ Base32
$vbits += 5;
if ($vbits >= 8) {
$vbits -= 8;
$output .= chr(($v & (0xFF << $vbits)) >> $vbits); // Добавляем декодированный байт
}
}
return $output;
}
}