Saltar al contenido
ecdsa.com

Llaves de acceso o contraseñas: phishing y filtraciones

Una contraseña es un secreto que el usuario escribe en cualquier página que se la pida. Una llave de acceso es un par de claves vinculado a un solo sitio, y su mitad privada nunca sale del dispositivo ni del gestor de contraseñas del usuario.

Llaves de acceso frente a contraseñas es la comparación entre un secreto compartido y la autenticación de clave pública. Una contraseña la conocen el usuario y, en forma de hash, el servidor, y funciona en cualquier página que la pida, incluida una falsa. Una llave de acceso es una credencial de WebAuthn: un par de claves creado para un solo sitio, que se desbloquea en el dispositivo del usuario con la huella dactilar, la cara o un PIN y que sirve para firmar un desafío nuevo vinculado a la identidad del sitio.

De un vistazo

ContraseñaLlave de acceso
Qué demuestra el usuarioQue conoce un secreto compartidoQue posee una clave privada, desbloqueada localmente con biometría o un PIN
Qué guarda el servidorSal y hash lento (Argon2id, scrypt, PBKDF2)ID de credencial y clave pública (91 bytes para P-256 en la demostración de más abajo)
Sitio de phishing que imita al realRecibe la contraseña si el usuario la escribeNo obtiene nada útil: la credencial está vinculada al RP ID y al origen del sitio
Reutilización entre sitiosHabitual, lo que hace posible el relleno de credenciales (credential stuffing)Imposible: un par de claves por sitio y cuenta
Filtración de la base de datosAdivinación sin conexión contra los hashesSolo se filtran claves públicas; no hay nada que reutilizar
NIST SP 800-63B-4 (julio de 2025)Un solo factor: al menos 15 caracteres, AAL1Criptográfico multifactor: AAL2; las claves de hardware vinculadas al dispositivo pueden llegar a AAL3, las llaves de acceso sincronizadas no
Dispositivo perdidoSin efectoLas llaves de acceso sincronizadas se restauran con la cuenta del proveedor; las claves vinculadas al dispositivo necesitan una credencial de respaldo
Otros dispositivosSe escribe en cualquier parteSincronizadas entre los dispositivos del proveedor; código QR y proximidad por Bluetooth (híbrido) para los demás
EstándaresNinguno aparte de los formularios HTML y la autenticación HTTPW3C WebAuthn Level 3 (recomendación desde el 25 de agosto de 2026), FIDO CTAP 2.2

Qué ocurre al iniciar sesión

En el registro, el autenticador (un teléfono, un equipo portátil, un gestor de contraseñas o una llave de seguridad) crea un par de claves para el ID de la relying party (RP ID) del sitio, por ejemplo example.com, y el sitio guarda la clave pública. Al iniciar sesión, el sitio envía un desafío aleatorio; el navegador añade el origen de la página a los datos del cliente firmados y el autenticador los firma junto con un hash del RP ID, después de la verificación del usuario. El servidor comprueba el desafío, el origen, el hash del RP ID y la firma.

El script siguiente (Node.js 26.10, Apple M4 Max) muestra qué deja cada enfoque en el servidor y qué comprobaciones detienen a un sitio que imita al real.

Qué guarda el servidor y por qué falla un origen que imita al real
import { argon2Sync, createHash, generateKeyPairSync, randomBytes, sign, verify } from "node:crypto";

const sha256 = (b) => createHash("sha256").update(b).digest();

// What a password database stores: an Argon2id hash (OWASP minimum: 19 MiB, t=2, p=1)
const params = { nonce: randomBytes(16), memory: 19456, passes: 2, parallelism: 1, tagLength: 32 };
const t0 = performance.now();
const tag = argon2Sync("argon2id", { message: "correct horse battery staple", ...params });
const ms = performance.now() - t0;
console.log(`password: stores salt + ${tag.length}-byte Argon2id hash; one guess costs ${ms.toFixed(0)} ms`);

// What a passkey database stores: the credential's public key
const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
const spki = publicKey.export({ type: "spki", format: "der" });
console.log(`passkey:  stores a ${spki.length}-byte public key; a leak lets nobody sign in`);

// Authenticator: signs authenticatorData || SHA-256(clientDataJSON)
function getAssertion(rpId, origin, challenge) {
  const clientDataJSON = Buffer.from(JSON.stringify({ type: "webauthn.get", challenge, origin }));
  const authData = Buffer.concat([sha256(rpId), Buffer.from([0x05]), Buffer.alloc(4)]);
  const signature = sign("sha256", Buffer.concat([authData, sha256(clientDataJSON)]), privateKey);
  return { clientDataJSON, authData, signature };
}

// Relying party: the checks from WebAuthn §7.2
function check(a, challenge) {
  const c = JSON.parse(a.clientDataJSON);
  return {
    challenge: c.challenge === challenge,
    origin: c.origin === "https://example.com",
    rpIdHash: a.authData.subarray(0, 32).equals(sha256("example.com")),
    signature: verify("sha256", Buffer.concat([a.authData, sha256(a.clientDataJSON)]), publicKey, a.signature),
  };
}

const challenge = randomBytes(32).toString("base64url");
console.log("real site:", check(getAssertion("example.com", "https://example.com", challenge), challenge));
console.log("look-alike:", check(getAssertion("examp1e.com", "https://examp1e.com", challenge), challenge));
Salida
password: stores salt + 32-byte Argon2id hash; one guess costs 25 ms
passkey:  stores a 91-byte public key; a leak lets nobody sign in
real site: { challenge: true, origin: true, rpIdHash: true, signature: true }
look-alike: { challenge: true, origin: false, rpIdHash: false, signature: true }

El caso del sitio imitador es deliberadamente generoso con el atacante: en la práctica, el navegador se niega por completo a usar credenciales de example.com en otro sitio, así que no se llega a producir ninguna firma. Incluso una firma obtenida retransmitiendo la sesión llevaría un origen y un hash del RP ID incorrectos, y por eso el NIST clasifica WebAuthn como resistente al phishing gracias a la vinculación con el nombre del verificador, mientras que las contraseñas, los códigos de un solo uso y las aprobaciones por notificación push no lo son. Argon2id hace que cada intento de adivinar una contraseña cueste decenas de milisegundos, pero quienes eligen contraseñas comunes o reutilizadas siguen cayendo rápido; una clave pública no le da al atacante nada que adivinar.

Dónde sigue fallando cada una

Las contraseñas fallan por la reutilización (relleno de credenciales con listas filtradas), por las páginas de phishing y los proxies de phishing en tiempo real que también capturan los códigos de un solo uso, y por las elecciones débiles. Por eso NIST SP 800-63B-4 exige al menos 15 caracteres para las contraseñas de un solo factor (8 dentro de una autenticación multifactor), prohíbe las reglas de composición y los cambios periódicos, exige comprobar las contraseñas nuevas contra valores filtrados y comunes, y recomienda permitir al menos 64 caracteres. Un gestor de contraseñas con autocompletado vinculado al dominio resuelve la reutilización y parte del phishing, pero no todos los usuarios tienen uno.

Las llaves de acceso trasladan el riesgo a dos lugares. El primero es la cuenta del proveedor: las llaves de acceso sincronizadas solo son tan seguras como la cuenta de Apple, Google, Microsoft o del gestor de contraseñas que las guarda, y por eso el NIST no admite claves sincronizadas y exportables en AAL3. El segundo es la recuperación de la cuenta en tu sitio: si una llave de acceso perdida se puede sustituir mediante un enlace por correo electrónico o un código por SMS, los atacantes usarán ese camino en su lugar. Los usuarios también necesitan una segunda credencial o un proveedor con sincronización para no quedarse sin acceso.

Cuándo elegir cada una

  • Sitios y aplicaciones para consumidores: llaves de acceso como método principal, creadas justo después del siguiente inicio de sesión con contraseña; contraseñas opcionales.
  • Inicio de sesión de empleados: llaves de acceso a través de tu proveedor de identidad; llaves de seguridad vinculadas al dispositivo para los administradores y otros roles AAL3.
  • Cuentas de consumidores de alto valor (banca, correo electrónico, consolas en la nube): llaves de acceso más un proceso de recuperación robusto, idealmente con una segunda llave de acceso registrada.
  • Dispositivos compartidos o quioscos: inicio de sesión híbrido desde el teléfono del usuario, en lugar de guardar credenciales en el dispositivo compartido.
  • Protocolos sin WebAuthn (IMAP, VPN heredadas, SSH): contraseñas específicas de aplicación o flujos OAuth que empiecen en un navegador donde funcionen las llaves de acceso.

Notas de migración

  1. Añade llaves de acceso junto a las contraseñas: después de un inicio de sesión correcto con contraseña, ofrece crear una llave de acceso, o usa la creación condicional de WebAuthn Level 3 para añadirla automáticamente cuando el gestor de contraseñas del navegador lo admita.
  2. Usa la interfaz de autocompletado: llama a navigator.credentials.get con mediation: "conditional" en la página de inicio de sesión para que las llaves de acceso aparezcan en las sugerencias del campo de nombre de usuario.
  3. Guarda lo que necesitas: el ID de credencial, la clave pública, el contador de firmas, los transportes, los indicadores de copia de seguridad y el AAGUID que identifica al proveedor; la búsqueda de AAGUID lo asocia a un producto.
  4. Arregla la recuperación antes de retirar las contraseñas: exige una llave de acceso existente, una segunda llave de acceso registrada o una verificación de identidad, no solo un enlace por correo electrónico.
  5. Mantén sincronizados a los proveedores con los métodos de señal de Level 3 cuando los usuarios eliminen credenciales o cambien de nombre, y declara los orígenes relacionados si tu inicio de sesión abarca varios dominios.
  6. Prueba en el entorno de pruebas de llaves de acceso e inspecciona respuestas reales con el inspector de WebAuthn antes del despliegue.

Referencias

Todas las comparaciones