Skip to content
Voltar ao blog
Segurança

Falha ao descriptografar AES: chave, IV, modo e padding

Falha ao descriptografar AES? Uma chave errada lança erro de padding e um IV errado corrompe só o primeiro bloco. Depure o seu texto cifrado online e de graça.

16 min de leitura

Falha ao descriptografar AES: chave, IV, modo e padding

Quando aparece uma falha ao descriptografar AES nos seus logs, a mensagem que você recebeu provavelmente descreve o problema errado. Quatro bugs sem relação entre si produzem sintomas quase idênticos, e o mais comum deles, uma chave errada, se anuncia como um erro de padding.

A ordem que economiza mais tempo, quando uma descriptografia CBC lança BadPaddingException:

  1. Os bytes da chave são diferentes nos dois lados. É de longe a causa mais provável.
  2. A derivação da chave é diferente. Mesma frase-senha, KDF ou número de iterações diferente, logo bytes de chave diferentes.
  3. O texto cifrado foi danificado no transporte: truncado, base64 corrompido ou submetido a uma ida e volta em codificação de texto.
  4. O IV está errado. Acontece de verdade, mas não lança erro de padding. Corrompe dezesseis bytes e fica quieto.

A ordem é estrutural. O CBC confere o padding como último passo da descriptografia, depois de aplicar a chave e desfazer o encadeamento, então o padding funciona como um checksum de tudo o que veio antes e falha em alto e bom som não importa qual peça anterior quebrou. Se preferir pular as medições, cole o seu texto cifrado na ferramenta de descriptografia AES e faça a bissecção da seção 9.

Tudo o que vem abaixo foi medido em java 1.8.0_162, node v25.8.2 e openssl 3.6.2. Os padrões mudam de versão para versão, então trate os números de versão como parte do resultado.

1. Comece pelo que o seu erro de fato descarta

Uma mensagem de falha do AES diz quase nada sobre a causa e muito sobre o que a causa não pode ser. Use-a para eliminar ramos, não para escolher um.

O que você vêO que isso descartaO que continua em jogo
BadPaddingException, bad decrypt, wrong final block lengthGCM; um erro só de IV; uma falha de decodificaçãochave errada, KDF errado, texto cifrado truncado, bytes do IV consumidos como texto cifrado, modo incompatível, esquema de padding divergente
GCM Authentication failed, Unsupported state or unable to authenticate datapadding; qualquer teoria que envolva saída parcialchave errada, nonce errado, tag destacada ou fora do lugar, tamanho de tag errado, AAD divergente
Nenhuma exceção, a saída é lixotodo modo autenticadoECB, CTR, CBC que deu sorte, modo incompatível, IV errado

BadPaddingException, bad decrypt, wrong final block length

O mesmo evento em três ecossistemas: Java, OpenSSL e .NET. Ele dispara no fim da descriptografia CBC ou ECB, quando o último bloco de texto claro não termina em um padrão PKCS#7 válido.

A parte útil é a negativa: chegar até aqui significa que o seu base64 ou hex decodificou e que a contagem de bytes era um múltiplo de 16 diferente de zero, ou seja, o transporte não picotou os dados e você não está em GCM. wrong final block length é a exceção. Ali a contagem não era múltiplo de 16, o que aponta para truncamento e não para a chave, então pule para a seção 8.

GCM Authentication failed e companhia

O GCM compara a tag antes de liberar um único byte de texto claro, como exige o NIST SP 800-38D. Isso o torna honesto de um jeito que o erro de padding não é: alguma coisa na tupla (chave, nonce, texto cifrado, dados autenticados adicionais, tag) não bate com o que o lado que criptografou usou. Ele não consegue dizer qual elemento, e nunca vai conseguir, porque afunilar isso está deliberadamente fora do que o algoritmo faz. A seção 6 cobre o elemento que mais quebra entre linguagens: a posição da tag, não o valor dela.

Nenhum erro, mas a saída é lixo

É o desfecho perigoso, porque um painel registra isso como sucesso. O CTR nunca lança exceção e o ECB nunca lança. O CBC só lança quando o padrão do último byte não passa na checagem de padding, e com uma chave errada esse byte é praticamente aleatório, então mais ou menos uma tentativa em 256 cai em 0x01 e valida. Pouco menos de 0,4% das descriptografias CBC com chave errada “dão certo”. Só que lixo tem forma, e a forma dá nome ao bug: as seções 4 e 5 trazem as duas assinaturas que vale a pena memorizar.

2. O erro mais enganoso do AES

Esta medição reordena a lista de suspeitos. Chave 0123456789abcdef, IV todo zerado, AES/CBC/PKCS5Padding, texto claro hello world, em java 1.8.0_162 com o provider SunJCE embutido no JDK:

CenárioMudançaResultado medido
AChave errada em 1 byte (último caractere fX)lança javax.crypto.BadPaddingException: Given final block not properly padded. O padding em si nunca esteve malformado; o erro é completamente enganoso
BChave certa, IV errado em 1 bytenenhuma exceção, o texto claro hello world voltou como iello world. Só o byte correspondente do primeiro bloco foi danificado
CChave certa, descriptografar o texto cifrado CBC com AES/ECBfuncionou silenciosamente, sem exceção. Um modo incompatível não precisa levantar nada

O cenário A desperdiça tardes inteiras na direção errada. O cenário C manda dado ruim para produção.

Por que uma chave errada produz um erro de padding

Não havia nada de errado com o padding. Quem criptografou acrescentou cinco bytes 0x05 para levar hello world até dezesseis, criptografou esse bloco, e ele está no seu texto cifrado intacto.

A falha acontece na saída. A descriptografia CBC roda a cifra de bloco ao contrário, faz XOR de cada resultado com o bloco de texto cifrado anterior, e só então lê o final do último bloco para decidir quantos bytes remover. Com a chave errada a cifra produz dezesseis bytes de ruído, e ruído quase nunca termina em um padrão PKCS#7 válido. A biblioteca relata o que viu, padding ruim, o que é verdade e é inútil.

Leia BadPaddingException como “o texto claro que eu reconstruí não termina do jeito que um texto claro com padding termina”. O motivo mais provável de a sua reconstrução estar errada é a chave, e por isso uma busca por aes decrypt wrong key e uma busca por bad padding exception caem nas mesmas discussões: os dois sintomas são um só. Em português a busca ainda se divide, porque a mesma operação circula como descriptografar, desencriptar e decifrar conforme o autor e o lado do Atlântico, enquanto a exceção que você vai colar no campo de busca continua em inglês. Uma nota de projeto, de passagem: nunca exponha essa distinção a quem chama, porque diferenciar “padding inválido” de “padding válido, conteúdo errado” é justamente o que alimenta um ataque de padding oracle (Vaudenay, EUROCRYPT 2002).

O que o GCM faz de diferente

O GCM inverte a ordem: confere a tag antes de produzir qualquer texto claro, então não existe janela em que bytes parcialmente corretos apareçam. Uma falha de GCM nunca deixa você em dúvida se a saída é real, porque não há saída. O GCM também não tem padding nenhum, já que por baixo é um modo de contador, então o comprimento do texto cifrado é igual ao do texto claro. Um erro de padding em um sistema que você achava que era GCM prova, portanto, que o sistema não é GCM, normalmente uma configuração que caiu de volta para CBC.

3. Os dois lados estão usando os mesmos bytes de chave?

O AES não enxerga a sua string de chave. Ele enxerga 16, 24 ou 32 bytes. Dois sistemas podem guardar material de chave idêntico em um arquivo de configuração e ainda assim divergir, porque “idêntico” é uma propriedade do texto, não dos bytes.

As três formas de transformar uma string de chave em bytes

Entregue a string literal 0123456789abcdef para três bibliotecas diferentes:

como hex          -> 8 bytes    (comprimento de chave AES inválido)
como base64       -> 12 bytes   (comprimento de chave AES inválido)
como UTF-8 cru    -> 16 bytes   (AES-128 válido)

Dezesseis caracteres, três contagens de bytes. Esse caso é traiçoeiro justamente porque é válido nas três leituras: todo caractere está tanto no alfabeto hex quanto no base64, e dezesseis caracteres é um comprimento legal para os dois decodificadores, então nada dá erro na hora de interpretar.

O guia JWT invalid signature: todas as causas e como corrigir cada uma traz a matriz completa de como cada ecossistema interpreta uma string de segredo; a versão curta para o AES é anotar em qual codificação está o seu material de chave e fazer os dois lados decodificarem explicitamente. A versão HMAC do mesmo bug morde quem recebe webhooks, e está em Falha na verificação de assinatura de webhook: causas e soluções.

O AES é rígido: exatamente 16, 24 ou 32 bytes

É aqui que o AES difere da primitiva que a maioria dos desenvolvedores conhece primeiro. O HMAC aceita qualquer tamanho de chave: a RFC 2104 aplica hash em qualquer coisa maior que o tamanho do bloco e completa com zeros qualquer coisa menor, então um gerador de HMAC aceita um segredo de 7 bytes ou de 700 bytes sem reclamar. O AES tem exatamente três tamanhos de chave legais e rejeita todo o resto antes de processar um único bloco.

Essa rigidez é um presente, porque o erro de comprimento é a única falha do AES que dá nome à própria causa em vez de se esconder atrás do padding. A nossa ferramenta escreve assim: Key must be 16, 24, or 32 bytes (AES-128/192/256). As armadilhas que produzem um comprimento errado:

  • Uma quebra de linha no final, vinda de KEY=$(cat key.txt) ou echo "$KEY". Use printf e echo -n. Um espaço no final, colado da interface de um gerenciador de segredos, faz a mesma coisa.
  • Um prefixo 0x copiado de um depurador: trinta e quatro caracteres que deixaram de ser hex válido.
  • Caracteres fora do ASCII. contraseña tem 10 caracteres e 11 bytes em UTF-8, então uma frase-senha de “32 caracteres” com uma letra acentuada tem 33 bytes.

SecretKeySpec e o charset padrão da plataforma

O Java tem uma versão disso que só aparece depois do deploy. "my secret".getBytes() sem argumento usa o charset padrão da plataforma, que antes do JDK 18 vinha da propriedade file.encoding e, portanto, do sistema operacional e do locale da máquina. Um notebook em UTF-8 e um contêiner em ANSI_X3.4-1968 produzem bytes diferentes para qualquer caractere fora do ASCII. A JEP 400 tornou o UTF-8 o padrão no JDK 18, o que conserta código novo e nada além disso.

// errado: os bytes dependem da máquina
SecretKeySpec ks = new SecretKeySpec(secret.getBytes(), "AES");

// certo: os bytes não dependem de nada
SecretKeySpec ks = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "AES");

Se o seu código funciona na sua máquina, falha no servidor com um erro de padding e a frase-senha tem qualquer coisa fora do ASCII, cheque isso primeiro.

4. O IV: onde ele vai e com que cara fica quando está errado

Um aes iv mismatch é a falha de que as pessoas suspeitam primeiro e que diagnosticam por último, porque ela não se comporta como as outras. É silenciosa e é local.

Um IV errado corrompe exatamente um bloco

Olhe de novo o cenário B. Chave certa, IV errado em um byte:

hello world   ->   iello world

Nenhuma exceção, um caractere. Escreva o passo do CBC para o primeiro bloco e fica óbvio: P1 = D(C1) XOR IV. O IV entra por XOR direto no primeiro bloco de texto claro e não toca em mais nada, então inverter um bit do IV inverte o mesmo bit do texto claro na mesma posição. Aqui o h (0x68) virou i (0x69), ou seja, o primeiro byte do IV se deslocou em exatamente 0x01.

A assinatura. No CBC, os primeiros 16 bytes virarem lixo e todo o resto sair limpo significa que o IV está errado e a chave está certa. Todo bloco virar lixo significa que a chave está errada. Essa única observação separa as duas causas mais comuns sem mudar uma linha de código, e a ferramenta de descriptografia AES mostra os bytes decodificados para você ler isso direto.

Por que nada foi lançado. hello world tem 11 bytes, ou seja, é um único bloco, e o padding PKCS#7 mora nos bytes 11 a 15 dele. O byte do IV que mudou foi o byte 0, então a região do padding ficou intocada e validou. Corrompa um byte do IV na posição 11 ou depois e você recebe um erro de padding, que é mais um caminho pelo qual o erro de padding mente para você.

Três convenções de transmissão

Não existe padrão para onde o IV vai, só três costumes que interoperam mal.

Na frente. iv || ciphertext, a convenção mais comum e o padrão nas nossas ferramentas. Os dois lados precisam concordar sobre quanto remover: 16 bytes para CBC e CTR, 12 para GCM. O bug espelhado é um produtor que coloca o IV na frente e um consumidor que não remove. Os primeiros 16 bytes do “texto cifrado” passam a ser o IV, todo bloco desloca, e você recebe um erro de padding.

Em um campo separado. {"iv": "...", "ciphertext": "..."} é mais limpo em princípio e dobra os lugares em que uma codificação pode divergir, já que agora o IV tem a própria questão base64 versus hex.

Uma constante fixa. Normalmente tudo zero, fixada no código porque alguém precisava de determinismo. Interopera perfeitamente, e é aí que mora o perigo: no CBC um IV fixo vaza igualdade entre registros, e no GCM reusar um nonce sob uma mesma chave revela o XOR dos dois textos claros e pode expor a subchave GHASH que autentica a tag. A SP 800-38D é explícita quanto à unicidade.

A opção de texto cifrado puro da ferramenta, com o IV informado explicitamente, testa as três convenções contra os mesmos bytes em um minuto.

O IV do GCM tem 12 bytes, não 16

Times que adotam GCM editando um caminho CBC existente carregam junto o IV de 16 bytes, e o resultado falha sem dar pista nenhuma.

A SP 800-38D padroniza um IV de 96 bits. Outros comprimentos são permitidos, mas não são simplesmente “um IV mais longo”: quando o IV não tem 96 bits, o GCM deriva o bloco de contador inicial passando o IV pelo GHASH em vez de usá-lo direto. Os mesmos 16 bytes usados como nonce produzem, portanto, um keystream e uma tag completamente diferentes do que os 12 primeiros produziriam, e você recebe uma falha de autenticação genérica. Se o texto cifrado veio de outro lugar e você está adivinhando o layout, conte de trás para frente: a tag são os últimos 16 bytes, e o nonce quase sempre os 12 primeiros.

5. Modo incompatível, inclusive o tipo silencioso

Cipher.getInstance("AES") é ECB

O Java deixa você nomear uma cifra sem nomear um modo nem um esquema de padding. Ele não recusa e não avisa. Sob o provider SunJCE embutido no JDK, ele preenche as lacunas com ECB e PKCS5Padding.

Provar isso exige o experimento certo: criptografe 32 bytes idênticos (dois blocos de A) com a chave 0123456789abcdef, depois veja se os dois blocos de texto cifrado batem. Em java 1.8.0_162:

getInstance("AES")           ciphertext = 3bfd04cc0d7ed55358e2cbe19de213833bfd04cc0d7ed55358e2cbe19de21383377222e061a924c591cd9c27ea163ed4
  block1 = 3bfd04cc0d7ed55358e2cbe19de21383
  block2 = 3bfd04cc0d7ed55358e2cbe19de21383   <- blocos idênticos = a assinatura do ECB (a estrutura do texto claro vaza)
getInstance("AES/CBC/PKCS5Padding")  blocos diferentes = o encadeamento está ativo

Idênticos byte a byte. Essa é a assinatura do ECB, a mesma propriedade que faz a famosa imagem do pinguim criptografado continuar parecendo um pinguim. O experimento só funciona com blocos de texto claro idênticos: dezesseis bytes A seguidos de dezesseis bytes B produzem dois blocos de texto cifrado diferentes sob ECB também, e você concluiria erradamente que o padrão era CBC.

Repare no escopo: o resultado vale para o provider SunJCE embutido no JDK, na versão acima. A transformação padrão é decisão do provider, então um provider de terceiros como o BouncyCastle pode resolver o mesmo atalho de outra forma. A generalização não é “Java quer dizer ECB”, e sim “uma string de transformação sem qualificação quer dizer o que o seu provider decidir, motivo pelo qual você nunca escreve uma”.

O modo errado pode não levantar erro

O cenário C descriptografou texto cifrado CBC com AES/ECB e devolveu o texto claro correto sem exceção. Isso parece impossível até você escrever a aritmética. A criptografia CBC do primeiro bloco é C1 = E(P1 XOR IV), e a descriptografia ECB desse bloco é D(C1) = P1 XOR IV. O IV aqui era todo zero, então P1 XOR 0 = P1 e o primeiro bloco descriptografa perfeitamente. hello world tem um bloco de comprimento, então “o primeiro bloco” era a mensagem inteira.

A regra geral: com um IV zerado, ECB e CBC concordam no primeiro bloco e discordam em todos os blocos seguintes. Descriptografe uma mensagem CBC longa como ECB e você recebe dezesseis bytes limpos seguidos de ruído, o inverso exato da assinatura do IV errado. Duas formas opostas, dois bugs diferentes, e nenhum dos dois emite mensagem de erro. IVs zerados fixados no código são comuns o bastante para isso não ser curiosidade de laboratório.

O que uma chamada mínima te dá em cada linguagem

EcossistemaChamada mínimaModo que você realmente recebe
Java (SunJCE)Cipher.getInstance("AES")ECB com PKCS5Padding, silenciosamente
Node cryptocreateDecipheriv('aes-256-cbc', key, iv)o que a string do algoritmo disser; não existe padrão
Web Cryptocrypto.subtle.decrypt({ name: 'AES-CBC', iv }, ...)nomeado explicitamente; ECB nem sequer é implementado
Python cryptographyCipher(algorithms.AES(key), modes.CBC(iv))o objeto de modo é obrigatório
PyCryptodomeAES.new(key, AES.MODE_ECB)argumento obrigatório, mas o ECB está ali no autocompletar
Go crypto/aesaes.NewCipher(key) devolve um cipher.Block cruchamar Decrypt nesse bloco é ECB; embrulhe em cipher.NewCBCDecrypter ou cipher.NewGCM
CryptoJSCryptoJS.AES.decrypt(ct, "passphrase")CBC, PKCS#7, EVP_BytesToKey com MD5 (veja a seção 7)

Ecossistemas em que o modo mora numa string ou num objeto nunca te surpreendem. Os dois que oferecem uma chamada de “só AES”, Java e Go, são de onde vêm os relatos de ECB acidental. Quando você não sabe qual modo produziu o seu texto cifrado, rode os mesmos bytes em CBC, CTR e GCM e veja qual deles devolve texto legível.

6. GCM: os mesmos bytes, APIs diferentes

A maioria das falhas de aes gcm auth tag entre linguagens não é criptográfica. Os dois lados calcularam os mesmos 16 bytes e discordam sobre onde esses bytes ficam.

A medição

Chave = 32 bytes 0123456789abcdef0123456789abcdef, IV = 12 bytes zerados, texto claro hello world, em node v25.8.2 e java 1.8.0_162:

Node   ciphertext = a616cd6d7d2328379d41e5                    (11 B)   <- update+final
       authTag    = c87af9f8ad7148e873fa797292c0af3f          (16 B)   <- obtida à parte via getAuthTag()
Java   doFinal()  = a616cd6d7d2328379d41e5c87af9f8ad7148e873fa797292c0af3f   (27 B)   <- texto cifrado e tag já concatenados

Node ciphertext || authTag é exatamente Java doFinal(), todos os 27 bytes. Nenhuma diferença de codificação, nada a negociar: o Node te entrega as duas peças separadas e o Java entrega as duas coladas. Repare também que 11 bytes de texto claro renderam 11 bytes de texto cifrado, porque o GCM não acrescenta padding. Um erro de padding nunca sai de um caminho GCM de verdade.

Concatenada ou separada, por runtime

RuntimeAPI de criptografiaOnde a tag vai parar
Node cryptoupdate() + final(), depois getAuthTag()separada
Java (SunJCE, AES/GCM/NoPadding)doFinal()anexada
Go cipher.AEADSeal()anexada
Python cryptography, AESGCMencrypt()anexada
Python cryptography, Cipher + modes.GCMfinalize(), depois encryptor.tagseparada
Web Cryptocrypto.subtle.encryptanexada

O Node é o ponto fora da curva entre as APIs de alto nível, e Node-para-qualquer-coisa é a direção de falha mais relatada. Para entregar a saída do Node a um consumidor Java, Go, Python ou navegador, empacote assim:

const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const packed = Buffer.concat([ct, cipher.getAuthTag()]);   // agora bate com doFinal()

Para desempacotar um blob concatenado no Node:

const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(packed.subarray(packed.length - 16));  // precisa vir antes de final()
const pt = Buffer.concat([
  decipher.update(packed.subarray(0, packed.length - 16)),
  decipher.final(),
]);

A restrição de ordem é real: chame setAuthTag() depois de final() e o Node lança Unsupported state or unable to authenticate data mesmo com todos os bytes corretos. Se não estiver claro em qual ponta do blob a tag mora, confira a posição dela contra os mesmos bytes antes de suspeitar da chave.

O tamanho da tag é variável, e a unidade não é

O GCM admite tags de 128, 120, 112, 104 ou 96 bits, com 64 e 32 reservados para aplicações restritas (SP 800-38D, Apêndice C). Quase todo mundo usa 128, e o problema é como cada API pede esse valor:

  • Java: new GCMParameterSpec(128, iv). O primeiro argumento é em bits.
  • Web Crypto: { name: 'AES-GCM', iv, tagLength: 128 }. Também em bits, com padrão 128.
  • Node: createCipheriv(algo, key, iv, { authTagLength: 16 }). Em bytes.

new GCMParameterSpec(16, iv) é uma linha Java com cara de legítima que pede uma tag de 16 bits; alguns JDKs rejeitam, e onde ela é aceita você trocou a sua garantia de integridade por um cara ou coroa de uma chance em 65.536. Quando os dois lados discordam sobre o tamanho da tag, os comprimentos empacotados também diferem, então quem recebe corta na fronteira errada e leva uma falha de autenticação que não tem nada a ver com a chave.

7. Você tem uma frase-senha, não uma chave

Se algum dos lados recebe uma string digitada por uma pessoa, existe uma função de derivação de chave entre essa string e o AES, e uma divergência de KDF é invisível. Ela nunca dá erro. Devolve 32 bytes perfeitamente bons que por acaso são os 32 bytes errados, e a falha aparece uma camada abaixo como (essa você já conhece) um erro de padding.

O PBKDF2 precisa de quatro coisas alinhadas

  • Salt. No formato Salted__ do OpenSSL são 8 bytes dentro do texto cifrado; no formato de frase-senha das nossas ferramentas é um prefixo de 16 bytes; em esquemas caseiros costuma ser uma constante fixada no código.
  • Iterações. O openssl enc -pbkdf2 usa 10.000 por padrão. A OWASP recomenda hoje 600.000 para PBKDF2-HMAC-SHA256, que é o que o nosso modo de frase-senha usa. Frameworks escolhem os próprios números.
  • Hash. SHA-1 versus SHA-256 versus SHA-512. Código antigo e alguns SDKs móveis ainda usam SHA-1 por padrão.
  • Comprimento da saída. Trinta e dois bytes para AES-256, dezesseis para AES-128. Alguns esquemas derivam chave e IV juntos em uma única chamada mais longa, o que nunca bate com uma derivação simples de 32 bytes.

EVP_BytesToKey, e por que o CryptoJS insiste em não funcionar

cryptojs aes decrypt not working costuma ser uma divergência bem específica. CryptoJS.AES.encrypt(text, "passphrase") não usa PBKDF2. Usa o EVP_BytesToKey, a derivação do OpenSSL anterior à 1.1, com MD5 e uma única iteração.

O EVP_BytesToKey ainda faz uma coisa que o PBKDF2 não faz: deriva a chave e o IV a partir da frase-senha e do salt em uma passada só. Daí um arquivo Salted__ do OpenSSL não carregar campo de IV separado, e daí reproduzir a saída do CryptoJS com PBKDF2 mais um IV aleatório estar errado em dobro.

O formato é reconhecível de bate-pronto: os 8 bytes ASCII Salted__ seguidos de um salt de 8 bytes, codificados em base64, sempre começam com U2FsdGVkX1. Se o seu texto cifrado começa assim, ele é derivado de frase-senha e você precisa saber qual derivação; a ferramenta de descriptografia AES detecta o prefixo e alterna entre as três sem mexer em código.

Por que a mesma senha gera chaves diferentes

Não existe “a senha do AES”. Cada biblioteca inventou o próprio caminho de string até chave:

ProdutorDerivaçãoResultado para uma frase-senha
CryptoJS AES.encrypt(text, pass)EVP_BytesToKey, MD5, 1 iteraçãochave A
openssl enc 1.0.2 e anterioresEVP_BytesToKey, MD5, 1 iteraçãochave A
openssl enc 1.1+ sem -pbkdf2EVP_BytesToKey, SHA-256, 1 iteraçãochave B
openssl enc -pbkdf2PBKDF2-HMAC-SHA256, 10.000 iteraçõeschave C
Nosso modo de frase-senhaPBKDF2-HMAC-SHA256, 600.000 iteraçõeschave D
Java, Python, Gonenhum padrão; você escreve a derivaçãoo que você tiver escrito

Quatro chaves a partir de uma senha, antes de alguém ter cometido qualquer erro. A atualização de 1.0.2 para 1.1 trocou o digest padrão de MD5 para SHA-256; foi assim que texto cifrado de scripts antigos parou de descriptografar com o mesmo comando numa máquina mais nova. Se você herdou dados e ninguém lembra da toolchain, teste as derivações nessa ordem. É um teste de três vias, não uma busca.

8. O que o transporte fez com os seus bytes

Texto cifrado é binário uniformemente aleatório, o que o torna o mais hostil possível a qualquer coisa que trate bytes como texto. Boa parte das falhas de AES nunca envolve a cifra.

Variantes de Base64 e padding que sumiu

O base64 padrão (RFC 4648 §4) usa + e /; a variante segura para URL (§5) usa - e _. Uma string segura para URL entregue a um decodificador padrão ou lança exceção ou, em decodificadores tolerantes, descarta silenciosamente os caracteres problemáticos e devolve bytes curtos e desalinhados. O Java traz Base64.getUrlDecoder() e Base64.getDecoder() como objetos separados por causa disso. Alguns codificadores também eliminam o = do final, alguns decodificadores exigem esse =, e caminhos de código próximos a JWT o removem por padrão.

Antes de suspeitar da chave, decodifique o texto cifrado e confira o comprimento dele contra o modo:

  • CBC e ECB: um múltiplo de 16 diferente de zero. Qualquer outra coisa é truncamento ou problema de decodificação, não problema de chave.
  • GCM: o comprimento do texto cifrado é igual ao do texto claro, mais 16 da tag, mais 12 na frente se o nonce vier prefixado.
  • CTR: qualquer comprimento, então essa checagem não diz nada.

O decodificador Base64 te dá a contagem de bytes em uma colada só, muitas vezes a medição mais rápida de toda a investigação.

Quebras de linha, aspas curvas e a ida e volta em UTF-8

O openssl base64 quebra a saída em 64 colunas a menos que você passe -A, e alguns decodificadores ignoram quebras de linha embutidas enquanto outros as rejeitam, então o mesmo arquivo decodifica em uma máquina e falha em outra. Copiar através de um cliente de chat ou de um editor de documentos transforma aspas retas em curvas e hifens em travessões, e a diferença é quase invisível num terminal.

A irrecuperável é a ida e volta em UTF-8. Se a saída bruta do AES for guardada como string sem ser codificada antes (new String(cipherBytes) em Java, bytes.decode('utf-8', errors='replace') em Python, um TextDecoder em qualquer lugar), toda sequência de bytes que não seja UTF-8 válido colapsa em U+FFFD, e codificar de volta te devolve EF BF BD onde os seus dados costumavam estar. Como mais ou menos metade dos bytes aleatórios está fora do ASCII, a maior parte do texto cifrado é destruída e nenhuma chave a recupera; o guia UTF-8 vs UTF-16 vs Unicode — Guia completo de codificação explica por que a perda é de mão única. Texto cifrado binário viaja como base64, como hex ou como binário, nunca como string.

Colunas de banco de dados

O armazenamento aplica o mesmo estrago de forma mais discreta. Texto cifrado gravado num VARCHAR(255) com um bloco a mais é cortado, e o MySQL fora do modo estrito faz isso sem erro. A cauda é onde moram o bloco de padding e a tag do GCM, então uma linha gravada “com sucesso” meses atrás agora falha, e se o corte caiu numa fronteira de 16 bytes a checagem de comprimento acima também não vai pegar. A conversão de charset faz o resto: uma coluna latin1 recebendo bytes UTF-8 reescreve os seus dados já na entrada.

Guarde texto cifrado em VARBINARY, BLOB ou bytea, ou guarde base64 numa coluna de texto com folga.

9. Um fluxo de bissecção que acha o problema em cinco minutos

Cada seção acima estreita uma variável. Rode todas em ordem contra uma implementação de referência que você controla e a busca converge rápido. As ferramentas de navegador funcionam bem como essa referência, porque você muda uma configuração de cada vez e enxerga os bytes; elas rodam inteiramente no seu navegador, então sua chave e seu texto cifrado não saem da página nem chegam a servidor nenhum.

  1. Passo 0: meça a forma. Decodifique o texto cifrado e anote a contagem de bytes, os primeiros bytes e se ele começa com U2FsdGVkX1. Confira a contagem contra a seção 8. Se não for múltiplo de 16 e você acredita estar em CBC, pare: isso é bug de transporte.
  2. Passo 1: criptografe um texto claro conhecido. Na ferramenta de criptografia AES, criptografe uma string curta e conhecida com os parâmetros que você acredita que a produção usa, depois compare a forma das duas saídas em vez dos valores: comprimento total, bytes de prefixo, presença de um cabeçalho de salt. Uma divergência significa que a sua suposição sobre o formato ou sobre o KDF está errada, e nenhuma quantidade de ajuste na chave conserta isso.
  3. Passo 2: percorra as derivações. Para dados derivados de frase-senha, rode PBKDF2 com o número exato de iterações, depois EVP-SHA256, depois EVP-MD5 na ferramenta de descriptografia AES. Exatamente uma pode estar certa. Se nenhuma funcionar, o bug está acima do KDF.
  4. Passo 3: remova toda convenção. Mude para chave bruta, ligue o texto cifrado puro, informe o IV explicitamente. Agora você está declarando exatamente quais bytes são chave, IV e texto cifrado, sem nada inferido. Se descriptografa aqui e não descriptografa no seu código, o seu bug é de empacotamento (um prefixo de IV não removido, uma tag no lugar errado) e não de criptografia.
  5. Passo 4: troque o modo. Teste CBC, depois CTR, depois GCM contra os mesmos bytes. CTR devolvendo texto legível onde o CBC falhou é modo incompatível, ponto final.
  6. Passo 5: leia o lixo. Primeiro bloco ruim e o resto limpo significa IV. Primeiro bloco limpo e o resto ruim significa que você descriptografou CBC como ECB com um IV zerado. Tudo ruim significa a chave ou a derivação.

10. Perguntas frequentes

Por que meu código AES funciona na minha máquina e falha em produção?

A diferença entre a sua máquina e a produção é alguma coisa do ambiente que não está no controle de versão. Suspeitos de sempre, em ordem: a chave chegou de uma variável de ambiente ou de um gerenciador de segredos com uma quebra de linha no final; o charset padrão da plataforma no Java difere entre notebook e contêiner, então getBytes() produziu bytes diferentes (seção 3); o OpenSSL de produção é 1.1+ enquanto os seus scripts locais miravam 1.0.2, o que troca o digest do EVP_BytesToKey de MD5 para SHA-256; ou uma coluna de banco trunca o texto cifrado em um ambiente só. Imprima o comprimento da chave e o comprimento do texto cifrado em bytes nos dois lados primeiro, porque esses dois números costumam resolver a questão.

Criptografei no Node e não consigo descriptografar no Java. Por onde começo?

Entre Node e Java, comece pela tag do GCM, a causa mais comum e menos óbvia. O Node devolve texto cifrado e tag separados; o doFinal() do Java espera os dois concatenados como ciphertext || tag, e a seção 6 mostra que, fora isso, os bytes são idênticos. Se for CBC, comece pela convenção do IV: o Node colocou o IV na frente, e o lado Java remove 16 bytes antes de descriptografar? Em terceiro vem a chave em si, em que Buffer.from(k, 'hex') e k.getBytes(StandardCharsets.UTF_8) produzem comprimentos diferentes a partir da mesma string.

O PKCS5Padding do Java é o mesmo que PKCS#7?

Para o AES, o PKCS5Padding do Java é na prática o mesmo que PKCS#7. O PKCS#5 (RFC 8018) é definido só para blocos de 8 bytes; o PKCS#7 (RFC 5652) generaliza o esquema para blocos de 1 a 255 bytes. O PKCS5Padding do Java aplicado a uma cifra de bloco de 16 bytes implementa o comportamento do PKCS#7, e o nome é uma sobra histórica, então esse nunca é o seu bug. Já o NoPadding é: ele exige um texto claro que já seja múltiplo de 16, e na descriptografia devolve o padding como dado, então você vê um texto plausível com bytes finais do tipo \x05\x05\x05\x05\x05.

Minha chave tem 32 caracteres, mas o AES diz que o comprimento é inválido. Por quê?

Um erro de comprimento significa que a biblioteca recebeu uma contagem de bytes que não é 16, 24 nem 32. Com uma string de 32 caracteres isso normalmente é uma quebra de linha no final (33 bytes), um prefixo 0x que torna a string um hex inválido, ou um caractere fora do ASCII ocupando dois ou três bytes em UTF-8. A variante mais perigosa é não receber erro nenhum: 32 caracteres hex decodificam para 16 bytes válidos e 32 caracteres base64 decodificam para 24 bytes válidos, os dois comprimentos legais de AES. A biblioteca aceita, usa a chave errada e te entrega uma falha de padding no lugar. Confira a contagem de bytes, não a de caracteres.

A descriptografia “funcionou”, mas a saída é lixo. O que deu errado?

Se a descriptografia “funcionou” e devolveu lixo, você está em um modo que não verifica nada. CTR e ECB nunca lançam exceção, e o CBC só lança quando o padrão do último byte não passa na checagem de padding, coisa que uma chave errada passa em pouco menos de 0,4% das vezes. Leia a forma: os primeiros 16 bytes corrompidos e o resto limpo significa IV; os primeiros 16 limpos e o resto corrompido significa que você descriptografou texto cifrado CBC como ECB com um IV zerado; tudo corrompido de forma uniforme significa a chave ou a derivação. Texto legível com alguns bytes estranhos no final significa NoPadding sobre dados com padding. A correção de longo prazo é GCM, para que “funcionou” queira dizer alguma coisa.

Ainda dá para descriptografar se eu perdi o IV?

Sem o IV você ainda descriptografa, no CBC, tudo menos os primeiros 16 bytes. Os blocos do 2 em diante são recuperados como D(C_i) XOR C_{i-1}, e toda entrada disso já está no texto cifrado, então só o primeiro bloco precisa do IV. Se você também souber como o texto claro começa, digamos um JSON que abre com {"userId":, dá para recuperar o IV inteiro como D(C1) XOR P1. No CTR o IV semeia o keystream inteiro, então perdê-lo é perder tudo. No GCM o nonce alimenta tanto o contador quanto a tag, então não existe recuperação parcial.

Dá para recuperar o texto claro se a tag do GCM foi truncada ou perdida?

Com a tag do GCM truncada ou perdida, matematicamente dá, na prática dá trabalho. Por baixo o GCM é modo CTR, então a chave e o nonce sozinhos reproduzem o keystream. Nenhuma biblioteca conhecida vai fazer isso por você: Java, Go, Python e Web Crypto se recusam a liberar texto claro sem uma tag válida, por projeto. A saída de emergência é descriptografar os mesmos bytes como AES-CTR com o bloco de contador inicial definido como o nonce de 12 bytes seguido de 00000002, que é onde começa o primeiro bloco de dados do GCM. Você recupera os dados e abre mão de toda garantia de integridade, então trate o resultado como não confiável. Se você ainda tem os 16 bytes da tag e a autenticação falha mesmo assim, a tag não está faltando e o seu bug é outro desta página. Leve para a ferramenta de descriptografia AES e comece pelo passo 0.

Tags: aes encryption debugging cryptography interoperability

Artigos relacionados

Ver todos os artigos