💻 Приложна криптография · Инженерен анализ

Как работи TOTP: Имплементация на алгоритъма RFC 6238 в Python, JavaScript и Go

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

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

В това подробно ръководство заобикаляме готовите библиотеки от трети страни (като 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: Замяна на брояча на събития с времето от епохата

Публикуван през май 2011 г. от N. M'Raihi, S. Machani, M. Pei и J. Rydell, RFC 6238 реши проблема с десинхронизацията на брояча, като замени нарастващото цяло число със стандартизирано Unix epoch време. Тъй като физическото време напредва равномерно по целия свят, и клиентът, и сървърът придвижват своите вътрешни броячи едновременно, без да изпращат нито един мрежов пакет.

02. Петте криптографски математически стъпки на 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-байтовия Big-Endian буфер на времето, което дава 20-байтов (160-битов) хеш за автентикация на съобщението $HS$:

HS = HMAC-SHA-1(Key, T)
4 Стъпка 4: Динамично съкращаване (Dynamic Truncation - DT)

За преобразуване на 20-те сурови байта в цяло число, RFC 4226 определя алгоритъма Dynamic Truncation:

  • Извличане на отместването (Offset): Проверява се последният байт $HS[19]$ и се маскира с 0x0F за извличане на младшите 4 бита, което дава отместване между 0 и 15: Offset = HS[19] & 0x0F;
  • Отрязване на 4 байта: Извличат се 4 последователни байта, започвайки от индекс Offset: $P = HS[Offset \dots Offset+3]$;
  • Маскиране на знаковия бит: Най-старшият бит (MSB) се маскира с 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-значни токени. Резултатът се допълва отляво с нули за получаване на точен 6-значен низ:

OTP = BinaryCode mod 10^6

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

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

totp_rfc6238.py Python 3.8+ (Zero Dependencies)
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: """ Generate an RFC 6238 compliant TOTP token. :param secret_base32: Shared secret key in Base32 format (e.g., ‘JBSWY3DPEHPK3PXP’) :param timestamp: Unix epoch timestamp in seconds. Defaults to time.time() :param step: Time-step window in seconds (default: 30) :param digits: Number of output digits (default: 6) :return: Left-padded OTP string (e.g., ‘045812’) """ if timestamp is None: timestamp = int(time.time())

# 1. Normalize Base32 string and append '=' padding characters
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. Compute 64-bit big-endian time-step counter T
time_counter = timestamp // step
time_bytes = struct.pack(">Q", time_counter)  # 8 bytes big-endian unsigned long long

# 3. Calculate HMAC-SHA1 digest (20 bytes)
hmac_digest = hmac.new(key_bytes, time_bytes, hashlib.sha1).digest()

# 4. Perform Dynamic Truncation
offset = hmac_digest[-1] & 0x0F  # Lower 4 bits of the 20th byte
# Unpack 4 bytes at offset and clear MSB sign bit
binary_code = struct.unpack(">I", hmac_digest[offset : offset + 4])[0] & 0x7FFFFFFF

# 5. Modulo arithmetic and zero-fill formatting
otp_code = binary_code % (10 ** digits)
return str(otp_code).zfill(digits)

if name == “main”: test_secret = “JBSWY3DPEHPK3PXP” print(f”Generated TOTP Code: {generate_totp(test_secret)}”)

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

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

totp_node.js Node.js Native Crypto
const crypto = require('crypto');

// RFC 4648 Base32 Decoder in Pure 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);

// Allocate 8-byte buffer in Big-Endian format const buffer = Buffer.alloc(8); buffer.writeBigUInt64BE(BigInt(counter), 0);

// Compute HMAC-SHA1 const hmac = crypto.createHmac(‘sha1’, key).update(buffer).digest();

// Dynamic Truncation 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(“Calculated TOTP:”, generateTOTP(“JBSWY3DPEHPK3PXP”));

Изпълнение от страна на клиента в браузъра чрез W3C Web Crypto API:

За съвременни уеб приложения, изискващи 100% офлайн сигурност (точната архитектура, върху която работи 2FA-Tool), W3C Web Crypto API предоставя асинхронни, хардуерно ускорени криптографски примитиви директно в пясъчника на браузъра без npm пакети от трети страни:

webcrypto_totp.js Browser 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. Import raw secret key into SubtleCrypto const cryptoKey = await window.crypto.subtle.importKey( “raw”, keyBytes, { name: “HMAC”, hash: { name: “SHA-1” } }, false, [“sign”] );

// 2. Encode 8-byte Big-Endian counter buffer const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setBigUint64(0, BigInt(counter), false); // false = Big-Endian

// 3. Perform cryptographic HMAC sign const signature = await window.crypto.subtle.sign(“HMAC”, cryptoKey, buffer); const hmacResult = new Uint8Array(signature);

// 4. Dynamic Truncation 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 (Golang) за продукционна среда

Високопроизводителните микроуслуги се възползват от изпитаната стандартна библиотека на Go:

totp.go Go 1.20+ Standard Library
package main

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

// GenerateTOTP computes standard RFC 6238 time-based tokens func GenerateTOTP(secretBase32 string, timestamp int64, step int64, digits int) (string, error) { // 1. Sanitize Base32 seed and decode 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. Encode 64-bit Big-Endian time-step counter
counter := uint64(timestamp / step)
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, counter)

// 3. Compute HMAC-SHA1
mac := hmac.New(sha1.New, key)
mac.Write(buf)
hash := mac.Sum(nil)

// 4. Dynamic Truncation
offset := hash[len(hash)-1] & 0x0f
binaryCode := binary.BigEndian.Uint32(hash[offset:offset+4]) & 0x7fffffff

// 5. Modulo arithmetic
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(“Golang Computed TOTP: %s\n”, code) }

06. Проверка с официалните тестови вектори от RFC 6238 (Апендикс B)

За доказване на съответствие със стандарта всяка криптографска реализация трябва да бъде верифицирана срещу официалните тестови вектори, публикувани в IETF RFC 6238 Апендикс B.

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

Unix Epoch таймстамп 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. Продукционно инженерство: Дрейф, Replay и лимити

При внедряване на TOTP верификация във високодостъпни корпоративни системи за автентикация трябва да се съблюдават три ключови изисквания за сигурност:

Правило 1 Прозорец за толеранс към времеви дрейф

Поради минимални неточности в часовниците на мобилните устройства сървърите трябва да проверяват прозореца [T - 1, T, T + 1] (предходните 30 сек, текущите 30 сек и следващите 30 сек). Никога не разширявайте прозореца над $\pm 1$ стъпка, тъй като всяка допълнителна стъпка увеличава повърхността за атаки с преиграване с още 30 секунди.

Правило 2 Задължителна еднократна употреба на токена (Anti-Replay)

Токенът трябва да бъде обезсилен незабавно след успешно използване. Записвайте Last_Verified_Time_Step в Redis или кеш памет. Ако клиент се опита да изпрати същия токен в рамките на същия 30-секунден интервал, отхвърлете заявката, за да блокирате Man-in-the-Middle атаки с преиграване.

Правило 3 Ограничаване на опитите (Rate Limiting) и сравнение с постоянно време

Тъй като 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: Влияят ли часовите зони или лятното часово време (DST) върху синхронизацията на TOTP? ▼

Не. Времето на Unix епохата е дефинирано като изминалите секунди от 1970-01-01 00:00:00 UTC. То е напълно независимо от часовите зони, високосните секунди и сезонните смени на часовото време. Във всеки точен момент сървър в Токио и смартфон в София или Калифорния имат абсолютно еднакъв целочислен таймстамп.

В3: По какво се различава 5-буквената автентикация на Steam Guard от RFC 6238? ▼

Steam Guard е патентован вариант, базиран на RFC 6238. Той използва абсолютно същата 30-секундна времева стъпка и HMAC-SHA1 изчисление, но във финалната фаза на редукция преобразува извлеченото цяло число през потребителски масив от 26 буквено-цифрови знака (23456789BCDFGHJKMNPQRTVWXY), за да генерира 5-значен низ.

Официален стандарт RFC 6238 · Клиентски изчисления с W3C Web Crypto API

Изпробвайте на живо 100% клиентско генериране на TOTP кодове

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