diff --git a/TOTP.php b/TOTP.php index 556d457..9031315 100644 --- a/TOTP.php +++ b/TOTP.php @@ -1,17 +1,23 @@ decodedSecret = $this->base32decode($secret); // Декодируем секрет один раз при создании объекта + } /** * Генерация случайного секретного ключа OTP заданной длины. @@ -20,7 +26,7 @@ readonly class TOTP * @param int $length Длина секретного ключа (по умолчанию: 20). * @return string Сгенерированный секретный ключ OTP. */ - public static function generateSecret($uniqueString, $length = 20): string + public static function generateSecret(string $uniqueString, int $length = self::DEFAULT_SECRET_LENGTH): string { // Уникальная строка + текущее время в микросекундах $seed = $uniqueString . microtime(true); @@ -47,19 +53,55 @@ readonly class TOTP * @param int $timeStep Временной шаг в секундах (по умолчанию: 30). * @return string Сгенерированный OTP. */ - public function generate(int $digits = 6, int $timeStep = 30): string + public function generate(int $digits = self::DEFAULT_DIGITS, int $timeStep = self::DEFAULT_TIME_STEP): 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 хеш + $timeCounter = floor(time() / $timeStep); // Текущее время, разделенное на временной шаг + return $this->computeOtp($timeCounter, $digits); // Вычисляем OTP + } + + /** + * Проверка одноразового пароля (OTP) с учётом временного окна. + * + * @param string $otp Введённый пользователем OTP. + * @param int $digits Количество цифр в OTP (по умолчанию: 6). + * @param int $timeStep Временной шаг в секундах (по умолчанию: 30). + * @param int $window Количество интервалов до и после текущего для проверки (по умолчанию: 1). + * @return bool Возвращает true, если OTP валиден в указанном временном окне. + */ + public function verify(string $otp, int $digits = self::DEFAULT_DIGITS, int $timeStep = self::DEFAULT_TIME_STEP, int $window = 1): bool + { + $currentTimeCounter = floor(time() / $timeStep); // Текущий временной счётчик + + // Проверяем OTP для текущего времени и соседних интервалов (±window) + for ($i = -$window; $i <= $window; $i++) { + $timeCounter = $currentTimeCounter + $i; + $generatedOtp = $this->computeOtp($timeCounter, $digits); // Вычисляем OTP + if ($otp === $generatedOtp) { + return true; // OTP совпал + } + } + + return false; // OTP не совпал ни в одном интервале + } + + /** + * Вычисление OTP для заданного временного счётчика. + * + * @param int $timeCounter Временной счётчик (время, разделённое на timeStep). + * @param int $digits Количество цифр в OTP. + * @return string Сгенерированный OTP. + */ + private function computeOtp(int $timeCounter, int $digits): string + { + $binaryTime = pack('N*', 0) . pack('N*', $timeCounter); // Упаковываем время в бинарный формат (big endian) + $hash = hash_hmac('sha1', $binaryTime, $this->decodedSecret, 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 + ) % 10 ** $digits; // Вычисляем значение OTP return str_pad((string)$otp, $digits, '0', STR_PAD_LEFT); // Форматируем OTP до указанной длины } @@ -78,7 +120,7 @@ readonly class TOTP for ($i = 0, $j = strlen($input); $i < $j; $i++) { $v <<= 5; - if ($input[$i] == '=') continue; // Пропускаем символы заполнения + if ($input[$i] === '=') {continue;} // Пропускаем символы заполнения $v += $base32charsFlipped[$input[$i]]; // Декодируем символ Base32 $vbits += 5;