💻 Kryptografia stosowana · Techniczny Deep Dive

Jak działa TOTP: Implementacja algorytmu RFC 6238 w Pythonie, JavaScript i Go

2F Laboratorium Badań Bezpieczeństwa 2FA-Tool · Zaktualizowano: październik 2026 · 16 min czytania (ponad 2700 słów)
💡 Streszczenie architektury i manifest inżynieryjny

Wśród fundamentalnych protokołów kryptograficznych we współczesnym bezpieczeństwie sieciowym RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) wyróżnia się matematyczną prostotą i elegancją. Nie wymaga interaktywnego uzgadniania sesji w sieci (handshake), scentralizowanej infrastruktury klucza publicznego (PKI) ani komunikacji w chmurze. Jedynie przy użyciu współdzielonego klucza tajnego Base32 oraz nieustannego upływu fizycznego czasu uniksowego (Unix epoch), klient i serwer obliczają identyczne, efemeryczne 6-cyfrowe tokeny uwierzytelniające co 30 sekund z pełną matematyczną pewnością.

W tym kompleksowym przewodniku omijamy zewnętrzne biblioteki typu „czarna skrzynka” (takie jak pyotp czy otplib), aby od podstaw zbudować kompletny potok kryptograficzny RFC 6238 w językach Python, JavaScript oraz Go, weryfikując każdą implementację względem oficjalnych wektorów testowych IETF.

01. Elegancja uwierzytelniania sterowanego czasem

Aby zrozumieć TOTP, musimy najpierw przeanalizować jego bezpośredniego poprzednika: RFC 4226 HOTP (HMAC-Based One-Time Password).

W HOTP klient i serwer dzielą licznik zdarzeń $C$. Za każdym razem, gdy użytkownik generuje token, licznik jest inkrementowany ($C = C + 1$), a token jest wyprowadzany z $\text{HMAC-SHA-1}(K, C)$. Choć matematycznie bezbłędny, HOTP cierpiał na operacyjną słabość: desynchronizację licznika. Jeśli użytkownik przypadkowo nacisnął przycisk na sprzętowym tokenie pięć razy w kieszeni, licznik klienta osiągał $C=6$, podczas gdy serwer uwierzytelniający pozostawał przy $C=1$, co skutkowało trwałym odrzucaniem prób logowania.

⏱️ Przełom RFC 6238: Zastąpienie liczników zdarzeń czasem epoki

Opublikowany w maju 2011 roku przez N. M'Raihi, S. Machani, M. Pei i J. Rydella standard RFC 6238 rozwiązał problem desynchronizacji licznika, zastępując inkrementowaną liczbę całkowitą ujednoliconym czasem epoki uniksowej (Unix epoch time). Ponieważ czas fizyczny upływa jednakowo na całym świecie, zarówno klient, jak i serwer przesuwają swoje wewnętrzne liczniki jednocześnie, bez wysyłania ani jednego pakietu przez sieć.

02. 5 kryptograficznych kroków algorytmu TOTP

Przekształcenie 16-znakowego ziarna Base32 (np. JBSWY3DPEHPK3PXP) w dynamiczny, 6-cyfrowy kod zmienny (np. 284915) obejmuje pięć precyzyjnych faz kryptograficznych:

1 Krok 1: Dekodowanie klucza tajnego Base32

Klucze rejestracyjne są kodowane w formacie RFC 4648 Base32 przy użyciu alfabetu A-Z oraz cyfr 2-7 (z wyłączeniem znaków niejednoznacznych, takich jak 0, 1, 8, 9). Algorytm najpierw normalizuje ten ciąg, usuwając białe znaki i dodając dopełnienie (=), aby jego długość była wielokrotnością 8, a następnie dekoduje go do surowych bajtów binarnych klucza $K$.

2 Krok 2: Obliczenie kroku czasowego (Licznik $T$)

Aktualny znacznik czasu Unix (liczba sekund od 1 stycznia 1970 UTC) jest mapowany na całkowitoliczbowy licznik $T$:

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

Ze standardowymi parametrami $T_0 = 0$ oraz czasem trwania kroku $X = 30$ sekund. Ta liczba całkowita $T$ jest następnie kodowana jako 8-bajtowa, 64-bitowa liczba całkowita bez znaku w formacie big-endian.

3 Krok 3: Obliczenie hasza HMAC-SHA1

Używając surowych bajtów klucza $K$ jako klucza tajnego, obliczamy skrót HMAC-SHA1 na 8-bajtowym buforze czasu big-endian, uzyskując 20-bajtowy (160-bitowy) hasz uwierzytelniania wiadomości $HS$:

HS = HMAC-SHA-1(Key, T)
4 Krok 4: Dynamiczne obcinanie (Dynamic Truncation - DT)

Aby przekształcić 20 surowych bajtów w liczbę całkowitą, standard RFC 4226 określa procedurę Dynamic Truncation:

  • Wyodrębnienie przesunięcia (Offset): Pobierz ostatni bajt $HS[19]$ i zamaskuj go wartością 0x0F, aby wyodrębnić 4 najmłodsze bity, uzyskując przesunięcie od 0 do 15: Offset = HS[19] & 0x0F;
  • Wycięcie 4 bajtów: Pobierz 4 kolejne bajty zaczynając od Offset: $P = HS[Offset \dots Offset+3]$;
  • Maskowanie bitu znaku: Zamaskuj najbardziej znaczący bit (MSB) wartością 0x7F, aby uniknąć problemów z liczbami ze znakiem:
    BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
5 Krok 5: Redukcja modulo do $N$ cyfr

Na koniec zredukuj BinaryCode modulo $10^6$ (1 000 000) dla standardowych 6-cyfrowych tokenów. Uzupełnij wynik wiodącymi zerami z lewej strony, aby uzyskać stałą długość ciągu 6 znaków:

OTP = BinaryCode mod 10^6

03. Działający kod 1: Biblioteka standardowa Python 3

Ta kompletna implementacja wykorzystuje wbudowane moduły języka Python: base64, struct, hmac oraz hashlib, nie wymagając instalacji żadnych pakietów zewnętrznych przez pip:

totp_rfc6238.py Python 3.8+ (Zero zależności)
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. Działający kod 2: JavaScript i Crypto w Node.js

Implementacja w czystym JavaScript dla środowisk backendowych Node.js z wykorzystaniem natywnego modułu crypto.createHmac:

totp_node.js Natywne Crypto w Node.js
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”));

Wykonywanie w przeglądarce po stronie klienta za pośrednictwem W3C Web Crypto API:

Dla nowoczesnych aplikacji internetowych wymagających 100% bezpieczeństwa offline (dokładnie taka architektura zasila 2FA-Tool), interfejs W3C Web Crypto API zapewnia asynchroniczne, akcelerowane sprzętowo prymitywy kryptograficzne bezpośrednio w piaskownicy przeglądarki bez potrzeby instalowania zewnętrznych pakietów npm:

webcrypto_totp.js Przeglądarkowe 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. Działający kod 3: Produkcyjny Golang (Go)

Mikrousługi o wysokiej przepustowości korzystają ze sprawdzonej w bojach biblioteki standardowej Go:

totp.go Biblioteka standardowa Go 1.20+
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] &amp; 0x0f
binaryCode := binary.BigEndian.Uint32(hash[offset:offset+4]) &amp; 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. Weryfikacja wektorów testowych RFC 6238 Dodatek B

Aby udowodnić zgodność ze standardem, każda implementacja kryptograficzna musi zostać zweryfikowana względem oficjalnych wektorów testowych opublikowanych w IETF RFC 6238 Dodatek B.

Oficjalny zestaw testów wykorzystuje klucz w formacie ASCII "12345678901234567890" (20 bajtów), który w Base32 koduje się jako:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (oceniany dla 8-cyfrowych tokenów wyjściowych z 30-sekundowymi krokami):

Znacznik czasu Unix Epoch Data i godzina UTC Oficjalny oczekiwany kod RFC Wynik implementacji
59 1970-01-01 00:00:59 94287082 ✓ ZALICZONE (Dokładne dopasowanie)
1111111109 2005-03-18 01:58:29 07081804 ✓ ZALICZONE (Dokładne dopasowanie)
1111111111 2005-03-18 01:58:31 14050471 ✓ ZALICZONE (Dokładne dopasowanie)
1234567890 2009-02-13 23:31:30 89005924 ✓ ZALICZONE (Dokładne dopasowanie)
2000000000 2033-05-18 03:33:20 69279037 ✓ ZALICZONE (Dokładne dopasowanie)

07. Inżynieria produkcyjna: Dryf zegara, powtórzenia i limity żądań

Podczas wdrażania weryfikacji TOTP w systemach uwierzytelniania korporacyjnego o wysokiej dostępności należy wymusić trzy krytyczne wymagania bezpieczeństwa:

Zasada 1 Okno tolerancji dryfu zegara (Clock Drift)

Ze względu na drobne niedokładności zegara w urządzeniach mobilnych serwery powinny oceniać okno [T - 1, T, T + 1] (sprawdzając poprzednie 30 s, bieżące 30 s i kolejne 30 s). Nigdy nie rozszerzaj okna poza $\pm 1$ krok, ponieważ każdy dodatkowy krok powiększa powierzchnię ataku powtórzeniowego (replay attack) o 30 sekund.

Zasada 2 Wymuszenie jednorazowości tokena (Ochrona przed replay attacks)

Token musi zostać natychmiast unieważniony po udanym użyciu. Zapisz Last_Verified_Time_Step w bazie Redis lub pamięci podręcznej. Jeśli klient spróbuje przesłać ten sam token w obrębie tego samego 30-sekundowego okna, odrzuć żądanie, aby zneutralizować ataki powtórzeniowe typu man-in-the-middle.

Zasada 3 Ograniczanie liczby żądań (Rate Limiting) i porównywanie w czasie stałym

Ponieważ 6-cyfrowe kody obejmują zaledwie 1 000 000 permutacji, punkty końcowe bez limitu żądań mogą zostać szybko przełamane atakiem brute-force. Wymuś progresywne opóźnienia i blokady konta po 3 nieudanych próbach z rzędu. W kodzie wykonuj porównania ciągów znaków za pomocą funkcji porównujących w czasie stałym (np. crypto/subtle.ConstantTimeCompare w Go lub crypto.timingSafeEqual w Node.js), aby udaremnić analizę czasową kanałami bocznymi (side-channel timing attacks).

08. Często zadawane pytania (FAQ)

P1: Skoro SHA-1 jest kryptograficznie złamany, dlaczego TOTP nadal domyślnie używa HMAC-SHA1? ▼

Teoretyczne ataki kolizyjne przeciwko surowej funkcji SHA-1 nie naruszają konstrukcji HMAC (RFC 2104). W HMAC podstawowa funkcja skrótu jest zagnieżdżona z wewnętrznymi i zewnętrznymi kluczami tajnymi, co neutralizuje ataki oparte na kolizjach. Do dziś HMAC-SHA1 pozostaje bezpieczny dla 2FA. Ponadto RFC 6238 w pełni obsługuje HMAC-SHA256 i HMAC-SHA512, jeśli wymagany jest wyższy poziom pewności kryptograficznej.

P2: Czy strefy czasowe lub czas letni (DST) wpływają na synchronizację TOTP? ▼

Nie. Czas uniksowy (Unix epoch) jest zdefiniowany jako liczba sekund, które upłynęły od 1970-01-01 00:00:00 UTC. Jest on całkowicie niezależny od stref czasowych, sekund przestępnych i zmian czasu letniego. W dowolnym dokładnym momencie serwer w Tokio i smartfon w Kalifornii mają dokładnie taki sam całkowitoliczbowy znacznik czasu.

P3: Czym różni się 5-znakowe uwierzytelnianie Steam Guard od standardu RFC 6238? ▼

Steam Guard to autorski wariant oparty na RFC 6238. Używa dokładnie tego samego 30-sekundowego kroku czasowego i obliczeń HMAC-SHA1, ale w końcowej fazie redukcji mapuje wyodrębnioną liczbę całkowitą przez niestandardową 26-znakową tablicę alfanumeryczną (23456789BCDFGHJKMNPQRTVWXY), tworząc 5-znakowy ciąg tokena.

Zgodność z RFC 6238 · Obliczenia po stronie klienta W3C Web Crypto API

Wypróbuj generowanie kodów TOTP w 100% po stronie klienta na żywo

2FA-Tool implementuje ten dokładny potok kryptograficzny bezpośrednio w Twojej przeglądarce za pomocą W3C Web Crypto API. Zero transmisji do serwera, zero przechowywania tokenów, natychmiastowa walidacja.