Como armazenar senhas com segurança usando PHP e MySQL
Autor: Gutierrez PS | Publicado: 2013-11-03T00:03:00-03:00


Desde que aprendi PHP, me disseram pra armazenar no banco de dados o hash das senhas dos usuários, particularmente aplicando o algoritmo MD5. Mas hoje fiquei com uma pulga atrás da orelha e resolvi pesquisar para saber se este é o método mais adequado para tal fim. E, adivinha só: não é. Topei com dois artigos muito bons que explicam um método muito mais seguro: utilizando a função crypt().

Primeiro, apresento como não armazenar senhas no banco de dados, e porquê.


Não salve as senhas como texto puro

Isto deveria ser óbvio. Se alguém conseguir acessar seu banco de dados, todos as contas de usuário estarão comprometidas. E não é só isso: as pessoas tendem a usar a mesma senha em sites diferentes, daí estas outras contas estariam comprometidas também. Seu site nem precisaria ser hackeado; um administrador do sistema de um servidor compartilhado poderia facilmente ver seu banco de dados.


Não invente um sistema de segurança de senhas

É muito provável que você não seja um expert em segurança. É melhor usar uma solução que funcione comprovadamente do que você inventar alguma coisa.


Não 'encripte' senhas

A encriptação pode parecer uma boa ideia, mas o processo é reversível. Qualquer um com acesso ao seu código não teria problema nenhum em transformar as senhas de volta. Segurança por obscuridade não é o suficiente.


Não utilize MD5

Armazenar os hashes das senhas é um passo na direção certa. Funções criptográficas como o MD5 são irreversíveis, o que torna mais difícil descobrir a senha original. Para validar um hash de uma senha, simplesmente aplique o hash na senha novamente quando o usuário fizer login e compare os hashes.

$senha = 'swordfish';
$hash = md5($senha); // Valor: 15b29ffdce66e10527a65bc6d71ad94d


Note que isto torna impossível recuperar a senha original do banco de dados. Se um usuário esquecê-la, simplesmente gere uma nova.


Então por que não o MD5? Porque é muito fácil fazer uma lista de milhões de hashes de senhas (uma rainbow table) e comparar os hashes para achar a senha original (o mesmo vale para outros métodos, como SHA-1).


O MD5 também está sujeito à força bruta (tentando todas as combinações com um script automatizado), particularmente devido à colisões. Isto significa que senhas diferentes podem ter o mesmo hash, tornando ainda mais fácil encontrar uma que funcione.


Não use um salt universal

Um salt ("sal", em tradução livre) é uma string, adicionada à senha, para que a maioria das rainbow tables (ou ataques por dicionário) não funcionem.

$senha = 'swordfish';
$salt = 'alguma coisa aleatória';
$hash = md5($salt . $senha); // Valor: 10ca832b34c90eccc658ead13e7485eb


Isto é melhor do que aplicar apenas o MD5, mas alguém com acesso ao seu código poderia descobrir o salt e gerar uma rainbow table.



Mãos à obra!

Para um sistema mais seguro de armazenamento de senhas, utilizaremos a função crypt() com um salt único. Muitas pessoas acham que um salt precisa ser único para cada usuário, o que é verdade para muitos casos. Mas com o crypt() podemos usar um salt que é único para uma determinada senha. O salt utilizado acabará estando dentro do hash da senha - então não precisamos armazená-lo separadamente.



Gerando um salt

Podemos usar a seguinte string como um salt

$salt = '$2a$15$abcdefghijklmnopqrstuv$'


O problema é que este não é único, nem aleatório. As letras utilizadas foram 22 caracteres dentro dos conjuntos a-z, A-Z e 0-9. Idealmente deveríamos utilizar uma função aleatória, que poderia utilizar um timestamp, ou até mesmo outro método maluco! Eis um salt mais ideal:

$salt = '$2a$15$Ku2hb./9aA71tPo/E015h.$'


Vamos ver como ele é composto - são três partes. A primeira, $2a$, é um identificador para informar ao PHP que estamos utilizando o algoritmo BlowFish. A segunda parte, 15, é o parâmetro de custo. É um número entre 4 e 31, que indica o número de iterações do algoritmo - basicamente, quanto maior o número, mais demorado é o ataque à força bruta, e mais demorada é a geração da senha.


Para gerar um salt aleatório, pode-se utilizar o código a seguir:

$salt = '$2a$10$';
$salt .= strtr(base64_encode(mcrypt_create_iv(16, MCRYPT_DEV_URANDOM)),'+','.');




Aplicando e verificando o hash

Aplicar o hash é tão simples quanto isto:

$hash = crypt($senha, $salt);


E pronto, temos o hash correspondente, pronto para ser armazenado no banco de dados.


Para verificar se uma senha é válida, utiliza-se o mesmo método, mas no lugar do salt fornecemos o hash armazenado no banco de dados, da seguinte maneira:

$hashArmazenado = '$2a$15$Ku2hb./9aA71tPo/E015h.LsNjXrZe8pyRwXOCpSnGb0nPZuxeZP2';
$senhaATestar = 'passwords1';

if( crypt($senhaATestar, $hashArmazenado) === $hashArmazenado ){
    echo 'Você entrou!';
} else{
    echo 'Senha inválida';
}


Aplicar o hash na senha utilizando seu próprio hash retorna o mesmo hash. Isto é, se a senha a ser testada estiver correta, aplicar a função crypt() fornecendo como salt o hash armazenado anteriormente retorna o próprio hash. Simples assim.


Considerações adicionais

Se você está levando a segurança do seu sistema a sério, adote outros mecanismos para prevenir ataques às contas de usuários, como:

Aumentar exponencialmente o tempo de espera após logins incorretos
Limitar a quantidade de logins incorretos
Enviar um email ao usuário para desbloquear a conta
Exiga senhas fortes (minúsculas, maiúsculas, números e símbolos, por exemplo)
Não limite o tamanho das senhas



Referências
Storing passwords with PHP. In: &lt;http://codular.com/storing-passwords-php&gt;
How to store passwords safely with PHP and MySQL. In: &lt;http://alias.io/2010/01/store-passwords-safely-with-php-and-mysql/&gt;