Guia de implementação AES-GCM: encriptação autenticada correcta
Implemente correctamente a encriptação AES-GCM na sua aplicação web. Gestão de nonces, chaves e erros comuns a evitar na encriptação autenticada.
AES-GCM (Galois/Counter Mode) combina encriptação AES-CTR com autenticação GHASH para produzir encriptação autenticada com dados associados (AEAD). Uma implementação correcta usa uma chave de 256 bits, um nonce de 96 bits (12 bytes) único por chave (nunca reutilizado), uma tag de autenticação de 128 bits e, opcionalmente, dados associados autenticados (AAD) que são verificados mas não encriptados. A norma NIST SP 800-38D especifica a construção exacta. Se qualquer destes parâmetros estiver errado — especialmente a reutilização de nonces — a segurança do GCM colapsa: um único par (chave, nonce) repetido permite a atacantes recuperar a chave de autenticação e forjar texto cifrado arbitrário. Este guia cobre a forma correcta de usar AES-GCM em contextos de navegador, Node e servidor.
O que o GCM garante efectivamente
Duas propriedades:
Confidencialidade: o texto simples não pode ser recuperado sem a chave. A camada de encriptação CTR do AES-GCM garante isto.
Integridade e autenticidade: qualquer modificação ao texto cifrado, nonce ou dados associados faz a desencriptação falhar. O GHASH produz uma tag de 128 bits verificada em tempo constante na desencriptação.
O que o GCM não garante: não-repúdio (é simétrico, pelo que qualquer pessoa com a chave pode produzir texto cifrado válido), protecção contra repetição (isso é uma preocupação de camada superior), ou ordenação (para fluxos, é necessário encadear de alguma forma).
A questão fundamental: o GCM só permanece seguro quando os nonces são únicos por chave. Não maioritariamente únicos, não geralmente únicos — efectivamente únicos. A prova de segurança desmorona-se em caso de reutilização.
Gestão de nonces: o que mais importa
Um nonce de 96 bits pode ser gerado de duas formas:
Aleatório: crypto.getRandomValues(new Uint8Array(12)). Com nonces aleatórios de 96 bits sob uma única chave, colisões por aniversário surgem por volta de 2^48 encriptações. O NIST sugere uma margem de segurança, por isso limite a 2^32 por chave.
Contador: incrementar um inteiro de 96 bits. Garante unicidade até 2^96 mensagens. Requer estado monótono fiável, difícil em sistemas distribuídos.
Para transferência de ficheiros com uma chave nova por ficheiro, nonces aleatórios são perfeitamente seguros — nunca chegará a 2^32 encriptações com uma chave. Para encriptação em blocos sob uma única chave de ficheiro, use um contador onde o nonce codifica o índice do bloco:
const nonce = new Uint8Array(12);
new DataView(nonce.buffer).setUint32(0, messageId);
new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex));
O caso catastrófico: múltiplos processos a encriptar sob a mesma chave partilhada com nonces aleatórios, chegando a milhões de encriptações por segundo. As colisões por aniversário tornam-se prováveis. Se tiver de partilhar chaves entre processos, use um contador coordenado com prefixo de ID de processo.
Não use nonces de 64 bits
O AES-GCM suporta comprimentos de nonce variáveis, mas apenas nonces de 96 bits usam a construção optimizada especificada no NIST 800-38D. Outros comprimentos (tipicamente 64 ou 128 bits) activam uma etapa de pré-processamento GHASH que reduz o desempenho e aumenta a complexidade. A Web Crypto API aceita IVs de comprimento diferente de 96 bits, mas a especificação recomenda 96. Use simplesmente 96.
Comprimento da tag: não reduza
A tag do GCM tem até 128 bits. Algumas especificações permitem truncagem para 96, 64 ou até 32 bits. Não o faça. Tags truncadas facilitam ataques de falsificação, e a poupança (4-12 bytes por mensagem) é irrelevante para transferência de ficheiros. O AES-GCM da Web Crypto usa por defeito tags de 128 bits através do parâmetro tagLength (padrão 128). Deixe-o como está.
Dados associados (AAD)
AAD são dados autenticados mas não encriptados. Use-os para metadados que quer vincular ao texto cifrado: nome do ficheiro, tipo de conteúdo, timestamp de expiração, ID do remetente.
await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: nonce,
additionalData: new TextEncoder().encode(JSON.stringify({
filename: "relatorio.pdf",
contentType: "application/pdf",
expires: 1712345678,
})),
},
key,
plaintext
);
Se um atacante modificar o AAD, a desencriptação falha. Isto previne ataques de substituição onde alguém troca o nome do ficheiro num texto cifrado armazenado sem detecção. O receptor deve conhecer o AAD exacto para desencriptar, por isso armazene-o junto ao texto cifrado.
Geração e derivação de chaves
Para chaves por ficheiro:
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
256 bits é o padrão em 2026. O AES de 128 bits ainda é seguro, mas tem menos margem pós-quântica (o algoritmo de Grover reduz a força efectiva para metade).
Para chaves derivadas de palavra-passe:
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"]
);
Armazene o salt junto ao texto cifrado. Não é secreto; apenas tem de ser único por palavra-passe.
O caminho crítico do código
Uma função de encriptação 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 };
}
Desencriptação, com tratamento adequado de erros:
async function decrypt(key, { nonce, ciphertext, aad }) {
try {
return await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonce, additionalData: aad },
key,
ciphertext
);
} catch (e) {
// Falha de autenticação
throw new Error("Desencriptação falhou: texto cifrado adulterado ou chave errada");
}
}
A chamada decrypt lança OperationError em caso de discordância de tag, texto cifrado curto ou chave errada. Trate qualquer excepção como falha de integridade; não tente distinguir.
Ficheiros grandes em blocos
Para ficheiros com mais de algumas centenas de megabytes, processe-os em blocos para evitar pressão de memória:
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;
}
Atenção: o AES-GCM em blocos não detecta truncagem. Um atacante pode eliminar blocos finais e cada bloco sobrevivente desencripta correctamente. Para defesa, inclua a contagem total de blocos no AAD de cada bloco, ou use crypto_secretstream da libsodium, que trata disto.
Desencriptação no servidor (Node.js)
O módulo crypto do Node pode desencriptar dados encriptados no 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()]);
}
A Web Crypto anexa a tag de 128 bits ao texto cifrado; a API do Node espera tag e texto cifrado separadamente. Divida em conformidade.
Números de desempenho
Em hardware típico de 2024-2026 com AES-NI:
- Nativo (OpenSSL, AES-NI): 3-5 GB/s por núcleo
- Web Crypto (navegador com aceleração hardware): 1-2 GB/s
- libsodium.js WASM AES-GCM: 400-800 MB/s
- JS puro (@noble/ciphers): 50-150 MB/s
Para um ficheiro de 1 GB, a encriptação Web Crypto demora 0,5-1 segundo. O JS puro demora 7-20 segundos. Escolha implementações com base nesta realidade; para a experiência do utilizador em transferência de ficheiros grandes, a Web Crypto é a escolha prática.
Erros comuns em resumo
- Reutilização de nonce: catastrófico. O maior modo de falha.
- Usar
Math.random()em vez decrypto.getRandomValues(). - Esquecer de autenticar metadados associados com AAD.
- Usar o modo CBC "porque estamos habituados a ele". O CBC requer um MAC separado para igualar a integridade do GCM; uma construção HMAC-CBC está correcta mas é complexa de implementar bem; o GCM evita a armadilha.
- Capturar erros de desencriptação em silêncio e retornar lixo. Falhe sempre de forma audível.
- Criar a sua própria implementação de GCM. Use Web Crypto, libsodium ou node:crypto. A implementação GHASH tem armadilhas de canal lateral que levaram anos a especialistas para resolver correctamente.
O HexaTransfer usa AES-256-GCM da Web Crypto com nonces aleatórios de 96 bits, tags de 128 bits e sem AAD porque a chave é por ficheiro e o nome do ficheiro é armazenado separadamente em metadados protegidos por AEAD. Simples, correcto e rápido.
Quando escolher outra alternativa
O AES-GCM é óptimo para transferência de ficheiros, mas considere alternativas em casos específicos:
- XChaCha20-Poly1305: nonces de 192 bits tornam a segurança de nonces aleatórios trivial em qualquer escala. Ligeiramente mais lento em hardware com AES-NI, mais rápido em ARM mais antigo sem AES-NI. A libsodium fornece-o.
- AES-GCM-SIV: resistente a uso indevido; a reutilização de nonces não vaza a chave, apenas revela se os textos simples eram iguais. Útil quando não consegue garantir unicidade de nonces.
Para a maioria das cargas de trabalho de transferência de ficheiros numa stack web padrão, AES-256-GCM com uma chave nova por ficheiro e nonces aleatórios de 96 bits é a escolha certa e a mais simples de implementar correctamente.
Experimente em hexatransfer.com — gratuito, sem conta, até 10 GB.
Envie arquivos grandes com segurança e criptografia de ponta a ponta
Transfira arquivos de até 10 GB gratuitamente com criptografia de ponta a ponta. Sem necessidade de conta. Seus arquivos são criptografados no navegador antes do envio — ninguém mais pode lê-los.
Enviar um arquivo