Entre los protocolos criptográficos fundamentales en la seguridad informática moderna, RFC 6238 (TOTP: Algoritmo de Contraseñas de Un Solo Uso Basadas en el Tiempo) destaca por su brillante simplicidad matemática. No requiere intercambios de red interactivos, ni infraestructura de clave pública (PKI) centralizada, ni consultas a servidores en la nube. Con únicamente una clave secreta compartida en Base32 y el avance físico e inexorable del tiempo Unix, el cliente y el servidor calculan tokens de autenticación de 6 dígitos idénticos y efímeros cada 30 segundos con un 100% de certeza matemática.
Los códigos se calculan de forma 100% local e independiente sin enviar un solo paquete de red.
Base32 → Paso T (30s) → HMAC-SHA1 → Truncamiento Dinámico → Módulo 10^6.
Implementable con las bibliotecas estándar nativas de Python, Node.js / Web Crypto y Go.
En esta guía exhaustiva, prescindimos de bibliotecas de terceros de caja negra (como pyotp u otplib) para construir desde cero el flujo criptográfico completo del RFC 6238 en Python, JavaScript y Go, verificando rigurosamente cada implementación frente a los vectores de prueba oficiales del IETF.
01. La elegancia de la autenticación basada en el tiempo
Para entender TOTP en profundidad, primero debemos analizar a su predecesor directo: RFC 4226 HOTP (HMAC-Based One-Time Password).
En HOTP, el cliente y el servidor comparten un contador de eventos discreto \(C\). Cada vez que el usuario genera un código, el contador se incrementa (\(C = C + 1\)) y el token se deriva a través de la función \(\text{HMAC-SHA-1}(K, C)\). Aunque matemáticamente sólido, HOTP sufría una grave fragilidad operativa: la desincronización del contador. Si un usuario presionaba accidentalmente el botón físico de su llavero cinco veces dentro del bolsillo, el contador del dispositivo avanzaba a \(C=6\) mientras el servidor de autenticación permanecía en \(C=1\), provocando rechazos sistemáticos de acceso hasta requerir una resincronización manual.
Publicado en mayo de 2011 por N. M'Raihi, S. Machani, M. Pei y J. Rydell, el estándar RFC 6238 resolvió definitivamente el problema de desincronización sustituyendo el entero incremental por la marca de tiempo estandarizada de la época Unix. Debido a que el tiempo físico avanza de forma homogénea en todo el planeta, tanto el dispositivo cliente como el servidor avanzan sus contadores internos al unísono sin requerir el intercambio de ningún paquete de red.
02. Los 5 pasos matemáticos y criptográficos de TOTP
Transformar una semilla secreta de 16 caracteres en Base32 (como JBSWY3DPEHPK3PXP) en un código numérico rotativo de 6 dígitos (como 284915) requiere atravesar cinco fases criptográficas rigurosas:
Las claves secretas compartidas proporcionadas por las plataformas se codifican según el estándar RFC 4648 Base32 mediante el alfabeto A-Z y 2-7 (excluyendo caracteres visualmente ambiguos como 0, 1, 8, 9). El algoritmo primero sanea la cadena, eliminando espacios en blanco y agregando caracteres de relleno (=) para que su longitud sea múltiplo exacto de 8, antes de decodificarla en los bytes binarios de la clave secreta \(K\).
La marca de tiempo Unix actual (segundos transcurridos desde el 1 de enero de 1970 UTC) se convierte en un contador entero \(T\) mediante división entera:
Con los parámetros estándar de la industria \(T_0 = 0\) y duración de paso \(X = 30\) segundos. Posteriormente, este entero \(T\) se codifica en un búfer binario de 8 bytes (64 bits) en formato big-endian sin signo.
Utilizando los bytes binarios decodificados \(K\) como clave criptográfica, se calcula el resumen HMAC-SHA1 sobre el búfer de 8 bytes del contador de tiempo, obteniendo un hash de autenticación de mensajes de 20 bytes (160 bits) \(HS\):
Para transformar los 20 bytes del resumen en un entero de 32 bits utilizable, el estándar RFC 4226 estipula el proceso de Truncamiento Dinámico:
- Extracción del desplazamiento (Offset): Se inspecciona el último byte \(HS[19]\) y se le aplica una máscara lógica
0x0Fpara extraer sus 4 bits inferiores, resultando en un índice de desplazamiento entre 0 y 15:Offset = HS[19] & 0x0F; - Extracción de 4 bytes contiguos: Se seleccionan 4 bytes consecutivos comenzando en la posición del desplazamiento: \(P = HS[Offset \dots Offset+3]\);
- Enmascaramiento del bit de signo (MSB): Se aplica una máscara con
0x7Fal bit más significativo para evitar ambigüedades derivadas de enteros con signo:
CodigoBinario = ((P[0] & 0x7F) << 24) | ((P[1] & 0xFF) << 16) | ((P[2] & 0xFF) << 8) | (P[3] & 0xFF)
Por último, se aplica la operación módulo \(10^6\) (1.000.000) sobre CodigoBinario para obtener tokens estándar de 6 dígitos. El resultado se formatea rellenando con ceros a la izquierda (left-pad) para garantizar una cadena inmutable de longitud fija:
03. Código ejecutable 1: Biblioteca estándar de Python 3
Esta implementación completa utiliza exclusivamente los módulos nativos de Python base64, struct, hmac y hashlib, sin requerir ninguna dependencia externa de pip:
import base64 import hashlib import hmac import struct import timedef generar_totp(secreto_base32: str, marca_tiempo: int = None, paso: int = 30, digitos: int = 6) -> str: """ Genera un token TOTP conforme al estándar RFC 6238. :param secreto_base32: Clave secreta compartida en formato Base32 (ej. ‘JBSWY3DPEHPK3PXP’) :param marca_tiempo: Marca de tiempo Unix en segundos. Por defecto toma time.time() :param paso: Ventana de tiempo en segundos (por defecto: 30) :param digitos: Número de dígitos del código generado (por defecto: 6) :return: Código OTP formateado con ceros a la izquierda (ej. ‘045812’) """ if marca_tiempo is None: marca_tiempo = int(time.time())
1. Normalizar la cadena Base32 y agregar el relleno ’=’ necesario
secreto_normalizado = secreto_base32.replace(” ”, "").upper() relleno_necesario = (8 - len(secreto_normalizado) % 8) % 8 secreto_normalizado += ”=” * relleno_necesario bytes_clave = base64.b32decode(secreto_normalizado)
2. Calcular el contador de tiempo T como entero de 64 bits en formato big-endian
contador_tiempo = marca_tiempo // paso bytes_tiempo = struct.pack(“>Q”, contador_tiempo) # 8 bytes big-endian sin signo
3. Calcular el resumen HMAC-SHA1 (20 bytes)
resumen_hmac = hmac.new(bytes_clave, bytes_tiempo, hashlib.sha1).digest()
4. Aplicar el Truncamiento Dinámico (Dynamic Truncation)
desplazamiento = resumen_hmac[-1] & 0x0F # 4 bits inferiores del byte 20
Desempaquetar 4 bytes en el desplazamiento y enmascarar el bit de signo MSB
codigo_binario = struct.unpack(“>I”, resumen_hmac[desplazamiento : desplazamiento + 4])[0] & 0x7FFFFFFF
5. Operación de módulo y formateo con ceros a la izquierda
codigo_otp = codigo_binario % (10 ** digitos) return str(codigo_otp).zfill(digitos)
if name == “main”: clave_prueba = “JBSWY3DPEHPK3PXP” print(f”Código TOTP Generado: {generar_totp(clave_prueba)}”)
04. Código ejecutable 2: JavaScript y Node.js Crypto
Implementación en JavaScript puro para entornos de backend Node.js utilizando el módulo nativo crypto.createHmac:
const crypto = require('crypto');// Decodificador Base32 RFC 4648 en JavaScript Puro function decodificarBase32(strBase32) { const alfabeto = ‘ABCDEFGHIJKLMNOPQRSTUVWXYZ234567’; const limpio = strBase32.replace(/=+$/, ”).toUpperCase().replace(/\s+/g, ”); let bits = 0, valor = 0, indice = 0; const salida = new Uint8Array(Math.floor((limpio.length * 5) / 8));
for (let i = 0; i < limpio.length; i++) { const val = alfabeto.indexOf(limpio[i]); if (val === -1) continue; valor = (valor << 5) | val; bits += 5; if (bits >= 8) { salida[indice++] = (valor >>> (bits - 8)) & 255; bits -= 8; } } return Buffer.from(salida.buffer, 0, indice); }
function generarTOTP(secretoBase32, marcaTiempo = Math.floor(Date.now() / 1000), paso = 30, digitos = 6) { const clave = decodificarBase32(secretoBase32); const contador = Math.floor(marcaTiempo / paso);
// Asignar búfer de 8 bytes en formato Big-Endian const buffer = Buffer.alloc(8); buffer.writeBigUInt64BE(BigInt(contador), 0);
// Calcular HMAC-SHA1 const hmac = crypto.createHmac(‘sha1’, clave).update(buffer).digest();
// Truncamiento Dinámico (Dynamic Truncation) const desplazamiento = hmac[hmac.length - 1] & 0x0f; const binario = ((hmac[desplazamiento] & 0x7f) << 24) | ((hmac[desplazamiento + 1] & 0xff) << 16) | ((hmac[desplazamiento + 2] & 0xff) << 8) | (hmac[desplazamiento + 3] & 0xff);
const otp = binario % (10 ** digitos); return otp.toString().padStart(digitos, ‘0’); }
console.log(“TOTP Calculado:”, generarTOTP(“JBSWY3DPEHPK3PXP”));
Ejecución en el navegador mediante la API W3C Web Crypto:
Para aplicaciones web modernas que priorizan una seguridad 100% fuera de línea (la arquitectura exacta que impulsa a 2FA-Tool), la API W3C Web Crypto proporciona primitivas criptográficas asíncronas y aceleradas por hardware directamente dentro del navegador, sin paquetes de terceros:
async function generarTOTPEnNavegador(secretoBase32, marcaTiempo = Math.floor(Date.now() / 1000)) { const bytesClave = decodificarBase32(secretoBase32); const contador = Math.floor(marcaTiempo / 30);// 1. Importar la clave secreta binaria en SubtleCrypto const claveCripto = await window.crypto.subtle.importKey( “raw”, bytesClave, { name: “HMAC”, hash: { name: “SHA-1” } }, false, [“sign”] );
// 2. Codificar búfer contador de 8 bytes en formato Big-Endian const buffer = new ArrayBuffer(8); const vista = new DataView(buffer); vista.setBigUint64(0, BigInt(contador), false); // false = Big-Endian
// 3. Ejecutar la firma criptográfica HMAC const firma = await window.crypto.subtle.sign(“HMAC”, claveCripto, buffer); const resultadoHmac = new Uint8Array(firma);
// 4. Truncamiento Dinámico const desplazamiento = resultadoHmac[resultadoHmac.length - 1] & 0x0f; const binario = ((resultadoHmac[desplazamiento] & 0x7f) << 24) | ((resultadoHmac[desplazamiento + 1] & 0xff) << 16) | ((resultadoHmac[desplazamiento + 2] & 0xff) << 8) | (resultadoHmac[desplazamiento + 3] & 0xff);
const otp = binario % 1000000; return otp.toString().padStart(6, ‘0’); }
05. Código ejecutable 3: Go (Golang) para entornos de producción
Los microservicios de alto rendimiento se benefician enormemente de la biblioteca estándar de Go, probada en entornos empresariales:
package mainimport ( “crypto/hmac” “crypto/sha1” “encoding/base32” “encoding/binary” “fmt” “math” “strings” “time” )
// GenerarTOTP calcula tokens temporales conformes al estándar RFC 6238 func GenerarTOTP(secretoBase32 string, marcaTiempo int64, paso int64, digitos int) (string, error) { // 1. Sanear la clave Base32 y decodificar limpio := strings.ToUpper(strings.ReplaceAll(secretoBase32, ” ”, "")) clave, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(limpio) if err != nil { clave, err = base32.StdEncoding.DecodeString(limpio) if err != nil { return "", err } }
// 2. Codificar el contador de tiempo de 64 bits en formato Big-Endian contador := uint64(marcaTiempo / paso) buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, contador)
// 3. Calcular HMAC-SHA1 mac := hmac.New(sha1.New, clave) mac.Write(buf) hash := mac.Sum(nil)
// 4. Truncamiento Dinámico desplazamiento := hash[len(hash)-1] & 0x0f codigoBinario := binary.BigEndian.Uint32(hash[desplazamiento:desplazamiento+4]) & 0x7fffffff
// 5. Aritmética modular mod := uint32(math.Pow10(digitos)) otp := codigoBinario % mod
formato := fmt.Sprintf(“%%0%dd”, digitos) return fmt.Sprintf(formato, otp), nil }
func main() { codigo, err := GenerarTOTP(“JBSWY3DPEHPK3PXP”, time.Now().Unix(), 30, 6) if err != nil { panic(err) } fmt.Printf(“TOTP Calculado en Go: %s\n”, codigo) }
06. Verificación de vectores de prueba del Apéndice B del RFC 6238
Para demostrar la rigurosa conformidad con el estándar, toda implementación criptográfica debe verificarse frente a los vectores de prueba oficiales publicados en el Apéndice B de IETF RFC 6238.
La suite de pruebas oficial utiliza la clave secreta ASCII "12345678901234567890" (20 bytes), cuya representación en Base32 es:
GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ (evaluada para salidas de 8 dígitos con pasos temporales de 30 segundos):
| Marca de Tiempo Unix | Fecha y Hora UTC | Código Oficial Esperado RFC | Resultado de la Implementación |
|---|---|---|---|
| 59 | 1970-01-01 00:00:59 | 94287082 | ✓ SUPERADO (Coincidencia Exacta) |
| 1111111109 | 2005-03-18 01:58:29 | 07081804 | ✓ SUPERADO (Coincidencia Exacta) |
| 1111111111 | 2005-03-18 01:58:31 | 14050471 | ✓ SUPERADO (Coincidencia Exacta) |
| 1234567890 | 2009-02-13 23:31:30 | 89005924 | ✓ SUPERADO (Coincidencia Exacta) |
| 2000000000 | 2033-05-18 03:33:20 | 69279037 | ✓ SUPERADO (Coincidencia Exacta) |
07. Ingeniería en producción: Desfase temporal, ataques de repetición y límites
Al desplegar la verificación TOTP en sistemas empresariales de alta disponibilidad, es imprescindible implementar tres mecanismos de defensa críticos:
Debido a ligeras descalibraciones en los relojes de los teléfonos móviles, los servidores de autenticación deben verificar la ventana temporal [T - 1, T, T + 1] (examinando los 30s anteriores, los 30s actuales y los 30s siguientes). Nunca amplíe la ventana más allá de \(\pm 1\) paso, ya que cada intervalo adicional expande la superficie de exposición a ataques de repetición en 30 segundos.
Cada token debe invalidarse de forma atómica e irreversible tras su primer uso exitoso. Almacene el identificador Ultimo_Paso_Tiempo_Verificado en Redis o en una memoria caché distribuida. Si un atacante o cliente intenta enviar el mismo código dentro de la misma ventana de 30 segundos, la solicitud debe rechazarse de inmediato para frustrar ataques de intermediario (Man-in-the-Middle).
Puesto que un código de 6 dígitos sólo abarca 1.000.000 de combinaciones posibles, los puntos de conexión sin tasa límite pueden ser atacados por fuerza bruta en pocos minutos. Aplique penalizaciones con retrasos progresivos y bloquee temporalmente la cuenta tras 3 intentos fallidos consecutivos. En el código fuente, realice las comparaciones de contraseñas utilizando funciones en tiempo constante (como crypto/subtle.ConstantTimeCompare en Go o crypto.timingSafeEqual en Node.js) para neutralizar ataques de canal lateral basados en análisis de tiempos de respuesta.
08. Preguntas frecuentes (FAQ)
P1: Dado que SHA-1 está roto criptográficamente, ¿por qué TOTP sigue utilizando HMAC-SHA1 por defecto? ▼
Los ataques teóricos de colisión contra la función SHA-1 pura no comprometen la construcción HMAC (RFC 2104). En HMAC, la función hash subyacente opera anidada con claves secretas internas y externas, lo que neutraliza los ataques basados en búsqueda de colisiones. Hasta la fecha, HMAC-SHA1 permanece criptográficamente seguro para 2FA. Además, el estándar RFC 6238 contempla soporte completo para HMAC-SHA256 y HMAC-SHA512 si las políticas de cumplimiento exigen algoritmos superiores.
P2: ¿Afectan las zonas horarias o el cambio de hora de verano (DST) a la sincronización de TOTP? ▼
No. La marca de tiempo de la época Unix se define formalmente como los segundos transcurridos desde 1970-01-01 00:00:00 UTC. Es completamente agnóstica respecto a husos horarios locales, segundos intercalares y horarios de verano. En cualquier milisegundo exacto, un servidor ubicado en Tokio y un smartphone en Madrid o Ciudad de México comparten exactamente el mismo entero temporal.
P3: ¿En qué se diferencia la autenticación de 5 caracteres de Steam Guard del estándar RFC 6238? ▼
Steam Guard es una variante propietaria basada en los fundamentos de RFC 6238. Conserva la misma ventana temporal de 30 segundos y el mismo algoritmo HMAC-SHA1, pero en la fase final de reducción modular, sustituye el cálculo sobre potencias de 10 por una proyección sobre un alfabeto alfanumérico personalizado de 26 caracteres (23456789BCDFGHJKMNPQRTVWXY), produciendo una cadena alfanumérica de 5 caracteres en lugar de los 6 dígitos numéricos convencionales.
📚 Guías de Seguridad Relacionadas
Cómo transferir cuentas 2FA entre teléfonos móviles mediante códigos QR sin conexión y sincronización en la nube.
Análisis criptográfico exhaustivo de RFC 4226 frente a RFC 6238, desincronización y escalabilidad.
Comparativa técnica detallada: Aegis Authenticator vs 2FAS vs Bitwarden vs Google Authenticator.
Prueba tu Clave Secreta 2FA en 2FA-Tool en Tiempo Real
2FA-Tool implementa este flujo criptográfico exacto directamente dentro de su navegador mediante la API W3C Web Crypto. Cero transmisiones a servidores externos, cero almacenamiento de claves y validación matemática instantánea.