Especialização em Backend com Python & FastAPI • FastAPI & Python 3.12+ • Pydantic v2, SQLModel, Async/Await e Microsserviços


🗺️ Mapa Conceitual do Tópico

flowchart TD
    A["Cadastro: Senha em Texto Claro"] --> B["Validação Pydantic (max_length=72 bytes)"]
    B --> C["CryptContext(schemes=['argon2', 'bcrypt'])"]
    C --> D["Salt Criptográfico Automático + KDF Lenta"]
    D --> E[("Armazenamento no Banco: senha_hash")]

    F["Login: Tentativa de Autenticação"] --> G["Busca Usuário no Banco pelo Identificador"]
    G --> H{"Usuário Existe?"}
    H -->|"Sim"| I["pwd_context.verify(senha_plana, hash_banco)"]
    H -->|"Não"| J["pwd_context.verify(senha_plana, HASH_DUMMY)"]
    
    I --> K{"Resultado Válido?"}
    J --> L["Rejeição Padronizada: HTTP 401 Credenciais Inválidas"]
    K -->|"Falha"| L
    K -->|"Sucesso"| M{"pwd_context.needs_rehash()?"}
    M -->|"Sim (Parâmetros Obsoletos)"| N["Re-hash Oportunista e Atualização no Banco"]
    M -->|"Não"| O["Emissão de Sessão / Token JWT"]
    N --> O

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
    style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
    style G fill:#fffde7,stroke:#fbc02d,stroke-width:2px
    style I fill:#e0f2f1,stroke:#009688,stroke-width:2px
    style J fill:#ffebee,stroke:#e53935,stroke-width:2px
    style L fill:#b71c1c,stroke:#b71c1c,stroke-width:2px,color:#fff
    style O fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px

🏛️ 1. Hashing vs Criptografia: A Matemática da Não-Reversibilidade

Em engenharia de software de alta segurança, um dos erros conceituais mais comuns é dizer que senhas são “criptografadas”.

  • Criptografia Simétrica ou Assimétrica: É uma operação estritamente reversível. Existe uma chave matemática (ou par de chaves) capaz de decifrar o texto cifrado de volta ao original. Se o servidor for comprometido e a chave vazada, todas as senhas dos usuários estarão imediatamente expostas em texto puro.
  • Função de Hash Criptográfico (KDF - Key Derivation Function): É uma transformação de via de mão única. Dado um hash, é computacionalmente impraticável reverter para a senha original. O sistema nunca “recupera” a senha: no momento do login, ele aplica a mesma KDF sobre o texto fornecido pelo cliente e compara se a saída bate com o hash registrado.

Por que SHA-256 e MD5 são Proibidos para Senhas

Funções da família SHA-2 (SHA-256, SHA-512) foram projetadas para verificação ultra-rápida de integridade de arquivos e assinaturas digitais. Uma única GPU moderna (como uma NVIDIA RTX 4090) consegue computar mais de 10 bilhões de hashes SHA-256 por segundo. Caso uma base de senhas com SHA-256 vaze, um invasor quebra senhas com ataques de dicionário e tabelas rainbow em questão de segundos.

Para senhas, a indústria exige algoritmos deliberadamente lentos (Work Factor / Cost Factor) e com alto consumo de memória RAM (Memory-Hardness):

Algoritmo Tipo de Custo Resistência a Ataques por GPU / ASIC Recomendação OWASP 2026
MD5 / SHA-1 CPU (Ultra-rápido) ❌ Nula (Colisões e Quebra Instantânea) Totalmente Proibido
SHA-256 / 512 CPU (Rápido) ❌ Inadequado (Bilhares de tentativas/segundo) Inaceitável para senhas
PBKDF2 CPU (Iterativo) ⚠️ Moderada (GPU paralela acelera o ataque) Aceitável como legado (FIPS-compliant)
Bcrypt CPU + Cache ✅ Alta (Gargalo de cache L1/L2 em GPUs) Padrão Indústria Consolidado
Argon2id CPU + RAM (Memory-Hard) 🛡️ Máxima (Inviabiliza paralelismo em hardware) Padrão Ouro Canônico Recomendado

⚙️ 2. O CryptContext e o Limite Crítico de 72 Bytes do Bcrypt

No FastAPI com Python, utilizamos o passlib (ou a biblioteca moderna pwdlib/bcrypt) configurado através do CryptContext. Um detalhe de segurança crítico do Bcrypt frequentemente ignorado é que o algoritmo Bcrypt trunca silenciosamente qualquer entrada após 72 bytes.

Se um usuário cadastrar uma senha de 100 caracteres, os últimos 28 caracteres serão ignorados pelo algoritmo de hash. Para evitar essa vulnerabilidade:

  1. Validar no schema Pydantic com Field(max_length=72).
  2. Ou aplicar um pré-hash SHA-256 transparente antes de repassar ao Bcrypt/Argon2.
from passlib.context import CryptContext
from pydantic import BaseModel, Field

# Configuração com múltiplos esquemas e migração automática
pwd_context = CryptContext(
    schemes=["argon2", "bcrypt"],
    deprecated="auto",
    argon2__memory_cost=65536,  # 64 MB de memória alocada por hash
    argon2__time_cost=3,        # 3 iterações internas
    argon2__parallelism=4,      # 4 threads paralelas
    bcrypt__rounds=12,          # Cost factor 2^12 iterações
)

class UsuarioRegistroSchema(BaseModel):
    email: str
    senha_plana: str = Field(
        min_length=8,
        max_length=72,  # Prevenção contra truncamento silencioso do Bcrypt
        description="Senha com tamanho seguro e dentro dos limites da KDF"
    )

def gerar_hash_senha(senha: str) -> str:
    return pwd_context.hash(senha)

def verificar_senha(senha_plana: str, senha_hasheada: str) -> bool:
    return pwd_context.verify(senha_plana, senha_hasheada)

🛡️ 3. Prevenção de Ataques de Tempo e Enumeração de Usuários

Quando um usuário tenta autenticar e o e-mail não existe no banco de dados, o código ingênuo costuma retornar imediatamente um erro HTTP 401. Contudo, calcular uma KDF como Argon2 ou Bcrypt consome cerca de 250ms de CPU. Se a rota responder em 5ms quando o e-mail não existe, e em 255ms quando o e-mail existe mas a senha está errada, um atacante consegue descobrir a lista de todos os usuários cadastrados medindo a variação no tempo de resposta (Timing Attack / User Enumeration).

A defesa canônica consiste em disparar uma verificação contra um hash fictício (Dummy Hash) mesmo quando o usuário não é encontrado:

from fastapi import HTTPException, status
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession

# Hash estático pré-calculado com o mesmo algoritmo e custo em produção
DUMMY_HASH = pwd_context.hash("senha_ficticia_para_equalizar_tempo_de_cpu")

async def autenticar_usuario_timing_safe(
    session: AsyncSession,
    email: str,
    senha_plana: str
) -> "Usuario":
    # 1. Consulta o usuário no banco
    stmt = select(Usuario).where(Usuario.email == email)
    resultado = await session.exec(stmt)
    usuario = resultado.first()

    # 2. Se o usuário não existir, compara contra o DUMMY_HASH para queimar exatamente o mesmo tempo de CPU
    hash_alvo = usuario.senha_hash if usuario else DUMMY_HASH
    senha_valida = pwd_context.verify(senha_plana, hash_alvo)

    # 3. Retorna credencial inválida com mensagem idêntica em ambos os casos
    if not usuario or not senha_valida:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Credenciais de acesso inválidas ou inexistentes",
            headers={"WWW-Authenticate": "Bearer"},
        )

    # 4. Re-hash oportunista transparente
    if pwd_context.needs_rehash(usuario.senha_hash):
        usuario.senha_hash = pwd_context.hash(senha_plana)
        session.add(usuario)
        await session.commit()

    return usuario

🔄 4. Re-hashing Oportunista (needs_rehash)

À medida que o poder computacional cresce, o fator de custo (rounds) precisa ser aumentado periodicamente (por exemplo, de 10 para 12, ou migrando de Bcrypt para Argon2id). Como a API não armazena a senha original em texto claro, é impossível rehashear todo o banco em lote.

O método pwd_context.needs_rehash(hash_banco) resolve esse desafio em tempo de execução: durante o login bem-sucedido, o sistema detecta se aquele hash utiliza parâmetros antigos e regera o hash com os novos parâmetros de forma totalmente transparente e imperceptível para o usuário.