Armazenar senhas de usuários de forma segura é uma das responsabilidades mais críticas no desenvolvimento web. Uma senha em texto puro ou com hash fraco é uma porta aberta para ataques de vazamento de dados. Nesta aula, você vai dominar as funções nativas do PHP password_hash() e password_verify(), entender por que algoritmos como MD5 e SHA1 são inaceitáveis, como gerenciar rehash e adotar boas práticas de segurança no armazenamento e validação de credenciais.

O PHP oferece desde a versão 5.5 um conjunto de funções específicas para senhas, que implementam algoritmos modernos e seguros, com salt automático e custo configurável. Ao final desta aula, você será capaz de implementar autenticação robusta em suas aplicações, protegendo os dados dos seus usuários contra os ataques mais comuns.

password_hash e password_verify

As funções password_hash() e password_verify() são as ferramentas nativas do PHP para criar e verificar hashes de senha. Elas utilizam algoritmos fortes como bcrypt, Argon2 ou Argon2id, que são projetados para serem computacionalmente custosos, dificultando ataques de força bruta e quebra por tabelas rainbow.

password_hash() gera um hash único para cada senha, incluindo um salt aleatório, e retorna uma string que contém todas as informações necessárias para verificação futura. Já password_verify() compara a senha fornecida com o hash armazenado, extraindo o salt e o custo do próprio hash, sem precisar de parâmetros adicionais.

Exemplo básico de criação e verificação:

O segundo parâmetro de password_hash() define o algoritmo. PASSWORD_DEFAULT usa o algoritmo mais forte disponível no PHP (atualmente bcrypt), e PASSWORD_BCRYPT é a opção clássica. Para Argon2, use PASSWORD_ARGON2I ou PASSWORD_ARGON2ID (disponível a partir do PHP 7.2 e 7.3, respectivamente). É possível passar um array de opções, como custo, para ajustar a segurança:

$hash = password_hash($senha, PASSWORD_BCRYPT, ['cost' => 12]);
// Para Argon2id:
$hash = password_hash($senha, PASSWORD_ARGON2ID, ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 1]);
?>

Por que não usar md5/sha1

MD5 e SHA1 são funções de hash criptográficas antigas, mas não são adequadas para senhas. Elas são extremamente rápidas, o que permite que um atacante tente bilhões de combinações por segundo. Além disso, não incluem salt automático, tornando os hashes vulneráveis a ataques de dicionário e tabelas rainbow (tabelas pré-computadas de hashes).

Um ataque comum é o uso de GPUs para quebrar senhas com MD5 em questão de minutos. Por isso, qualquer aplicação que ainda use md5($senha) ou sha1($senha) está em alto risco. Mesmo com salt, a velocidade desses algoritmos os torna inaceitáveis. O PHP descontinua o uso dessas funções para fins de segurança, e a documentação oficial recomenda explicitamente o uso de password_hash().

Exemplo do que NÃO fazer:

// NUNCA faça isso:
$hash = md5($senha); // inseguro
$hash = sha1($senha . 'salt'); // ainda inseguro, muito rápido
?>

Algoritmos como bcrypt, Argon2 e scrypt são deliberadamente lentos e consomem memória, tornando ataques de força bruta impraticáveis. O PHP implementa bcrypt e Argon2, e a diferença de segurança é enorme.

Rehash

Com o passar do tempo, algoritmos de hash evoluem e o custo recomendado aumenta. Por isso, é importante verificar periodicamente se o hash armazenado ainda atende aos padrões atuais. A função password_needs_rehash() permite verificar se o hash precisa ser atualizado, por exemplo, se o algoritmo mudou ou se o custo configurado aumentou.

O fluxo típico é: durante o login, após verificar a senha, verifique se o hash atual precisa de rehash. Se sim, atualize o hash no banco de dados. Isso garante que senhas antigas sejam migradas para algoritmos mais fortes de forma transparente para o usuário.

// Durante o login
if (password_verify($senha_digitada, $hash_armazenado)) {
    // Verifica se o hash precisa ser atualizado
    if (password_needs_rehash($hash_armazenado, PASSWORD_DEFAULT, ['cost' => 12])) {
        $novo_hash = password_hash($senha_digitada, PASSWORD_DEFAULT, ['cost' => 12]);
        // Atualize $novo_hash no banco de dados
    }
    // Login bem-sucedido
} else {
    // Senha incorreta
}
?>

O rehash é uma prática recomendada para manter a segurança ao longo do tempo. Ele não requer nenhuma ação do usuário e melhora a proteção de contas existentes.

Boas práticas

Além de usar as funções corretas, é fundamental adotar um conjunto de boas práticas para garantir a segurança das senhas no contexto completo da aplicação.

  • Nunca armazene senhas em texto puro ou com criptografia reversível (ex.: base64, AES). Use apenas hash com salt.
  • Use PASSWORD_DEFAULT ou escolha explicitamente um algoritmo forte como Argon2id, que é resistente a ataques com GPUs.
  • Defina um custo adequado (ex.: cost 12 para bcrypt) que equilibre segurança e desempenho. Ajuste conforme o hardware do servidor.
  • Utilize password_needs_rehash() para migrar hashes antigos para algoritmos mais fortes.
  • Evite limitar o tamanho da senha (mas defina um mínimo razoável, ex.: 8 caracteres) e não aplique trim() ou outras alterações antes de hash.
  • Nunca faça hash de senhas com funções como hash() sem salt e sem usar algoritmos lentos.
  • Proteja o banco de dados com criptografia em repouso e acesso restrito, pois mesmo hashes fortes podem ser quebrados se o banco vazar.
  • Implemente proteção contra força bruta no login (ex.: limite de tentativas, CAPTCHA) para complementar a segurança do hash.
  • Use prepared statements para consultas SQL e evite injeção de SQL, que pode comprometer o banco.

Exemplo de uma classe simples de autenticação:

class Usuario {
    public static function criarUsuario($email, $senha) {
        $hash = password_hash($senha, PASSWORD_DEFAULT);
        // Inserir no banco com prepared statement
        // INSERT INTO usuarios (email, senha_hash) VALUES (?, ?)
    }

    public static function autenticar($email, $senha) {
        // Buscar usuário pelo email
        // $usuario = SELECT * FROM usuarios WHERE email = ?
        if ($usuario && password_verify($senha, $usuario['senha_hash'])) {
            if (password_needs_rehash($usuario['senha_hash'], PASSWORD_DEFAULT)) {
                $novo_hash = password_hash($senha, PASSWORD_DEFAULT);
                // Atualizar no banco
            }
            return true;
        }
        return false;
    }
}
?>

Referências

Exercícios

  1. Crie uma função em PHP que receba uma senha em texto puro e retorne o hash usando password_hash() com custo 12. Teste com uma senha de exemplo e exiba o hash.
  2. ✓ Resposta:
    function criarHash($senha) {
        return password_hash($senha, PASSWORD_BCRYPT, ['cost' => 12]);
    }
    
    $hash = criarHash('MinhaSenha@123');
    echo $hash;
  3. Escreva um script que verifique se uma senha digitada corresponde a um hash armazenado usando password_verify(). Use um hash gerado no exercício anterior.
  4. ✓ Resposta:
    $hash = '$2y$12$...'; // hash do exercício 1
    $senhaDigitada = 'MinhaSenha@123';
    
    if (password_verify($senhaDigitada, $hash)) {
        echo "Senha correta!";
    } else {
        echo "Senha incorreta!";
    }
  5. Explique por que usar MD5 para senhas é inseguro e cite pelo menos três ataques que são facilitados por esse uso.
  6. ✓ Resposta: MD5 é extremamente rápido, permitindo ataques de força bruta em alta velocidade. Além disso, sem salt, é vulnerável a tabelas rainbow e ataques de dicionário. Também sofre de colisões, o que pode permitir autenticação com senhas diferentes.
  7. Implemente um trecho de código que, após verificar a senha com sucesso, verifique se o hash precisa de rehash e atualize-o.
  8. ✓ Resposta:
    if (password_verify($senha, $hashArmazenado)) {
        if (password_needs_rehash($hashArmazenado, PASSWORD_DEFAULT, ['cost' => 12])) {
            $novoHash = password_hash($senha, PASSWORD_DEFAULT, ['cost' => 12]);
            // Atualizar no banco de dados
        }
        // Login ok
    }
  9. Liste e explique três boas práticas adicionais para proteger senhas de usuários em uma aplicação web.
  10. ✓ Resposta:
    • Usar prepared statements para evitar injeção de SQL.
    • Implementar limitação de tentativas de login (ex.: 5 tentativas por minuto).
    • Manter o PHP e o banco de dados atualizados com patches de segurança.

Observações finais

Armazenar senhas com segurança é um processo contínuo. Sempre use as funções nativas do PHP e mantenha-se atualizado sobre as melhores práticas de segurança. Lembre-se de que a segurança não é um estado, mas um processo de melhoria constante.