💻 Criptografia aplicada · Immersió en enginyeria

Com funciona TOTP: Implementació de l'algorisme RFC 6238 en Python, JavaScript i Go

2F Laboratori d'investigació de seguretat de 2FA-Tool · Actualitzat l'octubre de 2026 · 15 min de lectura (més de 2.700 paraules)
💡 Resum arquitectònic i manifest d'enginyeria

D'entre els protocols criptogràfics fonamentals de la seguretat web moderna, l'RFC 6238 (TOTP: Time-Based One-Time Password Algorithm) destaca per la seva senzillesa matemàtica. No requereix negociacions interactives per xarxa, ni infraestructura de clau pública (PKI) centralitzada, ni peticions d'anada i tornada al núvol. Amb només una clau secreta en Base32 compartida i l'avanç constant i uniforme del temps Unix físic, tant el client com el servidor calculen testimonis d'autenticació de 6 dígits idèntics i efímers cada 30 segons amb un 100% de certesa matemàtica.

En aquesta guia exhaustiva, prescindim de les biblioteques de caixes negres de tercers (com ara pyotp o otplib) per construir el pipeline criptogràfic complet de l'RFC 6238 des de zero en Python, JavaScript i Go, verificant cada implementació amb els vectors de prova oficials de l'IETF.

01. L'elegància de l'autenticació basada en el temps

Per entendre TOTP, primer hem de remuntar-nos al seu predecessor: l'RFC 4226 HOTP (HMAC-Based One-Time Password).

A l'HOTP, el client i el servidor comparteixen un comptador d'esdeveniments $C$. Cada vegada que l'usuari genera un testimoni, el comptador s'incrementa ($C = C + 1$) i el testimoni es deriva de $\text{HMAC-SHA-1}(K, C)$. Tot i ser sòlid des del punt de vista matemàtic, l'HOTP presentava una debilitat operativa: la desincronització del comptador. Si un usuari premia inadvertidament el botó de la seva clau física cinc vegades a la butxaca, el comptador del client arribava a $C=6$ mentre que el servidor d'autenticació es quedava a $C=1$, donant lloc a rebutjos sistemàtics a l'inici de sessió.

⏱️ La innovació de l'RFC 6238: Substituir comptadors d'esdeveniments per temps d'època

Publicat el maig de 2011 per N. M'Raihi, S. Machani, M. Pei i J. Rydell, l'RFC 6238 va resoldre el problema de desincronització de comptadors substituint el nombre enter incremental pel temps d'època Unix estandarditzat. Com que el temps físic avança uniformement a tot el món, tant el client com el servidor avancen els seus comptadors interns simultàniament sense enviar ni un sol paquet per la xarxa.

02. Els 5 passos matemàtics i criptogràfics de TOTP

Transformar una llavor en Base32 de 16 caràcters (p. ex., JBSWY3DPEHPK3PXP) en un codi canviant de 6 dígits (p. ex., 284915) implica cinc fases criptogràfiques precises:

1 Pas 1: Descodificació de la clau secreta Base32

Les claus d'inscripció de les plataformes es codifiquen en Base32 segons l'RFC 4648 utilitzant l'alfabet A-Z i 2-7 (ometent caràcters ambigus com 0, 1, 8, 9). L'algorisme primer normalitza aquesta cadena, eliminant espais en blanc i aplicant el farciment (=) perquè la seva longitud sigui múltiple de 8, abans de descodificar-la en bytes binaris de clau en brut $K$.

2 Pas 2: Càlcul del pas de temps (Comptador $T$)

La marca de temps Unix actual (segons transcorreguts des de l'1 de gener de 1970 UTC) es correlaciona amb el comptador enter $T$:

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

Amb els paràmetres estàndard $T_0 = 0$ i una durada del pas $X = 30$ segons. Aquest enter $T$ es codifica després com un enter sense signe de 64 bits en format big-endian de 8 bytes.

3 Pas 3: Càlcul del hash HMAC-SHA1

Utilitzant els bytes binaris de clau $K$ com a clau secreta, calculem el resum HMAC-SHA1 sobre el búfer temporal de 8 bytes en big-endian, obtenint un resum d'autenticació de missatges $HS$ de 20 bytes (160 bits):

HS = HMAC-SHA-1(Key, T)
4 Pas 4: Truncament dinàmic (DT)

Per convertir 20 bytes en brut en un nombre enter, l'RFC 4226 especifica el truncament dinàmic:

  • Extreure el desplaçament (Offset): S'inspecciona l'últim byte $HS[19]$ i s'aplica una màscara amb 0x0F per extreure els seus 4 bits inferiors, obtenint un valor de desplaçament entre 0 i 15: Offset = HS[19] & 0x0F;
  • Extreure 4 bytes: S'extreuen 4 bytes consecutius a partir de la posició Offset: $P = HS[Offset \dots Offset+3]$;
  • Emmascarar el bit de signe: S'emmascara el bit més significatiu (MSB) amb 0x7F per evitar ambigüitats de desbordament de nombres enters amb signe:
    BinaryCode = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
5 Pas 5: Reducció mòdul a $N$ dígits

Finalment, s'aplica el mòdul $10^6$ (1.000.000) a BinaryCode per obtenir testimonis estàndard de 6 dígits. S'emplena el resultat amb zeros a l'esquerra per garantir una cadena de sortida coherent de 6 caràcters:

OTP = BinaryCode mod 10^6

03. Codi executable 1: Biblioteca estàndard de Python 3

Aquesta implementació completa utilitza els mòduls integrats de Python base64, struct, hmac i hashlib sense requerir cap paquet de pip addicional:

totp_rfc6238.py Python 3.8+ (Sense dependències)
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. Codi executable 2: Criptografia en JavaScript i Node.js

Implementació en JavaScript pur per a entorns d'execució de backend de Node.js utilitzant el mòdul natiu crypto.createHmac:

totp_node.js Criptografia nativa de 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”));

Execució al navegador al costat del client mitjançant la Web Crypto API del W3C:

Per a aplicacions web modernes que requereixen una seguretat 100% fora de línia (l'arquitectura exacta que impulsa 2FA-Tool), la Web Crypto API del W3C proporciona primitives criptogràfiques asíncrones i accelerades per maquinari directament a l'entorn aïllat del navegador sense dependències de paquets npm de tercers:

webcrypto_totp.js Web Crypto API del W3C per a navegadors
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. Codi executable 3: Golang (Go) per a entorns de producció

Els microserveis d'alt rendiment es beneficien de la biblioteca estàndard provada en batalla de Go:

totp.go Biblioteca estàndard de 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] & 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. Verificació amb els vectors de prova oficials de l'apèndix B de l'RFC 6238

Per demostrar el compliment de l'estàndard, tota implementació criptogràfica s'ha de verificar contra els vectors de prova oficials publicats a l'apèndix B de l'RFC 6238 de l'IETF.

El conjunt de proves oficial utilitza la clau secreta ASCII "12345678901234567890" (20 bytes), que es codifica en Base32 com a:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (avaluat per a sortides de 8 dígits amb passos de 30 segons):

Marca de temps Unix Epoch Data i hora UTC Codi oficial esperat per l'RFC Resultat de la implementació
59 1970-01-01 00:00:59 94287082 ✓ SUPERAT (Coincidència exacta)
1111111109 2005-03-18 01:58:29 07081804 ✓ SUPERAT (Coincidència exacta)
1111111111 2005-03-18 01:58:31 14050471 ✓ SUPERAT (Coincidència exacta)
1234567890 2009-02-13 23:31:30 89005924 ✓ SUPERAT (Coincidència exacta)
2000000000 2033-05-18 03:33:20 69279037 ✓ SUPERAT (Coincidència exacta)

07. Enginyeria de producció: Desviació, reproducció i límits de peticions

En desplegar la verificació TOTP en sistemes d'autenticació empresarial d'alta disponibilitat, cal aplicar tres requisits fonamentals de seguretat:

Regla 1 Finestra de tolerància a la desviació del rellotge

A causa de petites imprecisions del rellotge dels dispositius mòbils, els servidors han d'avaluar la finestra [T - 1, T, T + 1] (comprovant els 30 s anteriors, els 30 s actuals i els 30 s següents). Mai no amplieu la finestra més enllà de $\pm 1$ pas, ja que cada pas addicional augmenta la superfície d'atacs de reproducció en 30 segons.

Regla 2 Aplicació estricta de testimonis d'un sol ús (Antireproducció)

Un testimoni s'ha d'invalidar immediatament un cop s'ha consumit amb èxit. Emmagatzemeu Last_Verified_Time_Step a Redis o a la memòria cau. Si un client intenta enviar el mateix testimoni dins de la mateixa finestra de 30 segons, rebutgeu la petició per neutralitzar atacs de reproducció de tipus intermediari (Man-in-the-Middle).

Regla 3 Limitació de velocitat contra força bruta i comparació en temps constant

Com que els codis de 6 dígits només tenen 1.000.000 de permutacions possibles, els punts finals sense limitació es poden vulnerar per força bruta ràpidament. Apliqueu retards progressius i bloquejos de compte després de 3 intents fallits consecutius. Al codi, feu les comparacions amb funcions d'igualtat en temps constant (p. ex., crypto/subtle.ConstantTimeCompare a Go o crypto.timingSafeEqual a Node.js) per evitar atacs d'anàlisi de temps de canal lateral.

08. Preguntes freqüents (FAQ)

P1: Com que SHA-1 s'ha trencat criptogràficament, per què el TOTP continua utilitzant HMAC-SHA1 per defecte? ▼

Els atacs teòrics de col·lisió contra SHA-1 directe no trenquen la construcció HMAC (RFC 2104). A HMAC, la funció de resum subjacent està encapsulada dins de claus secretes internes i externes, cosa que neutralitza els atacs basats en col·lisions. Fins al moment, HMAC-SHA1 continua sent segur per al 2FA. A més, l'RFC 6238 admet totalment HMAC-SHA256 i HMAC-SHA512 si es requereix un nivell de garantia més alt.

P2: Les zones horàries o l'horari d'estiu (DST) afecten la sincronització del TOTP? ▼

No. El temps d'època Unix es defineix com els segons transcorreguts des de 1970-01-01 00:00:00 UTC. És estrictament independent de les zones horàries, els segons intercalars i els canvis d'horari d'estiu. En qualsevol moment exacte, un servidor a Tòquio i un telèfon intel·ligent a Califòrnia tenen exactament la mateixa marca de temps entera.

P3: En què es diferencia l'autenticació de 5 lletres d'Steam Guard de l'RFC 6238? ▼

Steam Guard és una variant propietària basada en l'RFC 6238. Utilitza exactament el mateix pas de temps de 30 segons i el mateix càlcul HMAC-SHA1, però en la fase final de reducció, fa correspondre l'enter extret amb una matriu personalitzada de 26 caràcters alfanumèrics (23456789BCDFGHJKMNPQRTVWXY) per produir una cadena de 5 caràcters.

Compliment de l'RFC 6238 · Càlcul al costat del client amb la Web Crypto API del W3C

Experimenteu la generació de codis TOTP 100% al client en directe

2FA-Tool implementa exactament aquest pipeline criptogràfic directament al vostre navegador mitjançant la Web Crypto API del W3C. Zero transmissions a servidors, zero emmagatzematge de testimonis i validació instantània.