Em 1995, Bruce Schneier publicou o que se tornaria uma das afirmações mais repetidas em segurança aplicada: "Anyone, from the most clueless amateur to the best cryptographer, can create an algorithm that he himself can't break. It's not even hard. What's hard is creating an algorithm that no one else can break, even after years of analysis." O corolário que a maioria dos desenvolvedores tirou dessa observação — corretamente — é que nenhum desenvolvedor de aplicação deveria implementar primitivas criptográficas. O corolário que muitos erroneamente extraíram é que criptografia é algo que a biblioteca "trata" e não precisa ser entendida.
Criptografia aplicada para engenheiros não é sobre implementar AES ou RSA — é sobre escolher o algoritmo correto para o problema correto, entender o que cada garantia criptográfica oferece, e reconhecer os erros de uso que transformam algoritmos seguros em sistemas vulneráveis. AES em modo ECB (Electronic Codebook) usa o mesmo algoritmo que AES-GCM mas é fundamentalmente inseguro para dados de mais de um bloco. MD5 é matematicamente interessante mas quebrado para uso em segurança desde 2004. Bcrypt é seguro para senhas mas Argon2id é melhor por razões específicas que o engenheiro deve entender antes de escolher.
Criptografia simétrica — AES e os modos de operação
AES (Advanced Encryption Standard), padronizado pelo NIST em 2001 após uma competição de cinco anos onde o algoritmo Rijndael de Vincent Rijmen e Joan Daemen venceu, é o standard mundial de criptografia simétrica. Simétrica significa que a mesma chave que cifra também decifra — a distribuição segura dessa chave é o problema central da criptografia simétrica.
AES em si é apenas um bloco cipher — cifra blocos de 128 bits com chave de 128, 192, ou 256 bits. Para cifrar dados de tamanho arbitrário, é necessário um modo de operação que define como blocos sucessivos são encadeados. A escolha do modo é tão importante quanto a escolha do algoritmo:
- ECB (Electronic Codebook): cada bloco cifrado independentemente com a mesma chave. Blocos de plaintext idênticos produzem blocos de ciphertext idênticos — o que revela padrões nos dados. Visualizado com uma imagem bitmap, ECB preserva a estrutura visual da imagem original (o "Linux penguin" é o exemplo clássico). Nunca usar para dados reais.
- CBC (Cipher Block Chaining): cada bloco é XOR com o ciphertext do bloco anterior antes de cifrar. Requer um IV (Initialization Vector) aleatório. Sem autenticação — vulnerável a padding oracle attacks (POODLE, Lucky13). Legado, não usar em sistemas novos.
- GCM (Galois/Counter Mode): combina cifração (modo CTR) com autenticação (GHASH). Produz ciphertext + authentication tag de 128 bits. O tag verifica integridade e autenticidade — qualquer alteração no ciphertext resulta em falha na verificação. O padrão atual para criptografia simétrica autenticada.
Criptografia autenticada (AEAD — Authenticated Encryption with Associated Data) como AES-GCM garante confidencialidade e integridade em uma operação. Criptografia sem autenticação (AES-CBC sem MAC) garante apenas confidencialidade — um atacante pode modificar o ciphertext de formas previsíveis. Em sistemas novos, nunca use criptografia sem autenticação para dados que precisam de integridade (que é praticamente todo dado que vale cifrar).
# Python — AES-256-GCM com cryptography library
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
# Geração de chave — sempre com CSPRNG
key = os.urandom(32) # 256 bits — NUNCA hardcoded, nunca derivado de string
# Cifração
nonce = os.urandom(12) # 96 bits para GCM — único por chave+mensagem
aesgcm = AESGCM(key)
# aad = Additional Authenticated Data — cifrado? Não. Autenticado? Sim.
# Use para dados que precisam ser verificados mas não cifrados (ex: user_id, timestamp)
aad = b"user_id:123"
ciphertext = aesgcm.encrypt(nonce, plaintext, aad) # ciphertext inclui o auth tag
# Decifração — lança InvalidTag se ciphertext ou aad foram alterados
try:
recovered = aesgcm.decrypt(nonce, ciphertext, aad)
except Exception:
raise ValueError("Autenticação falhou — dados corrompidos ou adulterados")
O nonce (number used once) em GCM deve ser único por (chave, mensagem). Reutilizar o mesmo nonce com a mesma chave é catastrófico — revela o XOR dos plaintexts e compromete a autenticação. A geração com os.urandom(12) (96 bits) garante unicidade probabilística — com 2^32 mensagens por chave, a probabilidade de colisão de nonce é 1 em 2^64. Se o volume for maior, rotacione a chave antes.
Criptografia assimétrica — RSA vs ECC
Criptografia assimétrica usa um par de chaves matematicamente relacionadas: a chave pública pode ser distribuída livremente; apenas a chave privada decifra o que foi cifrado com a pública, ou verifica que a assinatura foi feita pela privada. RSA (Rivest–Shamir–Adleman, 1977) baseia sua segurança na dificuldade de fatorar o produto de dois primos grandes. ECC (Elliptic Curve Cryptography) baseia-se no problema do logaritmo discreto em curvas elípticas — matematicamente diferente mas igualmente intratável com tamanhos de chave muito menores.
A comparação de tamanho de chave para segurança equivalente é o argumento principal para ECC:
- RSA-2048 ≈ ECC-224 (segurança de ~112 bits)
- RSA-3072 ≈ ECC-256 (segurança de ~128 bits)
- RSA-7680 ≈ ECC-384 (segurança de ~192 bits)
ECC com P-256 (NIST P-256, também chamada secp256r1 ou prime256v1) é o padrão para novos sistemas: chave de 256 bits, operações mais rápidas que RSA-2048, assinaturas menores (64 bytes vs 256 bytes para RS256). Ed25519 (curva de Edwards, Bernstein 2011) é uma alternativa ainda mais rápida com propriedades de segurança adicionais — resistente a implementações com side-channel por construção — e é a curva padrão do SSH moderno e do TLS 1.3 quando a performance é prioritária.
RSA ainda é necessário por compatibilidade com sistemas legados e com HSMs (Hardware Security Modules) mais antigos que não suportam ECC. Para sistemas novos sem restrição de compatibilidade, ECC é a escolha correta.
RSA com padding PKCS#1 v1.5 — o padding mais antigo e mais simples — é vulnerável a Bleichenbacher's attack (1998), um ataque de oracle de padding que permite decifrar mensagens cifradas com a chave pública sem conhecer a chave privada. O fix é usar RSA-OAEP (Optimal Asymmetric Encryption Padding) para cifração e RSASSA-PSS para assinatura. Bibliotecas modernas usam OAEP e PSS por padrão; a armadilha é quando o desenvolvedor sobrescreve o padrão por compatibilidade com sistema legado.
Hashing de senha — Argon2id e por que não bcrypt
Hash de senha é um caso especial de criptografia que merece tratamento separado. O objetivo não é cifrar a senha (recuperável) — é derivar um hash que seja: computacionalmente caro de calcular (para dificultar brute force), único por usuário mesmo com senha idêntica (via salt), e resistente a ataques com hardware especializado (GPU, ASIC).
A história da evolução dos algoritmos de hash de senha reflete a evolução do hardware de ataque:
- MD5 / SHA-1 / SHA-256 sem salt: rápidos demais, sem salt — tabelas rainbow e GPU com bilhões de hashes por segundo. Inseguros para qualquer uso de senha.
- bcrypt (1999, Niels Provos e David Mazières): introduziu work factor ajustável e salt automático. Ainda seguro com cost factor ≥ 12, mas foi projetado para CPU — GPUs com acesso a memória rápida conseguem acelerar bcrypt mais do que o esperado.
- scrypt (2009, Colin Percival): adiciona memory hardness — requer muita memória além de CPU, tornando GPUs e ASICs menos eficientes. Mais seguro que bcrypt contra hardware especializado.
- Argon2id (2015, vencedor do Password Hashing Competition): combina resistência de memória (Argon2d) com proteção contra side-channel timing attacks (Argon2i). O padrão atual — recomendado pelo OWASP e pelo NIST (SP 800-132).
// C# — Argon2id com Konscious.Security.Cryptography
using Konscious.Security.Cryptography;
using System.Security.Cryptography;
public static class PasswordHasher
{
// Parâmetros mínimos OWASP 2023:
// - Argon2id, 1 iteração, 64MB de memória, 4 threads
// - Ou 3 iterações, 64MB, 4 threads (mais seguro)
private const int Iterations = 3;
private const int MemorySize = 65536; // 64 MB em KB
private const int DegreeOfParallelism = 4;
private const int HashLength = 32;
private const int SaltLength = 16;
public static string HashPassword(string password)
{
var salt = RandomNumberGenerator.GetBytes(SaltLength);
using var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password))
{
Salt = salt,
Iterations = Iterations,
MemorySize = MemorySize,
DegreeOfParallelism = DegreeOfParallelism,
};
var hash = argon2.GetBytes(HashLength);
// Armazenar: $argon2id$v=19$m=65536,t=3,p=4$BASE64_SALT$BASE64_HASH
return $"$argon2id$v=19$m={MemorySize},t={Iterations},p={DegreeOfParallelism}" +
$"${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}";
}
public static bool VerifyPassword(string password, string storedHash)
{
// Parse dos parâmetros do hash armazenado
// (ou usar biblioteca que faz isso automaticamente)
// ... verificação com timing constante
return CryptographicOperations.FixedTimeEquals(computedHash, storedHashBytes);
}
}
Funções de hash criptográficas — SHA-256, SHA-3 e o que evitar
Funções de hash criptográficas produzem um digest de tamanho fixo a partir de input de tamanho arbitrário, com as propriedades: determinísticas (mesmo input → mesmo output), preimage resistant (impossível recuperar input a partir do hash), second preimage resistant (impossível encontrar outro input com o mesmo hash), e collision resistant (impossível encontrar dois inputs diferentes com o mesmo hash).
- MD5: colisões encontradas por Wang e Yu em 2004. Quebrado para uso criptográfico — não usar para integridade, assinaturas, ou qualquer propósito de segurança. Ainda aceitável para checksum não-criptográfico (verificar download não adulterado acidentalmente), mas mesmo assim SHA-256 é preferível.
- SHA-1: colisões teoricamente possíveis desde 2005 (Wang et al.), demonstradas práticas em 2017 (SHAttered, Google). Deprecado por todos os CAs e pelo NIST. Não usar.
- SHA-256 (SHA-2 family): seguro, amplamente suportado, padrão atual para assinaturas digitais, integridade de dados, e derivação de chaves. Parte da SHA-2 family (SHA-224, SHA-256, SHA-384, SHA-512).
- SHA-3 (Keccak): vencedor da competição do NIST em 2012, baseado em construção sponge — matematicamente diferente de SHA-2. Não é mais seguro que SHA-256 na prática, mas serve como alternativa independente se SHA-2 for comprometido. Menos suportado que SHA-2 em sistemas legados.
- BLAKE3: moderno, extremamente rápido (mais que SHA-256 em hardware moderno), e seguro. Crescendo em adoção em contextos onde performance de hash é crítica (verificação de integridade de arquivos grandes).
Para HMAC (Hash-based Message Authentication Code) — verificar integridade e autenticidade com chave compartilhada — use HMAC-SHA256 ou HMAC-SHA512. HMAC é adequado quando você tem uma chave simétrica compartilhada e precisa verificar que a mensagem não foi alterada e veio de quem tem a chave.
Geração de números aleatórios — por que CSPRNG importa
Toda primitiva criptográfica depende de aleatoriedade de alta qualidade. Chaves, IVs, nonces, salts — todos precisam ser imprevisíveis. PRNGs (Pseudo-Random Number Generators) como rand.random() em Python ou Math.random() em JavaScript usam algoritmos determinísticos seeded por tempo ou estado do processo — previsíveis para um atacante que conhece ou consegue inferir o estado inicial.
CSPRNGs (Cryptographically Secure PRNGs) usam fontes de entropia do sistema operacional (eventos de hardware, interrupções de I/O, movimentos de mouse) coletadas pelo kernel. Em sistemas modernos: os.urandom() em Python, crypto.randomBytes() em Node.js, RandomNumberGenerator.GetBytes() em C#, crypto/rand.Read() em Go. Todos chamam o CSPRNG do SO — /dev/urandom no Linux, CryptGenRandom no Windows.
Containers que iniciam com pouca entropia — especialmente em VMs ou containers efêmeros que nunca executaram código suficiente para acumular entropia do hardware — podem produzir tokens de sessão previsíveis nos primeiros segundos após o start. Em Kubernetes, o problema é mitigado pelo virtio-rng (dispositivo de entropia virtual) e pelo kernel moderno que usa /dev/urandom com ChaCha20 como CSPRNG interno. Verificar que o sistema tem entropia adequada (cat /proc/sys/kernel/random/entropy_avail) é um item de produção readiness para serviços de autenticação.
Derivação de chaves — HKDF e PBKDF2
Quando é necessário derivar múltiplas chaves a partir de um material de chave raiz, ou derivar uma chave de comprimento específico a partir de um segredo de comprimento diferente, use KDFs (Key Derivation Functions) em vez de aplicar hash diretamente.
HKDF (HMAC-based Key Derivation Function, RFC 5869) é o padrão para derivar múltiplas chaves a partir de um material raiz (como um segredo de DH key exchange): a fase de Extract deriva uma pseudorandom key do material, e a fase de Expand deriva múltiplas chaves de comprimento arbitrário a partir dela. Usado internamente pelo TLS 1.3 para derivar as chaves de sessão.
PBKDF2 (Password-Based Key Derivation Function 2, RFC 8018) é mais antigo que Argon2id mas ainda presente em muitos sistemas: deriva uma chave de uma senha com salt e iterações. Mais fraco que Argon2id (sem memory hardness), mas é o padrão FIPS 140 — obrigatório em sistemas que precisam de certificação federal americana. Para novos sistemas sem requisito FIPS, Argon2id é superior.
Criptografia de dados em repouso — o que proteger e como
Dados em repouso precisam de proteção quando: o storage físico pode ser comprometido (backup tape roubada, disco descartado incorretamente), o acesso ao sistema de arquivos não é suficientemente controlado, ou existe requisito regulatório (LGPD, HIPAA, PCI DSS).
As camadas de proteção, do mais amplo ao mais granular:
- Full disk encryption (LUKS no Linux, BitLocker no Windows): protege contra acesso físico ao disco. Transparente para a aplicação. Não protege quando o sistema está rodando e o disco está montado.
- Database transparent encryption: PostgreSQL com pg_crypto, Transparent Data Encryption (TDE) no SQL Server/MySQL Enterprise. Cifra os arquivos de dados no disco. Mesma limitação: não protege contra acesso via SQL com credenciais válidas.
- Application-level encryption: a aplicação cifra campos específicos antes de persistir. A chave de criptografia não está no banco — mesmo acesso direto ao banco não revela os dados. Mais granular, mais complexo de implementar, mais difícil de fazer queries sobre dados cifrados.
Para application-level encryption, o padrão prático é envelope encryption: os dados são cifrados com uma Data Encryption Key (DEK) gerada por registro ou por grupo de registros; a DEK é cifrada com uma Key Encryption Key (KEK) armazenada em KMS (Key Management Service) como AWS KMS, GCP KMS, ou HashiCorp Vault. A aplicação obtém a DEK do KMS, decifra os dados, e a DEK não fica em repouso em plaintext em nenhum lugar.
Decisões de engenharia
AES-256-GCM: padrão atual para a maioria dos sistemas. AEAD — cifração + autenticação em uma operação. Requer hardware AES-NI para performance ótima (disponível em x86 moderno).
ChaCha20-Poly1305: alternativa quando hardware AES-NI não está disponível (mobile, IoT, ARM sem AES-NI). Igualmente seguro, mais rápido em software puro. Usado pelo TLS 1.3 como cipher alternativo.
AES-CBC: legado apenas. Sem autenticação nativa — requer HMAC separado, o que é fácil de errar (encrypt-then-MAC vs MAC-then-encrypt). Não usar em sistemas novos.
ECC (P-256 / Ed25519): sistemas novos sem restrição de compatibilidade. Chaves 10x menores, assinaturas menores, operações mais rápidas. Ed25519 é resistente a timing attacks por construção.
RSA-2048+: necessário quando o HSM ou biblioteca parceira não suporta ECC, quando há requisito de compatibilidade com sistemas legados (TLS < 1.2 sem ECC), ou quando o ambiente de certificação requer RSA explicitamente.
Padding obrigatório: RSA-OAEP para cifração, RSASSA-PSS para assinatura. PKCS#1 v1.5 é vulnerável a Bleichenbacher's attack — nunca usar em sistemas novos.
Argon2id: padrão para sistemas novos. Memory hardness torna GPU/ASIC ineficientes. Parâmetros OWASP mínimos: 64MB memória, 3 iterações, 4 threads. Vencedor da PHC (Password Hashing Competition).
bcrypt: manutenção de sistemas existentes com cost ≥ 12. Ainda seguro para a maioria dos casos, mas sem memory hardness — menos resistente a ataques com hardware especializado moderno.
PBKDF2-SHA256: quando há requisito de certificação FIPS 140 (federal americano, algumas fintech). Mais fraco que Argon2id — compensar com iterações mais altas (≥ 600.000).
SHA-256 simples: checksums de integridade sem chave — verificação de downloads, fingerprint de certificados, identificadores de conteúdo. Não fornece autenticidade (qualquer um pode calcular o hash).
HMAC-SHA256: integridade + autenticidade com chave simétrica compartilhada. Assinatura de webhooks, verificação de mensagens entre serviços internos. Resistente a length extension attacks por construção.
HKDF: derivar múltiplas chaves a partir de um material raiz (segredo DH, master secret). Nunca usar SHA-256 direto como KDF — HKDF tem separação de contexto via info parameter que SHA puro não oferece.
Como praticar
-
Implementar cifração e decifração com AES-256-GCM e verificar falha de autenticação. Em qualquer linguagem, implemente uma função que: gera uma chave de 256 bits com CSPRNG, cifra um string com AES-GCM (nonce aleatório de 96 bits, com AAD opcional), serializa nonce + ciphertext + tag em base64 para armazenamento, e decifra corretamente. Teste dois casos de falha: (1) altere um byte do ciphertext antes de decifrar — deve lançar exceção de autenticação; (2) reutilize o mesmo nonce para duas mensagens distintas e observe o vazamento de informação.
Critério: a decifração de ciphertext adulterado levanta exceção clara (não silencia nem retorna garbage); a função de cifração usa CSPRNG para nonce (não contador estático); o armazenamento inclui nonce + ciphertext como unidade inseparável. -
Benchmark de algoritmos de hash de senha para calibrar parâmetros de produção. Em qualquer linguagem, implemente um benchmark que mede o tempo de hash com: bcrypt cost 10, bcrypt cost 12, Argon2id com parâmetros mínimos OWASP (64MB, 3 iterações), e Argon2id com parâmetros mais altos (128MB, 4 iterações). Meça em sua máquina de desenvolvimento e estime o tempo em produção (considerando diferença de hardware). O objetivo é calibrar parâmetros para um target de 300-500ms por hash — suficientemente lento para atacante, suficientemente rápido para UX de login.
Critério: os parâmetros escolhidos resultam em tempo entre 200ms e 600ms na máquina de referência; o benchmark mede memória consumida por operação (não apenas tempo); há documentação justificando os parâmetros escolhidos com base nos resultados. -
Auditar dependências de criptografia em um projeto existente. Em um projeto existente (seu ou open source), liste todos os pontos onde criptografia é usada: hashing de senha, geração de tokens, cifração de dados em repouso, HMAC de webhooks. Para cada um: qual algoritmo? Qual biblioteca? Os parâmetros estão configurados explicitamente ou usando defaults? O código usa CSPRNG para geração de aleatoriedade? Há algum uso de MD5, SHA-1, ECB, ou padding PKCS#1 v1.5?
Critério: documento mapeando cada uso criptográfico com status (seguro / atenção / crítico); pelo menos um problema real identificado com proposta de fix; nenhum falso negativo em relação a algoritmos quebrados — MD5 e SHA-1 para segurança devem aparecer como críticos. -
Implementar envelope encryption com simulação de KMS. Implemente o padrão de envelope encryption: uma função
encrypt_data(plaintext, key_id)que (1) gera uma DEK aleatória de 256 bits, (2) cifra o plaintext com AES-GCM usando a DEK, (3) cifra a DEK com uma KEK obtida do "KMS" (pode ser um dict local simulando um KMS), e (4) armazena{key_id, encrypted_dek, ciphertext, nonce}. Implemente tambémdecrypt_data(envelope)que reverte o processo.
Critério: a DEK não aparece em nenhum lugar em plaintext além da memória durante a operação; a KEK não precisa estar no banco — apenas no "KMS"; rotação da KEK requer apenas re-cifrar os encrypted_deks, não os dados originais. -
Verificar que comparações de hash usam tempo constante. Em um sistema de autenticação, identifique todos os pontos onde hashes ou tokens são comparados. Verifique se a comparação usa função de tempo constante (
hmac.compare_digest()em Python,CryptographicOperations.FixedTimeEquals()em C#,subtle.ConstantTimeCompare()em Go) em vez de==. Implemente um teste que verifica empiricamente a diferença de tempo entre uma comparação com timing attack vulnerability e uma com timing constant — usando medições de múltiplas execuções.
Critério: todos os pontos de comparação de hash/token encontrados usam funções de tempo constante; o teste empírico mostra diferença mensurável no tempo de comparação com==(string curta vs longa vs correta), e ausência de diferença mensurável comcompare_digest.
Perguntas de entrevista
Por que AES-ECB é inseguro enquanto AES-GCM é o padrão atual? Qual a diferença fundamental entre modos de operação?
AES em si (o bloco cipher) é seguro — o problema é como blocos de dados maiores que 128 bits são encadeados. Em ECB (Electronic Codebook), cada bloco de 128 bits do plaintext é cifrado independentemente com a mesma chave. Isso significa que blocos de plaintext idênticos produzem blocos de ciphertext idênticos — o ciphertext preserva padrões do plaintext. A imagem do pinguim Linux cifrada com ECB ainda mostra a silhueta do pinguim. Um atacante pode: identificar repetições de blocos (revelando estrutura dos dados), substituir blocos por outros blocos previamente observados (block substitution), e em alguns casos decifrar blocos individualmente com tabelas de lookup.
GCM (Galois/Counter Mode) combina cifração via CTR (cada bloco cifrado com uma versão incrementada de um counter + nonce, garantindo uniqueness de entrada mesmo com dados repetidos) com autenticação via GHASH (um MAC polinomial que gera um authentication tag de 128 bits). O resultado é um AEAD — o ciphertext é indistinguível de aleatoriedade E qualquer alteração de qualquer byte resulta em falha de verificação do tag. A garantia dupla (confidencialidade + integridade/autenticidade) é o que torna GCM o padrão.
Por que reutilizar o nonce em AES-GCM é catastrófico? O que exatamente um atacante consegue com dois ciphertexts com mesmo nonce e chave?
GCM usa o modo CTR internamente: o keystream é gerado como AES(key, nonce || counter) para cada bloco, e o ciphertext é plaintext XOR keystream. Se dois plaintexts diferentes P1 e P2 são cifrados com o mesmo par (nonce, chave), os keystreams são idênticos:
C1 = P1 XOR K e C2 = P2 XOR K
Um atacante com C1 e C2 pode calcular C1 XOR C2 = P1 XOR P2 — o XOR dos dois plaintexts. Isso parece menos grave do que parece, mas com contexto (conhecer parte de P1 ou P2, ou o formato estruturado dos dados) é possível recuperar os plaintexts inteiros — chamado de two-time pad attack, a mesma vulnerabilidade que o one-time pad tem quando reutilizado.
Pior: além do XOR dos plaintexts, a reutilização de nonce em GCM também compromete o authentication tag — um atacante pode forjar tags para outros ciphertexts, eliminando a garantia de integridade. O nonce deve ser único por (chave, mensagem): os.urandom(12) para geração probabilística é adequado até 2^32 mensagens por chave.
O que é o Bleichenbacher's attack em RSA e por que ele torna RSA-OAEP necessário?
Daniel Bleichenbacher demonstrou em 1998 que RSA com padding PKCS#1 v1.5 é vulnerável a um ataque de oracle de padding: se um sistema retorna uma resposta diferente (mesmo que sutil — diferença de timing, mensagem de erro específica, ou comportamento) quando um ciphertext decifrado tem padding inválido vs. válido, um atacante pode usar esse oracle para decifrar mensagens arbitrárias sem a chave privada.
O ataque funciona em ~1 milhão de queries ao oracle: o atacante envia ciphertexts modificados (usando propriedades multiplicativas do RSA) e observa se o oracle responde "padding válido" ou "padding inválido". Cada resposta revela informação sobre o plaintext original. O processo é iterativo — a cada query, o espaço de busca é reduzido, até que o plaintext original é recuperado. Na prática, um atacante com acesso a um servidor TLS que implementa RSA-PKCS#1 v1.5 pode decifrar sessões TLS capturadas.
RSA-OAEP (Optimal Asymmetric Encryption Padding) é randomizado — o padding inclui um componente aleatório — e a verificação de padding inválido não vaza informação sobre o plaintext. Mesmo que o servidor retorne erro de padding, a resposta não ajuda o atacante a recuperar informação. Bibliotecas modernas usam OAEP por padrão; a armadilha é sobrescrever o padrão por compatibilidade com sistemas legados.
Qual a diferença entre Argon2i, Argon2d e Argon2id? Por que Argon2id é o padrão para hash de senha?
Argon2 tem três variantes que diferem em como acessam a memória durante o cálculo do hash:
Argon2d: acessa a memória de forma dependente dos dados — o padrão de acesso à memória depende do valor que está sendo hasheado. Isso o torna maximamente resistente a ataques com GPU e ASIC (memory hardness máxima), mas vulnerável a side-channel attacks: um atacante com acesso aos padrões de acesso à memória (ex: via cache timing em ambiente compartilhado) pode inferir informação sobre o plaintext.
Argon2i: acessa a memória de forma independente dos dados — o padrão é determinístico e não depende do valor. Resistente a side-channel, mas menos resistente a GPU porque o padrão de acesso pode ser pré-computado e otimizado.
Argon2id: híbrido — usa Argon2i para a primeira metade do cálculo (protegendo contra side-channel durante a fase mais vulnerável) e Argon2d para a segunda (garantindo memory hardness contra GPU/ASIC). É o melhor de ambos os mundos para hash de senha de propósito geral — resistente tanto a ataques com hardware especializado quanto a ataques de side-channel em ambientes compartilhados (servidores multi-tenant). Por isso o OWASP e o NIST o recomendam como padrão.
Por que comparar hashes ou tokens com == é uma vulnerabilidade de timing attack? Como mitigar e por que funções de tempo constante realmente funcionam?
Quando strings são comparadas com == em virtualmente toda linguagem, a comparação é implementada como "compare byte a byte, retornar false no primeiro byte diferente." Isso significa que a comparação de "abc" com "abz" leva mais tempo que comparar "abc" com "xyz" — o primeiro falha no terceiro byte, o segundo no primeiro. Essa diferença de tempo (microssegundos ou nanossegundos) é mensurável via timing attacks remotos com suficientes repetições.
Para hashes de senha ou tokens de autenticação, um atacante que pode fazer milhares de requisições pode fazer timing oracle: enviar candidatos e medir o tempo de resposta para inferir quantos bytes do token correto ele acertou — guiando um ataque de busca progressiva byte a byte.
Funções de tempo constante como hmac.compare_digest() em Python sempre comparam todos os bytes, independente de onde a diferença aparece — usando operações bitwise que não têm branch-on-data (sem short-circuit). result = 0; for each byte: result |= (a[i] ^ b[i]); return result == 0 — cada byte contribui ao resultado, mas o tempo total é fixo. A mitigação funciona porque o timing leak vem exatamente do short-circuit; removê-lo elimina o sinal que o atacante explorava.
Referências para aprofundar
- book Cryptography Engineering — Ferguson, Schneier e Kohno (Wiley, 2010).
- book Real-World Cryptography — David Wong (Manning, 2021).
- article Password Hashing Competition — Argon2 (password-hashing.net, 2015).
- docs OWASP Cryptographic Storage Cheat Sheet.
- docs OWASP Password Storage Cheat Sheet.
- article SHAttered — First SHA-1 Collision — Stevens et al. (CWI e Google, 2017).
- article AES-GCM-SIV: Nonce Misuse-Resistant Authenticated Encryption — RFC 8452.
- vídeo Crypto 101 — Laurens Van Houtven (Pycon 2013, e livro gratuito em crypto101.io).
- docs NIST SP 800-175B — Guideline for Using Cryptographic Standards.
- article Don't Roll Your Own Crypto — A Practical Guide to Misusing Cryptography — NCC Group.
- book Applied Cryptography — Bruce Schneier (Wiley, 2nd ed. 1996).
- paper Bleichenbacher's RSA Padding Attack — Bleichenbacher (CRYPTO 1998).