Guía de implementación AES-GCM: cifrado autenticado bien hecho
Implementa correctamente el cifrado AES-GCM en tu aplicación web. Gestión de nonces, claves y errores comunes a evitar.
AES-GCM (Galois/Counter Mode) combina el cifrado AES-CTR con la autenticación GHASH para producir cifrado autenticado con datos asociados (AEAD). Una implementación correcta usa una clave de 256 bits, un nonce de 96 bits (12 bytes) único por clave (nunca reutilizado), una etiqueta de autenticación de 128 bits, y opcionalmente datos asociados autenticados (AAD) que se verifican pero no se cifran. NIST SP 800-38D especifica la construcción exacta. Equivocarse en cualquiera de estos puntos — especialmente la reutilización de nonces — hace colapsar la seguridad de GCM: un solo par (clave, nonce) repetido permite a los atacantes recuperar la clave de autenticación y falsificar textos cifrados arbitrarios. Esta guía cubre la forma correcta de usar AES-GCM en contextos de navegador, Node y servidor.
Qué garantiza GCM realmente
Dos propiedades:
Confidencialidad: el texto plano no puede recuperarse sin la clave. La capa de cifrado en modo CTR de AES-GCM proporciona esto.
Integridad y autenticidad: cualquier modificación del texto cifrado, del nonce o de los datos asociados hace fallar el descifrado. GHASH produce una etiqueta de 128 bits que se verifica en tiempo constante al descifrar.
Lo que GCM no garantiza: no repudio (es simétrico, así que cualquiera con la clave puede producir textos cifrados válidos), protección ante repetición (eso corresponde a una capa superior) u ordenamiento (en streams, necesitas encadenar de alguna manera).
La clave del asunto: GCM solo permanece seguro cuando los nonces son únicos por clave. No casi únicos, no normalmente únicos, sino únicos de verdad. La prueba de seguridad se rompe ante la reutilización.
Gestión de nonces: lo que más importa
Un nonce de 96 bits puede generarse de dos formas:
Aleatorio: crypto.getRandomValues(new Uint8Array(12)). Con nonces aleatorios de 96 bits bajo una única clave, las colisiones por límite de cumpleaños aparecen alrededor de 2^48 cifrados. NIST sugiere un margen de seguridad, así que limita los usos a 2^32 por clave.
Contador: incrementa un entero de 96 bits. Garantiza unicidad hasta 2^96 mensajes. Requiere un estado monotónico fiable, difícil en sistemas distribuidos.
Para transferencia de archivos con una clave nueva por archivo, los nonces aleatorios son perfectamente seguros — nunca alcanzarás 2^32 cifrados con una sola clave. Para el cifrado por fragmentos bajo una única clave de archivo, usa un contador donde el nonce codifique el índice del fragmento:
const nonce = new Uint8Array(12);
new DataView(nonce.buffer).setUint32(0, messageId);
new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex));
El caso desastroso: múltiples procesos cifrando bajo la misma clave compartida con nonces aleatorios, escalando a millones de cifrados por segundo. Las colisiones de cumpleaños se vuelven probables. Si debes compartir claves entre procesos, usa un contador coordinado con un prefijo de ID de proceso.
No uses nonces de 64 bits
AES-GCM admite longitudes de nonce variables, pero solo los nonces de 96 bits usan la construcción optimizada especificada en NIST 800-38D. Otras longitudes (típicamente 64 o 128 bits) activan un paso de preprocesamiento GHASH que reduce el rendimiento y aumenta la complejidad. La Web Crypto API acepta IVs que no son de 96 bits, pero la especificación recomienda 96. Usa 96.
Longitud de etiqueta: no la acortes
La etiqueta de GCM tiene hasta 128 bits. Algunas especificaciones permiten truncarla a 96, 64 o incluso 32 bits. No lo hagas. Las etiquetas truncadas facilitan los ataques de falsificación, y el ahorro (4-12 bytes por mensaje) es irrelevante para la transferencia de archivos. El parámetro tagLength de AES-GCM en Web Crypto tiene 128 bits por defecto. Déjalo así.
Datos asociados (AAD)
El AAD son datos que se autentican pero no se cifran. Úsalo para metadatos que quieras vincular al texto cifrado: nombre de archivo, tipo de contenido, marca de tiempo de expiración, ID del remitente.
await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: nonce,
additionalData: new TextEncoder().encode(JSON.stringify({
filename: "informe.pdf",
contentType: "application/pdf",
expires: 1712345678,
})),
},
key,
plaintext
);
Si un atacante modifica el AAD, el descifrado falla. Esto previene ataques de sustitución donde alguien reemplaza el nombre de archivo de un texto cifrado almacenado sin ser detectado. El receptor debe conocer el AAD exacto para descifrar, así que almacénalo junto al texto cifrado.
Generación y derivación de claves
Para claves por archivo:
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
256 bits es el estándar en 2026. AES de 128 bits sigue siendo seguro pero tiene menos margen post-cuántico (el algoritmo de Grover reduce la fortaleza efectiva a la mitad).
Para claves derivadas de contraseña:
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"]
);
Almacena la sal junto al texto cifrado. No es secreta; solo debe ser única por contraseña.
La ruta de código crítica
Una función de cifrado mínima:
async function encrypt(key, plaintext, aad = new Uint8Array()) {
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = new Uint8Array(
await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce, additionalData: aad },
key,
plaintext
)
);
return { nonce, ciphertext, aad };
}
Descifrado con manejo adecuado de errores:
async function decrypt(key, { nonce, ciphertext, aad }) {
try {
return await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonce, additionalData: aad },
key,
ciphertext
);
} catch (e) {
// Fallo de autenticación
throw new Error("Descifrado fallido: texto cifrado manipulado o clave incorrecta");
}
}
La llamada a decrypt lanza OperationError ante un fallo de etiqueta, texto cifrado demasiado corto o clave incorrecta. Trata cualquier excepción como un fallo de integridad; no intentes distinguir.
Archivos grandes por fragmentos
Para archivos de varios cientos de megabytes, fragmenta para evitar la presión de memoria:
async function encryptChunks(key, file, chunkSize = 1024 * 1024) {
const chunks = [];
let chunkIndex = 0;
for (let offset = 0; offset < file.size; offset += chunkSize) {
const chunk = await file.slice(offset, offset + chunkSize).arrayBuffer();
const nonce = new Uint8Array(12);
new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex++));
const ct = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce }, key, chunk
);
chunks.push(new Uint8Array(ct));
}
return chunks;
}
Advertencia: AES-GCM por fragmentos no detecta el truncamiento. Un atacante podría eliminar fragmentos finales y cada fragmento superviviente se descifra correctamente. Para defenderse, incluye el recuento total de fragmentos en el AAD de cada fragmento, o usa crypto_secretstream de libsodium, que gestiona esto.
Descifrado en el lado del servidor (Node.js)
El módulo crypto de Node puede descifrar datos cifrados en el navegador:
const { createDecipheriv } = require('crypto');
function decrypt(key, nonce, ciphertextWithTag) {
const tag = ciphertextWithTag.slice(-16);
const ct = ciphertextWithTag.slice(0, -16);
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ct), decipher.final()]);
}
Web Crypto añade la etiqueta de 128 bits al texto cifrado; la API de Node espera etiqueta y texto cifrado por separado. Divídelos según corresponda.
Cifras de rendimiento
En hardware típico de 2024-2026 con AES-NI:
- Nativo (OpenSSL, AES-NI): 3-5 GB/s por núcleo
- Web Crypto (navegador con aceleración hardware): 1-2 GB/s
- libsodium.js WASM AES-GCM: 400-800 MB/s
- JS puro (@noble/ciphers): 50-150 MB/s
Para un archivo de 1 GB, el cifrado con Web Crypto tarda 0,5-1 segundo. JS puro tarda 7-20 segundos. Elige implementaciones basándote en esta realidad; para la UX de transferencia de archivos grandes, Web Crypto es la elección práctica.
Resumen de errores comunes
- Reutilización de nonces: catastrófico. El mayor modo de fallo.
- Usar
Math.random()en lugar decrypto.getRandomValues(). - Olvidar autenticar metadatos asociados con AAD.
- Usar modo CBC "porque estamos acostumbrados a él". CBC requiere un MAC separado para igualar la integridad de GCM; una construcción HMAC-CBC es correcta pero compleja; GCM evita el error.
- Capturar silenciosamente errores de descifrado y devolver basura. Falla siempre de forma ruidosa.
- Implementar tu propio GCM. Usa Web Crypto, libsodium o node:crypto. La implementación de GHASH tiene trampas de canal lateral que los expertos tardaron años en resolver.
HexaTransfer usa AES-256-GCM de Web Crypto con nonces aleatorios de 96 bits, etiquetas de 128 bits y sin AAD porque la clave es por archivo y el nombre de archivo se almacena por separado en metadatos protegidos con AEAD. Sencillo, correcto y rápido.
Cuándo elegir otra opción
AES-GCM es óptimo para la transferencia de archivos, pero considera alternativas en casos específicos:
- XChaCha20-Poly1305: los nonces de 192 bits hacen que el uso de nonces aleatorios sea trivialmente seguro a cualquier escala. Ligeramente más lento en hardware con AES-NI, más rápido en ARM antiguo sin AES-NI. libsodium lo proporciona.
- AES-GCM-SIV: resistente a usos incorrectos; la reutilización de nonces no filtra la clave, solo revela si los textos planos eran iguales. Útil cuando no puedes garantizar la unicidad de nonces.
Para la mayoría de cargas de transferencia de archivos en una pila web estándar, AES-256-GCM con una clave nueva por archivo y nonces aleatorios de 96 bits es la elección correcta y la más sencilla de implementar correctamente.
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