Como sites com login não guardam sua senha (e por que não devem) - parte 2

Essa é a segunda parte do post sobre segurança no armazenamento de senhas - se você ainda não leu a primeira parte, leia-a aqui antes de continuar!

No último post entendemos como e por qual motivo as senhas não são salvas em sua versão original pra nenhum site que siga o mínimo das boas práticas. Mas só isso não é o suficiente pra 100% de segurança: existem alguns problemas que ainda têm que ser solucionados.

Então imaginemos que agora aconteceu o pior: nosso site foi hackeado e já baixaram o arquivo de banco de dados que contém toda a informação de login de nossos usuários. O que temos que fazer para garantir a segurança deles?

Algoritmos de hash

Como vimos antes, uma função hash nada mais é do que um algoritmo (uma sequência de passos) que transforma uma entrada qualquer em uma saída totalmente diferente de forma imprevisível. Talvez não seja óbvio, mas funções hash têm muitas possíveis utilidades:

  • Verificação de arquivos: pra saber se algum arquivo baixado da internet ou transferido sofreu alguma modificação (intencional ou não) no caminho, basta verificar se o hash dele e o anunciado no site ou calculado antes da transferência são iguais; se um bit que seja for diferente, o hash vai ser totalmente diferente
  • Tabelas de dispersão (hash tables) são uma estrutura de dados fundamental e dependem de funções hash para indexação de chaves
  • Estruturas probabilísticas aproveitam bastante o conceito e usam hashes pesadamente em suas implementações
  • A mineração de criptomoedas dependentes de proof of work (como Bitcoin) nada mais é do que o cálculo de hashes

Com uma gama tão ampla de aplicações, então, é natural que haja vários tipos de funções, com características diferentes que se adequem ao tipo de problema que deve ser solucionado.

Por exemplo, pra uma tabela de disperção, é importante que haja uma distribuição uniforme de hashes no espaço de output pra evitar colisões, que são detrimentais à performance. Pra verificação de arquivos, o ideal é que o cálculo da função seja o mais rápido possível, porque não queremos que o usuário fique esperando.

Pro nosso caso de uso em senhas, porém, o negócio é diferente: queremos uma função que não possa ser calculada tão rápida, pra evitar ataques de força bruta, mas também não tão lenta; o usuário tem que ter uma experiência fluida.

Ataques de força bruta são aqueles em que um atacante tenta várias combinações de palavras, números e senhas já vazadas pra tentar encontrar uma senha que funcione. Eles não são muito fáceis de fazer direto no site, porque geralmente são necessárias milhões de tentativas e um site pode ter o recurso de bloquear o acesso depois de algumas tentativas sem sucesso, mas se tornam uma ameaça real quando o adversário tem um arquivo de banco de dados e pode fazer tudo localmente na própria máquina.

Aqui, a diferença entre um hash que leva milissegundos pra ser calculado e um que leva nanossegundos é bastante significante, e pode ser a diferença entre um banco de dados em que quebrar uma senha leva algumas horas pra um banco de dados em que quebrar a mesma senha leva centenas de anos e é inviável.

Assim, você deve usar um algoritmo que ofereça resistência a força bruta pra proteger seus usuários. Alguns dos algoritmos de hash feitos especificamente pra senhas mais populares hoje são o bcrypt e Argon2. Esses algoritmos também são projetados pra que sejam difíceis de paralelizar ou serem calculados massivamente em placas de vídeo, ou que usem quantidade razoável de memória pra inviabilizar o desenvolvimento de ASICs.

Senhas duplicadas e rainbow tables

Um algoritmo de hash genérico sempre gera a mesma saída pra uma dada entrada. Isso tem duas consequências principais pro nosso problema:

  1. Usuários com as mesmas senhas vão ter os mesmos hashes no banco de dados
  2. Senhas comuns já podem ter sido computadas e estarem disponíveis em uma rainbow table

Vamos olhar mais de perto pro problema 1. Nosso banco de dados poderia estar mais ou menos assim:

Usuário Hash
[email protected] 7c67e713a4b4139702de1a4fac672344
[email protected] 391af4313d8c3d8093cb3fb0358f4fa6
... ...
[email protected] 7c67e713a4b4139702de1a4fac672344

Reparou que o hash do João e do Marcos são iguais? Pois é. Agora o atacante só tem que quebrar uma das senhas, e vai ter acesso à conta dos dois.

O segundo problema, você descobre se pegar esse hash duplicado 7c67e713a4b4139702de1a4fac672344 e pesquisar no Google. É o hash de uma senha comum ("minhasenha")!

Se o seu usário tiver uma senha fraca, então provavelmente alguém já a calculou e o resultado está acessível de modo mais fácil do que calcular do zero. As chamadas rainbow tables são justamente isso: pra um dado algoritmo, elas contém uma lista de senhas comuns, e o hash delas. Basta procurar o hash, e se for uma senha já vazada, ou que seja facilmente adivinhável (uma palavra qualquer e um número, por exemplo), provavelmente está contida na tabela de hashes já computados.

Solução: salting

Felizmente, a solução é simples!

Incluimos, antes de fazer o hash de senha, um salt, que nada mais é do que um outro pedaço de string que em combinação com a senha, faz uma string única. Por exemplo: Ao invés de calcular o hash de "minhasenha" pro banco de dados, calcula-se o hash de "[email protected]". Agora, as senhas serão diferentes

  • Entre usuários com a senha igual
  • Entre usuários em serviços diferentes (o nome do seu site é parte do hash).

Mais fácil, não?

Alguns algoritmos específicos pra senha, como o Argon2, fazem ainda um pouco diferente: usam um número aleatório pra fazer o salting (complementar a senha) e armazenam esse número junto como parte do hash em si. Assim, você não tem nem que se preocupar com juntar strings, o algoritmo cuida de tudo pra você.

Considerações adicionais

O Twitter recentemente mandou um e-mail pra uma parte dos usuários, recomendando a troca de senhas porque foi descoberto que as senhas foram armazenadas em texto simples. Como um gigante poderia fazer um erro desses?

Não é só no banco de dados que devemos tomar cuidado. O que se especula que aconteceu nesse caso é que algum sistema estava logando requisições inteiras, inclusive as de login, que obrigatoriamente contém, antes de ser validadas, as senhas em texto simples. É preciso ter isso em mente quando se projeta sistemas.

Uma ferramenta interessante que ajuda nesse ponto e pode dar ideias das boas práticas é o Pydantic: ele tem uma classe especial chamada "SecretStr" que guarda uma string que quando printada ou logada, somente mostra os caracteres "••••••", e pra realmente obter o valor secreto (como para passá-lo para o algoritmo de hash) você tem que explicitamente chamar a função get_secret_value do objeto. Isso evita que você acidentalmente dê um print no objeto e revele a senha de um usuário na tela ou em algum arquivo de saída.

Conclusão

  • Não guarde as senhas dos seus usuários em texto simples
  • Use um algoritmo de hash específico pra senhas (recomendo Argon2)
  • Tome cuidado com prints e logs

Por enquanto é isso. Espero que tenha sido útil!

Show Comments

Get the latest posts delivered right to your inbox.