Este artigo foi inspirado pelo vídeo O QUE NINGUÉM TE ENSINOU SOBRE ARMAZENAR SENHAS NO BANCO DE DADOS!, do Renato Augusto, do canal RenatoAugustoTech. Se você ainda não assistiu, vale muito o tempo — foi assistindo a ele que a ficha caiu sobre o quanto esse tema é negligenciado no dia a dia de quem desenvolve sistemas com autenticação. O Renato apresenta o problema com clareza e didática. A ideia aqui é ir um passo além: mostrar a implementação completa em Python com foco em decisões de arquitetura, incluindo o uso de pepper — uma técnica que a maioria das implementações deixa de fora e que pode ser a diferença entre um vazamento de banco de dados catastrófico e um incidente contido.
Existe um padrão que se repete em revisões de código de sistemas com autenticação. O desenvolvedor implementou o fluxo correto: formulário, validação, persistência. Mas na hora de gravar a senha, aparece algo assim:
| |
Às vezes é MD5. Às vezes é bcrypt com o fator de custo padrão de 2012. A lógica parece razoável — afinal, a senha não está em texto puro. O problema é que “não está em texto puro” e “está protegida” são coisas completamente diferentes.
Este artigo explica por que isso importa, o que o Argon2 faz diferente, como o pepper adiciona uma camada de proteção que o banco de dados sozinho não oferece e como implementar tudo isso corretamente em Python — incluindo o detalhe que a maioria dos tutoriais ignora: o que fazer quando seus parâmetros de segurança ficam desatualizados.
O problema com funções de hash genéricas
SHA-256, MD5 e SHA-1 foram projetados para velocidade. Eles existem para verificar integridade de arquivos, assinar certificados, construir MACs. Nesse contexto, velocidade é uma virtude.
Para senhas, velocidade é o inimigo.
Uma GPU moderna consegue calcular bilhões de hashes SHA-256 por segundo. Se um banco de dados vaza e o atacante tem os hashes, um ataque de dicionário com regras consegue quebrar a maioria das senhas comuns em minutos. A memória necessária para esse ataque é mínima, o paralelismo é trivial e o hardware especializado (ASICs) torna o processo ainda mais eficiente.
Funções de hash para senhas precisam ser deliberadamente lentas e consumir recursos suficientes para tornar ataques em larga escala economicamente inviáveis. Esse é exatamente o problema que o Argon2 resolve.
O que é o Argon2
O Argon2 venceu a Password Hashing Competition em 2015 — uma competição pública com critérios explícitos de segurança, análise de resistência a hardware especializado e revisão pela comunidade criptográfica. Ele é a recomendação atual do OWASP e do RFC 9106 para hashing de senhas.
O algoritmo tem três variantes:
- Argon2d: maximiza resistência a ataques com GPU, mas é vulnerável a ataques de canal lateral. Não use para senhas.
- Argon2i: resistente a ataques de canal lateral, indicado para derivação de chaves criptográficas.
- Argon2id: híbrido entre as duas. É a variante recomendada para autenticação de usuários.
O que diferencia o Argon2 de bcrypt e scrypt não é apenas a segurança maior — é a parametrização explícita e independente de três dimensões de custo:
| Parâmetro | O que controla | Efeito no atacante |
|---|---|---|
time_cost | Número de iterações | CPU mais cara por tentativa |
memory_cost | RAM consumida (em KiB) | Inviabiliza paralelismo com GPU/ASIC |
parallelism | Threads simultâneas | Exige hardware com múltiplos núcleos |
bcrypt só permite ajustar o tempo. scrypt permite memória e tempo, mas não threads. Argon2id controla os três de forma independente, o que permite calibrar o custo para o hardware disponível no servidor sem sacrificar nenhuma dimensão de resistência.
Salt e pepper: qual a diferença e por que você precisa dos dois
Antes de ir para o código, vale esclarecer dois conceitos que costumam ser confundidos — e que têm papéis distintos na proteção das senhas.
O salt é um valor aleatório gerado individualmente para cada senha no momento do hash. Ele é armazenado junto com o hash no banco de dados e serve para garantir que duas senhas idênticas produzam hashes diferentes — o que invalida ataques de rainbow table e força o atacante a quebrar cada senha individualmente. O Argon2 gera e gerencia o salt automaticamente.
O pepper é um segredo global da aplicação — uma string longa e aleatória, única para toda a aplicação, que é concatenada à senha antes do hash e nunca armazenada no banco de dados. Ela vive exclusivamente como variável de ambiente no servidor.
A diferença estratégica é importante: o salt protege contra ataques que partem dos hashes no banco. O pepper protege contra o cenário em que o banco de dados inteiro vaza. Se um atacante obtém o dump do banco mas não tem acesso ao servidor (e portanto não conhece o pepper), os hashes são computacionalmente inúteis — mesmo com Argon2, sem o pepper não há como verificar nenhuma senha.
senha_final = senha_digitada + pepper
hash = argon2id(senha_final) ← isso vai para o banco
pepper = APP_PEPPER ← isso fica só no servidor
Juntos, salt e pepper cobrem cenários de ataque complementares. Separados, cada um cobre apenas metade do problema.
Instalação
| |
argon2-cffi é o binding Python para a implementação de referência em C do Argon2. dynaconf é uma biblioteca de gerenciamento de configuração que segue os princípios do 12-factor app — ela unifica variáveis de ambiente, arquivos .toml/.yaml/.json e secrets em uma única interface, com suporte a múltiplos ambientes (development, testing, staging, production) e validação declarativa de configuração. É a forma mais robusta de lidar com o pepper e com os demais parâmetros da aplicação sem espalhar os.environ pelo código.
Configurando o Dynaconf
Inicialize o Dynaconf na raiz do projeto:
| |
O comando gera três arquivos:
.
├── config.py # ponto de importação do objeto settings
├── settings.toml # configurações da aplicação (vai para o repositório)
└── .secrets.toml # segredos locais (não vai para o repositório — já está no .gitignore)
Defina os parâmetros não sensíveis em settings.toml:
| |
E o pepper em .secrets.toml — que nunca deve ser versionado:
| |
Para gerar um pepper seguro:
python -c "import secrets; print(secrets.token_hex(32))"
Em produção, prefira injetar o pepper via variável de ambiente do servidor (ou um serviço como AWS Secrets Manager) em vez de usar o .secrets.toml — o Dynaconf lê automaticamente variáveis de ambiente prefixadas — e o prefixo é configurável via envvar_prefix no config.py, não precisa ser necessariamente DYNACONF_. Você pode usar o nome da sua aplicação, APP_, ou qualquer convenção que faça sentido para o projeto:
| |
O config.py gerado pelo dynaconf init já está pronto, mas adicione o envvar_prefix desejado e um Validator para garantir que o pepper esteja sempre presente antes de a aplicação subir:
| |
Se APP_PEPPER não estiver definido — ou for menor que 32 caracteres — a aplicação falha na inicialização com uma mensagem clara, antes de qualquer requisição ser processada. Esse comportamento ruidoso e precoce é exatamente o que se quer para uma configuração de segurança crítica.
Uso básico com pepper
Com o Dynaconf configurado, o pepper e os parâmetros do Argon2 são lidos do objeto settings:
| |
O ponto crítico: o pepper é aplicado antes do hash, não depois. O Argon2 recebe senha + pepper como entrada e gera o hash a partir disso. Na verificação, o mesmo pepper precisa ser aplicado antes de chamar verify — sem ele, a verificação sempre falhará, o que torna um vazamento de banco completamente inócuo para o atacante.
Calibrando os parâmetros
Os parâmetros definidos em settings.toml são um ponto de partida razoável. A recomendação mínima do OWASP para Argon2id é memory_cost=19456 (19 MB), time_cost=2 e parallelism=1. Com o Dynaconf é simples sobrescrever esses valores por ambiente sem tocar no código — basta ajustar a seção [production] do settings.toml ou exportar a variável de ambiente correspondente no servidor.
Um critério prático: o hash de uma senha deve levar entre 200 ms e 500 ms no seu hardware de produção. Se estiver abaixo disso, aumente ARGON2_MEMORY_COST ou ARGON2_TIME_COST. Se estiver acima, reduza — mas nunca abaixo dos mínimos do OWASP.
O detalhe que a maioria ignora: rehash
O hash do Argon2 carrega os parâmetros com os quais foi gerado. Isso significa que é possível detectar, no momento do login, se o hash armazenado foi criado com parâmetros desatualizados — e recriá-lo silenciosamente com os parâmetros atuais, sem exigir nenhuma ação do usuário.
| |
O fluxo é simples: autenticou → verificou se precisa de rehash → se sim, recriou o hash com pepper e parâmetros atuais → devolveu para ser persistido. Quando os parâmetros em settings.toml forem atualizados, os hashes antigos migrarem automaticamente a cada login — sem reset de senha, sem janela de manutenção.
Implementação completa
Juntando tudo em uma estrutura utilizável em projetos reais:
| |
Decisões de arquitetura
Alguns pontos que surgem quando essa implementação entra em um sistema maior:
Onde instanciar o PasswordHasher? Uma única instância por aplicação, centralizada — seja como singleton, seja injetada via container de dependências. Com o Dynaconf, os parâmetros vêm do settings e podem ser sobrescritos por ambiente sem nenhuma mudança no código.
O rehash deve acontecer na camada de serviço ou de repositório? Na camada de serviço. O repositório não precisa saber que um hash foi atualizado — ele só precisa persistir o que recebe. A decisão de quando e como rehashear é lógica de domínio.
O que acontece se o pepper vazar junto com o banco? O pepper perde eficácia se estiver no mesmo ambiente comprometido. Por isso ele nunca deve estar em arquivos de configuração versionados, nunca no banco, nunca em logs. O .secrets.toml do Dynaconf é automaticamente incluído no .gitignore — mas em produção a recomendação é injetar o pepper via variável de ambiente do servidor ou via serviço dedicado de secrets (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault), que o Dynaconf também suporta nativamente.
E as senhas já armazenadas sem pepper? A migração é progressiva via rehash no login: ao autenticar com sucesso, aplique o pepper, recalcule o hash e persista o resultado. Usuários inativos nunca migrarão — o que exige manter o código de verificação legado (sem pepper) enquanto a migração ocorre, com uma flag no banco indicando qual versão do hash cada usuário possui.
Timing attacks são uma preocupação aqui? A biblioteca argon2-cffi implementa comparação em tempo constante internamente. Mas o tempo de resposta do endpoint de login em si ainda pode vazar informação se você retornar imediatamente em caso de usuário inexistente. O padrão correto é sempre executar o verify com um hash dummy antes de retornar, para que o tempo de resposta seja consistente independentemente de o usuário existir ou não.
Comparativo final
| MD5 / SHA-256 | bcrypt | Argon2id | Argon2id + pepper | |
|---|---|---|---|---|
| Velocidade deliberadamente lenta | ❌ | ✅ | ✅ | ✅ |
| Resistência a GPU | ❌ | ⚠️ Moderada | ✅ | ✅ |
| Custo de memória configurável | ❌ | ❌ | ✅ | ✅ |
| Paralelismo configurável | ❌ | ❌ | ✅ | ✅ |
| Rehash automático | ❌ | ❌ | ✅ | ✅ |
| Protege contra vazamento isolado do banco | ❌ | ❌ | ❌ | ✅ |
| Recomendação OWASP atual | ❌ | ⚠️ Legacy | ✅ | ✅ |
Conclusão
Argon2id resolve o problema de velocidade que torna SHA-256 e MD5 inadequados para senhas. O pepper resolve um problema diferente e complementar: garante que um vazamento isolado do banco de dados não seja suficiente para comprometer nenhuma credencial, porque o segredo necessário para verificar qualquer hash nunca esteve lá. O Dynaconf amarra tudo isso com uma camada de configuração que separa o que é parâmetro do que é segredo, valida a presença do pepper antes de a aplicação subir e facilita a evolução dos valores por ambiente sem tocar no código.
Usados juntos, os três eliminam os vetores de ataque mais comuns em sistemas de autenticação e deixam a configuração de segurança explícita, versionável e auditável.
A decisão de qual algoritmo usar para senhas não deveria ser tratada como detalhe de implementação. Ela afeta diretamente o risco que seus usuários correm em caso de vazamento — e vazamentos acontecem. A pergunta não é se, é quando.
O código está pronto. As bibliotecas são estáveis. Não há justificativa razoável para não fazer.
Gostou do artigo? Tem dúvidas ou discordâncias? Me encontra no Mastodon em @riverfount@bolha.us — é lá que acontece a conversa.