Web Crypto API: tutorial completo para cifrar archivos
Domina la Web Crypto API para cifrar archivos desde el navegador: AES-GCM, RSA-OAEP y gestión de claves en aplicaciones JavaScript.
La Web Crypto API permite cifrar archivos directamente en el navegador usando los métodos nativos de window.crypto.subtle, sin ninguna biblioteca externa. Para el cifrado de archivos, normalmente derivarás una clave a partir de una contraseña mediante PBKDF2 (210.000 iteraciones, SHA-256) y luego cifrarás los bytes del archivo con AES-GCM usando un IV de 96 bits y una etiqueta de autenticación de 128 bits. Los flujos de trabajo con clave pública usan RSA-OAEP con claves de 4096 bits para envolver la clave simétrica. La API está disponible sobre HTTPS en todos los navegadores modernos y se ejecuta en el backend de criptografía nativa del navegador, no en JavaScript. Esta combinación es precisamente la que usan herramientas como HexaTransfer para garantizar que las claves nunca abandonen el dispositivo del usuario.
Por qué SubtleCrypto supera a las bibliotecas JavaScript puras
window.crypto.subtle llama al backend de criptografía nativa auditada del navegador, normalmente BoringSSL en Chromium o CommonCrypto en Safari. Comparado con opciones de JavaScript puro como CryptoJS o sjcl, SubtleCrypto es entre 30 y 80 veces más rápido para AES-GCM, evita los canales laterales de tiempo en los intérpretes JavaScript y no envía ningún byte adicional a los usuarios. El compromiso es una API basada en Promesas que solo opera con objetos ArrayBuffer y CryptoKey, por lo que pasarás mucho tiempo convirtiendo entre Uint8Array, Blob y ReadableStream. Para tamaños de archivo superiores a 100 MB, esa fontanería importa más que la velocidad bruta de criptografía.
Derivar una clave a partir de una contraseña con PBKDF2
Nunca uses una contraseña directamente como clave AES. En su lugar, importa la contraseña como material en bruto y deriva una clave de 256 bits:
async function deriveKey(password, salt) {
const enc = new TextEncoder();
const material = await crypto.subtle.importKey(
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']
);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 210000, hash: 'SHA-256' },
material,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
La guía de OWASP para 2026 recomienda al menos 600.000 iteraciones con SHA-256, aunque 210.000 sigue siendo aceptable para contextos de bajo riesgo. Genera una sal nueva de 16 bytes por archivo con crypto.getRandomValues y almacénala junto al texto cifrado. Argon2id sería más sólido, pero todavía no está expuesto por SubtleCrypto.
Cifrar un archivo con AES-GCM
AES-256-GCM ofrece confidencialidad y autenticidad en un solo paso. La regla crítica es no reutilizar nunca un par (clave, IV):
async function encryptFile(file, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const plaintext = await file.arrayBuffer();
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
plaintext
);
return { iv, ciphertext };
}
Para un archivo de 2 GB, file.arrayBuffer() asignará el buffer completo, lo que a menudo bloquea Mobile Safari. Divide el archivo en fragmentos de 4 MB, cifra cada uno con un IV único derivado de un contador concatenado con un prefijo aleatorio, y antepone un byte de versión y la sal para que el descifrador sepa con qué está tratando.
Transmitir archivos grandes mediante TransformStream
Para evitar el aumento de memoria, envuelve el cifrado en un TransformStream y canaliza el archivo a través de él:
const chunkSize = 4 * 1024 * 1024;
const encryptor = new TransformStream({
async transform(chunk, controller) {
const iv = nextIV(counter++);
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, chunk);
controller.enqueue(new Uint8Array([...iv, ...new Uint8Array(ct)]));
}
});
await file.stream()
.pipeThrough(sliceByChunks(chunkSize))
.pipeThrough(encryptor)
.pipeTo(uploadSink);
file.stream() devuelve un ReadableStream<Uint8Array> que lee desde el disco de forma diferida. El rebanador produce fragmentos de tamaño fijo para que las etiquetas GCM se alineen de forma predecible. La memoria máxima se mantiene por debajo de 20 MB incluso para una subida de 10 GB.
Envolver la clave simétrica con RSA-OAEP
Cuando necesitas compartir un archivo con un destinatario específico, genera su par de claves RSA una vez y publica la clave pública:
const keypair = await crypto.subtle.generateKey(
{ name: 'RSA-OAEP', modulusLength: 4096,
publicExponent: new Uint8Array([1,0,1]), hash: 'SHA-256' },
true, ['wrapKey', 'unwrapKey']
);
Genera una clave AES-GCM para el archivo y luego envuélvela:
const wrapped = await crypto.subtle.wrapKey(
'raw', fileKey, keypair.publicKey,
{ name: 'RSA-OAEP' }
);
Las claves RSA de 4096 bits ofrecen aproximadamente 150 bits de seguridad hasta 2030 según NIST SP 800-57. Si necesitas secreto hacia adelante o resistencia post-cuántica, combina RSA-OAEP con ECDH sobre P-384 o migra a ML-KEM (Kyber) una vez que el grupo de trabajo WebCrypto lo incorpore.
Almacenar claves de forma segura en IndexedDB
Los objetos CryptoKey son no extraíbles por defecto, lo que significa que puedes persistirlos en IndexedDB sin exponer nunca los bytes en bruto a JavaScript:
const db = await openDB('keystore', 1);
await db.put('keys', keypair.privateKey, 'user-signing-key');
Los navegadores serializan la clave usando el algoritmo de clonación estructurada y mantienen los bytes reales en el backend de criptografía. Un script comprometido puede llamar a encrypt o decrypt con la clave almacenada pero no puede leer su material. Este es un paso de fortalecimiento significativo comparado con guardar claves en base64 en localStorage.
Gestionar los errores que lanza la API
SubtleCrypto lanza OperationError para los fallos de descifrado autenticado, lo que normalmente significa que el texto cifrado fue manipulado, el IV es incorrecto o el usuario escribió la contraseña incorrecta. Lanza DataError cuando el buffer de entrada tiene la longitud incorrecta, NotSupportedError cuando el algoritmo no está implementado, e InvalidAccessError cuando la clave no fue importada con los indicadores de uso correctos. Envuelve siempre el descifrado en try/catch, muestra un mensaje neutro del tipo "el archivo no pudo descifrarse" y evita filtrar si fue la etiqueta o la estructura lo que falló.
Problemas reales a tener en cuenta
Firefox en Android limita las iteraciones de deriveKey a alrededor de 1 millón antes de que el hilo de UI se bloquee durante varios segundos, así que ejecuta la derivación de claves dentro de un Worker dedicado. Safari por debajo de la versión 16.4 no soporta crypto.subtle.verify con el relleno PSS. Chrome limita las llamadas a getRandomValues por encima de 64 KB por invocación, así que haz un bucle si necesitas más entropía. Y las transferencias de ArrayBuffer mediante postMessage son de copia cero pero desvinculan el original, lo que sorprende a la gente.
Uniendo todo
Un flujo mínimo de subida cifrada: genera la sal y el IV con getRandomValues, deriva una clave AES-GCM a partir de la contraseña del usuario mediante PBKDF2, canaliza el archivo a través de un TransformStream que cifra cada fragmento de 4 MB, antepone una pequeña cabecera que contiene la versión, la sal y el recuento de fragmentos, y envía el resultado al servidor. En la descarga, invierte el proceso fragmento a fragmento, capturando OperationError como señal de contraseña incorrecta o corrupción. La Web Crypto API te da todo lo que necesitas, y la implementación nativa del navegador superará a cualquier alternativa JavaScript en un orden de magnitud.
HexaTransfer usa exactamente este pipeline de AES-256-GCM más PBKDF2 para cada subida, con las claves derivadas en un Worker y el texto cifrado transmitido al almacenamiento sin que el servidor vea nunca el texto plano.
Pruébalo en https://hexatransfer.com — gratuito, 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