Ir para conteúdo
  • Cadastre-se

[Tutorial Php] Armazenando Senhas Com +Segurança


Posts Recomendados

Todo programador de PHP, desde o sobrinho até o sênior já pensaram se de fato armazenar senhas criptografadas com sha1, md5, crc32, sha256, entre outros hashes, é seguro ou não.

A resposta é simples: Não!

A sua provável pergunta: Mas esses hashes não são impossíveis de serem descriptografados, por serem de mão única? Sim, a resposta é sim. Eles realmente são impossíveis de serem descriptografados, porém, podem ser comparados.

Para facilitar, imaginemos o código em PHP abaixo:


<?php

// Script que simplesmente escreve na tela o Hash md5 da senha 123456789

$senha = '123456789';

echo md5($senha);

?>

Ele escreverá: 25f9e794323b453885f5181f1b624d0b Execute esse script 1000 vezes ou mais... ele escreverá 1000 vezes o mesmo hash md5. Nesse caso, bastaria ele listar todas as senhas e agrupar por hash, assim ele saberá quantas vezes tal senha se repete no BD. Usando um dicionário (coleção de milhões de senhas mais comuns) criptografado em sha1, md5, crc32, sha256, etc... e comparar com o hash das que estão em seu banco de dados, o hacker passa automaticamente ter as senhas, sem ter que ser um matemático maluco para criar uma função matemática de alta complexidade que consiga quebrar essa senha. Existe até um site que quebra Sha1 e MD5 online e grátis: http://www.md5decrypter.co.uk/ Surpreso? Sim? Relaxe, há solução: Uma senha não pode ter o mesmo hash que a mesma senha de outro usuário. Imagine uma função que criptografa uma mesma senha 1 milhão de vezes e gera 1 milhão de hashes diferentes, provavelmente essa é a solução para o problema de comparação de senhas. Mesmo conhecendo a função, o hacker teria que um trabalhão para comparar o dicionário dele com as possibilidades de criptografia, levaria muito, mas muito tempo mesmo para conseguir quebrar umas poucas senhas por comparação, pois ele não conseguiria mais saber quantas vezes a mesma senha se repete no BD, pois os hashes são diferentes. Logo, teria comparar cada senha do dicionario usando a função de comparação de senhas apresentada abaixo, o que levaria um tempo muito grande para conseguir umas poucas senhas. Possibilidade = Tamanho do dicionário * quantidade de registros que você possui na sua base de dados. Os dicionários mais pequenos possuem cerca de 10 milhões de senhas. Imagine seu DB com um mil a 20 mil registros... Iriam haver bilhões de possibilidades para serem testadas... E por mais que ele disponha de um cluster de altíssimo desempenho, levaria no mínimo alguns minutos até conseguir UM resultado. E isso é... se ele conseguir acesso a função. Muitas vezes ele só consegue acesso ao BD por vulnerabilidades de SQL ou no SGDB. Um pequeno teste, criptografando 10 vezes a senha 123456:

Gerando 10 Hashes da Senha enviada: 123456



396c90757445df5e8db8a93ae0201a07eb1f8155cd9a3d0d5dc509344acceb46

b79953b7f1b694218b867ac648d0e95819815abed6c2a1a983ebdcb0d256655b

5c42701ed452451311505a2a29fed81f3cd1cfd87eb76794158498ad651bb4d6

f7cb61852ca097865dc48209971b2b0b62a39d199554774b8a360273a8c36ec6

41f963b46bf68637e299785cac3d5febb2ae8c7afadbe89705beaefc66ccf428

ced67aad8ae007d2aeff6406d919b7e4428cb4a0a99865eeb6bb8294cb2ece1a

788361a5bf38937540a8e6503a2d1c17c39e72cbaaf65a13c1e0f6fa0f8650f7

b2dd8d6b50b5f93870ac19faa3da1429c2922f36dd5760aaf65a13c1e0f6fa0f

ef50ca95b7426344fabe0361358afefcf8cf83df985ccb7047be2e3e1e633706

b20134a4d97bef39dbc0f35ee84525e71088cd3af5293f6b7598747472e6781a

Conhecendo agora o santo da coisa: Função de Criptografia:

<?php

define('PASS_TOKEN1', 'k,QK<&qq:5c,n?uB!;Pc{aur{qL?m~x_');

define('PASS_TOKEN2', '8>t&&WAnkJk(!nPa,,pcmQ*-x!aG:~),');

/**

* Criptografar Senhas:

* Gera um hash de uma senha baseado em sha256 e também uma chave para

* verificação da senha. Não é possível descriptografar. A validação se dá pela

* comparação das strings geradas. A quantidade mínima de caracteres é 6.

* @param string $senha	 (*)Senha a ser criptografada

* @return string,bool	 Se a senha não for informada ou for menor que o informado, será retornodo um valor booleano false.

*/

function cSenha($senha){

if(!empty($senha) or strlen($senha) >= 6){

	 $lim1 = rand(0,25);

	 $hash = substr(hash('sha256',(crc32($senha.PASS_TOKEN2).md5($senha.PASS_TOKEN1).sha1(time() * cos(pi() / rand(1,90))))),$lim1,24);

	 $senha = sha1(md5($hash.$senha.PASS_TOKEN2).hash('sha256',$senha.$hash).sha1(PASS_TOKEN1.$hash)).$hash;

	 return $senha;

}else return false;


unset($senha, $lim1, $hash);

Basicamente funciona assim: Cria-se 2 tokens aletórios que você pode gerar aqui: http://www.freepasswordgenerator.com/ com a quantidade de caracteres que quiser. Uso 64 ou mais. No exemplo tem 32... A função cSenha recebe uma varíavel tipo string e verifica se ela tem 6 ou mais caracteres. Se não houver, ela retorna um booleano false, ou se estiver correto, procede o processamento. Na sequência é gerado um número aleatório qualquer que servirá para cortar um pedaço de um hashe gerado. Esse pedacinho do hashe ($hash) é o responsável pela validação da senha no futuro. A partir de uma sopa de hashes + combinação de tokes + senha, foi gerada a senha criptografado com o $hash como sufixo da senha. Pronto! Senha criptografada com sucesso. Função de validação: Obviamente se faz necessário validar regularmente a senha gerada, pois será utilizada em sistemas de autenticação ou algo parecido. A função para validação é:

/**

* Descriptografar Senhas:

* Verifica se a senha informada é igual ao hash previamente gerado pela funçao

* cSenha($senha). O resultado é um valor booleano true ou false.

* @param string $senha	 (*)Senha a ser verificada

* @param string $hash	 (*)Hash da senha armazenada

* @return bool			 true ou false

*/

function vSenha($senha, $hash){

if(!empty($senha) and !empty($hash) and strlen($hash) == 64 and strlen($senha) >= 6){

	 $hash1 = substr($hash, 40, 24);

	 $novaSenha = sha1(md5($hash1.$senha.PASS_TOKEN2).hash('sha512',$senha.$hash1).sha1(PASS_TOKEN1.$hash1)).$hash1;

	 return $hash == $novaSenha? true:false;

}else return false;


unset($senha, $hash, $hash1, $novaSenha);

}

Basicamente o que se faz é receber a senha informada pelo usuário e a senha criptografada armazenada no BD (aquela gerada na função cSenha). Se a senha informada estiver em branco, ter menos que 6 caracteres e a senha criptografada não ter 64 caracteres, um booleano false é retornado. Caso contrário, prossigamos.

No início recortamos a senha para pegar aquele pedacinho ($hash) da função anterior. Ela está localizada no final da senha. A partir daqui, pegamos a senha informada pelo usuário e criptografamos exatamente da mesma forma que a outra para verificar se a nova senha gerada é igual a informada.

Um hacker fará o mesmo procedimento acima, porém.. ele terá que repetir o procedimento quantas vezes se façam necessárias para o número de senhas que você ter armazenadas vezes o número de registros do dicionário dele.

É isso! Bom uso!

Quer isso tudo num pacotinho para brincar? Baixe aqui no anexo do post o exemplo em PHP: senhas.zip

Arquivos:

demo.php -> Criptografia da senha;

valida.php -> Valida uma senha e hash gerado no arquivo anterior;

funcao.php -> As funções que usamos aqui.

Link para o comentário
Compartilhar em outros sites

Acho que um simples:

$senha = "12345";

$token = "HikKegs$dh$RWr@fh!sJuY/EnM-AH=sL]gh*"

$senha = sha1($senha . $token);

Já seria suficiente, não?

http://blog.thiagobe...e-forma-segura/

Não! Se você criptografar 10x a senha 123456 aí vai ser a mesma coisa!!!!

Lendo o tutorial do gajo, o que fiz se assemelha ao "salt dinâmico" junto com o que ele chama de "salt fixo", ou seja, eu fiz uma mistura de ambos, porém o dele não explica como você vai validar, além da fragilidade de se conter apenas a contenação simples de $senha.$salt.

Ele tem um outro post que fala do bcript do Cake. O bcript é um script bem próximo desse que fiz, e inclusive algumas coisas são parecidas:

1. O bcript gera um hashe de 60 caracteres. Meu script gera com 64.

2. O salt do bycript tem 22 caracteres. Meu script gera um salt de 24 caracteres.

Li e reli a parte do atraso do tempo e não vi nada que pudesse melhorar a segurança. Na verdade só fico pensando no gasto de CPU que você vai ter para validar aquilo... dado que vai ter que executar o loop 1000x para validar também. Os ataques de Brute Force nada tem a ver com o tempo de resposta.... sinceramente acho que ele quis dizer algo.. mas não foi feliz na hora de escrever.

Link para o comentário
Compartilhar em outros sites

Opaaa... no código que tu colou no tópico, na função cSenha($senha), na lilha em que é atribuído um valor da váriavel $senha, você está chamando a função hash do php com o algoritmo sha256, mas no arquivo pra download está sha512...

Confesso que não consegui entender direito, parece que você vai tirando vários hashs, concatenando e tirando hash... e acaba utilizando o tempo atual pra gerar a senha, mas desconsidera ele na hora da validação?

Link para o comentário
Compartilhar em outros sites

Opaaa... no código que tu colou no tópico, na função cSenha($senha), na lilha em que é atribuído um valor da váriavel $senha, você está chamando a função hash do php com o algoritmo sha256, mas no arquivo pra download está sha512...

Confesso que não consegui entender direito, parece que você vai tirando vários hashs, concatenando e tirando hash... e acaba utilizando o tempo atual pra gerar a senha, mas desconsidera ele na hora da validação?

Quanto ao 512, vai funcionar normal. Realmente não reparei nisso.. mas funciona perfeitamente do jeito que está.

Note que a variável $hash que é o elemento chave da descriptografia.

Eu gerei ela a partir de uma sopa de funções de tudo quanto é coisa. E no final da senha, eu só criptografo mais uma sopa e adiciono ela na íntegra no final da senha criptografada:

$senha = sha1(SOPINHA) . $hash;

Entendeu agora?

Para descriptografar, basta usar a mesma SOPINHA. Porém, observe que o $hash (agora chamado de $hash1) também faz parte da SOPINHA.

Link para o comentário
Compartilhar em outros sites

Hmm... agora eu entendi, muito bom... nessa linha:

$hash = substr(hash('sha256',(crc32($senha.PASS_TOKEN2).md5($senha.PASS_TOKEN1).sha1(time() * cos(pi() / rand(1,90))))),$lim1,24);[/CODE]

Tas criando um salt dinâmico pra gerar a senha e ao invés de armazená-lo em um lugar separado, está concatenando na própria senha que vai ser salva no BD.

Então quando eu perguntei sobre a função time(), não fazia sentido, porque ela é usada pra calcular só o salt mesmo...

Agora eu também entendi porque não tava funcionando... eu tava usando para testar a senha "teste", eu conseguia gerar o hash dela por que no if do método cSenha, tava assim:

if(!empty($senha) or strlen($senha) >= 6)

Por causa do "or" eu conseguia gerar senha com 5 caracteres, mas na validação ele já retornava false porque lá ele ta verificando certinho o tamanho da senha...

Agora uma pergunta sobre o código... porque tas utilizando tantas funções de hash pra calcular o salt? Uma função simples gerando uma string randômica com tamanho 24 não seria o sufciente, garantindo a aleatoriedade, visto que esse salt não é usado pra fazer comparação? Tornaria o processo menos custoso também.

Link para o comentário
Compartilhar em outros sites

Por causa do "or" eu conseguia gerar senha com 5 caracteres, mas na validação ele já retornava false porque lá ele ta verificando certinho o tamanho da senha...

Agora uma pergunta sobre o código... porque tas utilizando tantas funções de hash pra calcular o salt? Uma função simples gerando uma string randômica com tamanho 24 não seria o suficiente garantindo a aleatoriedade, visto que esse salt não é usado pra fazer comparação? Tornaria o processo menos custoso também.

Vou corrigir o OR... Obrigado pelo aviso, de fato está errado.

O salt é usado sim na comparação... veja:


#Aqui eu pego o "salt", cortando o hash guardado no BD.

$hash1 = substr($hash, 40, 24);


#Agora, eu faço uma sopa usando o salt pelo meio de tudo

$novaSenha = sha1(md5($hash1.$senha.PASS_TOKEN2).hash('sha512',$senha.$hash1).sha1(PASS_TOKEN1.$hash1)).$hash1;

Melhorou?

Qualquer função que gere 24 caracteres aleatórios também não é tão trivial. No minimo você tem um vetor grande pacas + um for de 24 passos, ou então usar um hash do time, de um número aleatório, ou de uma concatenação. O problema é que nesse segundo formato, você pode acabar repetindo salt em senhas iguais.

Logo, como a possibilidade de uma duas strings terem o mesmo hash é minúscula, inveitei uma fórmula bem maluca para gerar strings gigantes totalmente diferentes...

Minha preocupação foi gerar 10 milhões de vezes a senha 123456 e não conseguir repetir o hash nenhuma vez. Fato alcançado 100% dos testes. E alias, fiz na época uns 15 testes de 10 milhões, juntei todos resultados num BD e pelo group by do mysql... nenhum repetido, ou seja, sucesso absoluto. Lembro que a máquina ficou 1 dia inteiro executando esse group by maluco :D

Link para o comentário
Compartilhar em outros sites

Na verdade eu me expressei mal... o que eu quis dizer é que não importa os valores que geraram o salt, porque tu não usa eles para fazer verificação...

Aí vai um pouco de cada um também, eu acredito que gerar uma string aleatória tenha menor complexidade do que calcular um hash, e no teu caso, tu garante a aleatoriedade devido ao uso do tempo atual... a verdade é que tem várias formas de implementar isso...

Nossa... tu gerou 150 milhões de senhas e todas elas deram resultados diferentes... se o teu objetivo era garantir a não repetição... acho que tu conseguiu sim.... hahah... aí não tem rainbow table que dê jeito....bom, pelo nível de segurança que tu buscou, acredito que o sistema também deve não estar permitindo enumeração de usuários e força bruta.

Link para o comentário
Compartilhar em outros sites

Visitante
Este tópico está impedido de receber novos posts.
  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...

Informação Importante

Concorda com os nossos termos?