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.
Publicado por HexaTransfer, servicio de Gaprod — Groupe Dwenola SAS. Estas guías presentan nuestro servicio junto con otros productos. Sobre el editor
HexaTransfer genera una clave AES-256-GCM aleatoria mediante Web Crypto. No la deriva de la contraseña opcional. El fragmento tras # se separa antes de la petición HTTP, como explica RFC 3986. La contraseña es un control de acceso distinto cuyo hash guarda el servidor. AES-GCM aporta autenticación; este formato no añade un HMAC separado.
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 genera una clave AES-256-GCM aleatoria mediante Web Crypto. No la deriva de la contraseña opcional. El fragmento tras # se separa antes de la petición HTTP, como explica RFC 3986. La contraseña es un control de acceso distinto cuyo hash guarda el servidor. AES-GCM aporta autenticación; este formato no añade un HMAC separado.
Pruébalo en https://hexatransfer.com — gratuito, sin cuenta, hasta 50 GB.
Envía archivos grandes de forma segura con cifrado de extremo a extremo
Envía hasta 50 GiB gratis con cifrado de extremo a extremo, sin cuenta ni correo electrónico. Elige de 1 a 30 días; 14 por defecto.
Enviar un archivo