Executando verificação de segurança...
54

Armazenamento seguro de senhas: Um guia prático (texto puro, hash, salt e mais)

Como funciona o login nos sistemas que você já desenvolveu? É com um usuário e senha? Como esses dados são salvos?

Nas primeiras vezes que precisei trabalhar com isso, pesquisei para entender quais eram as melhores formas. Depois disso, se tornou algo quase automático, sem muita reflexão sobre o assunto e a importância dele. "Já sei como faz isso."

Mas, as coisas mudam. Com o tempo, são desenvolvidas novas técnicas ou algoritmos melhores e mais seguros, ou descobrem brechas sobre o que é amplamente utilizado.

Assistindo algumas aulas sobre o assunto no curso.dev, eu aprendi algumas coisas novas, e o tema foi tão bem estruturado que resolvi compartilhar parte dele aqui, com algumas pesquisas complementares.

O jeito mais simples: sem tratamento

A primeira forma que aprendemos a salvar dados é salvando eles de forma pura, sem nenhum tratamento. E é bem provável que existam sistemas hoje que salvem senhas assim.

usuáriosenha
rafael12345678
anonimosenhasecreta

Toda forma de armazenamento tem seus lados positivos e negativos. Então, o que parece positivo aqui? Salvar assim é simples e rápido, não existe nenhum trabalho extra no desenvolvimento, qualquer sistema pode usar as credenciais salvas nesse banco de dados sem dificuldade, e o usuário pode recuperar a senha por e-mail caso esqueça. Parece bom, certo?

Só que salvar assim é o jeito mais fácil de ter a senha vazada. Qualquer pessoa com acesso ao banco de dados pode ver a senha de todos os usuários, e nem precisa ser um "hacker" para ver o banco: pode ser um desenvolvedor do sistema. E, mesmo que seja um sistema super simples, sem nenhum problema caso a senha seja descoberta, lembre-se: muita gente usa as mesmas senhas em sistemas diferentes.

Uma história curta de como já tive um problema com um sistema assim: mais de uma década atrás, quando estudava na ETEC, eles resetavam a senha no sistema a cada semestre. Então, enviavam a senha para o aluno, e o aluno podia alterá-la. Uma vez eu precisei acessar o sistema para algo, mas ainda não tinha a senha, então resetei e fiz meu acesso. Na semana seguinte, na sala de aula, passaram uma lista com a senha de todos os alunos.

A lista continha as senhas no padrão do sistema (ex: ABC123), mas a minha senha alterada também estava ali (ex: s3nh4D1f1c1l). Resultado: Quem gerou a lista sabia a senha de todo mundo, quem imprimiu também, além de todos os alunos saberem a senha de todos. E pior: todo mundo conseguiu ver a minha senha "pessoal", porque eu já tinha alterado ela.

⚠️ Então, na verdade, conseguir recuperar a senha é um ponto negativo! O ideal é que o usuário consiga acessar o sistema mesmo sem o sistema saber a senha original do usuário.

Pelo bem do seu usuário, mesmo sendo um sistema simples, não salve senhas sem tratamento. Dá para fazer algo melhor com pouco esforço adicional.

Hora de criptografar?

Melhor do que salvar a senha de forma pura, é salvar a senha de forma "ilegível", certo?

O exemplo abaixo usa a Cifra de César, com rotação à direita de três posições, apenas para ilustrar o mecanismo básico de criptografia. Na prática, ela é trivial de quebrar.

usuáriosenha
rafael89012345
anonimopbkexpbzobqx

Salvar uma senha assim não é tão simples quanto salvar o texto puro, porque precisamos de uma função para criptografar, mas quem ver a senha criptografada não vai conseguir entrar no sistema. Então, acabamos de aumentar a segurança do sistema! Quer dizer, apenas se a pessoa com acesso às senhas criptografadas não tiver a chave/método de descriptografia.

Isso porque a criptografia é um processo reversível, e esse é o principal problema dela, quando falamos sobre salvar senhas. A criptografia não evitaria o problema da história que citei no tópico anterior. Na verdade, nem tem como saber se, naquele caso, a senha era salva de forma pura ou criptografada.

Se o invasor descobrir a criptografia usada, seja porque teve acesso ao código fonte ou porque conseguiu quebrar a criptografia, ele consegue descobrir as senhas de todos os usuários.

Para resolver esse problema, precisamos de um jeito que não seja possível obter a senha original.

Uma solução não reversível: Hash!

Um hash é uma função matemática: você dá a ele um valor de entrada e a função emite um valor de saída; e a mesma entrada sempre produz a mesma saída. A função hash é projetada de modo que a saída seja imprevisível, isto é, que não seja possível "quebrar" o hash.

Como o hash é uma função unidirecional ("one-way function"), não existe um "deshash". Logo, não é possível obter a senha original a partir do hash. Veja como ficaria nossa tabela usando o SHA-1. Esse algoritmo não é recomendado para armazenar senhas, mas estou usando aqui porque vou falar sobre ele depois.

usuáriosenha
rafael7c222fb2927d828af22f592134e8932480637c0d
anonimo23edf28ca696fa24cb13a400a868c99b0cb37f63

Mas, se não conseguimos saber a senha do usuário, como vamos saber se o que foi informado está certo? É simples: fazemos uma comparação de dois hashes! Por exemplo: "23edf28ca696fa24cb13a400a868c99b0cb37f63" == sha1("senhasecreta").

Outra característica importante do hash para senhas é ser altamente resistente a colisões. Uma colisão ocorre quando duas entradas diferentes geram a mesma saída. Embora colisões sejam teoricamente possíveis em qualquer hash com saída finita, em um hash seguro, deve ser extremamente difícil (computacionalmente inviável) encontrar duas entradas que colidam. Se fosse fácil encontrar colisões, seria fácil acessar uma conta usando uma senha errada.

Para garantir que vamos usar uma boa função de hash, precisamos contar com especialistas nisso ao invés de criar a nossa própria função, porque assim aumentamos as chances em usar uma função mais segura.

Inicialmente, o MD5 era um hash muito utilizado para armazenar senhas, e provavelmente ainda existe código não-mantido hoje em dia que utiliza o MD5. Ele é muito rápido de ser calculado, praticamente instantâneo, e isso não é bom para senhas. Imagine, mais uma vez, que o atacante tem acesso ao banco de dados. Se você utilizou MD5, ele pode tentar adivinhar as senhas "por força bruta" e cada tentativa de acertar a senha não demora nada!

Mas não foi só por isso que o MD5 passou a não ser recomendado. Ele foi lançado em 1991, e em 2004, com uma hora de computação em cluster, conseguiram criar uma colisão. Em 2006, já era possível gerar colisões em menos de um minuto usando um notebook comum. Em 2008, passou a ser considerado "criptograficamente quebrado e inadequado para uso". Você pode ver os detalhes da história no Wikipedia.

O SHA-1, projetado pela Agência de Segurança Nacional dos Estados Unidos em 1995, era recomendado como substituto do MD5 já em 1996. Mesmo assim, ele foi considerado obsoleto em 2011 para fins de segurança, e em 2017 foi demonstrado um ataque de colisão, bem mais demorado do que o MD5, demandando um poder de processamento equivalente a 6.500 anos de cálculos de uma CPU e 110 anos de cálculos de uma GPU. Mais detalhes sobre o SHA-1 no Wikipedia.

Mas, se é tão demorado quebrar o SHA-1, por que não usar ele? Ótima pergunta. O ponto é que é possível gerar o SHA-1 para várias senhas comumente utilizadas e salvar isso numa grande tabela, chamada de rainbow table. Então, se colarmos os SHA-1 que utilizei de exemplo na tabela acima em um site que encontrei no Google, olha o resultado:

Os dois hashes foram encontrados na tabela

Eu informei apenas os hashes, e o site encontrou as duas senhas na rainbow table! Ou seja, apesar do SHA-1 não ser reversível, se a senha for comum ou fraca e o atacante possuir uma rainbow table, ele ainda consegue descobrir a senha em questão.

Fora esse problema do hash, se o atacante criar um cadastro no seu sistema e só depois ver a lista de senhas no banco de dados, ele conseguirá identificar todos os outros usuários que têm a mesma senha que a dele.

Então, utilizar apenas um hash forte ainda não é uma boa solução para salvar senhas.

Adicionando um tempero: Hash + Salt

Para melhorar a segurança, como podemos fazer a mesma senha gerar hashes diferentes, e ainda assim, conseguir comparar uma senha pura com um hash salvo?

Se, para gerar hashes diferentes, precisamos de valores diferentes, então essencialmente temos que fazer senhas iguais, na verdade, serem diferentes. Pode parecer estranho, mas é bem simples: se concatenarmos um texto com a senha do usuário, por exemplo TabNews + 12345678, significa que mudamos a senha do usuário. Mas, se o texto sempre for igual, então a mesma senha sempre irá gerar o mesmo hash. Portanto, precisamos de um valor único por usuário para concatenar com a senha.

E esse é o conceito de salt (em português, "sal"). Como o salt irá mudar o texto que faremos hash, precisamos salvar o salt no banco de dados também para realizar a mesma verificação no login. Para cada senha, o salt deve ser único e aleatório.

Apesar do SHA-1 não ser recomendado, vou continuar o exemplo com ele apenas para mostrar que os hashes são diferentes, e também criei o usuário rafael2 que tem a mesma senha que o rafael (12345678):

usuáriosenhasalt
rafael26b4aab6e57475e89e5040ba5271b349d25ce630kifa12p3
rafael224da5626e6b7f70768130af884a3581ec8fe6bed$spo012P
anonimo3ea9b5badc8dc046f029ea16fee344ef61d535c3Os_3l1W!

E a comparação que fazemos para verificar a senha precisa da concatenação do salt: "26b4aab6e57475e89e5040ba5271b349d25ce630" == sha1("kifa12p312345678").

Se procurarmos os hashes acima na mesma rainbow table que procurei no tópico sobre hash, olha o resultado:

Nenhuma senha encontrada

Então, com o salt nós conseguimos resolver os dois problemas que encontramos antes: usuários diferentes podem usar a mesma senha e elas serão salvas como valores diferentes no banco de dados, e as senhas não serão encontradas na rainbow table!

Novamente, vamos supor que um atacante tenha acesso ao banco de dados. Ele ainda pode realizar um ataque offline direcionado a um usuário específico. Usando o salt da vítima (que está armazenado no banco), ele pode testar milhões ou bilhões de senhas comuns (ataque de dicionário) ou combinações (força bruta) contra aquele hash específico, basta sempre fazer hash(salt_do_usuario + senha_tentada).

Tem como melhorar o armazenamento de senhas para deixar o sistema ainda mais seguro?

Dá para melhorar colocando mais tempero? Hash + Salt + Pepper

Escolhendo um bom hash, a solução anterior é ótima, mas considerando o vazamento do banco de dados, tem alguma forma de defender o usuário caso o atacante tente um ataque offline depois de ter o acesso ao banco de dados?

Quando falei do salt, ele precisava ser diferente para a mesma senha gerar hashes diferentes, e por ser único para cada usuário, precisávamos ter acesso à ele de alguma forma para conseguir reconstruir o hash na autenticação.

Agora, se nós concatenarmos a senha do usuário com um valor secreto, que só a aplicação sabe, não precisamos armazenar esse valor no banco de dados. Na verdade, para ser ainda mais seguro e não vazar com o código fonte, podemos armazenar esse segredo nas variáveis de ambiente. Isso é chamado de pepper (em português, "pimenta").

Se usarmos Hash + Pepper, perdemos o benefício de hashes diferentes. Se usarmos Hash + Salt, perdemos o benefício de ter algo secreto que dificulta que o atacante realize ataques offline de forma eficiente. Então, podemos juntar as duas técnicas e ter Hash + Salt + Pepper.

O exemplo abaixo, ainda com SHA-1 para manter a consistência, faz uso de um pepper simples, TabNews, mas na prática é recomendado algo tão seguro quanto uma senha, com alta entropia.

usuáriosenhasalt
rafaele0c3a3babfb4fa7c4c8aa975722fc2ce78a5b77dkifa12p3
rafael25104649eba79b92a776e97616b841831cbdd4506$spo012P
anonimob6671d15c53f9f85989e8160913c2c63d7dff4d8Os_3l1W!

Nesse exemplo, a comparação que fazemos para verificar a senha precisa da
concatenação do salt no início e do pepper no fim: "e0c3a3babfb4fa7c4c8aa975722fc2ce78a5b77d" == sha1("kifa12p312345678TabNews").

Se o atacante conseguir acesso ao pepper, além do banco de dados, voltamos ao mesmo problema do tópico anterior, visto que não temos nenhum segredo.

Mesmo assim, parece que chegamos num nível muito bom de proteção da senha. Nos defendemos de vários ataques possíveis, e existem três informações necessárias para gerar o hash que estão em locais diferentes: a senha com o usuário, o salt com o banco de dados, e o pepper nas variáveis de ambiente.

E o que é um hash forte?

Eu disse que o SHA-1 não é mais recomendado para armazenar senhas, e que um hash forte é importante, mas o que é um hash forte nesse contexto?

Ao contrário dos hashes usados para verificar a integridade de arquivos ou dados (como o próprio SHA-1 ou SHA-256), que são projetados para serem rápidos, um hash para senhas é projetado propositalmente para ser lento e computacionalmente caro de calcular.

Essas características são importantes porque o principal ataque a senhas vazadas (mesmo com salt) é tentar adivinhar a senha original por força bruta ou dicionário. Se cada tentativa de adivinhar a senha leva muito tempo e consome muitos recursos do atacante, o ataque se torna inviável em larga escala.

A lentidão controlada é obtida através de "fatores de custo" ou "fatores de trabalho" configuráveis no algoritmo. Isso geralmente envolve realizar o processo de hashing milhares ou milhões de vezes (iterações), usar uma quantidade significativa de memória RAM (tornando ataques com hardware especializado como GPUs mais difíceis) ou aproveitar múltiplos núcleos de CPU.

Alguns dos algoritmos mais recomendados atualmente são:

  • bcrypt: Criado em 1999, é um dos primeiros algoritmos populares a incorporar um fator de trabalho ajustável. Usamos ele no TabNews com a biblioteca bcryptjs.
  • scrypt: Criado em 2009 com o objetivo de exigir bastante memória RAM, além de CPU, é mais resistente a ataques com hardware dedicado.
  • Argon2: Criado em 2015 e vencedor da Password Hashing Competition, é considerado o estado da arte. Possui modos que permitem balancear o uso de CPU, memória e paralelismo, sendo o Argon2id o mais recomendado.

Você pode ler algumas discussões sobre os algoritmos acima em:

Não falei sobre tudo

Esta publicação, apesar de longa, é um resumo sobre o assunto, contando um pouco da história sobre por quê salvamos as senhas como salvamos. No curso.dev você vai encontrar mais informações, porque foi de lá que eu extraí a maior parte do conteúdo que escrevi acima.

Se você quer se aprofundar ainda mais no assunto, precisa pesquisar bastante, ler sobre o que mudou ao longo do tempo e testar na prática para conseguir entender bem como armazenar senhas de forma segura e como esse processo tem evoluído. Pode também tentar entender as diferenças entre os algoritmos de hash e qualquer outra coisa que não citei aqui.

Dá para deixar as senhas mais seguras?

Isso é assunto para outro tópico, porque você precisa entender o que faz uma senha ser considerada forte. Mas, uma boa dica que pode ajudar a se defender de ataques de dicionário é, justamente, não deixar o usuário utilizar senhas amplamente conhecidas (Wikipedia:10,000 most common passwords).

Outra opção parecida é não deixar o usuário utilizar senhas com referência a algo pessoal, como data de nascimento ou número de telefone, caso você tenha esses dados.

O problema de restringir muito a senha que o usuário pode escolher é que isso pode causar o efeito contrário do desejado, diminuindo a segurança. Por exemplo, pode ser que ele escolha uma senha difícil e deixe anotada em um lugar acessível, como um arquivo não protegido no computador ou uma nota autoadesiva no monitor.

Alguns navegadores, como o Chrome, exibem alertas no momento da criação ou alteração da senha, se ela consta em grandes listas de senhas vazadas. Existem serviços como a API Have I Been Pwned que podem ajudar nisso.

Precisamos de senhas?

Existem outras formas de segurança que não envolvem senha, ou envolvem senha e algo mais.

Você pode utilizar senhas e uma segurança complementar, como uma autenticação de dois fatores, que irá demandar a senha e um código que o usuário recebeu no e-mail, ou um código que é gerado num aplicativo, por exemplo.

Você também pode usar senhas, mas não armazenar senha nenhuma. Basta usar um "login social", onde o usuário precisa ter uma conta em outro lugar, como o Google, e usará essa conta para acessar o seu site. Já vi o Auth0 ser bem recomendado para isso, mas nunca usei, e também ouvi que é bem caro.

Uma alternativa sem senha é o uso de "link mágico", onde o usuário apenas insere o login dele, e precisará acessar um link por outro lugar para finalizar o login, como o e-mail. O Medium utiliza esse tipo de autenticação.

Também existe o "passkey", que é mais novo e eu não conheço praticamente nada sobre isso. Você pode se aprofundar lendo sobre WebAuthn no Wikipedia, ou passkeys no portal de desenvolvedores do Google.

Faltou algo?

Segurança é um assunto delicado, e sempre que alguém que não é especialista no assunto fala sobre isso, corre o risco de cometer um deslize. Então, se você acha que faltou alguma informação, algo ficou confuso no texto, ou eu disse algo errado, por favor, me complemente e corrija nos comentários.

Carregando publicação patrocinada...
4
2
1

Finalmente encontrei um conteúdo que aborda um problemão que enfrento há anos! Preciso admitir, contudo, que ao iniciar minha participação nesta comunidade, sinto-me um pouco deslocada. Não sou programadora, então, pessoal, perdoem-me caso eu diga algo que não faça muito sentido!

O texto aborda segurança de senhas, mas não chega a sugerir locais específicos para armazená-las. Ele menciona, no entanto, que não se deve salvar senhas em texto puro e apresenta alguns métodos de armazenamento seguro, como o uso de hashing. Mas, sinceramente, onde eu, uma mera mortal, devo guardar essas informações? Nós, leigos, muitas vezes recorremos às sugestões dos aplicativos que já vêm nos smartphones, como navegadores (Edge Wallet Password), os famosos Microsoft Authenticator, Google Authenticator (no PC e no celular) e, claro, o “rei dos reis”: salvar tudo diretamente na conta do Google, que é praticamente indispensável para qualquer coisa hoje em dia.

Minha realidade é um caos: senhas espalhadas por todos os lados! E o pior: elas frequentemente não aparecem para preenchimento automático ou, ainda pior, são preenchidas incorretamente! Alguém aí tem a solução definitiva para essa bagunça?

2

Acredito que o problema que está enfrentando seja relacionado ao gerenciamento de senhas pessoais, este texto no entanto se trata do armazenamento de senhas dos usuarios de algum sistema seu, como um site ou aplicação, esse texto não vai te ajudar muito no gerenciamento de senhas pessoais.

Eu não tenho muito conhecimento sobre o assunto então só posso falar sobre o básico.

O que não deve ser feito


  1. Não faça uso da sua conta Google para logar em sites, se sua conta Google for invadida sua vida toda estará arruinada, é até mais inseguro que usar a mesma senha para todos os sites, o que também não deve ser feito.
  2. Não use o gerenciador de senhas do navegador, a criptografia deles geralmente são faceis de quebrar, e são uma opção pior no geral do que um gerenciador de senhas dedicado.
  3. Não use autenticação por SMS, autenticação por SMS é vulneravel a SIM Swapping (roubo/troca de chip)
  4. Não salve senhas em locais inseguros, como por exemplo um arquivo de texto no seu computador ou em uma mensagem em um aplicativo de mensagem.
  5. Não crie senhas pequenas ou com palavras comuns do dicionario, a maioria dos ataques de hoje em dia conseguem acertar senhas pequenas ou com palavras comuns em minutos, o recomendado é 10 caracteres ou mais para uma senha.

Onde armazenar senhas


Um gerenciador de senhas é um local seguro e criptografado onde você pode armazenar senhas e outras informações pessoais como identidade e cartão de credito, os navegadores normalmente possuem um já integrado, mas como eu disse antes, não é recomendado usar eles.

Os gerenciadores de senhas exigem uma senha principal que deve ser apenas memorizada, todas as outras senhas devem ser criadas pelo gerenciador de senhas, que gerará uma senha segura. Eles irão preencher automaticamente as senhas para você quando for logar em um site. Existem pagos e gratuitos, minhas recomendações são: Proton Pass (Gratuito e Pago), Bitwarden (Gratuito e Pago) e 1Password (Pago)

Você pode usar um gerenciador de senhas como uma extensão no navegador ou como um aplicativo, usar como um aplicativo é a opção mais segura, entretanto você vai perder o preenchimento automatico por conta disso e a extensão no navegador já é suficiente.

Como criar boas senhas


Considerando que você vá usar um gerenciador de senhas, agora você precisa criar uma boa senha principal. Existem muitos criterios para se ter uma boa senha e eu também não conheço muito desse assunto, o que posso te recomendar é que crie uma senha memorizavel que possua 10 ou mais caracteres e que não possua informações pessoais identificaveis (como data de nascimento, nome de um familiar) frases comuns ou qualquer uma das senhas mais usadas.

A senha pode não conter simbolos caso ela seja longa o suficiente, o que pode ajudar na memorização, Para memorizar você também pode anotar temporariamente a senha em um papel onde você poderá olhar até memorizar a senha, precisa ser em algo fisico, não crie um arquivo de texto para armazenar a senha.

Isso é apenas o basico do basico, mas deve ser o suficiente pra ter senhas organizadas e seguras, se tiver alguma duvida pode perguntar, espero que tenha ajudado.

1

Na minha aplicação que estou desenvolvendo, optei pela opção de não ter senha.

O usuário terá que validar um código pelo e-mail ou celular. Terá a opção de 'browser confiável' para manter-se conectado. Só não defini de qto em qto tempo vou pedir para se conectar novamente.

E no futuro, vou implementar a opção de usar Google Authenticator e afins.