Nunca armazene senhas em texto puro
Armazenar senhas em texto puro é uma das falhas de segurança mais graves que um desenvolvedor pode cometer. Embora pareça óbvio que isso deve ser evitado, inúmeros incidentes de segurança continuam ocorrendo porque equipes de desenvolvimento negligenciam esse princípio fundamental. Este artigo explica por que essa prática é desastrosa, quais alternativas seguras adotar e como implementar corretamente o armazenamento de senhas.
1. Por que armazenar senhas em texto puro é um desastre
1.1. O impacto de um vazamento de dados
Quando um banco de dados é comprometido, senhas em texto puro ficam imediatamente disponíveis para atacantes. Isso significa que qualquer pessoa com acesso ao dump do banco pode ler todas as credenciais dos usuários. O impacto vai além da exposição individual: a empresa sofre danos reputacionais severos, perde a confiança dos clientes e pode enfrentar processos judiciais.
1.2. Reuso de senhas
Grande parte dos usuários reutiliza senhas entre diferentes serviços. Se um atacante obtém uma senha em texto puro do seu sistema, ele pode tentar a mesma combinação em outros serviços populares (e-mail, redes sociais, bancos). Uma única falha no armazenamento de senhas pode comprometer a vida digital inteira de um usuário.
1.3. Obrigações legais e regulatórias
Legislações como a LGPD (Brasil) e a GDPR (Europa) exigem que empresas adotem medidas técnicas adequadas para proteger dados pessoais. Armazenar senhas em texto puro é considerado negligência grave, sujeitando a organização a multas que podem chegar a 2% do faturamento anual (GDPR) ou 50 milhões de reais (LGPD).
2. O que NÃO fazer: práticas inseguras comuns
2.1. Armazenamento direto em banco de dados
INSERT INTO usuarios (email, senha) VALUES ('user@exemplo.com', 'minhaSenha123');
Esse código salva a senha exatamente como o usuário digitou. Se o banco for vazado, todas as senhas estão expostas.
2.2. Uso de criptografia reversível
Alguns desenvolvedores tentam "proteger" senhas usando criptografia simétrica como AES:
senha_criptografada = aes_encrypt('minhaSenha123', chave_secreta)
INSERT INTO usuarios (email, senha) VALUES ('user@exemplo.com', senha_criptografada);
O problema é que, se a chave de criptografia for descoberta (o que acontece com frequência em vazamentos), todas as senhas podem ser descriptografadas. Senhas nunca devem ser reversíveis.
2.3. Hashing sem salt
senha_hash = sha256('minhaSenha123')
INSERT INTO usuarios (email, senha) VALUES ('user@exemplo.com', senha_hash);
Usar apenas hash sem salt torna o sistema vulnerável a ataques de dicionário e rainbow tables. Atacantes podem pré-computar hashes de senhas comuns e compará-los diretamente com os valores armazenados.
3. O fundamento correto: hashing com salt
3.1. Funções hash criptográficas
Uma função hash criptográfica (como SHA-256) transforma uma entrada em uma saída de tamanho fixo, teoricamente irreversível. No entanto, hashes rápidos como SHA-256 são projetados para velocidade, o que os torna inadequados para senhas — atacantes podem testar bilhões de combinações por segundo.
3.2. O papel do salt
Salt é um valor aleatório único gerado para cada senha. Ele é concatenado à senha antes do hashing e armazenado junto com o hash. Mesmo que dois usuários tenham a mesma senha, os hashes serão diferentes porque os salts são únicos.
salt = gerar_salt_aleatorio(16 bytes)
senha_hash = hash_lento(salt + 'minhaSenha123')
INSERT INTO usuarios (email, salt, senha) VALUES ('user@exemplo.com', salt, senha_hash);
3.3. Hashing rápido vs. lento
Hashes rápidos (SHA-256, MD5) permitem que atacantes testem milhões de senhas por segundo. Hashes lentos (bcrypt, argon2) são propositalmente custosos computacionalmente, tornando ataques de força bruta impraticáveis.
4. Algoritmos recomendados para hashing de senhas
4.1. bcrypt
bcrypt é um algoritmo amplamente adotado que incorpora automaticamente o salt e permite configurar o "custo" (número de iterações). Um custo de 12 significa 2^12 (4096) iterações, o que é seguro para a maioria das aplicações.
4.2. Argon2
Argon2 venceu a Password Hashing Competition (PHC) em 2015. A variante Argon2id oferece proteção contra ataques side-channel e resistência a ataques com GPU. É o algoritmo mais recomendado atualmente.
4.3. PBKDF2
PBKDF2 é um padrão antigo, mas ainda válido se configurado com um número alto de iterações (pelo menos 600.000). No entanto, bcrypt e Argon2 são preferíveis por oferecerem melhor resistência a hardware especializado.
5. Implementação segura: passo a passo
5.1. Gerando um salt criptograficamente aleatório
import secrets
salt = secrets.token_hex(16) # 16 bytes aleatórios
5.2. Exemplo de hash com bcrypt (custo 12)
import bcrypt
senha = "minhaSenha123"
salt = bcrypt.gensalt(rounds=12)
hash_armazenado = bcrypt.hashpw(senha.encode('utf-8'), salt)
# hash_armazenado contém salt e hash juntos
# Exemplo: b'$2b$12$LJ3m...'
5.3. Exemplo de hash com Argon2id
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4)
hash_armazenado = ph.hash("minhaSenha123")
# Verificação posterior
try:
ph.verify(hash_armazenado, "minhaSenha123")
print("Senha correta")
except:
print("Senha incorreta")
5.4. Validação de senha
# Recupera hash do banco
hash_banco = buscar_hash_do_usuario("user@exemplo.com")
# Compara com a senha fornecida
if bcrypt.checkpw(senha_fornecida.encode('utf-8'), hash_banco):
print("Login autorizado")
else:
print("Senha incorreta")
6. Mitigando riscos adicionais além do armazenamento
6.1. Proteção contra vazamentos
Mesmo com hashing seguro, o banco de dados deve ser criptografado em repouso. Use criptografia em nível de disco (LUKS) ou criptografia de banco de dados (TDE).
6.2. Rate limiting e bloqueio de contas
Implemente limites de tentativas de login (ex.: 5 tentativas em 15 minutos) e bloqueio temporário da conta. Isso impede ataques de força bruta mesmo contra hashes seguros.
6.3. Rotação periódica de salts
Embora salts não precisem ser trocados com frequência, ao atualizar o algoritmo de hashing (ex.: migrar de bcrypt para Argon2), todos os hashes devem ser recalculados durante o próximo login do usuário.
7. Testando e auditando sua implementação
7.1. Verificando logs e respostas de API
Nunca logue senhas ou inclua hashes de senhas em respostas de API. Ferramentas como GitLeaks podem detectar vazamentos acidentais em repositórios.
7.2. Ferramentas de análise estática
Use ferramentas como Bandit (Python), Brakeman (Ruby) ou Semgrep para detectar padrões inseguros de armazenamento de senhas no código-fonte.
7.3. Simulação de vazamento
Periodicamente, realize testes de penetração simulando um vazamento do banco de dados. Verifique se os hashes resistem a ataques com ferramentas como Hashcat e John the Ripper usando dicionários comuns.
Conclusão
Armazenar senhas em texto puro é inaceitável em qualquer aplicação moderna. A combinação de hashing lento (bcrypt, Argon2id) com salts únicos por usuário é a abordagem mínima necessária para proteger credenciais. Além disso, medidas complementares como rate limiting, criptografia do banco e auditorias regulares garantem uma defesa em profundidade. Lembre-se: a segurança das senhas dos seus usuários é sua responsabilidade como desenvolvedor.
Referências
- OWASP Password Storage Cheat Sheet — Guia oficial da OWASP com recomendações detalhadas sobre armazenamento seguro de senhas, incluindo algoritmos e práticas recomendadas.
- Argon2: The Password Hashing Competition — Site oficial da Password Hashing Competition com especificações do Argon2 e documentação técnica.
- bcrypt documentation - PyPI — Documentação oficial da biblioteca bcrypt para Python, com exemplos de uso e parâmetros de segurança.
- NIST SP 800-63B: Digital Identity Guidelines — Padrão do NIST sobre autenticação e armazenamento de senhas, referência regulatória internacional.
- LGPD - Lei Geral de Proteção de Dados (Lei 13.709/2018) — Texto oficial da legislação brasileira que exige medidas técnicas para proteção de dados pessoais.
- Hashcat - Advanced Password Recovery — Ferramenta utilizada para testar a resistência de hashes de senhas contra ataques de força bruta e dicionário.
- Bandit - Security linter for Python — Ferramenta de análise estática que detecta armazenamento inseguro de senhas e outras vulnerabilidades em código Python.