O ECB (Electronic Codebook) é o modo de operação mais simples que existe: cada bloco da mensagem é cifrado sozinho, sempre do mesmo jeito. Essa simplicidade tem um preço exato e mensurável. O ECB vaza a relação de igualdade entre os blocos, e isso basta para quebrar a definição formal de segurança e para viabilizar ataques práticos, por mais forte que seja o AES.
Esta é a primeira parte de uma série sobre o AES e seus modos de operação. Começamos pelo ECB porque ele isola uma pergunta fundamental: uma cifra de bloco segura é suficiente para cifrar dados? A resposta é não, e entender por que é o primeiro passo para entender todos os outros modos. Neste artigo, o AES é tratado como uma caixa-preta; o funcionamento interno dele (rodadas, S-box, expansão de chave) terá uma parte própria da série.
Todos os códigos foram executados e as saídas mostradas são reais. Para reproduzir, basta pip install cryptography e colocar os arquivos na mesma pasta.
1.Cifras de bloco: o objeto matemático
Uma cifra de bloco transforma blocos de tamanho fixo em blocos do mesmo tamanho, controlada por uma chave. Formalmente:
Uma cifra de bloco com chaves de bits e blocos de bits é uma função
tal que, para toda chave , a função é uma permutação (bijeção) de , e tanto quanto sua inversa são eficientemente computáveis a partir de .
O AES é uma cifra de bloco com e , padronizada no FIPS 197. Ao longo do artigo, denota o tamanho do bloco em bytes.
Duas consequências imediatas da Definição 1 serão usadas o tempo todo. Por ser função, é determinística; por ser bijeção, é injetiva. Juntas, elas dão a equivalência que está no centro de tudo o que segue:
O que significa uma cifra de bloco ser "segura"? A noção padrão é a de permutação pseudoaleatória (PRP): sem conhecer , ninguém consegue distinguir de uma permutação escolhida uniformemente ao acaso entre todas as permutações de .
Seja o conjunto das permutações de . A vantagem de um distinguidor , com acesso a um oráculo, é
Dizemos que é uma PRP segura se essa vantagem é desprezível para todo eficiente.
Acredita-se que o AES seja uma PRP segura: depois de mais de duas décadas de criptoanálise pública, não se conhece ataque prático contra ele. Mas observe o detalhe: uma PRP processa exatamente um bloco de 16 bytes. Mensagens reais têm tamanhos arbitrários. É aí que entram os modos de operação.
2.Esquemas de cifragem e modos de operação
Um modo de operação é uma receita para construir, a partir de uma cifra de bloco, um esquema que cifra mensagens de qualquer tamanho.
Um esquema de cifragem simétrica é uma tripla de algoritmos :
- sorteia uma chave ;
- produz um texto cifrado a partir da mensagem , possivelmente usando aleatoriedade;
- devolve uma mensagem ou um símbolo de erro .
Exige-se corretude: para toda chave e toda mensagem .
Um ponto que será decisivo: a definição permite que seja probabilístico, isto é, que a mesma mensagem produza textos cifrados diferentes a cada cifragem. O ECB não usa essa liberdade. Veremos que isso não é um detalhe de implementação: é a raiz do problema.
3.A definição do ECB
3.1 Padding: completando o último bloco
Para cifrar bloco a bloco, o tamanho da mensagem precisa ser múltiplo de . O padrão mais usado é o PKCS#7 (RFC 5652).
Para com bytes, seja , de modo que . Então
A remoção lê o último byte , verifica que e que os últimos bytes valem todos , e os remove; caso contrário, devolve .
O fato de é essencial: sempre se acrescenta pelo menos um byte, então o último byte do texto completado sempre informa quanto remover. Por isso para todo , sem ambiguidade. A consequência é que, quando já é múltiplo de , acrescenta-se um bloco inteiro de padding (). Isso vai aparecer no primeiro experimento.
3.2 Cifragem e decifragem
Seja , com . Então
ou seja, para cada . A decifragem aplica a inversa bloco a bloco e remove o padding:
A corretude é imediata: como é a identidade, a decifragem recupera , e recupera .
A implementação abaixo segue a definição ao pé da letra: a função E é a permutação AES aplicada a um único bloco, e o ECB é construído em cima dela. O módulo será reutilizado em todos os experimentos.
# aes_ecb.py — ECB implementado a partir da permutação E_K de um único bloco import os from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes N = 16 # tamanho do bloco do AES, em bytes (128 bits) def E(K: bytes, x: bytes) -> bytes: """E_K(x): a permutação AES aplicada a exatamente um bloco.""" assert len(x) == N enc = Cipher(algorithms.AES(K), modes.ECB()).encryptor() return enc.update(x) + enc.finalize() def D(K: bytes, y: bytes) -> bytes: """E_K^{-1}(y): a permutação inversa.""" assert len(y) == N dec = Cipher(algorithms.AES(K), modes.ECB()).decryptor() return dec.update(y) + dec.finalize() def blocos(m: bytes) -> list[bytes]: return [m[i:i + N] for i in range(0, len(m), N)] def pad(m: bytes) -> bytes: """PKCS#7: k bytes de valor k, com k = N - (|m| mod N) em {1, ..., N}.""" k = N - (len(m) % N) return m + bytes([k]) * k def unpad(m: bytes) -> bytes: k = m[-1] if not 1 <= k <= N or m[-k:] != bytes([k]) * k: raise ValueError("padding inválido") return m[:-k] def ecb_encrypt(K: bytes, M: bytes) -> bytes: return b"".join(E(K, P) for P in blocos(pad(M))) def ecb_decrypt(K: bytes, C: bytes) -> bytes: assert len(C) % N == 0 return unpad(b"".join(D(K, Ci) for Ci in blocos(C)))
4.Primeiro experimento: o padrão aparece
Ciframos uma mensagem de três blocos com estrutura e verificamos, bloco a bloco, se :
import os from aes_ecb import E, blocos, ecb_encrypt, ecb_decrypt K = os.urandom(16) M = b"BLOCO-A........." b"BLOCO-B........." b"BLOCO-A........." # estrutura A B A C = ecb_encrypt(K, M) for i, Ci in enumerate(blocos(C)): print(f"C{i} = {Ci.hex()}") # C_i = E_K(P_i): cada bloco cifrado depende só do seu próprio bloco claro print("C0 == E_K(P0) calculado isoladamente:", blocos(C)[0] == E(K, M[:16])) print("C0 == C2:", blocos(C)[0] == blocos(C)[2]) print("Decifra corretamente:", ecb_decrypt(K, C) == M)
C0 = 02758068ae6197136d2b2871ee8c019a C1 = fbeb6c0d635e83432fa63ffa31f83900 C2 = 02758068ae6197136d2b2871ee8c019a C3 = 4094ffcf75c03352d6792912b0e9ec7a C0 == E_K(P0) calculado isoladamente: True C0 == C2: True Decifra corretamente: True
Três observações, todas previstas pela teoria:
- , porque e é função. E é diferente porque e é injetiva. É a equivalência (1) em ação.
- coincide com calculado isoladamente: a posição do bloco e os vizinhos não influenciam o resultado.
- Apareceu um quarto bloco, . A mensagem tem 48 bytes, múltiplo de 16; pela Definição 4, o padding acrescenta um bloco inteiro de 16 bytes de valor
0x10.
Como a chave é sorteada a cada execução, os valores hexadecimais mudam se você rodar de novo. A igualdade , não.
5.O que exatamente vaza
"O ECB vaza padrões" é a frase que se costuma repetir. Podemos ser muito mais precisos e dizer exatamente o que vaza, nem mais, nem menos.
Seja . Para todo par de índices :
O mesmo vale entre mensagens diferentes cifradas com a mesma chave: se e cifram e , então .
Por definição, e (ou ), com a mesma permutação , pois a chave é a mesma e nada mais entra na cifra. Basta aplicar a equivalência (1). ∎
A proposição diz que quem observa os textos cifrados conhece a partição dos índices em classes de blocos iguais. Vamos dar nome a isso.
Para , o padrão de igualdades de é a relação de equivalência sobre dada por . A função de vazamento do ECB é
Na mensagem do experimento, é o número de blocos e as classes , , : a estrutura . O atacante não sabe o que são e , mas sabe que o primeiro e o terceiro blocos são iguais.
A pergunta natural é se o ECB vaza mais do que isso. A resposta, sob a hipótese de que a cifra de bloco é ideal, é não:
Substitua por uma permutação uniforme em . Se , então os textos cifrados de e têm exatamente a mesma distribuição.
Sejam os valores distintos entre os blocos de , na ordem da primeira ocorrência, e a classe do bloco . Então .
Como é uniforme em e os são distintos, a tupla é uniforme sobre as -uplas de blocos distintos. Esse fato não depende dos valores , só de serem distintos. Logo, a distribuição de é determinada apenas por e pela função , isto é, por . Se , as duas distribuições coincidem. ∎
Com o AES real no lugar de , a mesma conclusão vale a menos da vantagem PRP (Definição 2): um atacante que distinguisse as duas distribuições serviria de distinguidor para o AES.
A Proposição 2 mostra que o defeito não está no AES. Mesmo com uma cifra de bloco perfeita, o ECB vaza , e nenhuma cifra de bloco, por mais forte que seja, conserta isso. O defeito está no modo de operação, que é determinístico e processa os blocos de forma independente. As seções 6 e 7 mostram que esse vazamento, que parece pequeno, é suficiente para quebrar a segurança.
Uma intuição útil: fixada a chave, é uma tabela gigante "bloco claro → bloco cifrado", com linhas. Daí o nome Electronic Codebook, livro de códigos eletrônico. O ECB é, literalmente, uma cifra de substituição cujo alfabeto tem símbolos. O atacante não consegue calcular a tabela, mas cada par (claro, cifrado) que observa é uma linha que passa a conhecer, válida para sempre e em qualquer posição.
6.Segurança semântica: por que o ECB não é IND-CPA
A noção padrão de confidencialidade para cifragem simétrica é a indistinguibilidade sob ataque de texto claro escolhido (IND-CPA). A ideia: mesmo podendo cifrar o que quiser, o adversário não consegue dizer qual de duas mensagens, escolhidas por ele, foi cifrada.
- O desafiante sorteia e um bit .
- O adversário , com acesso ao oráculo , escolhe com .
- Recebe o desafio , continua podendo consultar o oráculo e devolve um palpite .
A vantagem de é
é IND-CPA se essa vantagem é desprezível para todo eficiente.
Contra o ECB, o adversário vence sem esforço, e nem precisa usar o oráculo:
Existe um adversário que faz zero consultas ao oráculo, roda em tempo constante e tem .
Fixe dois blocos e escolha e . Ao receber , responda se e caso contrário.
Se , os dois primeiros blocos completados são e, pela Proposição 1, : o palpite é 0. Se , são com e, pela Proposição 1, : o palpite é 1. Logo e , e a vantagem é 1. ∎
Simulando o jogo 10 mil vezes, com uma chave nova a cada rodada:
import os, secrets from aes_ecb import ecb_encrypt, blocos def jogo_ind_cpa(adversario, rodadas=10_000): acertos = 0 for _ in range(rodadas): K = os.urandom(16) # chave nova a cada rodada M0, M1 = adversario.escolher() b = secrets.randbits(1) # bit secreto do desafiante C = ecb_encrypt(K, (M0, M1)[b]) acertos += adversario.adivinhar(C) == b return acertos / rodadas class Adversario: A, B = b"A" * 16, b"B" * 16 def escolher(self): return self.A + self.A, self.A + self.B # mesmo tamanho: 32 bytes def adivinhar(self, C): C0, C1 = blocos(C)[:2] return 0 if C0 == C1 else 1 p = jogo_ind_cpa(Adversario()) print(f"Taxa de acerto: {p:.4f} -> vantagem = |2p - 1| = {abs(2*p - 1):.4f}")
Taxa de acerto: 1.0000 -> vantagem = |2p - 1| = 1.0000
O Teorema 1 é caso particular de um fato mais geral: nenhum esquema de cifragem determinístico (e sem estado) é IND-CPA. Mesmo com mensagens de um único bloco, o adversário pode consultar o oráculo com e comparar a resposta com o desafio. Para ser IND-CPA, um esquema precisa ser probabilístico (ou manter estado, como um contador), de modo que cifrar a mesma mensagem duas vezes produza resultados diferentes. É exatamente o que os outros modos fazem com o IV ou o nonce.
7.Do vazamento ao ataque
Um teorema de vantagem 1 é convincente para um criptógrafo, mas a pergunta de quem defende um sistema é outra: o que um atacante consegue fazer na prática? Os quatro ataques a seguir partem de modelos de ameaça realistas e exploram duas propriedades da Definição 5: o determinismo (Proposição 1) e a independência entre blocos.
| Ataque | O que o atacante precisa | O que obtém | Custo |
|---|---|---|---|
| 7.1 Análise de padrões | Só observar textos cifrados | Estrutura e repetições dos dados | Nenhum |
| 7.2 Dicionário | Domínio pequeno + cifrar valores escolhidos | O valor de cada registro | cifragens |
| 7.3 Recorte e colagem | Obter textos cifrados de entradas escolhidas | Mensagens forjadas aceitas como válidas | Poucas consultas |
| 7.4 Decifragem byte a byte | Prefixar dados a um segredo cifrado | O segredo inteiro | consultas por byte |
7.1 Análise de padrões
É o ataque passivo: o atacante apenas observa. Pela Proposição 1, ele aprende , e em dados reais isso costuma revelar muito. O exemplo clássico é uma imagem bitmap cifrada em ECB: regiões de cor uniforme viram blocos repetidos, e o contorno da figura continua visível no texto cifrado. O mesmo acontece com registros de formato fixo, cabeçalhos de protocolos e campos que se repetem.
Como a chave é a mesma entre mensagens, o vazamento também é temporal: se o mesmo bloco cifrado aparece em duas mensagens enviadas em dias diferentes, o atacante sabe que o conteúdo se repetiu.
7.2 Ataque de dicionário em domínios pequenos
Considere uma coluna de banco de dados cifrada com ECB para "proteger" um PIN de 4 dígitos, um tipo sanguíneo ou uma faixa salarial. Seja o conjunto de valores possíveis. Se o atacante consegue obter a cifragem de valores escolhidos (por exemplo, cadastrando a própria conta várias vezes), ele monta a tabela completa
com consultas e, pela injetividade, inverte qualquer registro com uma busca. Para PINs, :
import os from aes_ecb import ecb_encrypt K = os.urandom(16) # Banco de dados: PIN de 4 dígitos cifrado de forma determinística usuarios = {"ana": "4821", "bruno": "0007", "carla": "9314"} banco = {nome: ecb_encrypt(K, pin.encode()) for nome, pin in usuarios.items()} # O atacante consegue cadastrar valores próprios (oráculo de cifragem) # e lê a coluna cifrada. Ele monta a tabela inteira: 10^4 consultas. tabela = {ecb_encrypt(K, f"{n:04d}".encode()): f"{n:04d}" for n in range(10_000)} for nome, cifrado in banco.items(): print(f"{nome:6s} -> PIN {tabela[cifrado]}")
ana -> PIN 4821 bruno -> PIN 0007 carla -> PIN 9314
Sem oráculo nenhum, a análise de frequências ainda funciona, exatamente como contra uma cifra de substituição clássica: se 40% dos registros têm o mesmo bloco cifrado, esse bloco provavelmente corresponde ao valor mais comum.
7.3 Recorte e colagem: forjando um cookie de administrador
Agora o ataque explora a independência entre blocos. Um servidor guarda o perfil do usuário num cookie cifrado com ECB:
email=<e-mail>&uid=10&role=user
O atacante só controla o e-mail. Como cada bloco cifrado decifra para o mesmo bloco claro em qualquer posição e em qualquer mensagem, ele pode recombinar blocos de cookies legítimos. O plano é alinhar as fronteiras dos blocos a seu favor:
| Cookie | Bloco 0 | Bloco 1 | Bloco 2 | Bloco 3 |
|---|---|---|---|---|
c1 | email=AAAAAAAAAA | admin + 11 × 0x0b | &uid=10&role=use | r + padding |
c2 | email=fran@exemp | .lo&uid=10&role= | user + padding | — |
| forjado | bloco 0 de c2 | bloco 1 de c2 | bloco 1 de c1 | — |
O bloco 1 de c1 é a cifragem de admin já com um padding PKCS#7 válido, então ele pode encerrar a mensagem forjada. O filtro que remove & e = do e-mail não ajuda: o atacante não precisa injetar nenhum desses caracteres.
import os from aes_ecb import ecb_encrypt, ecb_decrypt, blocos, pad K = os.urandom(16) # segredo do servidor # ---------- servidor ---------- def emitir_cookie(email: str) -> bytes: email = email.replace("&", "").replace("=", "") # "sanitização" perfil = f"email={email}&uid=10&role=user" return ecb_encrypt(K, perfil.encode()) def ler_cookie(cookie: bytes) -> dict: texto = ecb_decrypt(K, cookie).decode("latin-1") return dict(par.split("=", 1) for par in texto.split("&")) # ---------- atacante (só usa emitir_cookie) ---------- # 1) "admin" + padding PKCS#7 ocupando sozinho o bloco 1 # "email=" tem 6 bytes; 10 bytes de enchimento fecham o bloco 0 e1 = "A" * 10 + pad(b"admin").decode("latin-1") c1 = emitir_cookie(e1) # 2) "role=" terminando exatamente no fim do bloco 1 # 6 ("email=") + 13 (e-mail) + 13 ("&uid=10&role=") = 32 bytes e2 = "fran@exemp.lo" c2 = emitir_cookie(e2) forjado = c2[:32] + c1[16:32] print("Blocos de c2:", [b.hex()[:8] for b in blocos(c2)]) print("Blocos de c1:", [b.hex()[:8] for b in blocos(c1)]) print("Forjado :", [b.hex()[:8] for b in blocos(forjado)]) print("Servidor lê :", ler_cookie(forjado))
Blocos de c2: ['179fa309', '4df675d5', '0b793315']
Blocos de c1: ['686a878c', '899e245a', '2da77af2', '7af41891']
Forjado : ['179fa309', '4df675d5', '899e245a']
Servidor lê : {'email': 'fran@exemp.lo', 'uid': '10', 'role': 'admin'}Repare que o servidor decifrou e aceitou a mensagem forjada sem perceber nada. Cifrar garante (no máximo) confidencialidade; não garante integridade. A falta de integridade não é exclusiva do ECB, mas a independência entre blocos torna a falsificação trivial.
7.4 Decifragem byte a byte: de exponencial a linear
Este é o ataque mais impressionante. Suponha que o servidor cifre a entrada do usuário seguida de um segredo : um token, um identificador de sessão, dados de outro usuário. O atacante tem acesso ao oráculo
Isso acontece, por exemplo, quando um sistema monta um token cifrado concatenando um campo controlado pelo usuário com dados internos. Adivinhar por força bruta custaria tentativas. O ataque byte a byte recupera com custo linear em .
A ideia. Escolha um enchimento com . O primeiro bloco cifrado é , em que é o primeiro byte do segredo. O atacante conhece 15 dos 16 bytes desse bloco. Basta testar os 256 valores possíveis de , consultando , e comparar o primeiro bloco: pela Proposição 1, há coincidência se e somente se . Depois, com , o primeiro bloco passa a ser , e o atacante já conhece . E assim por diante.
O caso geral. Para recuperar o byte , use . Então , e o byte cai na última posição do bloco de índice . Os 15 bytes anteriores a ele são os 15 últimos de , todos conhecidos. Chamando esses 15 bytes de :
e, pela equivalência (1), .
Descobrindo o tamanho de . Com PKCS#7, . Aumentando a partir de zero, o tamanho do texto cifrado salta pela primeira vez quando atinge um múltiplo de , isto é, quando . Logo , em que é o menor tamanho de que causa o salto.
import os from aes_ecb import ecb_encrypt, N K = os.urandom(16) # segredo desconhecido do atacante S = b"token_api=7f3a9c2e1b5d;usuario=admin;validade=2027-01-31" consultas = 0 def oraculo(x: bytes) -> bytes: """O servidor devolve ECB_K(x || S).""" global consultas consultas += 1 return ecb_encrypt(K, x + S) # Passo 1: descobrir |S| pelo salto no tamanho do texto cifrado t0 = len(oraculo(b"")) j = 1 while len(oraculo(b"A" * j)) == t0: j += 1 tamanho_S = t0 - j print("Tamanho do segredo descoberto:", tamanho_S) # Passo 2: confirmar ECB (dois blocos iguais de entrada -> dois blocos iguais de saída) c = oraculo(b"A" * 2 * N) print("ECB confirmado:", c[:N] == c[N:2 * N]) # Passo 3: recuperar S byte a byte conhecido = b"" for i in range(tamanho_S): k = N - 1 - (i % N) # põe o byte i no fim de um bloco prefixo = b"A" * k idx = i // N # índice do bloco que contém o byte i alvo = oraculo(prefixo)[idx * N:(idx + 1) * N] janela = (prefixo + conhecido)[-(N - 1):] # os 15 bytes que antecedem o byte i for c in range(256): if oraculo(janela + bytes([c]))[:N] == alvo: conhecido += bytes([c]) break print("Segredo recuperado:", conhecido.decode()) print("Correto:", conhecido == S, "| consultas ao oráculo:", consultas)
Tamanho do segredo descoberto: 56 ECB confirmado: True Segredo recuperado: token_api=7f3a9c2e1b5d;usuario=admin;validade=2027-01-31 Correto: True | consultas ao oráculo: 4902
O custo. Para cada byte, o ataque faz uma consulta para obter o alvo e no máximo 256 para testar os candidatos. Somando a fase de descoberta do tamanho (no máximo consultas) e a confirmação do ECB (uma consulta):
Para , isso dá no máximo 14.410 consultas, contra da força bruta. O código testa os candidatos em ordem crescente e para no primeiro acerto, então gasta consultas no byte . Como o segredo é texto ASCII (valor médio dos bytes ≈ 85), o total observado foi 4.902. Este é o ataque dos desafios 12 e 14 do Cryptopals, e a mesma ideia de controlar onde caem as fronteiras dos blocos reaparece em ataques a outros modos, como o BEAST contra o TLS.
8.Como identificar ECB
8.1 Em textos cifrados capturados
Pela Proposição 1, blocos repetidos no texto cifrado são um forte indício de ECB: em um modo probabilístico seguro, a chance de dois blocos de 128 bits coincidirem por acaso é desprezível (da ordem de para blocos, pelo paradoxo do aniversário). O teste é simples: contar repetições.
import os from collections import Counter from aes_ecb import ecb_encrypt from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes def blocos_repetidos(C: bytes, n: int = 16) -> int: contagem = Counter(C[i:i + n] for i in range(0, len(C), n)) return sum(v - 1 for v in contagem.values() if v > 1) def ctr_encrypt(K, M): enc = Cipher(algorithms.AES(K), modes.CTR(os.urandom(16))).encryptor() return enc.update(M) + enc.finalize() K = os.urandom(16) alinhado = b"status=ativo;plano=basico;uf=SP;" * 8 # registro de 32 bytes desalinhado = b"status=ativo;plano=basico;" * 8 # registro de 26 bytes print("Alinhado | ECB:", blocos_repetidos(ecb_encrypt(K, alinhado)), "| CTR:", blocos_repetidos(ctr_encrypt(K, alinhado))) print("Desalinhado | ECB:", blocos_repetidos(ecb_encrypt(K, desalinhado)), "| CTR:", blocos_repetidos(ctr_encrypt(K, desalinhado)))
Alinhado | ECB: 14 | CTR: 0 Desalinhado | ECB: 0 | CTR: 0
O caso desalinhado é um alerta importante: a ausência de repetições não prova que o modo não é ECB. Registros de 26 bytes nunca se alinham às fronteiras de 16 bytes dentro dessa mensagem (o padrão só se repetiria a cada bytes), então nenhum bloco se repete, embora o vazamento continue lá. É por isso que, nos ataques da seção 7, o atacante controla o alinhamento com enchimentos.
8.2 Numa revisão de código
Em auditoria, o ECB costuma aparecer de forma explícita ou, pior, como valor padrão de uma API:
| Linguagem / biblioteca | Como o ECB aparece |
|---|---|
| Java (JCA) | Cipher.getInstance("AES") sem modo explícito usa AES/ECB/PKCS5Padding no provedor padrão; também "AES/ECB/..." |
Python cryptography | modes.ECB() |
| Python PyCryptodome | AES.new(chave, AES.MODE_ECB) |
| Node.js | crypto.createCipheriv("aes-128-ecb", ...) |
| OpenSSL (C) | EVP_aes_128_ecb(), EVP_aes_256_ecb() |
| .NET | CipherMode.ECB |
Uma busca inicial no repositório já encontra a maioria dos casos:
grep -rniE 'ecb|getInstance\("AES"\)' --include=*.{java,kt,py,js,ts,go,c,cpp,cs} .Ferramentas de análise estática como Semgrep e CodeQL têm regras prontas para esses padrões. Ao encontrar o problema, o achado no relatório pode ser descrito assim: "dados cifrados com AES-ECB, sem autenticação; vazamento de igualdade entre blocos e possibilidade de recombinação de blocos permitem [consequência concreta]", com a severidade definida pela consequência: escalonamento de privilégio, recuperação de segredos, inferência sobre dados pessoais.
9.Como corrigir
A seção 6 mostrou que o problema de fundo é o determinismo, e a seção 7.3 mostrou que falta integridade. A correção precisa resolver os dois. Trocar ECB por outro modo sem autenticação resolve só o primeiro, e cada modo tem suas próprias armadilhas, que serão assunto das próximas partes da série.
A recomendação padrão é cifragem autenticada (AEAD), como AES-GCM ou ChaCha20-Poly1305: um nonce único por mensagem torna a cifragem não determinística, e a tag de autenticação faz qualquer alteração ser rejeitada.
import os from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.exceptions import InvalidTag chave = AESGCM.generate_key(bit_length=128) aead = AESGCM(chave) def emitir_cookie(perfil: bytes) -> bytes: nonce = os.urandom(12) # nunca repetir com a mesma chave return nonce + aead.encrypt(nonce, perfil, b"cookie-v1") def ler_cookie(cookie: bytes) -> bytes: nonce, ct = cookie[:12], cookie[12:] return aead.decrypt(nonce, ct, b"cookie-v1") # InvalidTag se foi alterado m = b"email=fran@exemp.lo&uid=10&role=user" c1, c2 = emitir_cookie(m), emitir_cookie(m) print("Mesma mensagem, cifrados iguais?", c1 == c2) # Tentativa de recorte e colagem: troca um trecho do cookie por outro forjado = c1[:28] + c2[28:] try: ler_cookie(forjado) except InvalidTag: print("Cookie alterado rejeitado: InvalidTag")
Mesma mensagem, cifrados iguais? False Cookie alterado rejeitado: InvalidTag
Três cuidados que costumam aparecer em revisões:
- O nonce nunca pode se repetir com a mesma chave. No GCM, a repetição de nonce compromete tanto a confidencialidade quanto a autenticidade; para nonces aleatórios de 96 bits, limite o número de mensagens por chave.
- Autorização não deve depender de dados no cliente. Mesmo cifrado e autenticado, um cookie com
role=adminmerece uma pergunta de arquitetura: por que essa decisão não está no servidor? - Quando o determinismo é desejado (por exemplo, para buscar por igualdade num campo cifrado), ele deve ser uma escolha consciente, com um esquema projetado para isso, como o AES-SIV, e com o vazamento documentado e aceito. Para domínios pequenos, a seção 7.2 mostra que isso pode equivaler a não cifrar.
Existe algum uso legítimo para o ECB? Apenas como componente interno de outras construções, aplicado a um único bloco de conteúdo imprevisível. Para proteger dados, a resposta prática é: não use.
10.Resumo
- O ECB cifra cada bloco de forma independente: , sem IV, encadeamento ou contador.
- Como é determinística e injetiva, , dentro de uma mensagem e entre mensagens (Proposição 1).
- Com uma cifra de bloco ideal, o ECB vaza exatamente o padrão de igualdades (Proposição 2). O defeito é do modo, não do AES.
- Esse vazamento basta para vencer o jogo IND-CPA com vantagem 1, sem nenhuma consulta (Teorema 1). Nenhum esquema determinístico é IND-CPA.
- Na prática, o vazamento vira análise de padrões, ataques de dicionário, falsificação por recorte e colagem e recuperação de segredos com custo linear.
- A correção é cifragem autenticada com nonce único, como AES-GCM ou ChaCha20-Poly1305.
Nas próximas partes da série, vamos abrir o AES por dentro e analisar os outros modos de operação com o mesmo nível de rigor: o que cada um garante, sob quais hipóteses e como cada um falha quando essas hipóteses são violadas.
Referências
- NIST. FIPS 197: Advanced Encryption Standard (AES).
- NIST. SP 800-38A: Recommendation for Block Cipher Modes of Operation.
- Housley, R. RFC 5652: Cryptographic Message Syntax (seção 6.3, padding).
- Katz, J.; Lindell, Y. Introduction to Modern Cryptography, 3ª ed. CRC Press (capítulo sobre cifragem simétrica e modos de operação).
- Boneh, D.; Shoup, V. A Graduate Course in Applied Cryptography.
- Bellare, M.; Desai, A.; Jokipii, E.; Rogaway, P. "A Concrete Security Treatment of Symmetric Encryption". FOCS 1997.
- Cryptopals, Set 2 (desafios 12, 13 e 14).
- Documentação da biblioteca cryptography para Python.
