O que Todo Dev Deve Saber sobre Criptografia na Prática: Implementando Corretamente sem Reinventar a Roda Já leu

Princípios Fundamentais da Criptografia A criptografia é a arte e ciência de proteger informações através de transformações matemáticas que tornam dados ilegíveis para quem não possui a chave correta. Quando falamos em implementar criptografia "corretamente", estamos falando sobre usar algoritmos consolidados, públicos e auditados pela comunidade acadêmica e profissional — nunca criar seus próprios algoritmos. O maior erro que vemos em projetos reais é a tentativa de inventar sistemas de segurança caseiros, que invariavelmente apresentam vulnerabilidades graves. A razão disso é simples: criptografia forte não vem da criatividade, mas do rigor matemático testado por milhares de pesquisadores ao longo de décadas. Quando você usa bibliotecas reconhecidas como OpenSSL, libsodium ou as bibliotecas nativas de linguagens modernas, você está se beneficiando desse trabalho coletivo. Além disso, essas bibliotecas são constantemente auditadas e atualizadas quando novas vulnerabilidades surgem. Os Dois Pilares: Simétrica e Assimétrica Existem dois paradigmas principais de criptografia que você precisa entender. A criptografia simétrica usa a mesma chave tanto

Princípios Fundamentais da Criptografia

A criptografia é a arte e ciência de proteger informações através de transformações matemáticas que tornam dados ilegíveis para quem não possui a chave correta. Quando falamos em implementar criptografia "corretamente", estamos falando sobre usar algoritmos consolidados, públicos e auditados pela comunidade acadêmica e profissional — nunca criar seus próprios algoritmos. O maior erro que vemos em projetos reais é a tentativa de inventar sistemas de segurança caseiros, que invariavelmente apresentam vulnerabilidades graves.

A razão disso é simples: criptografia forte não vem da criatividade, mas do rigor matemático testado por milhares de pesquisadores ao longo de décadas. Quando você usa bibliotecas reconhecidas como OpenSSL, libsodium ou as bibliotecas nativas de linguagens modernas, você está se beneficiando desse trabalho coletivo. Além disso, essas bibliotecas são constantemente auditadas e atualizadas quando novas vulnerabilidades surgem.

Os Dois Pilares: Simétrica e Assimétrica

Existem dois paradigmas principais de criptografia que você precisa entender. A criptografia simétrica usa a mesma chave tanto para cifrar quanto para decifrar — é rápida, eficiente, mas exige que ambas as partes já possuam a chave antes de se comunicarem. Já a criptografia assimétrica usa um par de chaves: uma pública (que qualquer um pode ter) e outra privada (que você guarda com segurança). Isso permite que desconhecidos lhe enviem mensagens cifradas apenas com sua chave pública, mas apenas você consegue decifrar com sua chave privada.

Na prática, a maioria dos sistemas modernos combina ambas: usa criptografia assimétrica para trocar uma chave simétrica com segurança, e depois usa essa chave simétrica para cifrar o volume de dados em alta velocidade. Isso é exatamente o que HTTPS faz. Compreender quando e por que usar cada uma é essencial para implementar segurança de forma correta.

Criptografia Simétrica: AES na Prática

O Advanced Encryption Standard (AES) é o algoritmo de criptografia simétrica mais usado no mundo. Aprovado pelo governo dos EUA, implementado em hardware de processadores modernos e verificado por décadas de análise criptográfica, o AES é sua escolha padrão quando você precisa cifrar dados onde ambas as partes compartilham uma chave.

O AES opera em blocos de 128 bits e suporta chaves de 128, 192 ou 256 bits. Quanto maior a chave, maior a segurança teórica, mas também maior o custo computacional. Para a maioria dos cenários reais, AES-256 é considerado inquebrantável mesmo considerando avanços futuros em computação clássica. Porém, não basta usar AES puro — você precisa usar um modo de operação correto e adicionar um vetor de inicialização (IV) aleatório a cada mensagem. O modo GCM (Galois/Counter Mode) é excelente porque não apenas cifra, mas também autentica os dados, prevenindo que um atacante modifique a mensagem cifrada sem você detectar.

Implementação com Python e cryptography

Vejamos uma implementação prática e segura usando a biblioteca cryptography, que é a escolha recomendada em Python:

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2
import os
import base64

class CriptografiaSimetrica:
    """
    Implementação segura de criptografia simétrica com AES-256-GCM.
    Não reinventa a roda — usa bibliotecas bem testadas.
    """

    @staticmethod
    def derivar_chave(senha: str, salt: bytes = None) -> tuple:
        """
        Deriva uma chave de 256 bits a partir de uma senha.
        Usa PBKDF2 com 480.000 iterações (padrão OWASP 2023).
        Retorna (chave, salt) para armazenamento.
        """
        if salt is None:
            salt = os.urandom(16)

        kdf = PBKDF2(
            algorithm=hashes.SHA256(),
            length=32,  # 256 bits
            salt=salt,
            iterations=480000,
        )
        chave = kdf.derive(senha.encode())
        return chave, salt

    @staticmethod
    def cifrar(mensagem: str, chave: bytes) -> str:
        """
        Cifra uma mensagem usando AES-256-GCM.
        Retorna string base64 contendo: (nonce || ciphertexto || tag).
        """
        nonce = os.urandom(12)  # 96 bits, recomendado para GCM
        cipher = AESGCM(chave)
        ciphertexto = cipher.encrypt(nonce, mensagem.encode(), None)

        # Retorna tudo junto: nonce + ciphertexto (que inclui a tag de autenticação)
        resultado = nonce + ciphertexto
        return base64.b64encode(resultado).decode()

    @staticmethod
    def decifrar(cifrado_b64: str, chave: bytes) -> str:
        """
        Decifra uma mensagem cifrada.
        Verifica autenticação automaticamente — lança exceção se inválida.
        """
        dados = base64.b64decode(cifrado_b64)
        nonce = dados[:12]
        ciphertexto = dados[12:]

        cipher = AESGCM(chave)
        mensagem = cipher.decrypt(nonce, ciphertexto, None)
        return mensagem.decode()


# Exemplo de uso prático
if __name__ == "__main__":
    # Usuário informa uma senha
    senha = "minha_senha_super_secreta_123"

    # Derive chave da senha (armazene o salt para depois!)
    chave, salt = CriptografiaSimetrica.derivar_chave(senha)
    print(f"Salt (armazene em banco de dados): {base64.b64encode(salt).decode()}")

    # Cifre uma mensagem
    mensagem_original = "Saldo da conta: R$ 10.000,00"
    cifrado = CriptografiaSimetrica.cifrar(mensagem_original, chave)
    print(f"Mensagem cifrada: {cifrado}")

    # Decifre para verificar
    mensagem_recuperada = CriptografiaSimetrica.decifrar(cifrado, chave)
    print(f"Mensagem decifrada: {mensagem_recuperada}")

    # Tente alterar 1 caractere do cifrado (vai falhar na autenticação)
    cifrado_alterado = cifrado[:-1] + ('A' if cifrado[-1] != 'A' else 'B')
    try:
        CriptografiaSimetrica.decifrar(cifrado_alterado, chave)
    except Exception as e:
        print(f"❌ Detecção de alteração: {type(e).__name__}")

Note que neste código: (1) usamos GCM que autentica automaticamente, (2) geramos um nonce aleatório para cada mensagem, (3) usamos PBKDF2 com iterações fortes para derivar a chave da senha, (4) armazenamos o salt junto com o ciphertexto ou em banco de dados separado. Isso previne ataques de dicionário, garante que mensagens diferentes geram ciphertextos diferentes, e impede que dados sejam modificados sem detecção.

Criptografia Assimétrica: RSA e Curvas Elípticas

Quando você precisa que pessoas que nunca se encontraram antes possam se comunicar com segurança, a criptografia assimétrica é necessária. O padrão mais antigo e ainda amplamente usado é RSA, que se baseia na dificuldade de fatorar números muito grandes. Um par RSA de 2048 bits é considerado seguro para a maioria das aplicações atuais, embora 4096 bits seja preferível para dados que precisem resistir por décadas.

Mais moderno e eficiente são os algoritmos baseados em Curvas Elípticas, especialmente Ed25519 para assinatura digital e Curve25519 para troca de chaves. Eles oferecem segurança equivalente ao RSA com chaves muito menores e operações mais rápidas. Se você está começando um projeto novo, prefira curvas elípticas. Se precisa manter compatibilidade com sistemas legados, RSA é inevitável.

Implementação com Assinatura Digital

A seguir, um exemplo prático de assinatura digital com Ed25519, que garante autenticidade e não-repúdio:

from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
import base64

class AssinaturaDigital:
    """
    Implementação de assinatura digital com Ed25519.
    Prova que você criou o documento e que ele não foi alterado.
    """

    @staticmethod
    def gerar_par_chaves() -> tuple:
        """
        Gera um par de chaves Ed25519.
        Chave privada: GUARDAR COM SEGURANÇA
        Chave pública: COMPARTILHAR LIVREMENTE
        """
        chave_privada = ed25519.Ed25519PrivateKey.generate()
        chave_publica = chave_privada.public_key()
        return chave_privada, chave_publica

    @staticmethod
    def salvar_chave_privada(chave_privada, caminho: str, senha: str = None):
        """
        Salva chave privada em arquivo PEM, opcionalmente criptografada.
        """
        if senha:
            formato_criptografia = serialization.BestAvailableEncryption(
                senha.encode()
            )
        else:
            formato_criptografia = serialization.NoEncryption()

        pem = chave_privada.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=formato_criptografia,
        )
        with open(caminho, 'wb') as f:
            f.write(pem)

    @staticmethod
    def carregar_chave_privada(caminho: str, senha: str = None):
        """
        Carrega chave privada do arquivo PEM.
        """
        with open(caminho, 'rb') as f:
            pem = f.read()

        if senha:
            chave = serialization.load_pem_private_key(
                pem, password=senha.encode()
            )
        else:
            chave = serialization.load_pem_private_key(pem, password=None)

        return chave

    @staticmethod
    def assinar(mensagem: str, chave_privada) -> str:
        """
        Assina uma mensagem com a chave privada.
        Retorna a assinatura em base64.
        """
        assinatura = chave_privada.sign(mensagem.encode())
        return base64.b64encode(assinatura).decode()

    @staticmethod
    def verificar(mensagem: str, assinatura_b64: str, chave_publica) -> bool:
        """
        Verifica se a assinatura é válida.
        Retorna True se válida, lança exceção se inválida.
        """
        assinatura = base64.b64decode(assinatura_b64)
        try:
            chave_publica.verify(assinatura, mensagem.encode())
            return True
        except Exception:
            return False


# Exemplo de uso: Contrato Digital
if __name__ == "__main__":
    # Alice gera seu par de chaves
    chave_privada_alice, chave_publica_alice = AssinaturaDigital.gerar_par_chaves()

    # Alice salva sua chave privada de forma segura
    AssinaturaDigital.salvar_chave_privada(
        chave_privada_alice, 
        "chave_privada_alice.pem",
        senha="senha_muito_segura_123"
    )

    # Alice cria e assina um contrato
    contrato = "Vendo imóvel no valor de R$ 500.000,00 para Bob"
    assinatura = AssinaturaDigital.assinar(contrato, chave_privada_alice)
    print(f"Contrato: {contrato}")
    print(f"Assinatura: {assinatura[:50]}...")

    # Bob (ou qualquer pessoa) verifica a assinatura com a chave pública de Alice
    # (que foi publicamente compartilhada)
    valido = AssinaturaDigital.verificar(
        contrato, 
        assinatura, 
        chave_publica_alice
    )
    print(f"✅ Assinatura válida: {valido}")

    # Se alguém tentar alterar o contrato, a verificação falha
    contrato_alterado = contrato.replace("500.000", "50.000")
    valido_alterado = AssinaturaDigital.verificar(
        contrato_alterado, 
        assinatura, 
        chave_publica_alice
    )
    print(f"✅ Assinatura do contrato alterado: {valido_alterado}")

Observe que assinatura digital não cifra a mensagem — qualquer um pode ler. O que faz é provar que você (detentor da chave privada) criou ou aprovou aquela mensagem. Se você precisa tanto cifrar quanto assinar, use ambas as técnicas: primeiro cifre com criptografia simétrica, depois assine o ciphertexto com sua chave privada.

Troca de Chaves e Segredo Compartilhado

Um dos desafios práticos da criptografia é: como duas pessoas que nunca se conheceram conseguem compartilhar uma chave simétrica de forma segura? A resposta clássica é o Diffie-Hellman, mas versões modernas com curvas elípticas (como X25519) são muito mais eficientes e seguras. O algoritmo permite que Alice e Bob publiquem valores que, combinados com suas chaves privadas, resultam no mesmo segredo compartilhado — e um atacante que vê tudo isso não consegue calcular o segredo.

Na prática, em HTTPS você usa exatamente esse mecanismo: durante o handshake TLS, cliente e servidor fazem um Diffie-Hellman de curva elíptica para derivar uma chave de sessão simétrica, que é então usada para cifrar toda a comunicação. Você raramente vai implementar isso do zero — bibliotecas TLS fazem tudo por você — mas compreender o conceito ajuda a entender por que HTTPS funciona.

Implementação de Diffie-Hellman Moderno

from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
import base64

class TrocaChavesSegura:
    """
    Implementa troca de chaves segura com X25519 (Elliptic Curve Diffie-Hellman).
    Sem transmissão de segredo — apenas valores públicos são trocados.
    """

    @staticmethod
    def gerar_chave_privada():
        """
        Gera uma chave privada X25519.
        Cada parte gera a sua, mantém privada e compartilha apenas a pública.
        """
        return X25519PrivateKey.generate()

    @staticmethod
    def obter_chave_publica(chave_privada) -> bytes:
        """
        Obtém a chave pública (para compartilhar com a outra parte).
        """
        return chave_privada.public_key().public_bytes_raw()

    @staticmethod
    def derivar_segredo_compartilhado(
        chave_privada_minha, 
        chave_publica_outra: bytes
    ) -> bytes:
        """
        Calcula o segredo compartilhado.
        Mesmo resultado em ambas as partes, mas impossível de calcular
        vendo apenas as chaves públicas.
        """
        from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey

        chave_publica_outra_obj = X25519PublicKey.from_public_bytes(chave_publica_outra)
        segredo_bruto = chave_privada_minha.exchange(chave_publica_outra_obj)

        # Não use o segredo bruto diretamente — derive chaves dele com HKDF
        hkdf = HKDF(
            algorithm=hashes.SHA256(),
            length=32,  # 256 bits para AES-256
            salt=None,
            info=b'aplicacao-secreta',
        )
        chave_derivada = hkdf.derive(segredo_bruto)
        return chave_derivada


# Simulação de Alice e Bob estabelecendo comunicação segura
if __name__ == "__main__":
    print("=== Troca de Chaves Segura (Diffie-Hellman com Curvas Elípticas) ===\n")

    # Fase 1: Cada um gera sua chave privada (mantida secreta)
    print("1️⃣  Alice gera sua chave privada (SECRETA)")
    chave_privada_alice = TrocaChavesSegura.gerar_chave_privada()
    chave_publica_alice = TrocaChavesSegura.obter_chave_publica(chave_privada_alice)
    print(f"   Chave pública de Alice (PÚBLICA): {base64.b64encode(chave_publica_alice).decode()[:30]}...")

    print("\n2️⃣  Bob gera sua chave privada (SECRETA)")
    chave_privada_bob = TrocaChavesSegura.gerar_chave_privada()
    chave_publica_bob = TrocaChavesSegura.obter_chave_publica(chave_privada_bob)
    print(f"   Chave pública de Bob (PÚBLICA): {base64.b64encode(chave_publica_bob).decode()[:30]}...")

    # Fase 2: Trocam chaves públicas (pode ser feito em canal aberto)
    print("\n3️⃣  Alice recebe chave pública de Bob (via canal aberto)")
    print("    Bob recebe chave pública de Alice (via canal aberto)")

    # Fase 3: Cada um calcula o segredo compartilhado independentemente
    print("\n4️⃣  Alice calcula segredo compartilhado")
    segredo_alice = TrocaChavesSegura.derivar_segredo_compartilhado(
        chave_privada_alice, 
        chave_publica_bob
    )

    print("5️⃣  Bob calcula segredo compartilhado")
    segredo_bob = TrocaChavesSegura.derivar_segredo_compartilhado(
        chave_privada_bob, 
        chave_publica_alice
    )

    # Verificação
    print("\n6️⃣  Verificação:")
    print(f"   Segredo de Alice == Segredo de Bob: {segredo_alice == segredo_bob}")
    print(f"   Segredo compartilhado: {base64.b64encode(segredo_alice).decode()}")

    print("\n✅ Agora Alice e Bob podem usar esse segredo para cifrar com AES!")
    print("   Um atacante que viu as chaves públicas não consegue calcular o segredo.")

Este exemplo mostra o poder da matemática de curvas elípticas: ambas as partes chegam ao mesmo resultado sem nunca transmitir o segredo. Qualquer pessoa que monitore a rede vê apenas valores públicos inúteis. Isso é a base do TLS moderno e de qualquer comunicação criptografada segura na internet.

Armadilhas Comuns e Como Evitá-las

Após anos em produção vendo falhas de segurança, posso listar os erros mais recorrentes que você deve evitar:

1. Usar Mode ECB ou CBC sem autenticação. ECB (Electronic Codebook) é perigoso porque cifra blocos idênticos em valores idênticos, vazando padrões. CBC requer um IV aleatório cada vez, mas ainda precisa de autenticação separada para evitar ataques de alteração. Use GCM, ChaCha20-Poly1305 ou similares que combinam confidencialidade e autenticidade.

2. Derivar chaves de senhas com funções rápidas. Usar SHA256 ou similar para derivar chaves de senha é perigoso — é rápido demais e permite ataques de força bruta. Use PBKDF2, bcrypt, scrypt ou Argon2 com parâmetros fortes (configurados de forma a levar ~100ms por derivação).

3. Reutilizar IVs ou Nonces. Cada mensagem cifrada com a mesma chave precisa de um IV/nonce único. Reutilizar quebra a segurança. Gere um novo com os.urandom() cada vez.

4. Armazenar senhas em texto plano. NUNCA faça isso. Sempre use uma função de derivação de chave forte e armazene apenas o hash derivado (junto com o salt). Mesmo que o banco de dados vaze, senhas não são recuperáveis.

5. Confiar em TLS sem validar certificados. Code que aceita qualquer certificado (desabilitar verificação) não tem segurança nenhuma. Sempre valide a cadeia de certificados.

6. Implementar criptografia caseira. Não tente melhorar ou "ajustar" algoritmos consolidados. A comunidade criptográfica já fez isso por você.

# ❌ ERRADO: Derivar chave com SHA256 é muito rápido
import hashlib
senha = "minha_senha"
chave_fraca = hashlib.sha256(senha.encode()).digest()

# ✅ CORRETO: Usar PBKDF2, bcrypt, etc.
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2
from cryptography.hazmat.primitives import hashes

salt = os.urandom(16)
kdf = PBKDF2(
    algorithm=hashes.SHA256(),
    length=32,
    salt=salt,
    iterations=480000,  # Padrão OWASP 2023
)
chave_forte = kdf.derive(senha.encode())

Outra armadilha comum: não considerar ataques do lado do tempo. Funções de comparação ingênuas (==) podem vazar informação através do tempo que levam. Para comparar HMACs ou assinaturas, use hmac.compare_digest():

import hmac

# ❌ ERRADO: Vaza informação pelo tempo
if assinatura_recebida == assinatura_calculada:
    print("válido")

# ✅ CORRETO: Comparação constante
if hmac.compare_digest(assinatura_recebida, assinatura_calculada):
    print("válido")

Conclusão

Dominar criptografia na prática significa compreender que você está montando um quebra-cabeça com peças prontas, não inventando novas peças. Primeiro: escolha algoritmos consolidados (AES para simétrica, Ed25519 para assinatura, X25519 para troca de chaves). Segundo: use bibliotecas bem testadas (cryptography em Python, OpenSSL em C, NaCl em múltiplas linguagens) e não implemente algoritmos do zero. Terceiro: combine técnicas apropriadamente — cifre dados sensíveis, assine o que precisa de autenticidade, e derive chaves de senhas com funções fortes.

A implementação correta de criptografia não é sobre ser criativo — é sobre ser rigoroso. Cada erro pode quebrar toda a segurança. Por isso sempre escolha a opção consolidada e testada, entenda os conceitos suficientemente para saber quando usar cada ferramenta, mas deixe a engenharia pesada para bibliotecas especializadas. Seu trabalho é orquestrar essas ferramentas com sabedoria.

Referências


Artigos relacionados