Autenticação de forma segura com criptografia
Quando estamos desenvolvendo um sistema web, eventualmente nos deparamos com os dados de autenticação dos nossos usuários: normalmente login e senha. Vamos fazer um pequeno exercício mental para verificar o quão seguros esses dados estão.
Pense por um instante nas informações mais importantes que você tem em um serviço online. Talvez seja seu email, talvez uma conta na sua rede social preferida, talvez alguma aplicação bancária. Pense na senha que você utiliza para esse serviço e, em seguida, lembre todos os outros serviços nos quais você utiliza essa mesma senha. O que aconteceria hoje se as informações guardadas em qualquer um desses bancos de dados vazassem? Muito possivelmente seus dados de login e senha do serviço principal também estariam comprometidos.
Vemos sempre por aí notícias de alguns sites famosos que sofreram algum vazamento de informações, como Dropbox, Patreon, etc..
Inclusive, você já checou se você foi vítima de algum vazamento de informações? Dê uma olhada no site "Have I been pwned?" e descubra.
Para resolver esse problema, temos uma solução que funciona como no mundo real. Você utiliza chaves diferentes para abrir a sua casa, o seu carro, o seu armário, etc. No mundo virtual, temos a mesma solução disponível: um chaveiro eletrônico. Um software que gerencia senhas aleatórias e seguras e, mais importante, diferentes para cada um dos serviços que você queira utilizar. O meu chaveiro favorito é o KeePassX, mas também existem vários outros por aí.
Pronto. Problema resolvido. Nossas senhas já estão guardadas em um chaveiro. Então discussão encerrada, certo?
Hmmmm, vamos com calma... Apesar da solução do chaveiro ser definitivamente superior, sabemos que a esmagadora maioria dos usuários nunca nem ouviu falar dela. Alguns até ouviram falar mas não utilizam. Usuários esses que utilizarão nossos serviços com a mesma senha que utilizam nos serviços que consideram mais importantes. Quem sabe o serviço mais importante de um usuário não seja exatamente o seu!
Para garantir que o seu serviço tenha uma ótima qualidade e seja confiável, precisamos garantir que os dados dos usuários estejam muito bem protegidos. Além disso, se grandes empresas como Patreon e Dropbox tem seus dados roubados, temos motivos o suficiente para acreditar que nenhum sistema é impenetrável.
Então como podemos fazer para nos assegurar de que, mesmo que os dados de autenticação dos nossos usuários sejam acessados, não seja possível que algum atacante os utilize?
Imagine que os dados do seu usuário, ao invés de serem guardados assim:
Sejam guardados assim:
Um atacante que tivesse acesso, teria muito mais dificuldade para desvendar as informações!
É aí que podemos utilizar a criptografia para deixar os dados irreconhecíveis para pessoas não autorizadas.
A criptografia como ferramenta para acesso aos dados
A criptografia transforma o conteúdo dos dados de uma maneira que apenas as partes autorizadas (no caso, você) possa ler.
Vamos ver um exemplo de uma técnica bem simples, aplicada sobre a senha password
:
Você consegue identificar como foi feita a transformação? Nesse caso, simplesmente trocamos cada uma das letras pela letra seguinte. Então a letra ‘p’ vira ‘q’, a letra ‘a’ vira ‘b’ e assim vai até o final da palavra.
Esse método é conhecido como Cifra de César, e foi utilizada pelo imperador romano Júlio César para esconder informações sigilosas. Nele, apenas quem sabe qual foi a transformação utilizada consegue resgatar a informação imediatamente. Quem não souber vai ter que tentar várias técnicas diferentes até conseguir chegar no texto original. Quando uma pessoa já sabe ou descobre qual foi a transformação feita, então dizemos que essa pessoa tem a chave para acessar a informação.
É claro que, no nosso caso, vamos utilizar uma técnica muito mais avançada para criptografar nossa informação! A figura abaixo mostra um exemplo de como poderia ficar guardada a senha password
de maneira transformada.
Como utilizamos a criptografia para autenticação?
Qual é o processo que executamos quando vamos autenticar o nosso usuário, ou seja, nos certificar que ele é quem ele realmente diz ser?
Normalmente, temos vários pares de logins e senhas guardados. Quando o usuário diz que ele é o login joao
com a senha senhaseguradojoao
, nós buscamos no nosso banco de dados o par com login joao
e verificamos se a senha guardada bate com a senha fornecida pelo usuário.
Se elas forem iguais, ótimo, temos uma garantia de que aquele usuário realmente é o joao
pois apenas ele deveria saber sua senha. Se elas não foram iguais, negamos o acesso desse usuário à conta do joao
.
Mas, peraí, se precisamos checar se a senha que o usuário forneceu é a mesma que temos guardada, como vamos fazer se ela está guardada criptografada?
Precisaríamos primeiro descriptografá-la, e depois comparar as duas senhas em texto limpo.
Então teríamos o seguinte fluxo:
Receber o login e a senha do usuário
Buscar a senha criptografada do login fornecido
Descriptografar essa senha com uma chave e comparar com a senha fornecida
Mas onde guardaríamos essa chave? No nosso banco de dados? Vamos guardá-la em texto limpo? Ou criptografado? Mas criptografado com qual chave?
Já viu onde isso vai parar, né?
Podemos guardar essa chave no ambiente de produção, mas estaríamos abertos à mesma vulnerabilidade que tínhamos no começo. É claro que o atacante agora precisa descobrir qual é a chave e qual técnica de criptografia estamos usando, mas ele ainda tem todos os dados na sua mão para desvendar a senha de todos os usuários, caso tivesse acesso ao nosso banco de dados.
Por quê isso acontece? Por quê o atacante consegue se passar por um usuário se ele tiver acesso ao nosso banco?
Isso acontece pois estamos guardando a senha do nosso usuário. Mas e agora? Se não podemos guardar a senha do nosso usuário, como iremos checar se a sua senha está correta???
A ideia aqui é guardar alguma informação derivada da senha que não dê ao atacante o poder de fazer o caminho contrário e descobrir a senha que gerou essa informação.
A criptografia de mão única
Imagine o seguinte exemplo: você definiu que a senha para o seu sistema é apenas uma palavra. O usuário definiu que a senha dele é bote
. Podemos então utilizar uma informação derivada dessa palavra, como por exemplo o seu significado Canoa; pequeno barco.
Agora nosso atacante precisa ler essa informação e trabalhar nela para chegar à senha original. Se a informação fosse Ação de brincar, divertimento.
ficaria fácil para o atacante descobrir a senha original: brincadeira
. Ainda mais, se ele escolhesse uma palavra parecida, como por exemplo jogo
, é capaz que o significado fosse o mesmo e ele fosse autenticado pelo nosso sistema.
Isso acontece pois a regra que estamos seguindo é bem simples: olhamos no dicionário o significado da palavra. Pense no que aconteceria se alterássemos um pouco nossa regra para: a sequência de números que representa o tamanho das palavras do significado da senha. Ou seja, se a informação fosse brincadeira
, ao invés de guardarmos Ação de brincar, divertimento.
, guardaríamos 4 2 6 12
, pois Ação
tem 4 letras, de
tem 2 letras, brincar
tem 6 letras e divertimento
tem 12 letras.
Com essa nova regra, o atacante precisaria procurar em um dicionário alguma palavra que tivesse um significado cujas palavras tivessem exatamente esse tamanho: 4 2 6 12
. Note que ainda é possível encontrar uma palavra que entre nessa regra (como a palavra brincadeira
) mas ficou muito mais difícil. Ele vai precisar do mesmo dicionário que você está utilizando e daí sair procurando palavra por palavra até encontrar uma que gere esse "código".
Essa é a ideia que chamamos de função de espalhamento (também chamado de função de hash). É um procedimento que recebe uma entrada e gera uma saída com algumas propriedades específicas: cada entrada gera sempre a mesma saída e, dado uma saída, é muito, mas muito difícil (vamos considerar que é impossível) descobrir qual foi a entrada dada.
Nas funções de espalhamento que vamos utilizar, ainda temos outras propriedades que nos ajudam mais ainda, por exemplo, a propriedade do caos. Ou seja, uma pequena alteração na entrada gera uma alteração muito grande na saída, o que dificulta mais ainda o trabalho do atacante.
Um exemplo de função de espalhamento é o algoritmo MD5, que transforma o texto Ola, mundo.
nesse hash: fda090a568170fdc553dc4223ca9e367
e o texto Ola, Mundo.
nesse outro hash: e372160d7e067a6c4b917e459a70a2e9
. Como podemos ver, a diferença entre os dois hashes gerados é muito grande, apesar da pequena mudança na entrada de apenas 1 bit (Note que mudamos a letra m
para M
).
Adicionando um tempero ao Hash
Qual será que é a senha equivalente ao hash 6fd720fb42d209f576ca23d5e437a7bb
?
Se você tentar algumas das senhas mais usadas, vai eventualmente descobrir que utilizamos a string senha
e o MD5 para calcular esse hash. Ou seja, se um atacante tiver acesso ao seu banco de dados e descobrir que você está usando o MD5, logo vai fazer um catálogo das senhas mais famosas hasheadas em MD5 e fazer uma busca no seu banco de dados.
Assim, novamente, o atacante conseguiu identificar as senhas dos usuários. Mais do que isso, já existem grandes tabelas relacionando senhas e hashes pela internet. Elas se chamam Rainbow Tables. É fácil encontrar uma tabela de gigabytes de senhas e hashes que facilitam a busca de qualquer atacante com acesso ao seu banco de dados.
Poxa, mas agora complicou. Como fazemos para evitar esse tipo de tabelas?
A solução é adicionar um certo "tempero" à nossa string original, ou também "sal", como é conhecido. A ideia aqui é jogar uma pitada de aleatoriedade à senha do usuário, para que ela não apareça numa Rainbow Table. Por exemplo, se a senha original era senha
, nós sorteamos uma string JBjGSJKcVf
e concatenamos à string original, tendo JBjGSJKcVfsenha
como string final. Assim, a propriedade do caos da função de espalhamento garante que essa nova string terá um hash completamente diferente da string original.
No caso, utilizando o MD5 e a senha sem sal, temos:
senha
-> 6fd720fb42d209f576ca23d5e437a7bb
Já com o sal, temos:
JBjGSJKcVfsenha
-> af34077cca67787a67faf84d044285e7
Melhorando o tempero
Olhando a imagem do nosso banco em texto aberto, vemos que a Maria tem duas contas com a mesma senha: login: "maria"
senha: “senha”
e login: “outraContaDaMaria”
senha: “senha”
porém no banco criptografado, temos dois hashes diferentes:
login: "maria"
senha: “$2a$10$OTPxoxsAf18ZSQMD/EOwkeub7TKjNduBn5sY/tSd1SmkUhc54ap9.”
e
login: "outraContaDaMaria"
senha: “$2a$10$C3aGfh2ZRVf7d64jJgS36e90Gz.xxg/GkB8WwNFFSd0WeHhSX9Zb.”
Isso é uma outra técnica que podemos utilizar para atrapalhar mais ainda o atacante do nosso banco de dados. Agora, mesmo se ele conseguir decodificar uma senha comum, ele não terá acesso às outras contas que utilizam essa mesma senha. Como os usuários tem o péssimo hábito de usar senhas comuns e repetidas, isso é um grande avanço no nosso sistema.
Isso é feito utilizando um sal diferente para cada senha, o que acaba criando hashes completamente diferentes para duas senhas iguais temperadas com um sal diferente. No caso do nosso banco, esse sal é guardado junto com o hash.
Bom, ganhamos bastante segurança mas agora precisamos desenvolver uma função de espalhamento, guardar o sal de cada uma das senhas e escrever os métodos que fazem tanto o hash da senha quanto a autenticação de um usuário. Aí vem a pergunta...
Como usar isso no meu projeto?
Escrever o nosso módulo de segurança do zero, que inclui uma função de hash, um jeito de inserir um usuário novo e um jeito de autenticar um usuário existente é muito complicado e muito propenso a erros, então é recomendável utilizar uma biblioteca já existente para fazer esse trabalho.
O padrão BCrypt é um ótimo método e existem implementações dele nas mais variadas linguagens.
Em Java, por exemplo, você pode adicionar a biblioteca jBCrypt e utilizá-la com um código bem simples, que utiliza as funções BCrypt.gensalt
, BCrypt.hashpw
e BCrypt.checkpw
para gerar o sal, gerar o hash e checar uma senha candidata, respectivamente. A biblioteca é livre e pode ser baixada diretamente em seu código fonte no site oficial do projeto. Um exemplo de como ficaria o código é o seguinte:
public void adicionaCriptografado(Usuario usuario) {
// Gera um sal aleatório
String salGerado = BCrypt.gensalt();
System.out.println("O sal gerado foi $" + salGerado + "$");
// Gera a senha hasheada utilizando o sal gerado
String senhaHasheada = BCrypt.hashpw(usuario.getSenha(), salGerado);
//Atualiza a senha do usuário
usuario.setSenha(senhaHasheada);
//Salva no banco
adicionaNoBanco(usuario);
}
public boolean autentica(Usuario usuarioCandidato) {
String loginDoCandidato = usuarioCandidato.getLogin();
String senhaDoCandidato = usuarioCandidato.getSenha(); // Essa senha está em texto puro, sem hash.
Usuario usuarioComSenhaHasheada = this.pegaUsuarioDoBanco(loginDoCandidato);
String senhaDoBanco = usuarioComSenhaHasheada.getSenha(); // Essa senha está hasheada.
// Usa o BCrypt para verificar se a senha passada está correta.
// Isso envolve ler a senhaDoBanco, separar o que é sal e o que é hash
// usar o sal para criar um hash da senhaDoCandidato e, por fim
// verificar se os hashes gerados são iguais.
boolean autenticacaoBateu = BCrypt.checkpw(senhaDoCandidato, senhaDoBanco);
return autenticacaoBateu;
}
Agora é hora de deixar o seu projeto mais seguro!
Nesse post vimos alguns casos em que a segurança dos dados dos usuários, apesar de ter uma importância muito grande, foi deixada de lado causando alguns problemas para os próprios usuários e também para as empresas envolvidas.
Aprendemos como funciona a criptografia de mão-única, utilizando um sal aleatório e individual para cada senha e como ela protege a senha dos nossos usuários. Note que nem mesmo o desenvolvedor do sistema tem acesso à senha uma vez que ela está no banco!
Por fim, vimos o padrão BCrypt e a biblioteca jBCrypt que facilita o nosso trabalho e está disponível para várias linguagens de programação.
Para aprender mais sobre autenticação de usuários e segurança de informação no geral, a Alura tem vários cursos que vão desde a explicação de como ser seguro na web até a implementação na tecnologia específica, como o Java, o Spring Framework, o Play Framework e também utilizando o ASP.NET para você que utiliza o C#.