Hash seguro de senhas em Go
5 de dezembro de 2023
0 minutos de leituraCredenciais de usuário são as informações necessárias para autenticar a identidade de uma pessoa e conceder acesso a um sistema ou aplicativo. Normalmente, incluem um nome de usuário ou endereço de e-mail e uma senha. Embora um nome de usuário possa ser armazenado como texto simples em um banco de dados, informações confidenciais, como endereços de e-mail e senhas, não devem ser.
Se uma pessoa mal-intencionada conseguir acessar o banco de dados onde você armazena essas informações, você não vai querer que ela obtenha esses dados com facilidade. Em vez disso, é importante garantir que ela não consiga acessá-los, mesmo que faça um grande esforço e use todos os recursos disponíveis. É aí que o hashing se torna importante.
Dando continuidade ao nosso post anterior sobre hashing de senhas em Java, neste artigo vamos abordar o hashing de senhas em Go. Para começar, vamos relembrar alguns conceitos e práticas recomendadas de hashing de senhas.
Use hashing para senhas
Um hash é uma função matemática que recebe uma entrada — neste caso, uma senha — e gera uma sequência de caracteres de tamanho fixo, geralmente composta por letras e números. O resultado, conhecido como valor de hash ou código hash, é exclusivo para aquela entrada. As funções de hash são projetadas para serem unidirecionais, ou seja, é computacionalmente inviável recuperar a entrada original a partir do valor de hash. Para proteger senhas, elas são convertidas em hashes e armazenadas como valores de hash, em vez de texto simples.
A diferença entre hashing e criptografia é que, enquanto o hashing é unidirecional, a criptografia é bidirecional e permite recuperar o valor original a partir do valor criptografado. A criptografia é comumente usada para dados confidenciais, como e-mails e outras informações de identificação pessoal (PII). Para senhas, porém, o recomendado é usar hashing.
Mas, se o hashing é unidirecional, como verificamos as credenciais de usuário? O processo é simples: criamos um hash da senha em texto simples fornecida pela pessoa e o comparamos com o hash armazenado no banco de dados. Se os hashes forem iguais, a senha está correta. Caso contrário, as credenciais são inválidas. A vantagem do hashing é que nunca precisamos saber a senha real de uma pessoa!
Métodos de ataque
Como mencionamos, é importante garantir que ninguém consiga acessar essas informações, mesmo que faça um grande esforço e use todos os recursos disponíveis. Uma pessoa mal-intencionada pode usar alguns métodos para obter a senha a partir do hash.
Uma pessoa hacker pode ter acesso a uma lista das suas senhas obtida de outras fontes, como contas invadidas. Ela pode usar uma tabela rainbow para tentar quebrar o hash. Se os hashes forem iguais, a senha foi descoberta. Isso também pode envolver ataques de força bruta, nos quais diferentes combinações de senhas são testadas para verificar se o hash gerado corresponde ao hash da senha original.
Embora isso exija recursos de alta velocidade ou grande poder computacional, esses métodos já se mostraram eficazes contra algoritmos de hash mais antigos, como os da família SHA1, considerando o poder computacional disponível atualmente.
Por isso, é extremamente importante usar algoritmos de hash mais recentes e seguir outras práticas recomendadas para garantir que seja impossível quebrar o hash gerado — ou que isso leve várias décadas de tentativas de combinações e permutações.
Práticas recomendadas para hashing
Veja algumas práticas recomendadas para usar métodos modernos de hashing.
Salt
Um salt é um valor gerado aleatoriamente — geralmente dados binários, como bytes aleatórios — que é adicionado à senha durante a criação do hash. Cada senha tem seu próprio salt exclusivo. Isso acrescenta uma camada de segurança, pois a pessoa mal-intencionada precisaria quebrar o salt e o hash. Assim, quebrar o hash se torna um processo em duas etapas, que exige mais recursos.
O tamanho do salt depende do algoritmo de hash utilizado. Nos algoritmos mais recentes, o salt é gerado automaticamente, sem que você precise fornecer um. Para saber mais, consulte esta folha de dicas da OWASP sobre armazenamento de senhas.
Algoritmos de hash
Os algoritmos de hash desempenham um papel fundamental no hashing de senhas. É essencial escolher um algoritmo resistente a tentativas de quebra. Os algoritmos modernos permitem personalizar configurações como o fator de trabalho e o número de iterações, tornando o hash mais difícil de quebrar. Considerando o poder computacional disponível, é recomendável escolher valores mais altos. Como regra geral, quanto maior o fator de trabalho ou o número de iterações, mais difícil será quebrar o hash.
Como prática recomendada, escolha o maior fator de trabalho ou número de iterações que seu sistema consiga suportar sem impacto significativo no desempenho.
O que considerar nos algoritmos de hash
Veja alguns pontos a considerar ao implementar um algoritmo de hash.
Como lidar com colisões de hash
Colisões de hash ocorrem quando duas entradas diferentes geram o mesmo valor de hash. Embora os algoritmos modernos sejam projetados para minimizar a probabilidade de colisões, elas ainda podem ocorrer. Para reduzir esse risco, recomenda-se usar valores de hash mais longos, como SHA-256 ou SHA-512, que oferecem um espaço de saída maior e diminuem a probabilidade de colisão.
Número de iterações
O número de iterações é um fator importante no hashing de senhas. Quanto maior esse número, mais recursos serão necessários para quebrar o hash, o que torna o ataque mais lento. No entanto, é importante equilibrar segurança e desempenho, pois um número excessivo de iterações pode causar atrasos para usuários legítimos durante a autenticação. Recomenda-se escolher um valor que ofereça segurança adequada sem afetar significativamente o desempenho.
Atualize os algoritmos de hash regularmente
Com o avanço da tecnologia e o aumento do poder computacional, novos algoritmos de hash são desenvolvidos, enquanto os mais antigos ficam obsoletos. Isso acontece porque os algoritmos antigos deixam de acompanhar esses avanços e se tornam alvos fáceis para atacantes.
Hashing de senhas em Go
O pacote crypto do Go oferece funções de hash integradas para algoritmos populares, como argon2, scrypt, bcrypt, pbkdf2 e outros. Essas funções são muito úteis para implementar o hashing de senhas. O pacote bcrypt também oferece a função CompareHashAndPassword, que compara senhas em texto simples com senhas em hash e verifica se elas correspondem.
Argon2
Atualmente, o Argon2 é considerado o algoritmo de hash mais seguro, embora isso possa mudar no futuro. Ele tem três variantes: Argon2d, que maximiza a resistência a ataques de quebra com GPU; Argon2i, otimizado para resistir a ataques de canal lateral; e Argon2id, uma combinação dos dois.
A folha de dicas da OWASP sobre armazenamento de senhas recomenda o uso do algoritmo híbrido Argon2id para armazenar senhas. A folha de dicas recomenda os seguintes valores mínimos para o tamanho da memória (m), o número de iterações (t) e o grau de paralelismo (p):
m=47104 (46 MiB), t=1, p=1 (não use com Argon2i)
m=19456 (19 MiB), t=2, p=1 (não use com Argon2i)
m=12288 (12 MiB), t=3, p=1
m=9216 (9 MiB), t=4, p=1
m=7168 (7 MiB), t=5, p=1
Implementação em Go
Agora, vamos analisar uma implementação de exemplo do Argon2id para gerar um hash a partir de uma senha em texto simples e compará-lo com o hash.
Configuração
Para começar, vamos criar uma struct para armazenar todos os parâmetros de configuração necessários para fazer hashing com Argon2id:
A struct Argon2idHash contém os seguintes parâmetros: time, memory (custo computacional), threads (paralelismo), keyLen e saltLen. Esses parâmetros são usados para gerar um hash a partir de uma senha em texto simples.
Em seguida, vamos escrever uma função construtora para inicializar a struct com os valores fornecidos.
Gerar o salt
Para facilitar a geração de valores aleatórios, especialmente de salts, desenvolvemos a função randomSecret. Ela recebe o tamanho desejado para o valor secreto aleatório e usa a função rand.Read para gerá-lo.
Gerar o hash
Em seguida, criamos o método de hashing GenerateHash na struct Argon2idHash. Esse método permite gerar um hash com os valores configurados. Se nenhum salt aleatório for fornecido, geramos um com o mesmo tamanho definido na configuração. Depois, usamos todos os valores configurados para gerar o hash com a função argon2.IDKey. Se a geração for bem-sucedida, retornamos o par de hash e salt.
Comparação de senhas
Para comparar senhas, criamos outro método — Compare — na struct Argon2idHash. Esse método recebe o hash e o salt armazenados, além da senha a ser comparada.
Primeiro, geramos um hash usando a função GenerateHash criada na etapa anterior. Essa função recebe como entradas a senha fornecida e o salt.
Em seguida, comparamos o hash gerado com o armazenado usando a função bytes.Equal. Se os dois hashes forem iguais, encontramos uma correspondência. Caso contrário, o hash armazenado não corresponde à senha fornecida.
Neste exemplo, usamos a função argon2.IDKey para gerar o hash. Como mencionamos, outros pacotes de criptografia do Go têm funções semelhantes, que também podem ser usadas para gerar hashes seguros de senhas, como fizemos neste exemplo.
Scrypt
O Scrypt é o segundo algoritmo de hash da lista. Sua primeira opção deve ser o Argon2id, mas, se ele não estiver disponível, use scrypt como alternativa.
Assim como para o Argon2id, a folha de dicas da OWASP sobre armazenamento de senhas recomenda os seguintes valores para os campos: parâmetro mínimo de custo de CPU/memória (N), tamanho do bloco (r) e grau de paralelismo (p).
N = 2^17 (128 MiB), r = 8 (1024 bytes), p = 1
N = 2^16 (64 MiB), r = 8 (1024 bytes), p = 2
N = 2^15 (32 MiB), r = 8 (1024 bytes), p = 3
N = 2^14 (16 MiB), r = 8 (1024 bytes), p = 5
N = 2^13 (8 MiB), r = 8 (1024 bytes), p = 10
A documentação do pacote Go para o pacote scrypt traz um exemplo de como gerar uma chave de hash usando scrypt. As operações de geração e comparação devem ser muito parecidas com as implementadas para a struct Argon2idHash.
Bcrypt
O bcrypt deve ser sua última opção. Se for necessário cumprir a norma FIPS 140, use PBKDF2, que abordaremos a seguir. De acordo com a folha de dicas da OWASP sobre armazenamento de senhas, o fator de trabalho deve ser o maior que o desempenho do servidor de verificação permitir, com valor mínimo de 10.
O pacote bcrypt do Go já inclui funções para gerar e comparar hashes. Por isso, não vamos apresentar um exemplo aqui. Consulte a documentação do pacote Go para ver exemplos.
PBKDF2 (compatível com FIPS 140)
De acordo com a folha de dicas da OWASP sobre armazenamento de senhas, o fator de trabalho do PBKDF2 é implementado por meio do número de iterações, que deve variar de acordo com o algoritmo de hash usado internamente.
PBKDF2-HMAC-SHA1: 1.300.000 iterações
PBKDF2-HMAC-SHA256: 600.000 iterações
PBKDF2-HMAC-SHA512: 210.000 iterações
Exemplo de hashing
Como o PBKDF2 é o único algoritmo da nossa lista compatível com FIPS 140, vamos ver como usá-lo para fazer hashing de senhas em Go. Vamos começar.
A função ToKey do pacote pbkdf2, que usaremos para gerar um hash a partir de uma senha em texto simples, é mostrada abaixo:
Para realizar o hash, você precisará da senha e do salt como fatias de bytes, além do número de iterações e do tamanho desejado para a chave ou o hash gerado. Também é necessária uma função de hash. Neste caso, a senha é a senha em texto simples que queremos transformar em hash, enquanto o salt é um valor gerado aleatoriamente. Você pode configurar o número de iterações e o tamanho da chave. Para a função de hash, usaremos sha3.New256.
Configuração
Gerando o hash
Comparação de senhas
Isso é muito parecido com a nossa implementação de Argon2idHash. Portanto, você pode modificar o exemplo completo de código para usar PBKDF2Hash.
Código completo
Agora, vamos juntar tudo.
Estrutura do projeto
O arquivo argon2id.go:
O arquivo main.go:
Escolha seu algoritmo com sabedoria
Os exemplos acima são uma ótima introdução à implementação de algoritmos seguros de hash para armazenar senhas em Go. Lembre-se de que nem todos os algoritmos de hash são iguais e que você deve escolhê-los com cuidado, considerando suas necessidades e o estado atual da segurança do algoritmo. O que hoje é considerado um algoritmo robusto pode mudar com o tempo, por isso é recomendável reavaliar sua escolha periodicamente.
Além disso, ferramentas SAST como Snyk Code podem ajudar você a identificar algoritmos desatualizados e orientar sobre como atualizá-los, se necessário. Escolha com cuidado o algoritmo de hash de senhas, mantenha-se informado e bom código!
