segunda-feira, 1 de outubro de 2012

Algoritmos de Hash Unidirecionais para Senhas: I - Introdução

Nesta série de artigos vou falar um pouco sobre criptografia, em particular no uso dela para proteger senhas e garantir a segurança de sistemas.
Isso tudo começou quando respondi uma pergunta em um fórum sobre os motivos de não se modificar a implementação de funções de hash em senhas de aplicativos. Como criptografia é um assunto complexo, uma coisa vai puxando outra e quando percebi já tinha estourado o limite de caracteres.
Portanto aqui vou fazer a coisa toda com calma. Começando do começo e, se tudo der certo, chegar no ponto pretendido.
Para começar vamos entender melhor quais são as práticas mais comuns na hora de proteger senhas de usuários em aplicações.
Se você já tem conhecimento sobre algoritmos MAC e suas variantes, talvez queira pular para a parte II desta série de artigos.

O Básico: por que criptografar?


Se você desenvolve um software multiusuário, precisa armazenar os dados de cada usuário, incluindo o login e a senha que são utilizados para a autenticação, evitando que um usuário tenha acesso a conteúdo de outro, ou que alguém não cadastrado tenha acesso a dados indevidos.
Para fazer isso funcionar, tudo que você precisa é de um formulário que colete esses dados do usuário no momento de seu cadastramento. Armazenando as informações em um banco de dados, o software pode acessá-las facilmente para verificar, no momento do login, se as informações inseridas (em especial a senha) conferem com o cadastro do usuário.
Isto funciona, e você pode construir um software que vai rodar a vida toda sem nenhum problema com essa implementação.
No entanto, se em algum momento uma pessoa mal-intencionada conseguir acesso ao seu banco de dados, você estará entregando de bandeja todas as senhas de todos os usuários.
Então, obviamente esta não é a forma certa de fazer.
Por isso, NUNCA, JAMAIS, DE FORMA ALGUMA devemos armazenar senhas em texto puro no banco de dados.

 Determinístico ou Não-Determinístico


É aí que entra a maravilhosa ciência da criptografia, que consiste simplesmente em transformar uma informação em um código que não revele a um observador nenhuma informação sobre a mensagem original e/ou não permita que esta mensagem seja violada. Em outras palavras, a criptografia serve para garantir o sigilo e/ou a integridade de informações.
Neste caso, os algoritmos criptográficos podem ser divididos em duas categorias: determinísticos e não-determinísticos. O primeiro é o tipo clássico e mais conhecido de algoritmo, onde a função que realiza a criptografia segue o modelo abaixo:
F(M, K) = C
Onde F é a função do algoritmo que recebe a mensagem M e uma chave K retornando o código C. Os algoritmos determinísticos são criados de tal forma a manterem a lógica inversa:
F(C, K) = M
Ou seja, fornecendo o código criptografado e a chave, o algoritmo é capaz de recuperar a mensagem original.
Existem vários algoritmos deterministícos considerados inquebráveis.
Observe que considera-se "inquebrável" um algoritmo que, dado um código gerado por ele, não é possível computar a mensagem original. Nada impede que você pegue um código desses, digite um chave aleatória e por coincidência, consiga retornar a mensagem original, mas obviamente a chance de isso ocorrer é a mais remota possível. Os algoritmos não são imunes às técnicas de força bruta mas são feitos de forma que os recursos computacionais necessários para se conseguir quebrar o código com tentativa e erro não valham a pena fazê-lo.
Algoritmos determinísticos são fundamentais para manter a segurança de dados que precisam ser enviados por meios não seguros e chegar ao seu destino para poderem ser recuperados de novo. Por exemplo, no envio de emails, mensagens instantâneas, VoIP, transações bancárias, etc.
No entanto, os algoritmos não-determinísticos possuem um grau maior de segurança. Isso porque estes algoritmos não podem ser recuperados por uma função inversa do algoritmo. Estes algoritmos utilizam funções não-lineares e não necessitam de uma chave definida. 
Ou seja, algoritmos não-determinísticos pegam a mensagem e transformam em um código único que não pode ser transformado de volta na mensagem original.
Mas como isso pode ser útil? Se eu não conseguir recuperar a mensagem criptografada como vou saber qual a mensagem?
Na realidade a origem desses algoritmos são os MAC's (Message Authentication Code) e depois os HMAC's (Hash-based Message Authentication Code) e seu principal objetivo é garantir a integridade das informações. Os códigos gerados são únicos para aquele conjunto de dados (seja um documento, uma imagem ou uma string). Portanto, se você tem um documento no computador você pode gerar um hash único dele, posteriormente se você modificar o arquivo e gerar um novo hash, este será diferente do primeiro. Este tipo de algoritmo é extensamente utilizado em conexões Peer-to-Peer e em repositórios de arquivos públicos para verificar se dados foram corrompidos ou modificados indevidamente.
Acontece que esse tipo de algoritmo é ideal para ser usado em senhas. Isso porque não precisamos mesmo saber qual é a senha. Quando o usuário digitar a sua senha, no momento do cadastro, geramos um código criptografado, chamado de hash, desta senha e é isso que armazenamos no banco de dados. Agora, toda vez que o usuário fizer login no sistema geramos um hash da senha que ele digitou e comparamos com o hash da senha que está armazenada no banco de dados para aquele usuário. Se os códigos forem iguais, significa que a senha digitada está correta. Desta forma conseguimos fazer a autenticação do usuário sem precisar saber nem armazenar a senha que o usuário escolheu.
Mas para isso funcionar, o hash gerado precisa ser único para cada senha. E é exatamente o que fazem os algoritmos de hash unidirecionais: recebem como entrada uma string, por exemplo, e retornam um hash único e uniforme (futuramente falaremos sobre colisões de hash).
Observe um esquema simplificado abaixo:
Neste exemplo, o usuário digita uma senha "agua" e, após passar pelo algoritmo (neste caso MD5), o código resultante aparece na direita, uma sequência alfanumérica de 32 caracteres. Esta informação agora pode ser guardada no banco de dados e para qualquer observador que tiver acesso a ele, mesmo visualizando os códigos, não é possível descobrir a senha original que o gerou.
Note, no mesmo exemplo, como uma modificação simples na senha (o acréscimo de um acento) muda completamente o hash resultante.
Funções de algoritmos de hash resultam em códigos indistinguíveis de aleatório (foi a melhor tradução que consegui para "indistinguishable from random"). Isto significa que se você pegar um código resultante de uma função de algoritmo de hash e pegar um código semelhante gerado por uma função que imprime valores aleatórios, um observador não deve saber distinguir as duas.
Este é um princípio muito importante da criptografia porque se baseia no preceito elementar de que um código seguro não deve revelar nenhuma informação sobre a mensagem original.

Algoritmos Comuns


No exemplo da imagem acima, eu utilizei o algoritmo MD5, um dos mais conhecidos algoritmos de hash unidirecionais. No entanto, o MD5 não é aconselhado para áreas críticas, como transações financeiras ou comunicações por ser um algoritmo relativamente antigo e com vulnerabilidades a ataques específicos. Sistemas simples que já implementam o MD5 não precisam trocar de algoritmo, uma vez que, quando bem implementado, ele fornece boas condições de segurança.
Outro algoritmo bastante popular é o SHA-1. Criado para ser o sucessor do MD5, o SHA-1 possui falhas conhecidas que permitem ataques de relativo sucesso em condições ideais.
É bom ressaltar que para hash de strings, como senhas, as vulnerabilidades do MD5 e do SHA-1 não são uma ameaça direta, uma vez que o espectro de combinações é mais restrito.
Para softwares novos, é aconselhado utilizar um algoritmo mais moderno como o SHA-256. Porém, qualquer um destes 3 algoritmos pode ser usado com segurança se for bem implementado.


Portanto, neste primeiro artigo da série vimos a importância de criptografar e que para senhas devemos utilizar algoritmos não-determinísticos, os algoritmos de hash unidirecionais, como MD5, SHA-1 ou SHA-256. No próximo artigo, vamos ver mais a fundo como funciona um algoritmo de hash por dentro.

Nenhum comentário:

Postar um comentário