Серед фундаментальних криптографічних протоколів сучасної веб-безпеки 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$, через що наступні спроби входу блокувалися.
Опублікований у травні 2011 року інженерами N. M'Raihi, S. Machani, M. Pei та J. Rydell, стандарт RFC 6238 вирішив проблему розсинхронізації шляхом заміни лічильника подій на стандартизований час епохи Unix. Оскільки фізичний час спливає однаково в усьому світі, клієнт і сервер оновлюють свої внутрішні стани одночасно без передачі жодного мережевого пакета.
02. 5 математичних кроків криптографічного алгоритму TOTP
Перетворення 16-символьного рядка Base32 (наприклад, JBSWY3DPEHPK3PXP) на 6-значний динамічний код (наприклад, 284915) складається з п'яти послідовних криптографічних операцій:
Секретні ключі кодуються за стандартом RFC 4648 Base32 з використанням символів A-Z та 2-7 (без візуально схожих знаків 0, 1, 8, 9). Алгоритм нормалізує рядок, видаляє пробіли, додає заповнення (символ =) до кратності 8 байтів та декодує рядок у бінарний масив байтів $K$.
Поточний час Unix (кількість секунд, що минули з 1 січня 1970 року UTC) перетворюється на ціле число кроків $T$:
При стандартних параметрах $T_0 = 0$ та тривалості кроку $X = 30$ секунд. Отримане ціле число $T$ кодується як 8-байтне беззнакове 64-бітне ціле число в порядку від старшого до молодшого байта (Big-Endian).
Використовуючи байти ключа $K$, ми обчислюємо значення гешу HMAC-SHA1 від 8-байтного буфера часу, отримуючи 20-байтний (160-бітний) результат $HS$:
Щоб перетворити 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)
Наостанок беремо залишок від ділення BinaryCode на $10^6$ (1 000 000) для отримання 6-значного коду та доповнюємо його нулями зліва за потреби:
03. Робочий код 1: Стандартна бібліотека Python 3
Повна реалізація з використанням лише стандартних модулів Python base64, struct, hmac та hashlib без сторонніх пакетів:
import base64 import hashlib import hmac import struct import timedef 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:
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 забезпечує апаратно прискорені операції безпосередньо в браузері:
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:
package mainimport ( “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 у високонавантажених корпоративних системах необхідно враховувати три вимоги безпеки:
Через невеликі розбіжності годинників сервер повинен перевіряти діапазон [T - 1, T, T + 1] (попередні 30 с, поточні 30 с та наступні 30 с). Ніколи не розширюйте вікно більше ніж на $\pm 1$ крок, оскільки кожен зайвий інтервал збільшує час для атак перехоплення на 30 секунд.
Токен повинен анулюватися одразу після успішної перевірки. Зберігайте значення Last_Verified_Time_Step у Redis або швидкому кеші. Якщо клієнт намагається повторно надіслати той самий код у межах того ж 30-секундного вікна, запит відхиляється.
Оскільки 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) замість десяткових цифр.
Спробуйте генерацію кодів TOTP на 100% у своєму браузері
2FA-Tool реалізує цей точний криптографічний конвеєр безпосередньо у вашому браузері за допомогою W3C Web Crypto API. Нуль передач на сервер, нуль зберігання ключів, миттєва верифікація.