💻 Sovellettu kryptografia · Tekninen syväsukellus

Miten TOTP toimii: RFC 6238 -algoritmin toteutus Pythonilla, JavaScriptillä ja Golla

2F 2FA-Tool-tietoturvatiimi · Päivitetty lokakuussa 2026 · 16 min lukuaika (2 700+ sanaa)
💡 Arkkitehtuurin tiivistelmä ja tekninen manifesti

Modernin verkkoturvallisuuden perustavanlaatuisten kryptografisten protokollien joukossa RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) erottuu matemaattisen yksinkertaisuutensa ansiosta. Se ei vaadi vuorovaikutteisia verkkokättelyjä, ei keskitettyä julkisen avaimen infrastruktuuria (PKI) eikä pilvikyselyjä. Pelkän jaetun Base32-salaisuuden ja fyysisen Unix-ajan vääjäämättömän etenemisen avulla asiakasohjelma ja palvelin laskevat identtiset, lyhytikäiset 6-numeroiset todennuskoodit 30 sekunnin välein 100-prosenttisella matemaattisella varmuudella.

Tässä kattavassa oppaassa ohitamme valmiit kolmansien osapuolten kirjastot (kuten pyotp tai otplib) ja rakennamme täydellisen RFC 6238 -kryptografiaputken alusta alkaen Pythonilla, JavaScriptillä ja Golla varmistaen jokaisen toteutuksen IETF:n virallisia testivektoreita vasten.

01. Aikaan perustuvan todennuksen toimintaperiaate

TOTP:n ymmärtämiseksi meidän on ensin tarkasteltava sen edeltäjää: RFC 4226 HOTP (HMAC-Based One-Time Password).

HOTP:ssa asiakas ja palvelin jakavat tapahtumalaskurin $C$. Joka kerta kun käyttäjä luo koodin, laskuri kasvaa ($C = C + 1$), ja koodi johdetaan arvosta $\text{HMAC-SHA-1}(K, C)$. Vaikka se oli matemaattisesti vahva, HOTP kärsi toiminnallisesta heikkoudesta: laskurin epäsynkroniasta. Jos käyttäjä painoi vahingossa taskussaan olleen laitetunnistimen painiketta viisi kertaa, asiakaslaskuri siirtyi arvoon $C=6$, kun taas todennuspalvelin pysyi arvossa $C=1$, mikä johti toistuviin kirjautumishylkäyksiin.

⏱️ RFC 6238:n läpimurto: tapahtumalaskureiden korvaaminen Unix-epookkiajalla

N. M'Raihin, S. Machanin, M. Pein ja J. Rydellin toukokuussa 2011 julkaisema RFC 6238 ratkaisi laskurin synkronointiongelman korvaamalla kasvavan kokonaisluvun standardoidulla Unix-epookkiajalla. Koska fyysinen aika etenee tasaisesti ympäri maailmaa, sekä asiakas että palvelin kasvattavat sisäisiä laskureitaan samanaikaisesti lähettämättä yhtäkään verkkopakettia.

02. TOTP:n 5 kryptografista matemaattista vaihetta

16-merkkisen Base32-siemenen (esim. JBSWY3DPEHPK3PXP) muuntaminen 6-numeroiseksi vaihtuvaksi koodiksi (esim. 284915) sisältää viisi tarkkaa kryptografista vaihetta:

1 Vaihe 1: Base32-salaisen avaimen dekoodaus

Käyttöönottovaiheen avaimet on koodattu RFC 4648 Base32 -muodossa käyttäen aakkostoa A-Z ja 2-7 (jättäen pois helposti sekoittuvat merkit kuten 0, 1, 8, 9). Algoritmi ensin normalisoi tämän merkkijonon poistamalla välilyönnit ja lisäämällä täytteen (=), jotta sen pituus on 8:lla jaollinen, ennen kuin se dekoodataan raaoiksi binaariavainbaiteiksi $K$.

2 Vaihe 2: Aika-askeleen laskeminen (Laskuri $T$)

Nykyinen Unix-aikaleima (kuluneet sekunnit tammikuun 1. päivästä 1970 UTC) muunnetaan kokonaislukulaskuriksi $T$:

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

Käyttäen standardiparametreja $T_0 = 0$ ja aika-askelta $X = 30$ sekuntia. Tämä kokonaisluku $T$ koodataan sen jälkeen 8-tavuiseksi big-endian 64-bittiseksi etumerkittömäksi kokonaisluvuksi.

3 Vaihe 3: HMAC-SHA1-tiivistelaskenta

Käyttämällä raakoja avaintavuja $K$ salaisena avaimena laskemme HMAC-SHA1-tiivisteen 8-tavuisen big-endian-aikapuskurin yli, jolloin tuloksena saadaan 20-tavuinen (160-bittinen) viestin todennustiiviste $HS$:

HS = HMAC-SHA-1(Key, T)
4 Vaihe 4: Dynaaminen katkaisu (Dynamic Truncation, DT)

Muuttaakseen 20 raakabittiä/tavua kokonaisluvuksi RFC 4226 määrittelee dynaamisen katkaisun:

  • Määritä siirtymä (Offset): Tarkista viimeinen tavu $HS[19]$ ja sovella maskia 0x0F sen alimpien 4 bitin erottamiseksi, mikä antaa siirtymäarvon välillä 0 ja 15: Offset = HS[19] & 0x0F;
  • Leikkaa 4 tavua: Poimi 4 peräkkäistä tavua alkaen kohdasta Offset: $P = HS[Offset \dots Offset+3]$;
  • Maskaa etumerkkibitti: Maskaa eniten merkitsevä bitti (MSB) arvolla 0x7F etumerkillisen kokonaisluvun ylivuodon välttämiseksi:
    BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
5 Vaihe 5: Modulopienennys $N$ numeroon

Lopuksi suorita BinaryCode-luvulle modulo $10^6$ (1 000 000) standardeja 6-numeroisia koodeja varten. Täytä tulos vasemmalta nollilla varmistaaksesi tasaisen 6-merkkisen tulosteen:

OTP = BinaryCode mod 10^6

03. Suoritettava koodi 1: Python 3 -vakiokirjasto

Tämä täydellinen toteutus käyttää Pythonin sisäänrakennettuja moduuleja base64, struct, hmac ja hashlib ilman erillisiä pip-paketteja:

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. Suoritettava koodi 2: JavaScript ja Node.js Crypto

Puhdas JavaScript-toteutus Node.js-taustajärjestelmiin käyttäen natiivia crypto.createHmac-moduulia:

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”));

Asiakaspuolen selainajo W3C Web Crypto API -rajapinnan kautta:

Nykyaikaisille verkkosovelluksille, jotka vaativat 100-prosenttista offline-tietoturvaa (täsmälleen sama arkkitehtuuri, jolla 2FA-Tool toimii), W3C Web Crypto API tarjoaa asynkroniset, laitteistokiihdytetyt kryptografiset primitiivit suoraan selaimen eristetyssä ympäristössä ilman ulkopuolisia npm-paketteja:

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. Suoritettava koodi 3: Tuotantotason Golang (Go)

Suuren suorituskyvyn mikropalvelut hyötyvät Gon koetellusta vakiokirjastosta:

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 liitteen B testivektoreiden varmennus

Standardinmukaisuuden todistamiseksi jokainen kryptografinen toteutus on todennettava asiakirjan IETF RFC 6238 liitteessä B (Appendix B) julkaistuja virallisia testivektoreita vasten.

Virallinen testisarja käyttää ASCII-salaisuutta "12345678901234567890" (20 tavua), joka koodataan Base32-muodossa seuraavasti:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (testattu 8-numeroisilla tulosteilla ja 30 sekunnin aika-askeleilla):

Unix-epookkiaikaleima UTC-päivämäärä ja -aika Virallinen RFC:n odotettu koodi Toteutuksen tulos
59 1970-01-01 00:00:59 94287082 ✓ HYVÄKSYTTY (Tarkka vastaavuus)
1111111109 2005-03-18 01:58:29 07081804 ✓ HYVÄKSYTTY (Tarkka vastaavuus)
1111111111 2005-03-18 01:58:31 14050471 ✓ HYVÄKSYTTY (Tarkka vastaavuus)
1234567890 2009-02-13 23:31:30 89005924 ✓ HYVÄKSYTTY (Tarkka vastaavuus)
2000000000 2033-05-18 03:33:20 69279037 ✓ HYVÄKSYTTY (Tarkka vastaavuus)

07. Tuotantosuunnittelu: aikapoikkeama, toistohyökkäykset ja rajoitukset

Kun TOTP-varmennusta otetaan käyttöön yritystason korkean käytettävyyden todennusjärjestelmissä, on huomioitava kolme kriittistä tietoturvavaatimusta:

Sääntö 1 Kellon driftin toleranssi-ikkuna

Mobiililaitteiden kellojen vähäisten epätarkkuuksien vuoksi palvelimien tulisi arvioida aikaikkunaa [T - 1, T, T + 1] (tarkistaen edelliset 30 s, nykyiset 30 s ja seuraavat 30 s). Älä koskaan laajenna ikkunaa yli $\pm 1$ askeleen, sillä jokainen lisäaskel kasvattaa toistohyökkäysten hyökkäyspinta-alaa 30 sekunnilla.

Sääntö 2 Kertakäyttöisten koodien pakotus (toistohyökkäysten esto)

Koodi on mitätöitävä välittömästi onnistuneen käytön jälkeen. Tallenna Last_Verified_Time_Step Redisiin tai välimuistiin. Jos asiakas yrittää lähettää saman koodin saman 30 sekunnin ikkunan aikana, hylkää pyyntö välimieshyökkäysten (man-in-the-middle replay) estämiseksi.

Sääntö 3 Brute-force-suojaus ja vakioaikainen vertailu (Constant-Time Comparison)

Koska 6-numeroisilla koodeilla on vain 1 000 000 permutaatiota, rajoittamattomat päätepisteet voidaan murtaa nopeasti brute-force-hyökkäyksellä. Ota käyttöön progressiiviset viiveet ja tilin lukitseminen kolmen peräkkäisen virheellisen yrityksen jälkeen. Suorita koodissa koodien vertailu vakioaikaisilla funktioilla (kuten crypto/subtle.ConstantTimeCompare Gossa tai crypto.timingSafeEqual Node.js:ssä) sivukanavien ajoitushyökkäysten estämiseksi.

08. Usein kysytyt kysymykset (UKK)

K1: Koska SHA-1 on kryptografisesti murrettu, miksi TOTP käyttää edelleen oletuksena HMAC-SHA1-tiivistettä? ▼

Teoreettiset törmäyshyökkäykset pelkkää SHA-1-tiivistettä vastaan eivät murra HMAC-rakennetta (RFC 2104). HMAC:ssa taustalla oleva tiivistefunktio on suojattu sisäisillä ja ulkoisilla salaisilla avaimilla, mikä neutraloi törmäyspohjaiset hyökkäykset. Tähän päivään asti HMAC-SHA1 on pysynyt täysin turvallisena 2FA-käytössä. Lisäksi RFC 6238 tukee täysin HMAC-SHA256- ja HMAC-SHA512-algoritmeja, jos vaaditaan korkeampaa suojaustasoa.

K2: Vaikuttavatko aikavyöhykkeet tai kesäaika (DST) TOTP-synkronointiin? ▼

Eivät vaikuta. Unix-epookkiaika on määritelty kuluneina sekunteina hetkestä 1970-01-01 00:00:00 UTC. Se on täysin riippumaton aikavyöhykkeistä, karkaussekunneista tai kesäaikasiirroista. Minä tahansa tarkkana ajanhetkenä Tokiossa sijaitsevalla palvelimella ja Kaliforniassa olevalla älypuhelimella on täsmälleen sama kokonaislukuaikaleima.

K3: Miten Steam Guardin 5-merkkinen todennus eroaa RFC 6238:sta? ▼

Steam Guard on RFC 6238 -standardiin perustuva Valven oma muunnelma. Se käyttää täsmälleen samaa 30 sekunnin aika-askelta ja HMAC-SHA1-laskentaa, mutta viimeisessä numeropienennysvaiheessa se muuntaa saadun kokonaisluvun mukautetun 26-merkkisen aakkosnumeerisen merkkitaulukon (23456789BCDFGHJKMNPQRTVWXY) läpi luodakseen 5-merkkisen merkkijonon.

RFC 6238 -yhteensopiva · Asiakaspuolen laskenta W3C Web Crypto APIlla

Kokeile 100 % asiakaspuolen TOTP-koodien luontia reaaliajassa

2FA-Tool toteuttaa tämän tarkan kryptografisen putken suoraan selaimessasi käyttämällä W3C Web Crypto API -rajapintaa. Ei palvelinsiirtoja, ei tunnisteiden tallennusta, välitön vahvistus.