Jak działa TOTP: algorytm kryjący się za kodami z aplikacji uwierzytelniającej
Kilka razy w tygodniu przepisujesz 6-cyfrowy kod z aplikacji uwierzytelniającej i jakimś cudem ten sam kod pojawia się na telefonie oraz zgadza się z tym, czego oczekuje serwer, choć urządzenia nigdy ze sobą nie rozmawiają. Jak więc działa TOTP? Ten sam współdzielony sekret produkuje nową liczbę co 30 sekund, a odpowiedź sprowadza się do niewielkiego, deterministycznego algorytmu, który obie strony wykonują niezależnie. Żaden kod nie wędruje przez sieć. Żaden centralny serwer nie rozdaje tej liczby.
TOTP (Time-based One-Time Password), zdefiniowany w RFC 6238, zamienia współdzielony sekret oraz bieżący czas w krótki kod numeryczny, obliczając HMAC z czasu i skracając wynik. Uwierzytelnianie dwuskładnikowe (2FA) opiera się na tym, że obie strony wyliczają tę samą wartość bez jej wymiany, więc cały model zaufania sprowadza się do tego algorytmu.
Ten przewodnik przeprowadza przez algorytm od początku do końca na konkretnych liczbach, a potem zajmuje się tą połową, którą większość wyjaśnień pomija: jak serwer faktycznie weryfikuje kod i co 2FA powstrzymuje, a czego nie. Żywy kod możesz wyliczyć w naszym generatorze TOTP.
Czym właściwie jest TOTP?
TOTP (Time-based One-Time Password), zdefiniowany w RFC 6238, to algorytm, który łączy współdzielony sekret z bieżącym czasem, aby wyprodukować krótki kod zmieniający się w stałym interwale. Zarówno aplikacja uwierzytelniająca, jak i serwer przechowują ten sam sekret, odczytują ten sam zegar i wykonują tę samą matematykę, więc dochodzą do tego samego kodu, nigdy go nie przesyłając.
Warto zatrzymać się przy tym ostatnim punkcie. Podczas konfiguracji kod nigdy nie jest wysyłany, przesyła się jedynie sekret, a później każda strona wyprowadza kody samodzielnie. W kanale komunikacji nie ma nic do przechwycenia poza sekretem w momencie rejestracji i 6 cyframi, które użytkownik wpisuje przy logowaniu. Z trzech wejść powstaje jedno wyjście:
| Wejście | Rola | Typowa wartość |
|---|---|---|
| Współdzielony sekret | Długowieczny klucz, uzgadniany raz przy rejestracji | JBSWY3DPEHPK3PXP (Base32) |
| Krok czasowy | Licznik przesuwający się naprzód | Okno 30-sekundowe |
| Wyjście | Krótki kod wyprowadzony z obu | 324550 |
Sekret prawie zawsze zapisuje się w Base32 (litery A–Z i cyfry 2–7), ponieważ ten alfabet nie rozróżnia wielkości liter i przetrwa wydrukowanie, przepisanie czy spakowanie do kodu QR. Sekret rejestrujesz, skanując URI otpauth://, które można wyrenderować jako kod QR dla aplikacji uwierzytelniającej, albo wpisując ciąg Base32 ręcznie.
TOTP vs HOTP vs SMS vs Passkeys: krajobraz 2FA
TOTP to jedna z kilku opcji i żeby wybrać sensownie, trzeba zobaczyć całe pole. Zależność, którą warto znać, mieści się w jednym zdaniu: TOTP to HOTP, w którym licznik zastąpiono liczbą kroków czasowych od epoki Unix. Reszta to kompromis między odpornością na phishing, wygodą i tym, jakiej infrastruktury potrzebujesz.
| Mechanizm | Czynnik napędowy | Czas życia kodu | Odporny na phishing? | Wymaga sieci? | Typowe zastosowanie |
|---|---|---|---|---|---|
| HOTP (RFC 4226) | Inkrementowany licznik | Do użycia | Nie | Nie | Tokeny sprzętowe, starsze systemy |
| TOTP (RFC 6238) | Bieżący czas | ~30 sekund | Nie | Nie (po rejestracji) | Aplikacje uwierzytelniające |
| SMS OTP | Serwer wysyła kod | Kilka minut | Nie | Tak (komórkowa) | Awaryjna opcja dla użytkowników |
| Zatwierdzenie push | Monit serwera na urządzeniu | Na żądanie | Częściowo | Tak | 2FA oparte na aplikacji |
| Passkey / FIDO2 | Wyzwanie klucza publicznego | Na żądanie | Tak (powiązany z origin) | Tak | Nowoczesne konta |
Tabela układa się w jeden wzorzec. TOTP i HOTP działają offline po rejestracji, co czyni je odpornymi i prywatnymi, ale żaden z nich sam w sobie nie jest odporny na phishing: przekonująca fałszywa strona może poprosić o kod i przekazać go dalej. SMS dodaje kanał sieciowy, który wprowadza własną powierzchnię ataku. Passkeys zamykają lukę phishingową, wiążąc poświadczenie z origin witryny, i właśnie w tę stronę zmierza branża. TOTP wypada pośrodku: jest silny, dostępny wszędzie i darmowy, co tłumaczy, dlaczego pozostaje tak powszechny.
Jak działa algorytm TOTP, krok po kroku
Oto cały algorytm w czterech krokach. Każdy z nich przejdziemy z testowym sekretem z RFC JBSWY3DPEHPK3PXP oraz ustalonym czasem Unix 1700000000, więc każda liczba jest odtwarzalna.
- Zdekoduj sekret Base32 do surowych bajtów klucza.
- Oblicz licznik kroku czasowego z bieżącego czasu Unix.
- Oblicz HMAC z licznika przy użyciu klucza sekretu.
- Skróć skrót do 6-cyfrowego kodu.
Krok 1 — Dekodowanie sekretu Base32 do bajtów
Base32 upakowuje 5 bitów w każdym znaku. Dekoder grupuje znaki z powrotem w 8-bitowe bajty. Sekret JBSWY3DPEHPK3PXP dekoduje się do 10 surowych bajtów 48 65 6c 6c 6f 21 de ad be ef. To ta tablica bajtów, a nie drukowalny ciąg, jest kluczem HMAC.
Krok 2 — Obliczenie licznika kroku czasowego
Licznik to liczba pełnych kroków czasowych, które upłynęły od punktu początkowego: T = floor((unixTime − T0) / period). Domyślne wartości z RFC to T0 = 0 (epoka Unix) i period = 30. Przy unixTime = 1700000000 daje to T = floor(1700000000 / 30) = 56666666. Ta liczba całkowita jest następnie kodowana jako 8-bajtowa wartość big-endian: 00 00 00 00 03 60 aa 2a. Ponieważ licznik zmienia się dopiero wtedy, gdy zaczyna się nowe 30-sekundowe okno, każdy kod jest stabilny przez długość jednego okna, a potem przeskakuje.
Krok 3 — Obliczenie HMAC z licznika przy użyciu sekretu
Algorytm oblicza HMAC-SHA1 z 8-bajtowego licznika, używając bajtów sekretu jako klucza. HMAC to jednokierunkowa funkcja z kluczem: bez sekretu nie odwrócisz skrótu ani nie podrobisz prawidłowego, i to właśnie czyni kod niemożliwym do sfałszowania. Dla naszych wejść skrótem jest 20 bajtów 1d 70 6e 94 1a c7 6b 6d 4a 46 dd 6f af a4 5f e3 35 11 bf 86.
Krok 4 — Dynamiczne skracanie do 6-cyfrowego kodu (RFC 4226)
20-bajtowy skrót jest zbyt długi, by go przepisać, więc dynamiczne skracanie z RFC 4226 wydobywa z niego liczbę. Weź młodszą półbajtówkę (nibble) ostatniego bajtu jako przesunięcie: ostatni bajt to 0x86, jego młodsza półbajtówka to 6, więc przesunięcie wynosi 6. Odczytaj 4 bajty począwszy od tego przesunięcia (6b 6d 4a 46), wymaskuj najstarszy bit pierwszego z nich, aby liczba pozostała dodatnia, a otrzymasz liczbę całkowitą 1802324550. Zredukuj ją modulo 10^6 i uzupełnij zerami: 1802324550 % 1000000 = 324550. To jest kod, który Twoja aplikacja pokazuje dla tego sekretu w tej chwili.
Oto algorytm w JavaScript przy użyciu natywnego Web Crypto API przeglądarki, bez żadnych zależności. Każdy komentarz mapuje blok na jeden z czterech powyższych kroków:
// TOTP per RFC 6238 — SHA-1, 6 digits, 30s period (the defaults).
async function generateTotp(base32Secret, unixTime = Date.now() / 1000) {
// Step 1: decode the Base32 secret (A-Z, 2-7) to raw key bytes.
const alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let bits = '';
for (const ch of base32Secret.replace(/=+$/, '').toUpperCase()) {
bits += alpha.indexOf(ch).toString(2).padStart(5, '0');
}
const keyBytes = new Uint8Array(
bits.match(/.{8}/g).map((b) => parseInt(b, 2)));
// Step 2: counter = number of 30s steps since the epoch (8-byte big-endian).
let counter = Math.floor(unixTime / 30);
const msg = new Uint8Array(8);
for (let i = 7; i >= 0; i--) {
msg[i] = counter & 0xff;
counter = Math.floor(counter / 256);
}
// Step 3: HMAC-SHA1 the counter with the secret key.
const key = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
const hmac = new Uint8Array(await crypto.subtle.sign('HMAC', key, msg));
// Step 4: dynamic truncation (RFC 4226) -> 6-digit code.
const offset = hmac[hmac.length - 1] & 0x0f;
const binary = ((hmac[offset] & 0x7f) << 24) | (hmac[offset + 1] << 16) |
(hmac[offset + 2] << 8) | hmac[offset + 3];
return (binary % 1_000_000).toString().padStart(6, '0');
}
const code = await generateTotp('JBSWY3DPEHPK3PXP', 1700000000);
console.log(code); // -> "324550"
Ten sam algorytm w Pythonie, z użyciem wyłącznie biblioteki standardowej (hmac i struct):
import base64, hmac, hashlib, struct, time
def totp(secret, for_time=None, period=30, digits=6, digest='sha1'):
if for_time is None:
for_time = time.time()
# Step 1: Base32-decode the secret to raw key bytes.
key = base64.b32decode(secret.upper())
# Step 2: counter = number of time steps since the epoch (8-byte big-endian).
counter = int(for_time // period)
msg = struct.pack(">Q", counter)
# Step 3: HMAC the counter with the secret.
h = hmac.new(key, msg, digest).digest()
# Step 4: dynamic truncation (RFC 4226) -> N-digit code.
offset = h[-1] & 0x0f
binary = ((h[offset] & 0x7f) << 24 |
(h[offset + 1] & 0xff) << 16 |
(h[offset + 2] & 0xff) << 8 |
(h[offset + 3] & 0xff))
return str(binary % (10 ** digits)).zfill(digits)
print(totp('JBSWY3DPEHPK3PXP', 1700000000)) # -> 324550
Obie implementacje drukują 324550 dla naszego ustalonego czasu i obie odtwarzają oficjalne wektory testowe z RFC 6238 (na przykład wektor SHA-1 przy T = 59 daje 94287082). Jeśli zamienisz SHA-1 na SHA-256 albo SHA-512 lub zmienisz liczbę cyfr, weryfikator po drugiej stronie musi dokładnie dopasować te same wybory, inaczej kody nigdy się nie zgodzą.
Weryfikacja kodu TOTP po stronie serwera
Wygenerowanie kodu to połowa systemu. Drugą połową jest decyzja serwera o tym, czy zaakceptować 6 cyfr, które użytkownik właśnie wpisał, i to ten krok niesie ze sobą wszystkie kompromisy istotne dla bezpieczeństwa.
Serwer nie przechowuje kodów. Przechowuje sekret, a przy logowaniu ponownie wylicza oczekiwany kod z tego sekretu oraz bieżącego czasu, a następnie porównuje. Problemem jest dryf zegara: urządzenie użytkownika i serwer rzadko zgadzają się co do sekundy, więc ścisłe sprawdzenie równości odrzuciłoby kody przy granicy okna. Rozwiązaniem jest niewielkie okno walidacji. Akceptuj bieżący krok oraz po jednym kroku z każdej strony, czyli sprawdzaj kody dla liczników T−1, T i T+1. Szersze okno toleruje większy dryf, ale powiększa powierzchnię zgadywania, więc okno 1 (tolerancja ±30 sekund) jest powszechnym kompromisem. Tę samą tolerancję ±1 kroku można zobaczyć na karcie Weryfikacja w generatorze TOTP.
import { createHmac, timingSafeEqual } from 'crypto';
function verifyTotp(secret, code, { window = 1, period = 30, digits = 6 } = {}) {
const counter = Math.floor(Date.now() / 1000 / period);
const submitted = Buffer.from(code);
// Check the current step and ±window steps for clock drift.
for (let i = -window; i <= window; i++) {
const expected = Buffer.from(totpAt(secret, counter + i, digits));
// Constant-time compare so timing can't leak a partial match.
if (expected.length === submitted.length &&
timingSafeEqual(expected, submitted)) {
return counter + i; // matched step — store it to block replay
}
}
return false;
}
Dwa kolejne szczegóły decydują o tym, czy taki kod jest tylko sprawdzany, czy faktycznie bezpieczny. Po pierwsze, zapobieganie powtórzeniom (replay): zapisuj ostatni licznik zaakceptowany dla każdego użytkownika i odrzucaj każdy kod z kroku równego mu lub niższego, tak aby kod podsłuchany raz nie mógł zostać użyty ponownie w tym samym oknie. Dlatego verifyTotp zwraca dopasowany krok, a nie suche true. Po drugie, ograniczanie liczby prób (rate limiting): 6-cyfrowy kod to jedna z miliona wartości, a okno ±1 sprawia, że w każdej chwili poprawne są trzy z nich, więc bez dławienia napastnik może przeszukać przestrzeń metodą brute force. Zablokuj konto lub dodaj opóźnienie (backoff) po kilku nieudanych próbach. Sekret jest natomiast długowiecznym kluczem, więc szyfruj go w spoczynku, trzymaj poza systemem kontroli wersji i traktuj dokładnie jak hasło. Wraz z nim wygeneruj silne kody odzyskiwania na dzień, w którym urządzenie zaginie.
Przed czym TOTP chroni — a przed czym nie
TOTP to realny krok naprzód względem samych haseł, ale nie rozwiązuje wszystkiego, a strony marketingowe mają skłonność do przemilczania luk. Oto uczciwy podział.
| TOTP powstrzymuje | TOTP NIE powstrzymuje |
|---|---|
| Wyciekłe lub ponownie użyte hasła | Phishing w czasie rzeczywistym / adversary-in-the-middle |
| Credential stuffing | Malware odczytujące sekret z urządzenia |
| Zdalny brute force hasła | Słabe ścieżki odzyskiwania konta pomijające 2FA |
| Wyciek bazy ujawniający tylko skróty haseł | (te wymagają innych zabezpieczeń) |
Po stronie zysków: skoro logowanie wymaga teraz kodu, który może wyprodukować tylko sekret, wyciekłe hasło już nie wystarcza, co od razu kładzie kres credential stuffingowi i zdalnemu brute force. Jeśli Twoja baza danych wycieknie, ale sekrety TOTP są zaszyfrowane w spoczynku, napastnik nadal nie może wybić kodów.
Luki są równie realne. Phishingowe proxy działające w czasie rzeczywistym (strona adversary-in-the-middle) może pokazać użytkownikowi idealną kopię, przechwycić żywy kod i przekazać go do prawdziwej witryny w obrębie tego samego okna. TOTP nie potrafi rozpoznać, że kod wpisano w niewłaściwym miejscu. Malware na urządzeniu, które eksfiltruje sekret, pokonuje go całkowicie, a niechlujna ścieżka odzyskiwania „zapomniałem 2FA” może go w ogóle obejść. Warto też rozwiać jedno częste nieporozumienie: ataki SIM-swap pokonują jednorazowe kody SMS, a nie TOTP, bo TOTP nie ma kanału opartego na numerze telefonu, więc napastnik nie ma czego przekierować.
Co dalej? Passkeys oraz FIDO2/WebAuthn są powiązane z origin, więc z założenia są odporne na phishing: poświadczenie po prostu odmawia uwierzytelnienia w niewłaściwej domenie. Traktuj TOTP jako silny, powszechnie dostępny krok w górę względem haseł, a nie jako punkt docelowy. Dobrze współgra z resztą Twojego stosu uwierzytelniania: po warstwę tokenów sesyjnych nadbudowaną nad zweryfikowanym logowaniem sięgnij do najlepszych praktyk bezpieczeństwa JWT, a po warstwę haseł w spoczynku, którą 2FA uzupełnia, do haszowania haseł (bcrypt vs Argon2).
Częste pułapki przy wdrażaniu TOTP
Większość błędów TOTP tkwi nie w samym algorytmie, ustalonym przez RFC, lecz w okablowaniu wokół niego. Te akurat gryzą wdrażających najczęściej.
- Dryf zegara serwera. Jeśli serwer nie korzysta z NTP, jego pojęcie „teraz” odsuwa się od urządzenia użytkownika i kody przestają się zgadzać u wszystkich. Włącz synchronizację czasu sieciowego na każdym hoście.
- Sekrety w postaci jawnej lub wrzucone do repozytorium. Sekret w pliku konfiguracyjnym zatwierdzonym w gicie to stała furtka. Przechowuj go zaszyfrowany w menedżerze sekretów, nigdy w systemie kontroli wersji.
- Brak ochrony przed powtórzeniem. Jeśli akceptujesz kod bez zapisania kroku, który dopasował, ten sam kod zadziała ponownie w obrębie swojego okna. Utrwalaj ostatnio użyty krok dla każdego użytkownika i odrzucaj ponowne użycie.
- Okno zbyt szerokie lub zbyt wąskie. Zbyt szerokie mnoży kody możliwe do odgadnięcia i osłabia bezpieczeństwo; zbyt wąskie odrzuca uprawnionych użytkowników przy drobnym dryfie. Okno 1 to zwykły kompromis.
- Niezgodność parametrów. Jeśli rejestracja koduje SHA-256 i 8 cyfr w URI
otpauth://, a weryfikator zakłada SHA-1 i 6 cyfr, żaden kod nigdy się nie zwaliduje. Odczytaj algorytm, liczbę cyfr i okres z URI i użyj ich po obu stronach. - Brak kodów zapasowych lub kodów odzyskiwania. Gdy telefon zginie, jedyną drogą powrotu jest ścieżka odzyskiwania. Wydaj kody odzyskiwania przy konfiguracji i uczyń je tak silnymi, jak zasługuje na to konto — ta sama logika, która stoi za entropią hasła, dotyczy także sekretów odzyskiwania.
FAQ
Czy TOTP jest odporny na phishing?
Nie. TOTP powstrzymuje wyciekłe hasła i zdalny brute force, ale phishingowe proxy działające w czasie rzeczywistym może pokazać fałszywe logowanie, przechwycić żywy kod i przekazać go do prawdziwej witryny w obrębie tego samego 30-sekundowego okna. Passkeys i FIDO2 to ulepszenie odporne na phishing, ponieważ wiążą poświadczenie z origin witryny.
Czy TOTP jest bezpieczniejszy niż 2FA przez SMS?
Tak. Kody SMS wędrują przez sieć komórkową i mogą zostać przechwycone przez ataki SIM-swap lub SS7, a w dodatku zależą od bezpieczeństwa Twojego operatora. TOTP nie ma kanału opartego na numerze telefonu i w ogóle nie przesyła kodu, więc nie ma nic do przechwycenia w trakcie transmisji. Sekret wymienia się raz, przy konfiguracji.
Co się stanie, jeśli zgubię telefon lub aplikację uwierzytelniającą?
Potrzebujesz kopii zapasowej przygotowanej z wyprzedzeniem. Opcje to kody odzyskiwania zapisane przy konfiguracji 2FA, drugie urządzenie zarejestrowane z tym samym sekretem albo oryginalny sekret Base32 przechowywany w bezpiecznym miejscu. Bez jednej z nich utrata urządzenia oznacza zablokowanie dostępu do konta.
Jak serwer weryfikuje kod TOTP?
Ponownie wylicza oczekiwany kod ze współdzielonego sekretu i bieżącego czasu, a następnie porównuje przesłany kod z bieżącym krokiem czasowym oraz jednym krokiem z każdej strony, aby uwzględnić dryf zegara. Zapisuje też, który krok się dopasował, dzięki czemu ten sam kod nie może zostać powtórzony, oraz ogranicza liczbę prób, aby zablokować zgadywanie.
Dlaczego kody TOTP odświeżają się co 30 sekund?
Trzydzieści sekund to domyślny okres z RFC 6238: dość długi, by wygodnie odczytać i przepisać kod, a zarazem dość krótki, by kod przechwycony przez napastnika wygasł niemal natychmiast. Niektóre systemy używają okresu 60-sekundowego, który URI otpauth:// zapisuje, aby weryfikator go dopasował.
Czy dwa urządzenia mogą współdzielić jeden sekret TOTP?
Tak. Każde urządzenie przechowujące ten sam sekret Base32 z zsynchronizowanym zegarem generuje identyczne kody, ponieważ algorytm jest deterministyczny. Dokładnie tak działają wielourządzeniowe kopie zapasowe aplikacji uwierzytelniających i dlatego też sekret musi pozostać prywatny: każdy, kto go skopiuje, może wyprodukować każdy przyszły kod.
Czy TOTP to to samo co Google Authenticator?
Nie. TOTP to otwarty algorytm zdefiniowany w RFC 6238. Google Authenticator, Authy i 1Password to aplikacje, które go implementują. Ponieważ standard jest wspólny, każda zgodna aplikacja współpracuje z każdą usługą korzystającą z TOTP — nie ma uzależnienia od konkretnego dostawcy.
Podsumowanie
Najważniejsze rzeczy mieszczą się w kilku punktach:
- TOTP zamienia współdzielony sekret oraz bieżący czas w kod za pomocą HMAC i skracania.
- Obie strony wyliczają kod niezależnie; nigdy nie jest on wysyłany przez sieć.
- Weryfikuj z oknem ±1 kroku oraz ochroną przed powtórzeniem i ograniczaniem liczby prób.
- Powstrzymuje ataki na hasła, ale nie phishing w czasie rzeczywistym; tę lukę zamykają passkeys.
- Utrzymuj zegary serwera zsynchronizowane przez NTP, a sekret zaszyfrowany i prywatny.
Chcesz zobaczyć, jak algorytm produkuje prawdziwe liczby, i sprawdzić własne okno weryfikacji? Otwórz generator TOTP / 2FA i wylicz oraz zweryfikuj kody w całości w przeglądarce, gdzie sekret nigdy nie opuszcza Twojego urządzenia.