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