Ir al contenido
HexaTransfer
Volver al blog
Cifrado y seguridad

Guía Web Crypto API: cifrado nativo del navegador para desarrolladores

Domina la Web Crypto API para aplicaciones de transferencia cifrada. Guía completa de AES-GCM, RSA-OAEP y gestión de claves en el navegador.

La Web Crypto API (especificada en la recomendación W3C Web Cryptography API, expuesta como window.crypto.subtle) es la forma nativa del navegador de ejecutar criptografía sin necesitar enviar una librería de cifrado por la red. Soporta AES-GCM, AES-CBC, AES-CTR, AES-KW, HMAC, RSA-OAEP, RSA-PSS, RSASSA-PKCS1-v1_5, ECDH, ECDSA, HKDF y PBKDF2 en todos los navegadores modernos (Chrome 37+, Firefox 34+, Safari 10.1+, Edge 79+). Para aplicaciones de transferencia de archivos esto importa porque cada byte de texto cifrado puede generarse en el cliente antes de subirse, con el navegador proporcionando una implementación auditada y de tiempo constante. Esta guía recorre los primitivos relevantes para la transferencia cifrada de archivos y los errores que tropieza en toda primera implementación.

SubtleCrypto es asíncrono y basado en Promesas

Cada método de crypto.subtle devuelve una Promesa. Es deliberado: las operaciones criptográficas pueden delegarse a hardware o hilos en segundo plano, así que forzar el asincronismo evita que la API se use de formas que bloquearían el hilo principal. Forma del código:

const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true, // extractable
  ["encrypt", "decrypt"]
);

El segundo argumento (true) marca la clave como extraíble, lo que significa que luego puede exportarse mediante crypto.subtle.exportKey(). Para claves de larga duración, establécelo en false para mantener los bytes crudos inaccesibles desde JavaScript. Para claves que necesitas serializar en un fragmento de URL (el patrón de HexaTransfer), establécelo en true.

El tercer argumento es un array de usos de clave. Una clave generada con ["encrypt"] no puede usarse para descifrar, aunque AES-GCM sea simétrico. Esta separación evita que un flujo de cifrado comprometido sea abusado para descifrar datos históricos.

AES-GCM para el cifrado simétrico de archivos

AES-GCM es el caballo de batalla para el contenido de los archivos. Proporciona cifrado autenticado con datos asociados (AEAD): texto cifrado más etiqueta de autenticación más datos asociados opcionales que se autentican pero no se cifran. Para transferencia de archivos, usa una clave de 256 bits y un nonce de 96 bits siguiendo las recomendaciones NIST SP 800-38D.

const iv = crypto.getRandomValues(new Uint8Array(12)); // nonce de 96 bits
const ciphertext = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv },
  key,
  plaintext
);

La salida incluye una etiqueta de autenticación GCM de 128 bits añadida al texto cifrado. El descifrado verifica automáticamente la etiqueta y lanza una excepción si no coincide. Nunca reutilices un nonce con la misma clave; las propiedades de seguridad de GCM colapsan catastróficamente ante la reutilización de nonce (los atacantes pueden recuperar la clave de autenticación). Para transferencias de archivos donde cada archivo recibe una clave nueva, los nonces aleatorios son seguros; para claves de larga duración, usa un contador.

PBKDF2 para claves derivadas de contraseña

Cuando los usuarios escriben una contraseña para proteger un archivo, no puedes usar la contraseña directamente como clave AES. Primero pásala por PBKDF2:

const passwordKey = await crypto.subtle.importKey(
  "raw",
  new TextEncoder().encode(password),
  "PBKDF2",
  false,
  ["deriveKey"]
);

const aesKey = await crypto.subtle.deriveKey(
  {
    name: "PBKDF2",
    salt: crypto.getRandomValues(new Uint8Array(16)),
    iterations: 600000,
    hash: "SHA-256",
  },
  passwordKey,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"]
);

La guía de hash de contraseñas OWASP 2023 recomienda 600.000 iteraciones para PBKDF2-SHA-256. Cualquier valor por debajo de 310.000 está por debajo de las mejores prácticas actuales. El salt debe ser aleatorio y almacenarse junto al texto cifrado (no es secreto, solo debe ser único).

Para código nuevo en 2026, considera Argon2id en lugar de PBKDF2. Argon2 no está todavía en la Web Crypto API, pero librerías como argon2-browser o @noble/hashes proporcionan implementaciones en JavaScript/WASM. Argon2id resiste los ataques por GPU mucho mejor que PBKDF2.

RSA-OAEP para el encapsulado de claves

Para escenarios donde quieres cifrar la clave AES de un archivo con la clave pública de un destinatario, usa RSA-OAEP. Genera las claves:

const keyPair = await crypto.subtle.generateKey(
  {
    name: "RSA-OAEP",
    modulusLength: 4096,
    publicExponent: new Uint8Array([1, 0, 1]), // 65537
    hash: "SHA-256",
  },
  true,
  ["encrypt", "decrypt"]
);

Usa modulusLength 4096 para claves nuevas; 2048 es aceptable pero comenzará a deprecarse a medida que los plazos cuánticos se consoliden. RSA-OAEP solo cifra cargas útiles pequeñas (como máximo modulusLength/8 - 2*hashLength - 2 bytes), así que encapsula una clave AES de 256 bits en lugar de cifrar el contenido del archivo directamente.

Para aplicaciones sensibles al rendimiento, ECDH con P-256 o P-384 es una alternativa mejor a RSA. La generación de claves es un orden de magnitud más rápida y los tamaños de clave son mucho menores.

Streaming para archivos grandes

Un archivo de 2 GB no cabe cómodamente en un ArrayBuffer del navegador. Chrome, Firefox y Safari permiten leer archivos con File.stream() que devuelve un ReadableStream y procesar en fragmentos. La Web Crypto API en sí no tiene métodos de cifrado/descifrado en streaming todavía (es una laguna de la especificación), así que hay dos alternativas:

  1. Dividir en fragmentos (64 KB o 1 MB) y cifrar cada uno con un nonce único. El destinatario los concatena en orden. Esto pierde el AEAD verdadero sobre el archivo completo pero funciona para la mayoría de casos.
  2. Usar una librería de cifrado WASM (libsodium.js, @noble/ciphers con backend WASM) que soporta modos AEAD en streaming como XChaCha20-Poly1305 o AES-GCM-SIV.

Para transferencias de unos pocos cientos de megabytes, AES-GCM en buffer funciona perfectamente y es mucho más sencillo. Por encima de eso, el streaming se vuelve necesario para evitar presión de memoria.

Exportación, importación de claves y fragmentos de URL

Para flujos al estilo HexaTransfer donde la clave viaja en el fragmento de URL:

const rawKey = await crypto.subtle.exportKey("raw", aesKey);
const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)));
// URL compartida como https://example.com/archivo/abc123#key=keyBase64

Los fragmentos de URL nunca se envían a los servidores en las peticiones HTTP (el navegador los elimina). Esto mantiene la clave en el cliente aunque el usuario comparta un enlace. En el lado del destinatario:

const keyBase64 = window.location.hash.slice(5); // eliminar "#key="
const rawKey = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
const key = await crypto.subtle.importKey(
  "raw", rawKey, "AES-GCM", false, ["decrypt"]
);

Usa codificación base64url (reemplaza + por -, / por _, elimina el padding) para evitar problemas de codificación en la URL.

Errores comunes

Usar Math.random() para salts o IVs. Math.random() no es criptográficamente seguro. Usa siempre crypto.getRandomValues().

Reutilizar IVs con la misma clave. Las propiedades de seguridad de GCM se rompen completamente ante la reutilización de nonce. Los nonces aleatorios de 96 bits colisionan después de ~2^48 cifrados con la misma clave (límite de cumpleaños). Para transferencias donde cada archivo tiene su propia clave, es seguro; para claves de larga duración, usa un contador.

Olvidar HTTPS. crypto.subtle solo está disponible en contextos seguros (HTTPS o localhost). En un origen inseguro, crypto.subtle es undefined.

Almacenar claves extraíbles en IndexedDB sin protección. Si necesitas persistir claves, encapsúlalas (p. ej., con una clave derivada de una frase de contraseña) antes de almacenarlas. Nunca guardes claves AES crudas en localStorage, accesible para cualquier script en el origen.

Confiar en contraseñas de usuario sin PBKDF2. Una contraseña convertida a bytes UTF-8 no es una clave de 256 bits. Siempre deriva.

No verificar las etiquetas de autenticación. crypto.subtle.decrypt() lo hace automáticamente para AES-GCM, pero si implementas protocolos personalizados encima, no omitas la verificación.

Peculiaridades de compatibilidad entre navegadores

Todos los navegadores principales soportan Web Crypto en HTTPS. Algunas particularidades:

  • PBKDF2 de Safari era más lento que Chrome/Firefox durante años; la brecha se cerró en Safari 15.
  • Firefox aplica una validación de entrada más estricta; código que funciona en Chrome puede lanzar OperationError en Firefox. Prueba en ambos.
  • Web Crypto en service workers funciona pero requiere que el ámbito de registro sea HTTPS.
  • Node.js proporciona require("crypto").webcrypto con una API compatible desde Node 15, útil para código isomorfo.

Cuándo usar una librería en su lugar

Web Crypto cubre bien los fundamentos pero carece de primitivos modernos como ChaCha20-Poly1305, Argon2, X25519 y Ed25519 (aunque Ed25519 está llegando). Para esos casos, libsodium.js (vía WASM) o @noble/ciphers / @noble/curves (JavaScript puro, auditado) son las principales opciones. HexaTransfer usa los primitivos Web Crypto directamente para AES-GCM y PBKDF2, ya que cubren la ruta de transferencia de archivos sin dependencias externas.

Para un flujo de transferencia cifrada completo, Web Crypto te lleva allí en menos de 100 líneas de código: genera la clave AES, cifra el archivo, sube el texto cifrado, comparte el enlace con la clave en el fragmento, el destinatario importa la clave y descifra. Eso es todo.

Pruébalo en hexatransfer.com — gratis, sin cuenta, hasta 10 GB.

Envía archivos grandes de forma segura con cifrado de extremo a extremo

Transfiere archivos de hasta 10 GB gratis con cifrado de extremo a extremo. Sin necesidad de cuenta. Tus archivos se cifran en tu navegador antes de subirlos: nadie más puede leerlos.

Enviar un archivo