Armazenando senhas com segurança
Nesta aula, você aprenderá a armazenar senhas de forma segura em PHP usando password_hash e password_verify, entenderá por que algoritmos como MD5 e SHA1 são inadequados, e verá como gerenciar rehash e aplicar boas práticas de segurança.
Armazenar senhas de usuários de forma segura é uma das responsabilidades mais críticas no desenvolvimento web. Uma senha comprometida pode levar a acessos não autorizados, vazamento de dados e danos à reputação. Nesta aula, você vai dominar as ferramentas nativas do PHP para hash de senhas, compreender os riscos de usar algoritmos antiquados e implementar práticas recomendadas para proteger seus usuários.
O PHP oferece funções robustas e fáceis de usar desde a versão 5.5: password_hash() e password_verify(). Elas encapsulam toda a complexidade de gerar hashes seguros, com sal automático e custo ajustável, eliminando a necessidade de implementar criptografia manualmente.
password_hash e password_verify
As funções password_hash() e password_verify() são a maneira recomendada de lidar com senhas em PHP. password_hash() cria um hash da senha usando um algoritmo forte (por padrão, bcrypt) e inclui automaticamente um salt aleatório e o custo. O salt é um valor único que é adicionado à senha antes do hash, garantindo que senhas idênticas produzam hashes diferentes e impossibilitando ataques de tabela rainbow.
A função password_verify() verifica se uma senha fornecida corresponde ao hash armazenado. Ela extrai o salt e o custo do próprio hash, tornando a verificação independente de configurações externas.
// Gerando um hash seguro
$senha = 'minha_senha_forte';
$hash = password_hash($senha, PASSWORD_DEFAULT);
echo $hash; // ex: $2y$10$vI8aWBnW3fdx.Z5LZAf8QeJ6Z7mQx3Gm2gYcPzQ0jR0sWzQ9uXa
// Verificando a senha
if (password_verify($senha, $hash)) {
echo "Senha válida!";
} else {
echo "Senha inválida!";
}
?>O algoritmo PASSWORD_DEFAULT atualmente usa bcrypt, mas pode mudar em versões futuras do PHP para algoritmos mais fortes. Sempre use essa constante, pois garante compatibilidade futura. Você também pode especificar algoritmos específicos, como PASSWORD_BCRYPT ou PASSWORD_ARGON2I, dependendo da disponibilidade.
Um ponto importante é que o hash gerado contém todas as informações necessárias para a verificação, então você não precisa armazenar o salt separadamente. Basta guardar o hash no banco de dados.
Por que não usar md5/sha1
Muitos desenvolvedores iniciantes usam funções como md5() ou sha1() para 'criptografar' senhas, mas isso é extremamente inseguro. Esses algoritmos são rápidos demais, permitindo que um atacante calcule bilhões de hashes por segundo e utilize ataques de força bruta ou tabelas rainbow. Além disso, sem um salt, senhas iguais geram hashes idênticos, facilitando a identificação de senhas comuns.
Para ilustrar, veja como é trivial quebrar uma senha com MD5:
$senha = '123456';
$hash_md5 = md5($senha); // 5f4dcc3b5aa765d61d8327deb882cf99
// Um atacante pode comparar esse hash com tabelas pré-computadas e descobrir a senha em segundosMesmo adicionando um salt manualmente, a implementação pode ser falha. Por exemplo, um salt fixo ou previsível não oferece proteção real. A função password_hash() resolve isso automaticamente com salt aleatório e um algoritmo deliberadamente lento (bcrypt), tornando ataques de força bruta inviáveis.
Rehash
Com o tempo, os algoritmos de hash podem se tornar mais fortes, e você pode querer aumentar o custo (número de iterações) para acompanhar o aumento do poder computacional. A função password_needs_rehash() permite verificar se um hash existente precisa ser atualizado para um algoritmo ou custo mais recente.
No momento do login, você pode verificar se o hash do usuário ainda atende aos padrões atuais. Se não, você gera um novo hash com o novo custo e atualiza o banco de dados. Isso garante que, gradualmente, todas as senhas sejam migradas para o novo padrão sem forçar o usuário a redefinir a senha.
// Verificando se o hash precisa ser recalculado
if (password_verify($senhaDigitada, $hashArmazenado)) {
if (password_needs_rehash($hashArmazenado, PASSWORD_DEFAULT, ['cost' => 12])) {
$novoHash = password_hash($senhaDigitada, PASSWORD_DEFAULT, ['cost' => 12]);
// Atualize o hash no banco de dados
}
// Login bem-sucedido
}O parâmetro 'cost' controla o custo computacional do bcrypt. Valores mais altos tornam o hash mais lento e mais seguro, mas também aumentam o tempo de processamento. O valor padrão (10) é razoável, mas você pode ajustá-lo conforme a capacidade do servidor.
Boas práticas
Além de usar as funções corretas, existem várias boas práticas para garantir a segurança das senhas:
- Use sempre PASSWORD_DEFAULT para se beneficiar de futuras melhorias.
- Não limite o comprimento da senha a um valor baixo (ex: 8 caracteres). Permita senhas longas, pois isso aumenta a entropia.
- Trate a senha como um dado sensível: nunca a exiba em logs, e-mails ou respostas de erro.
- Use HTTPS para proteger a transmissão da senha entre o cliente e o servidor.
- Implemente proteção contra força bruta, como bloqueio temporário após múltiplas tentativas falhas.
- Nunca crie seu próprio algoritmo de hash; confie em soluções testadas pela comunidade.
Outra prática é usar um custo adequado. Você pode testar o tempo de hash no seu servidor e ajustar o custo para que cada hash leve cerca de 0,1 a 0,5 segundos. Isso torna ataques de força bruta muito caros.
Referências
- Documentação oficial: password_hash
- Documentação oficial: password_verify
- Documentação oficial: password_needs_rehash
- Constantes de senha do PHP
- OWASP Password Storage Cheat Sheet
- OWASP Cheat Sheet Series
Exercícios
- Exercício 1: Crie um script PHP que receba uma senha via GET e exiba o hash gerado usando
password_hash(). Teste com duas senhas iguais e observe que os hashes são diferentes. - Exercício 2: Escreva uma função que verifica se uma senha está correta usando
password_verify(). A função deve receber a senha digitada e o hash armazenado, e retornar um booleano. - Exercício 3: Explique por que usar MD5 ou SHA1 para senhas é inseguro, mesmo com salt. Dê um exemplo de ataque que se torna viável.
- Exercício 4: Implemente um fluxo de login que verifique a senha e, se necessário, faça o rehash. Considere que o custo atual é 12 e que o hash pode ser antigo.
- Exercício 5: Liste três boas práticas adicionais para armazenamento de senhas além das mencionadas na aula.
$senha = $_GET['senha'] ?? '';
$hash = password_hash($senha, PASSWORD_DEFAULT);
echo $hash;
?>function verificarSenha($senhaDigitada, $hashArmazenado) {
return password_verify($senhaDigitada, $hashArmazenado);
}
?>// Suponha que $hashArmazenado vem do banco
$senhaDigitada = $_POST['senha'];
if (password_verify($senhaDigitada, $hashArmazenado)) {
// Verifica se o hash precisa ser atualizado
if (password_needs_rehash($hashArmazenado, PASSWORD_DEFAULT, ['cost' => 12])) {
$novoHash = password_hash($senhaDigitada, PASSWORD_DEFAULT, ['cost' => 12]);
// Atualize o hash no banco de dados
}
// Login bem-sucedido
} else {
// Senha incorreta
}
?>Considerações finais
Armazenar senhas com segurança é uma habilidade essencial para qualquer desenvolvedor PHP. Ao usar password_hash() e password_verify(), você garante que seus usuários estão protegidos contra os ataques mais comuns. Lembre-se de sempre manter-se atualizado com as melhores práticas de segurança e revisar seu código regularmente.