Cómo funciona TOTP: el algoritmo detrás de los códigos de tu aplicación de autenticación
Tecleas un código de 6 dígitos desde una aplicación de autenticación varias veces por semana, y de algún modo el mismo código aparece en tu teléfono y coincide con lo que el servidor espera, aunque los dos nunca se hablan. ¿Cómo funciona TOTP? Detrás hay un algoritmo pequeño y determinista que ambos lados ejecutan por su cuenta: el mismo secreto compartido produce un número nuevo cada 30 segundos. Ningún código viaja por la red, y no hay ningún servidor central repartiendo el número.
TOTP (Time-based One-Time Password), definido en RFC 6238, convierte un secreto compartido más la hora actual en un código numérico corto: ejecuta un HMAC sobre el tiempo y trunca el resultado. Como la autenticación de dos factores (2FA) depende de que ambos lados calculen el mismo valor sin intercambiarlo, todo el modelo de confianza descansa en ese algoritmo.
Esta guía recorre el algoritmo de principio a fin con números concretos y luego cubre la mitad que casi nadie explica: cómo verifica un código un servidor, y qué detiene la 2FA y qué no. Si quieres calcular un código en vivo mientras lees, tienes nuestro generador de TOTP.
¿Qué es TOTP, en realidad?
TOTP (Time-based One-Time Password), definido en RFC 6238, es un algoritmo que combina un secreto compartido con la hora actual para producir un código corto que cambia en un intervalo fijo. Tanto la aplicación de autenticación como el servidor guardan el mismo secreto, leen el mismo reloj y ejecutan la misma operación matemática, así que llegan al mismo código sin transmitirlo nunca.
Vale la pena detenerse en ese último punto. El código nunca se envía durante la configuración; solo se envía el secreto, y a partir de ahí cada lado deriva los códigos por su cuenta. No hay nada que interceptar en el cable, salvo el secreto en el momento del registro y los 6 dígitos que el usuario teclea al iniciar sesión. Piénsalo como tres entradas que se colapsan en una sola salida:
| Entrada | Función | Valor típico |
|---|---|---|
| Secreto compartido | La clave de larga duración, acordada una vez en el registro | JBSWY3DPEHPK3PXP (Base32) |
| Paso de tiempo | El contador que avanza | Ventana de 30 segundos |
| Salida | El código corto derivado de los dos | 324550 |
El secreto casi siempre se escribe en Base32 (las letras A–Z y los dígitos 2–7) porque ese alfabeto no distingue mayúsculas de minúsculas y aguanta que lo impriman, lo tecleen o lo metan en un código QR. Para registrar un secreto, escaneas una URI otpauth://, que puedes generar como un QR de autenticación, o escribes a mano la cadena Base32.
TOTP vs HOTP vs SMS vs passkeys: el panorama de la 2FA
TOTP es una opción entre varias, y para elegir bien hay que ver todo el campo. La idea de una línea que conviene tener clara: TOTP es HOTP con el contador reemplazado por el número de pasos de tiempo desde la época Unix. Lo demás es un compromiso entre resistencia al phishing, comodidad y la infraestructura que necesitas.
| Mecanismo | Lo que lo impulsa | Vida útil del código | ¿Resistente al phishing? | ¿Necesita red? | Uso típico |
|---|---|---|---|---|---|
| HOTP (RFC 4226) | Contador incremental | Hasta usarse | No | No | Tokens de hardware, sistemas heredados |
| TOTP (RFC 6238) | Hora actual | ~30 segundos | No | No (tras el registro) | Aplicaciones de autenticación |
| OTP por SMS | El servidor envía un código | Unos minutos | No | Sí (celular) | Respaldo de consumo |
| Aprobación push | Aviso del servidor a un dispositivo | Por solicitud | Parcialmente | Sí | 2FA basada en app |
| Passkey / FIDO2 | Desafío de clave pública | Por solicitud | Sí (ligada al origen) | Sí | Cuentas modernas |
La tabla deja ver el patrón. TOTP y HOTP funcionan sin conexión una vez registrados, lo que los hace resilientes y privados, pero por sí solos ninguno resiste el phishing: una página falsa convincente puede pedir el código y retransmitirlo. El SMS añade un canal de red, y con él su propia superficie de ataque. Las passkeys cierran la brecha del phishing al ligar la credencial al origen del sitio, y por eso la industria avanza en esa dirección. TOTP queda en un término medio cómodo: es fuerte, está en todas partes y es gratis. De ahí que siga siendo tan común.
Cómo funciona el algoritmo TOTP, paso a paso
El algoritmo completo cabe en cuatro pasos. Ejecutaremos cada uno con el secreto de prueba del RFC JBSWY3DPEHPK3PXP y una hora Unix fija de 1700000000, para que cada número sea reproducible.
- Decodifica el secreto Base32 a los bytes crudos de la clave.
- Calcula el contador de paso de tiempo a partir de la hora Unix actual.
- Aplica HMAC al contador con la clave secreta.
- Trunca el digest hasta un código de 6 dígitos.
Paso 1 — Decodificar el secreto Base32 a bytes
Base32 empaqueta 5 bits en cada carácter. El decodificador reagrupa los caracteres en bytes de 8 bits. El secreto JBSWY3DPEHPK3PXP se decodifica en los 10 bytes crudos 48 65 6c 6c 6f 21 de ad be ef. Este arreglo de bytes, no la cadena imprimible, es la clave del HMAC.
Paso 2 — Calcular el contador de paso de tiempo
El contador es el número de pasos de tiempo completos transcurridos desde un punto de partida: T = floor((unixTime − T0) / period). Los valores predeterminados del RFC son T0 = 0 (la época Unix) y period = 30. Con unixTime = 1700000000, eso da T = floor(1700000000 / 30) = 56666666. Este entero se codifica luego como un valor big-endian de 8 bytes: 00 00 00 00 03 60 aa 2a. Como el contador solo cambia cuando comienza una nueva ventana de 30 segundos, cada código es estable durante una ventana y luego salta.
Paso 3 — Aplicar HMAC al contador con el secreto
El algoritmo ejecuta HMAC-SHA1 sobre el contador de 8 bytes usando los bytes del secreto como clave. HMAC es una función con clave y de un solo sentido: sin el secreto no puedes revertir el digest ni falsificar uno válido, y por eso el código no se puede falsificar. Para nuestras entradas, el digest son los 20 bytes 1d 70 6e 94 1a c7 6b 6d 4a 46 dd 6f af a4 5f e3 35 11 bf 86.
Paso 4 — Truncamiento dinámico a un código de 6 dígitos (RFC 4226)
Un digest de 20 bytes es demasiado largo para teclear, así que el truncamiento dinámico del RFC 4226 extrae un número de él. Toma el nibble bajo del último byte como desplazamiento (offset): el último byte es 0x86, su nibble bajo es 6, así que el offset es 6. Lee 4 bytes a partir de ese offset (6b 6d 4a 46), enmascara el bit superior del primero para mantener el número positivo, y obtienes el entero 1802324550. Redúcelo módulo 10^6 y rellena con ceros a la izquierda: 1802324550 % 1000000 = 324550. Ese es el código que tu aplicación muestra para este secreto en este instante.
Este es el algoritmo en JavaScript con la API Web Crypto nativa del navegador, sin dependencias. Cada comentario asigna un bloque a uno de los cuatro pasos anteriores:
// TOTP según RFC 6238 — SHA-1, 6 dígitos, periodo de 30s (los predeterminados).
async function generateTotp(base32Secret, unixTime = Date.now() / 1000) {
// Paso 1: decodifica el secreto Base32 (A-Z, 2-7) a los bytes crudos de la clave.
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)));
// Paso 2: contador = número de pasos de 30s desde la época (big-endian de 8 bytes).
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);
}
// Paso 3: HMAC-SHA1 del contador con la clave secreta.
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));
// Paso 4: truncamiento dinámico (RFC 4226) -> código de 6 dígitos.
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"
El mismo algoritmo en Python, usando solo la biblioteca estándar (hmac y 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()
# Paso 1: decodifica el secreto en Base32 a los bytes crudos de la clave.
key = base64.b32decode(secret.upper())
# Paso 2: contador = número de pasos de tiempo desde la época (big-endian de 8 bytes).
counter = int(for_time // period)
msg = struct.pack(">Q", counter)
# Paso 3: aplica HMAC al contador con el secreto.
h = hmac.new(key, msg, digest).digest()
# Paso 4: truncamiento dinámico (RFC 4226) -> código de N dígitos.
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
Ambas implementaciones imprimen 324550 para nuestra hora fija, y ambas reproducen los vectores de prueba oficiales del RFC 6238 (por ejemplo, el vector SHA-1 en T = 59 produce 94287082). Si cambias SHA-1 por SHA-256 o SHA-512, o cambias el número de dígitos, el verificador del otro lado tiene que usar exactamente las mismas opciones o los códigos nunca van a concordar.
Verificar un código TOTP en el servidor
Generar un código es la mitad del sistema. La otra mitad es que el servidor decida si acepta los 6 dígitos que el usuario acaba de teclear, y ahí es donde se acumulan las decisiones sensibles para la seguridad.
El servidor no almacena códigos. Almacena el secreto y, al iniciar sesión, recalcula el código esperado a partir de ese secreto y la hora actual, y luego compara. La parte delicada es la deriva del reloj: el dispositivo del usuario y el servidor casi nunca coinciden al segundo exacto, así que una comprobación de igualdad estricta rechazaría códigos cercanos al límite de una ventana. La solución es una pequeña ventana de validación: aceptar el paso actual y un paso a cada lado, es decir, comprobar los códigos para los contadores T−1, T y T+1. Una ventana más amplia tolera más deriva pero agranda la superficie de adivinación, así que la ventana 1 (una tolerancia de ±30 segundos) es lo habitual. Es la misma tolerancia de ±1 paso que puedes ver en la pestaña Verificar de la herramienta.
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);
// Comprueba el paso actual y ±window pasos para la deriva del reloj.
for (let i = -window; i <= window; i++) {
const expected = Buffer.from(totpAt(secret, counter + i, digits));
// Comparación en tiempo constante para que el timing no filtre una coincidencia parcial.
if (expected.length === submitted.length &&
timingSafeEqual(expected, submitted)) {
return counter + i; // paso coincidente — guárdalo para bloquear la reproducción
}
}
return false;
}
Dos detalles más llevan esto de “funciona” a “es seguro”. Primero, prevención de reproducción (replay): almacena el último contador que aceptaste para cada usuario y rechaza cualquier código de un paso igual o inferior, para que un código capturado una vez no pueda reutilizarse dentro de la misma ventana. Por eso verifyTotp devuelve el paso coincidente y no un simple true. Segundo, limitación de tasa (rate limiting): un código de 6 dígitos es uno entre un millón de valores, y una ventana de ±1 deja tres válidos en cualquier momento, así que sin estrangulamiento un atacante puede recorrer el espacio por fuerza bruta. Bloquea la cuenta o añade un retardo (backoff) tras un puñado de fallos. Por último, el secreto es una clave de larga duración: cífralo en reposo, mantenlo fuera del control de versiones y trátalo igual que una contraseña. Genera junto a él códigos de recuperación fuertes para el día en que se pierda un dispositivo.
De qué protege TOTP, y de qué no
TOTP mejora de verdad la seguridad frente a usar solo contraseñas, pero no lo arregla todo, y las páginas de marketing suelen callar sus puntos ciegos. Esta es la división, sin adornos.
| TOTP detiene | TOTP NO detiene |
|---|---|
| Contraseñas filtradas o reutilizadas | Phishing en tiempo real / adversario en el medio |
| Relleno de credenciales (credential stuffing) | Malware que lee el secreto del dispositivo |
| Fuerza bruta remota de contraseñas | Flujos de recuperación de cuenta débiles que se saltan la 2FA |
| Una filtración de BD que expone solo hashes de contraseñas | (estos necesitan otras defensas) |
Las ganancias son grandes. Ahora el inicio de sesión exige un código que solo el secreto puede producir, así que una contraseña filtrada ya no basta, y eso liquida de golpe el relleno de credenciales y la fuerza bruta remota. Si tu base de datos se filtra pero los secretos TOTP están cifrados en reposo, el atacante todavía no puede acuñar códigos.
Los puntos ciegos son igual de reales. Un proxy de phishing en tiempo real (una página de adversario en el medio) puede mostrarle al usuario una réplica perfecta, capturar el código en vivo y retransmitirlo al sitio real dentro de la misma ventana. TOTP no tiene forma de saber que el código se tecleó en el lugar equivocado. El malware que extrae el secreto del dispositivo lo derrota por completo, y un flujo descuidado de “olvidé mi 2FA” puede saltárselo del todo. Conviene aclarar una confusión frecuente: los ataques de intercambio de SIM (SIM-swap) derrotan los códigos de un solo uso por SMS, no TOTP. TOTP no tiene canal de número de teléfono, así que no hay nada que un atacante pueda redirigir.
¿Y de aquí en adelante? Las passkeys y FIDO2/WebAuthn están ligadas al origen, así que resisten el phishing por construcción: la credencial se niega a autenticarse en el dominio equivocado. Trata TOTP como un buen escalón sobre las contraseñas, disponible en todas partes, pero no como la meta final. Encaja sin fricción con el resto de tu pila de autenticación: consulta mejores prácticas de seguridad de JWT para la capa de token de sesión que se monta sobre un inicio de sesión verificado, y hashing de contraseñas (bcrypt vs Argon2) para la capa de contraseña en reposo que la 2FA complementa.
Errores comunes al implementar TOTP
La mayoría de los errores de TOTP no están en el algoritmo, que el RFC deja fijado, sino en el cableado que lo rodea. Estos son los que más muerden a quienes lo implementan.
- Deriva del reloj del servidor. Si el servidor no corre NTP, su idea de “ahora” se aleja de la del dispositivo del usuario y los códigos dejan de coincidir para todo el mundo. Activa la sincronización de hora por red en cada host.
- Secretos en texto plano o subidos al repositorio. Un secreto en un archivo de configuración subido a git es una puerta trasera permanente. Almacénalo cifrado en un gestor de secretos, nunca en el control de versiones.
- Sin protección contra reproducción. Si aceptas un código sin registrar el paso con el que coincidió, el mismo código funciona de nuevo dentro de su ventana. Persiste el último paso usado por usuario y rechaza la reutilización.
- Una ventana demasiado amplia o demasiado estrecha. Demasiado amplia multiplica los códigos adivinables y debilita la seguridad; demasiado estrecha rechaza a usuarios legítimos ante una deriva menor. La ventana 1 es el equilibrio habitual.
- Discordancia de parámetros. Si el registro codifica SHA-256 y 8 dígitos en la URI
otpauth://pero el verificador asume SHA-1 y 6 dígitos, ningún código validará jamás. Lee el algoritmo, los dígitos y el periodo de la URI y úsalos en ambos lados. - Sin códigos de respaldo o de recuperación. Cuando se pierde un teléfono, la única forma de volver a entrar es una ruta de recuperación. Emite códigos de recuperación durante la configuración y hazlos tan fuertes como la cuenta lo amerite; la misma lógica de la entropía de contraseñas vale también para los secretos de recuperación.
FAQ
¿Es TOTP a prueba de phishing?
No. TOTP detiene las contraseñas filtradas y la fuerza bruta remota, pero un proxy de phishing en tiempo real puede mostrar un inicio de sesión falso, capturar el código en vivo y retransmitirlo al sitio real dentro de la misma ventana de 30 segundos. Las passkeys y FIDO2 son la mejora resistente al phishing porque ligan la credencial al origen del sitio.
¿Es TOTP más seguro que la 2FA por SMS?
Sí. Los códigos por SMS viajan por la red celular y pueden interceptarse mediante ataques de SIM-swap o SS7, y dependen de la seguridad de tu operador. TOTP no tiene canal de número de teléfono y nunca transmite el código en absoluto, así que no hay nada que interceptar en tránsito. El secreto se intercambia una sola vez, durante la configuración.
¿Qué pasa si pierdo mi teléfono o la aplicación de autenticación?
Necesitas un respaldo dispuesto de antemano. Las opciones son los códigos de recuperación guardados cuando configuras la 2FA, un segundo dispositivo registrado con el mismo secreto, o el secreto Base32 original almacenado en un lugar seguro. Sin alguno de estos, perder el dispositivo significa quedar bloqueado fuera de la cuenta.
¿Cómo verifica un servidor un código TOTP?
Recalcula el código esperado a partir del secreto compartido y la hora actual, y luego compara el código enviado contra el paso de tiempo actual y un paso a cada lado para permitir la deriva del reloj. También registra qué paso coincidió para que el mismo código no pueda reproducirse, y limita la tasa de intentos para bloquear la adivinación.
¿Por qué los códigos TOTP se actualizan cada 30 segundos?
Treinta segundos es el periodo predeterminado del RFC 6238: lo bastante largo para leer y teclear el código con comodidad, lo bastante corto para que un código capturado por un atacante expire casi de inmediato. Algunos sistemas usan un periodo de 60 segundos, que la URI otpauth:// registra para que el verificador lo iguale.
¿Pueden dos dispositivos compartir un mismo secreto TOTP?
Sí. Cualquier dispositivo que tenga el mismo secreto Base32 y el reloj sincronizado genera códigos idénticos, porque el algoritmo es determinista. Así funcionan los respaldos de las aplicaciones de autenticación en varios dispositivos, y por eso mismo el secreto debe seguir siendo privado: cualquiera que lo copie puede producir todos los códigos futuros.
¿Es TOTP lo mismo que Google Authenticator?
No. TOTP es el algoritmo abierto definido en RFC 6238. Google Authenticator, Authy y 1Password son aplicaciones que lo implementan. Como el estándar es compartido, cualquier app que lo cumpla funciona con cualquier servicio que use TOTP, sin que dependas de un proveedor en particular.
Conclusión
Las ideas centrales caben en la cabeza:
- TOTP convierte un secreto compartido más la hora actual en un código mediante HMAC y truncamiento.
- Ambos lados calculan el código por separado; nunca se envía por la red.
- Verifica con una ventana de ±1 paso, más protección contra reproducción y limitación de tasa.
- Detiene los ataques a contraseñas, pero no el phishing en tiempo real; ahí entran las passkeys.
- Mantén los relojes del servidor sincronizados con NTP y el secreto cifrado y privado.
Si quieres ver el algoritmo escupir números reales y probar tu propia ventana de verificación, abre el generador de TOTP / 2FA: calcula, configura y verifica códigos enteramente en tu navegador, sin que el secreto salga nunca de tu dispositivo.