💻 Прикладна криптографія · Інженерний аналіз

Алгоритм RFC 6238 TOTP: Повний математичний аналіз та реалізація на Python, JS, Go

2F Лабораторія досліджень безпеки 2FA-Tool · Оновлено: Жовтень 2026 · 16 хв читання (понад 2 700 слів)
💡 Архітектурний маніфест та короткий зміст

Серед фундаментальних криптографічних протоколів сучасної веб-безпеки RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) вирізняється своєю математичною лаконічністю. Він не потребує інтерактивного мережевого рукостискання, централізованої інфраструктури відкритих ключів (PKI) чи звернень до хмарних серверів. Маючи лише спільний секретний ключ Base32 та неперервний хід фізичного часу Unix, клієнт і сервер з математичною точністю обчислюють абсолютно однакові 6-значні токени кожні 30 секунд.

У цьому посібнику ми відмовимося від сторонніх готових бібліотек («чорних скриньок» на кшталт pyotp або otplib) та з нуля створимо повний криптографічний конвеєр RFC 6238 мовами Python, JavaScript та Go, підтвердивши кожну реалізацію офіційними тестовими векторами IETF.

01. Елегантність автентифікації на основі часу

Щоб зрозуміти TOTP, спочатку необхідно звернутися до його попередника: RFC 4226 HOTP (HMAC-Based One-Time Password).

В алгоритмі HOTP клієнт і сервер використовують спільний лічильник подій $C$. Щоразу, коли користувач генерує код, лічильник збільшується ($C = C + 1$), а одноразовий пароль обчислюється як $\text{HMAC-SHA-1}(K, C)$. Хоча математично HOTP є бездоганним, він мав суттєвий експлуатаційний недолік: розсинхронізацію лічильників. Якщо користувач випадково натискав кнопку на апаратному токені кілька разів у кишені, лічильник пристрою зміщувався вперед (наприклад, $C=6$), тоді як на сервері залишалося значення $C=1$, через що наступні спроби входу блокувалися.

⏱️ Прорив стандарту RFC 6238: заміна подій на час епохи Unix

Опублікований у травні 2011 року інженерами N. M'Raihi, S. Machani, M. Pei та J. Rydell, стандарт RFC 6238 вирішив проблему розсинхронізації шляхом заміни лічильника подій на стандартизований час епохи Unix. Оскільки фізичний час спливає однаково в усьому світі, клієнт і сервер оновлюють свої внутрішні стани одночасно без передачі жодного мережевого пакета.

02. 5 математичних кроків криптографічного алгоритму TOTP

Перетворення 16-символьного рядка Base32 (наприклад, JBSWY3DPEHPK3PXP) на 6-значний динамічний код (наприклад, 284915) складається з п'яти послідовних криптографічних операцій:

1 Крок 1: Декодування секретного ключа Base32

Секретні ключі кодуються за стандартом RFC 4648 Base32 з використанням символів A-Z та 2-7 (без візуально схожих знаків 0, 1, 8, 9). Алгоритм нормалізує рядок, видаляє пробіли, додає заповнення (символ =) до кратності 8 байтів та декодує рядок у бінарний масив байтів $K$.

2 Крок 2: Розрахунок кроку часу (лічильник $T$)

Поточний час Unix (кількість секунд, що минули з 1 січня 1970 року UTC) перетворюється на ціле число кроків $T$:

T = floor( (CurrentTime - T0) / X )

При стандартних параметрах $T_0 = 0$ та тривалості кроку $X = 30$ секунд. Отримане ціле число $T$ кодується як 8-байтне беззнакове 64-бітне ціле число в порядку від старшого до молодшого байта (Big-Endian).

3 Крок 3: Обчислення гешу HMAC-SHA1

Використовуючи байти ключа $K$, ми обчислюємо значення гешу HMAC-SHA1 від 8-байтного буфера часу, отримуючи 20-байтний (160-бітний) результат $HS$:

HS = HMAC-SHA-1(Key, T)
4 Крок 4: Динамічне усічення (Dynamic Truncation, DT)

Щоб перетворити 20 сирих байтів у компактне число, RFC 4226 використовує алгоритм динамічного усічення:

  • Визначення зміщення: Беремо останній байт $HS[19]$ і застосовуємо маску 0x0F, щоб вилучити молодші 4 біти — отримуємо число Offset від 0 до 15: Offset = HS[19] & 0x0F;
  • Вилучення 4 байтів: Починаючи з позиції Offset, беремо 4 послідовні байти: $P = HS[Offset \dots Offset+3]$;
  • Очищення знакового біта: Скидаємо старший біт маскою 0x7F для запобігання переповнення знакового цілого:
    BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
5 Крок 5: Редукція за модулем до $N$ цифр

Наостанок беремо залишок від ділення BinaryCode на $10^6$ (1 000 000) для отримання 6-значного коду та доповнюємо його нулями зліва за потреби:

OTP = BinaryCode mod 10^6

03. Робочий код 1: Стандартна бібліотека Python 3

Повна реалізація з використанням лише стандартних модулів Python base64, struct, hmac та hashlib без сторонніх пакетів:

totp_rfc6238.py Python 3.8+ (Без зовнішніх залежностей)
import base64
import hashlib
import hmac
import struct
import time

def generate_totp(secret_base32: str, timestamp: int = None, step: int = 30, digits: int = 6) -> str: """ Генерація коду TOTP згідно зі стандартом RFC 6238. :param secret_base32: Спільний секретний ключ Base32 (наприклад, ‘JBSWY3DPEHPK3PXP’) :param timestamp: Час Unix у секундах (за замовчуванням time.time()) :param step: Інтервал кроку в секундах (за замовчуванням 30) :param digits: Кількість цифр вихідного коду (за замовчуванням 6) :return: Рядок із 6 цифр (наприклад, ‘045812’) """ if timestamp is None: timestamp = int(time.time())

# 1. Нормалізація рядка Base32 та додавання заповнення '='
normalized_secret = secret_base32.replace(" ", "").upper()
padding_needed = (8 - len(normalized_secret) % 8) % 8
normalized_secret += "=" * padding_needed
key_bytes = base64.b32decode(normalized_secret)

# 2. Обчислення 64-бітного лічильника часу T (Big-Endian)
time_counter = timestamp // step
time_bytes = struct.pack(">Q", time_counter)

# 3. Розрахунок гешу HMAC-SHA1 (20 байтів)
hmac_digest = hmac.new(key_bytes, time_bytes, hashlib.sha1).digest()

# 4. Динамічне усічення (Dynamic Truncation)
offset = hmac_digest[-1] & 0x0F
binary_code = struct.unpack(">I", hmac_digest[offset : offset + 4])[0] & 0x7FFFFFFF

# 5. Редукція за модулем та доповнення нулями
otp_code = binary_code % (10 ** digits)
return str(otp_code).zfill(digits)

if name == “main”: test_secret = “JBSWY3DPEHPK3PXP” print(f”Згенерований код TOTP: {generate_totp(test_secret)}”)

04. Робочий код 2: JavaScript та Node.js Crypto

Чиста реалізація на JavaScript для серверного середовища Node.js за допомогою модуля crypto.createHmac:

totp_node.js Вбудований модуль Crypto в Node.js
const crypto = require('crypto');

// Декодер RFC 4648 Base32 на чистому JavaScript function base32Decode(base32Str) { const alphabet = ‘ABCDEFGHIJKLMNOPQRSTUVWXYZ234567’; const cleaned = base32Str.replace(/=+$/, ”).toUpperCase().replace(/\s+/g, ”); let bits = 0, value = 0, index = 0; const output = new Uint8Array(Math.floor((cleaned.length * 5) / 8));

for (let i = 0; i < cleaned.length; i++) { const val = alphabet.indexOf(cleaned[i]); if (val === -1) continue; value = (value << 5) | val; bits += 5; if (bits >= 8) { output[index++] = (value >>> (bits - 8)) & 255; bits -= 8; } } return Buffer.from(output.buffer, 0, index); }

function generateTOTP(secretBase32, timestamp = Math.floor(Date.now() / 1000), step = 30, digits = 6) { const key = base32Decode(secretBase32); const counter = Math.floor(timestamp / step);

// 8-байтний буфер у форматі Big-Endian const buffer = Buffer.alloc(8); buffer.writeBigUInt64BE(BigInt(counter), 0);

// Обчислення HMAC-SHA1 const hmac = crypto.createHmac(‘sha1’, key).update(buffer).digest();

// Динамічне усічення const offset = hmac[hmac.length - 1] & 0x0f; const binary = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff);

const otp = binary % (10 ** digits); return otp.toString().padStart(digits, ‘0’); }

console.log(“Розрахований код TOTP:”, generateTOTP(“JBSWY3DPEHPK3PXP”));

Клієнтське виконання у браузері через W3C Web Crypto API:

Для веб-додатків із повною автономністю (архітектура, на якій побудовано 2FA-Tool), інтерфейс W3C Web Crypto API забезпечує апаратно прискорені операції безпосередньо в браузері:

webcrypto_totp.js Стандартний W3C Web Crypto API
async function generateBrowserTOTP(secretBase32, timestamp = Math.floor(Date.now() / 1000)) {
  const keyBytes = base32Decode(secretBase32);
  const counter = Math.floor(timestamp / 30);

// 1. Імпорт ключа в SubtleCrypto const cryptoKey = await window.crypto.subtle.importKey( “raw”, keyBytes, { name: “HMAC”, hash: { name: “SHA-1” } }, false, [“sign”] );

// 2. Буфер лічильника Big-Endian на 8 байтів const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setBigUint64(0, BigInt(counter), false);

// 3. Криптографічний підпис HMAC const signature = await window.crypto.subtle.sign(“HMAC”, cryptoKey, buffer); const hmacResult = new Uint8Array(signature);

// 4. Динамічне усічення const offset = hmacResult[hmacResult.length - 1] & 0x0f; const binary = ((hmacResult[offset] & 0x7f) << 24) | ((hmacResult[offset + 1] & 0xff) << 16) | ((hmacResult[offset + 2] & 0xff) << 8) | (hmacResult[offset + 3] & 0xff);

const otp = binary % 1000000; return otp.toString().padStart(6, ‘0’); }

05. Робочий код 3: Промислова реалізація на Go

Для високопродуктивних мікросервісів найкраще підходить стандартна бібліотека Go:

totp.go Стандартна бібліотека Go 1.20+
package main

import ( “crypto/hmac” “crypto/sha1” “encoding/base32” “encoding/binary” “fmt” “math” “strings” “time” )

// GenerateTOTP обчислює токени стандарту RFC 6238 func GenerateTOTP(secretBase32 string, timestamp int64, step int64, digits int) (string, error) { // 1. Очищення та декодування Base32 cleaned := strings.ToUpper(strings.ReplaceAll(secretBase32, ” ”, "")) key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(cleaned) if err != nil { key, err = base32.StdEncoding.DecodeString(cleaned) if err != nil { return "", err } }

// 2. 64-бітний лічильник часу Big-Endian
counter := uint64(timestamp / step)
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, counter)

// 3. Обчислення HMAC-SHA1
mac := hmac.New(sha1.New, key)
mac.Write(buf)
hash := mac.Sum(nil)

// 4. Динамічне усічення
offset := hash[len(hash)-1] & 0x0f
binaryCode := binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7fffffff

// 5. Редукція за модулем
mod := uint32(math.Pow10(digits))
otp := binaryCode % mod

format := fmt.Sprintf("%%0%dd", digits)
return fmt.Sprintf(format, otp), nil

}

func main() { code, err := GenerateTOTP(“JBSWY3DPEHPK3PXP”, time.Now().Unix(), 30, 6) if err != nil { panic(err) } fmt.Printf(“Обчислений код TOTP у Go: %s\n”, code) }

06. Перевірка тестових векторів RFC 6238 Додаток B

Щоб підтвердити відповідність стандарту, кожна реалізація повинна перевірятися на офіційних тестових векторах, опублікованих у IETF RFC 6238 Додаток B.

Офіційний набір тестів використовує секретний ключ ASCII "12345678901234567890" (20 байтів), що в кодуванні Base32 має вигляд:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (для 8-значних кодів з кроком 30 секунд):

Часова мітка Unix Дата й час UTC Еталонний код RFC Результат тестування
59 1970-01-01 00:00:59 94287082 ✓ ПРОЙДЕНО (Точний збіг)
1111111109 2005-03-18 01:58:29 07081804 ✓ ПРОЙДЕНО (Точний збіг)
1111111111 2005-03-18 01:58:31 14050471 ✓ ПРОЙДЕНО (Точний збіг)
1234567890 2009-02-13 23:31:30 89005924 ✓ ПРОЙДЕНО (Точний збіг)
2000000000 2033-05-18 03:33:20 69279037 ✓ ПРОЙДЕНО (Точний збіг)

07. Промислова експлуатація: розбіжність годинників та захист від повторів

Під час впровадження перевірки TOTP у високонавантажених корпоративних системах необхідно враховувати три вимоги безпеки:

Правило 1 Вікно допустимої розбіжності часу (Clock Drift)

Через невеликі розбіжності годинників сервер повинен перевіряти діапазон [T - 1, T, T + 1] (попередні 30 с, поточні 30 с та наступні 30 с). Ніколи не розширюйте вікно більше ніж на $\pm 1$ крок, оскільки кожен зайвий інтервал збільшує час для атак перехоплення на 30 секунд.

Правило 2 Одноразовість використання токена (Anti-Replay)

Токен повинен анулюватися одразу після успішної перевірки. Зберігайте значення Last_Verified_Time_Step у Redis або швидкому кеші. Якщо клієнт намагається повторно надіслати той самий код у межах того ж 30-секундного вікна, запит відхиляється.

Правило 3 Обмеження спроб перебору та порівняння за сталий час

Оскільки 6-значний код має лише 1 000 000 можливих комбінацій, ендпоінти без лімітів можуть бути зламані перебором. Застосовуйте прогресивні затримки та блокування після 3 невдалих спроб. У коді здійснюйте порівняння за допомогою функцій сталого часу (наприклад, crypto/subtle.ConstantTimeCompare у Go або crypto.timingSafeEqual у Node.js).

08. Поширені запитання (FAQ)

П1: Оскільки SHA-1 вважається застарілим, чому TOTP досі використовує HMAC-SHA1? ▼

Теоретичні атаки на колізії в чистому SHA-1 не порушують безпеку конструкції HMAC (RFC 2104). У конструкції HMAC функція гешування запечатана внутрішніми та зовнішніми ключами, що повністю нівелює колізійні атаки. На сьогодні HMAC-SHA1 залишається повністю безпечним для 2FA. Крім того, RFC 6238 підтримує HMAC-SHA256 та HMAC-SHA512.

П2: Чи впливають часові пояси та перехід на літній час на синхронізацію TOTP? ▼

Ні. Час епохи Unix визначається як кількість секунд з 1970-01-01 00:00:00 UTC. Він є абсолютно незалежним від часових поясів та сезонних переводів годинників. У будь-який конкретний момент часу сервер у Токіо та смартфон у Києві мають абсолютно однакове ціле число секунди.

П3: Чим 5-значний автентифікатор Steam Guard відрізняється від стандарту RFC 6238? ▼

Steam Guard — це пропрієтарна модифікація RFC 6238. Вона використовує той самий 30-секундний крок часу та обчислення HMAC-SHA1, але на етапі редукції перетворює отримане число через власний 26-символьний буквено-цифровий масив (23456789BCDFGHJKMNPQRTVWXY) замість десяткових цифр.

Стандарт RFC 6238 · Обчислення на стороні клієнта через W3C Web Crypto API

Спробуйте генерацію кодів TOTP на 100% у своєму браузері

2FA-Tool реалізує цей точний криптографічний конвеєр безпосередньо у вашому браузері за допомогою W3C Web Crypto API. Нуль передач на сервер, нуль зберігання ключів, миттєва верифікація.