Por que modos de operação importam na prática
Em sistemas reais, raramente você cifra “uma mensagem” de uma vez só: você cifra arquivos, registros de banco, tokens, mensagens de fila, payloads de API, backups e fluxos contínuos. Um algoritmo de bloco (como AES) cifra apenas blocos de tamanho fixo (por exemplo, 128 bits). Para lidar com dados de tamanho arbitrário e para evitar padrões, você precisa de um modo de operação. O modo define como os blocos se encadeiam, como o IV/nonce é usado e como lidar com padding.
Erros de implementação em modos de operação são uma das causas mais comuns de falhas graves: reutilização de nonce, IV previsível, uso de modo sem autenticação, comparação de tags incorreta, padding mal validado, ou “consertos” caseiros como adicionar um hash sem chave. A forma mais consistente de evitar esses erros é: (1) escolher um modo apropriado ao caso de uso e (2) preferir criptografia autenticada (AEAD) sempre que possível, usando bibliotecas de alto nível.
O que um modo de operação resolve (e o que ele não resolve)
Confidencialidade vs integridade
Modos clássicos como CBC e CTR fornecem confidencialidade, mas não garantem integridade. Isso significa que um atacante pode modificar o ciphertext e, dependendo do modo e do contexto, provocar alterações previsíveis no plaintext após a decifragem, ou explorar diferenças de erro/tempo para extrair informação. Criptografia autenticada (AEAD) combina confidencialidade e integridade em uma única operação: ao decifrar, o sistema verifica uma tag de autenticação e só entrega o plaintext se a verificação passar.
IV, nonce e aleatoriedade
Quase todos os modos exigem um valor inicial: IV (Initialization Vector) ou nonce. Em muitos modos, esse valor não precisa ser secreto, mas precisa obedecer regras rígidas: em alguns casos deve ser imprevisível/aleatório (ex.: CBC), em outros deve ser único por chave (ex.: CTR e GCM). Confundir “aleatório” com “único” é um erro frequente: um nonce pode ser determinístico (contador), desde que nunca se repita com a mesma chave.
Modos comuns e armadilhas típicas
ECB: o modo que você deve evitar
ECB (Electronic Codebook) cifra cada bloco independentemente. Blocos iguais geram ciphertexts iguais, revelando padrões do conteúdo. Mesmo sem entrar em detalhes teóricos, na prática isso vaza estrutura (por exemplo, áreas repetidas em imagens, campos repetidos em registros). Não use ECB para dados estruturados ou repetitivos; na prática, “não use ECB” é uma regra segura para a maioria dos cenários.
Continue em nosso aplicativo
Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.
ou continue lendo abaixo...Baixar o aplicativo
CBC: confidencialidade com riscos de padding e de integridade
CBC (Cipher Block Chaining) encadeia blocos usando XOR com o bloco anterior. Ele exige um IV imprevisível (tipicamente aleatório) para o primeiro bloco. Problemas comuns:
- Padding oracle: CBC precisa de padding quando o tamanho não é múltiplo do bloco. Se o sistema vaza (por mensagem de erro, código HTTP, tempo de resposta) se o padding estava correto, um atacante pode recuperar plaintext sem a chave.
- Malleability: sem autenticação, um atacante pode alterar bits do ciphertext e provocar alterações controladas em partes do plaintext.
- IV reutilizado ou previsível: pode vazar relações entre mensagens e facilitar ataques dependendo do contexto.
Se você ainda precisar usar CBC por compatibilidade, a regra prática é: Encrypt-then-MAC (cifrar e depois autenticar o ciphertext com um MAC) e tratar erros de forma uniforme, sem distinguir “padding inválido” de “MAC inválido”. Ainda assim, a preferência moderna é migrar para AEAD.
CTR: simples e rápido, mas exige nonce único
CTR (Counter mode) transforma um cifrador de bloco em um cifrador de fluxo: gera um keystream cifrando um contador (nonce + contador) e faz XOR com o plaintext. Vantagens: paralelizável, sem padding, bom para streaming. Armadilhas:
- Reutilização de nonce: se o mesmo nonce for usado duas vezes com a mesma chave, o mesmo keystream é reutilizado. Isso permite que um atacante combine dois ciphertexts e obtenha XOR dos plaintexts, frequentemente suficiente para recuperar ambos (especialmente com conteúdo parcialmente conhecido).
- Sem integridade: alterações no ciphertext alteram o plaintext de forma previsível.
CTR deve ser combinado com autenticação (por exemplo, HMAC) ou substituído por AEAD.
GCM: AEAD popular, com uma regra crítica
AES-GCM é um modo AEAD muito usado em APIs e protocolos. Ele fornece confidencialidade e integridade e suporta dados associados (AAD): metadados autenticados mas não cifrados (por exemplo, cabeçalhos, IDs, versão do protocolo). A regra mais importante:
- Nunca reutilize o nonce com a mesma chave. Em GCM, reutilização de nonce é especialmente catastrófica: pode quebrar confidencialidade e permitir forjar tags.
Nonces em GCM normalmente têm 96 bits. Você pode gerá-los aleatoriamente com probabilidade de colisão extremamente baixa, ou determinística com contador por chave. Em sistemas distribuídos, contadores exigem coordenação; aleatório exige boa fonte de aleatoriedade e monitoramento de risco operacional (por exemplo, reinícios que resetam contadores).
ChaCha20-Poly1305: AEAD robusto para software
ChaCha20-Poly1305 é um AEAD amplamente adotado, eficiente em CPUs sem aceleração AES e com bom desempenho em software. Assim como GCM, exige nonce único por chave. É uma escolha comum para aplicações móveis, IoT e serviços onde AES-GCM não é acelerado por hardware.
Criptografia autenticada (AEAD) na prática
O que é AEAD e como ela evita classes de bugs
AEAD (Authenticated Encryption with Associated Data) faz duas coisas em conjunto: cifra e autentica. O resultado típico é: ciphertext + tag (tag de autenticação). Ao decifrar, a biblioteca verifica a tag; se falhar, ela retorna erro e não entrega plaintext. Isso reduz a chance de você “esquecer” de validar integridade ou de validar de forma incorreta.
AEAD também padroniza o uso de AAD. Exemplo: em uma API, você pode autenticar o método HTTP, a rota, o ID do usuário, a versão do esquema e um timestamp como AAD. Se alguém alterar esses metadados, a verificação falha, mesmo que o ciphertext não tenha sido alterado.
Estrutura recomendada de mensagem
Um formato simples e robusto para armazenar/transmitir dados cifrados com AEAD é:
- version: 1 byte ou string curta (para migrações futuras)
- algorithm_id: opcional, se você suporta mais de um AEAD
- nonce: tamanho fixo (ex.: 12 bytes em GCM)
- ciphertext: bytes cifrados
- tag: geralmente 16 bytes (depende do modo)
O nonce deve ser armazenado junto com o ciphertext. Ele não é segredo; é um parâmetro necessário para decifrar. A AAD não precisa ser armazenada dentro do blob se já estiver disponível no contexto (por exemplo, campos do protocolo), mas deve ser reconstruída exatamente igual na decifragem.
Passo a passo prático: cifrando um payload com AEAD (GCM/ChaCha20-Poly1305)
O objetivo aqui é um roteiro de implementação que reduz decisões perigosas. O passo a passo é conceitual e pode ser aplicado em qualquer linguagem com uma biblioteca criptográfica moderna.
1) Defina o que é plaintext e o que é AAD
Plaintext: dados que você quer manter confidenciais (ex.: JSON com dados pessoais, segredo de sessão, conteúdo de arquivo).
AAD: metadados que precisam ser protegidos contra alteração, mas podem ficar em claro (ex.: user_id, record_id, schema_version, timestamp, content-type).
Exemplo prático: você armazena no banco um “envelope” cifrado de um registro. Você quer que o record_id e a schema_version sejam autenticados para impedir troca de contexto (copiar ciphertext de um registro para outro).
2) Escolha o AEAD e tamanhos padrão
- Preferência: AES-256-GCM ou ChaCha20-Poly1305.
- Nonce: 12 bytes (96 bits) é o padrão mais comum.
- Tag: 16 bytes (128 bits) é o padrão mais comum.
Evite “otimizar” reduzindo tamanho de tag sem necessidade; isso reduz margem de segurança e aumenta chance de aceitação de falsificações em cenários de alto volume.
3) Gere/gerencie a chave corretamente (e separe por finalidade)
Use uma chave aleatória com tamanho adequado ao algoritmo. Evite reutilizar a mesma chave para finalidades diferentes (por exemplo, a mesma chave para cifrar dados e para assinar tokens). Na prática, isso significa: uma chave por “domínio” (ex.: db-record-encryption, file-encryption, message-encryption), idealmente derivadas de uma chave mestra via KDF dentro de um KMS ou módulo seguro.
4) Gere um nonce único por mensagem
Opção A (comum e simples): nonce aleatório de 12 bytes por mensagem, gerado por CSPRNG.
Opção B (determinístico): nonce como contador monotônico por chave, persistido de forma segura.
Checklist para evitar bugs:
- Não derive nonce de timestamp com baixa resolução.
- Não use contador que reseta em reinício sem persistência.
- Não use nonce “fixo” para simplificar testes e esquecer de trocar em produção.
5) Monte a AAD de forma canônica
AAD precisa ser exatamente igual na cifragem e na decifragem. Defina uma serialização canônica, por exemplo:
- Concatenação de campos em ordem fixa
- Codificação explícita (UTF-8) e separadores não ambíguos
- Ou serialização binária com comprimentos (TLV)
Exemplo de AAD canônica (pseudoformato): version || ":" || record_id || ":" || schema_version. Evite JSON como AAD sem canonicalização, pois mudanças de espaços/ordem de chaves podem quebrar a verificação.
6) Cifre e obtenha (ciphertext, tag)
Use a função AEAD da biblioteca, passando: chave, nonce, plaintext e AAD. O retorno será o ciphertext e a tag (às vezes a biblioteca já retorna tudo concatenado).
// Pseudocódigo (conceitual, independente de linguagem) nonce = RandomBytes(12) aad = EncodeAAD(version, record_id, schema_version) (ciphertext, tag) = AEAD_Encrypt(key, nonce, plaintext, aad) envelope = version || nonce || ciphertext || tag Store(envelope)7) Decifre com verificação obrigatória da tag
Na leitura, recupere version, nonce, ciphertext e tag. Reconstrua a AAD exatamente como antes. Chame a função de decifragem AEAD; se a tag falhar, trate como erro e não use o plaintext.
// Pseudocódigo envelope = Load() version = envelope[0] nonce = envelope[1..12] ciphertext = ... tag = ... aad = EncodeAAD(version, record_id, schema_version) plaintext = AEAD_Decrypt(key, nonce, ciphertext, aad, tag) // Se falhar: erro de autenticação, não retornar plaintextEvite qualquer lógica do tipo “se falhar, tenta de outro jeito” sem controle de versão explícito. Para migrações, use version para selecionar algoritmo e formato, e falhe de forma consistente.
Quando você não pode usar AEAD diretamente
Compatibilidade com CBC: Encrypt-then-MAC (EtM)
Se um sistema legado exige AES-CBC, a composição recomendada é:
- Cifrar com CBC usando IV aleatório e padding padrão.
- Calcular um MAC (ex.: HMAC) sobre IV || ciphertext (e também AAD se houver).
- Na decifragem: verificar o MAC antes de tentar remover padding e antes de retornar qualquer erro detalhado.
Isso evita padding oracle, porque ciphertexts alterados são rejeitados antes de qualquer validação de padding. Ainda assim, é mais fácil errar do que com AEAD, então trate como solução de transição.
// Pseudocódigo EtM iv = RandomBytes(16) ciphertext = CBC_Encrypt(enc_key, iv, plaintext) mac = HMAC(mac_key, aad || iv || ciphertext) envelope = iv || ciphertext || macComparação de MAC/tag em tempo constante
Ao verificar MAC/tag, use comparação em tempo constante fornecida pela biblioteca. Comparações ingênuas que retornam ao primeiro byte diferente podem vazar informação por timing em ambientes onde o atacante mede latência com precisão.
Erros de implementação frequentes e como evitá-los
1) Reutilizar nonce (CTR/GCM/ChaCha20-Poly1305)
Mitigação prática:
- Se usar nonce aleatório: use CSPRNG e registre métricas/alertas para colisões (por exemplo, detectar nonce repetido por chave).
- Se usar contador: persista o contador e garanta atomicidade (especialmente em múltiplas instâncias).
- Separe chaves por contexto; isso reduz o impacto caso um subsistema tenha bug de nonce.
2) “Autenticar” com hash sem chave
Adicionar SHA-256 do ciphertext não impede adulteração, porque qualquer um pode recalcular o hash. Use MAC com chave (HMAC) ou AEAD.
3) Usar o mesmo material de chave para cifrar e autenticar
Em composições manuais (CBC + HMAC), use chaves distintas: enc_key e mac_key. Derive ambas de uma chave mestra via KDF, com rótulos diferentes.
4) Esquecer de autenticar metadados críticos
Mesmo com AEAD, se você não inclui certos campos como AAD, um atacante pode “recontextualizar” uma mensagem: mover um ciphertext válido de um lugar para outro onde ele faça sentido. Autentique identificadores e versões relevantes como AAD (por exemplo, tenant_id, user_id, record_id, purpose).
5) Tratar erro de autenticação como “dado vazio”
Se a verificação falhar, o correto é falhar a operação. Retornar string vazia, objeto default ou “tentar decifrar mesmo assim” cria caminhos de exploração e corrupção silenciosa de dados.
6) Misturar compressão e criptografia sem cuidado
Comprimir antes de cifrar pode vazar informação em alguns cenários interativos onde o atacante controla parte do plaintext e observa tamanhos (ataques por canal lateral de compressão). Se você precisa de compressão, avalie se o atacante pode influenciar o conteúdo e observar tamanhos; em muitos casos, prefira comprimir dados não sensíveis separadamente ou padronizar tamanhos.
Escolhendo o modo certo por cenário
APIs e mensagens entre serviços
- Use AEAD (AES-GCM ou ChaCha20-Poly1305).
- Nonce aleatório por mensagem é comum; inclua no envelope.
- AAD: método/rota, IDs, versão do protocolo, timestamp/expiração.
Criptografia de registros em banco (envelope por linha)
- AEAD com AAD incluindo
table/record_id/tenant_id/schema_version. - Formato versionado para permitir rotação/migração.
- Considere chaves por tenant ou por domínio de dados.
Arquivos grandes e streaming
AEAD ainda é recomendado, mas você precisa lidar com streaming e limites de memória. Uma abordagem prática é cifrar em chunks (por exemplo, 1–16 MB), cada chunk com seu próprio nonce e tag, e AAD incluindo o índice do chunk e o ID do arquivo. Isso permite validação incremental e evita precisar carregar tudo na memória.
// Pseudocódigo de chunks file_id = RandomBytes(16) for chunk_index in 0..N: nonce = DeriveNonce(base_nonce, chunk_index) aad = file_id || chunk_index ciphertext, tag = AEAD_Encrypt(key, nonce, chunk, aad) write(chunk_index, nonce, ciphertext, tag)A derivação de nonce por índice deve garantir unicidade. Uma forma simples é usar um base_nonce aleatório e combinar com contador de forma definida (por exemplo, últimos 4 bytes como contador), respeitando o tamanho do nonce do algoritmo.
Checklist operacional para evitar erros
- Prefira AEAD; evite modos sem autenticação.
- Defina e documente regra de nonce: como é gerado, onde é armazenado, como garantir unicidade por chave.
- Inclua AAD para amarrar ciphertext ao contexto (IDs, versões, propósito).
- Versione o formato do envelope e mantenha migrações explícitas.
- Falhe fechado: tag/MAC inválido deve abortar a operação.
- Use bibliotecas de alto nível e evite implementar modos manualmente.
- Teste com casos negativos: nonce repetido, tag alterada, AAD alterada, truncamento de ciphertext, bytes extras.