Criptografia e segurança básica
Podemos trabalhar com recursos de criptografia e segurança usando packages da biblioteca padrão, como crypto/sha256 e crypto/rand, além de bibliotecas externas para operações específicas, como o bcrypt. Esses recursos permitem proteger informações, verificar a integridade de dados, armazenar senhas de forma segura e gerar valores aleatórios que não sejam facilmente previsíveis.
O que é hash?
Um hash é um valor gerado a partir de um conjunto de dados por meio de um algoritmo matemático. Ele funciona como uma espécie de impressão digital dos dados, permitindo identificar alterações no conteúdo original. Por exemplo, podemos gerar um hash de um texto usando o algoritmo SHA-256, disponível no package crypto/sha256.
package main
import (
"crypto/sha256"
"fmt"
)
func main() {
texto := "Olá, Go!"
// Gera um hash SHA-256 do texto.
hash := sha256.Sum256([]byte(texto))
// Mostra o hash em hexadecimal.
fmt.Printf("%x\n", hash)
}
Nesse exemplo, sha256.Sum256() recebe os dados como []byte e gera um hash de 256 bits, que podemos representar como um texto hexadecimal. Se executarmos novamente com o mesmo conteúdo, teremos o mesmo hash. Porém, se modificarmos apenas um caractere do texto, o resultado será diferente. O hash possui um tamanho fixo, independentemente do tamanho dos dados originais. Também não é possível reconstruir diretamente o conteúdo original a partir dele. Embora o SHA-256 seja útil para verificar a integridade de dados, não devemos usar SHA-256 diretamente para armazenar senhas, porque existem algoritmos específicos para essa finalidade, como o bcrypt.
Diferença entre hash e criptografia
Embora estejam relacionados, hash e criptografia de dados não funcionam da mesma maneira. O hash transforma informações em um valor de tamanho definido, sem oferecer um mecanismo para recuperar diretamente os dados originais. Por isso, é usado principalmente para verificar integridade e, com algoritmos próprios, armazenar senhas. Já a criptografia de dados, também chamada de cifragem, permite transformar informações em um conteúdo que não pode ser compreendido sem a chave necessária para recuperá-las.
Por exemplo, quando criptografamos um arquivo, podemos descriptografá-lo posteriormente usando a chave correta. Já quando geramos um hash de uma senha, não existe uma operação equivalente que permita recuperar a senha original a partir desse hash. Por isso, normalmente armazenamos hashes de senhas, e não senhas criptografadas, porque não precisamos recuperar a senha original para verificar se ela está correta.
Armazenando senhas com bcrypt
O bcrypt é um algoritmo de hash específico para senhas, que permite armazená-las sem precisar guardar seu valor original. Diferentemente do SHA-256, o bcrypt foi desenvolvido para tornar o cálculo do hash mais custoso, dificultando tentativas de descobrir senhas por meio de várias combinações. Ele também gera automaticamente um salt, que representa um valor aleatório adicionado ao processamento da senha, fazendo com que duas senhas iguais normalmente gerem hashes diferentes.
Em Go, podemos trabalhar com bcrypt usando o package externo golang.org/x/crypto/bcrypt. Primeiro, precisamos adicionar a dependência ao projeto:
go get golang.org/x/crypto/bcrypt
Depois, podemos gerar o hash de uma senha usando bcrypt.GenerateFromPassword().
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
senha := "minhaSenha123"
// Gera o hash da senha.
hash, err := bcrypt.GenerateFromPassword(
[]byte(senha),
bcrypt.DefaultCost,
)
if err != nil {
fmt.Println("Erro ao gerar hash:", err)
return
}
// Mostra o hash apenas para demonstração.
fmt.Println(string(hash))
}
O bcrypt.GenerateFromPassword() recebe a senha como []byte e um valor que representa o custo do processamento. Nesse exemplo, usamos bcrypt.DefaultCost, que define um custo padrão para gerar o hash. Quanto maior o custo, mais processamento será necessário para gerar e verificar a senha, dificultando tentativas de descobrir seu valor original.
O hash gerado contém as informações necessárias para sua verificação, incluindo o salt e o custo usado. Por isso, não precisamos armazenar o salt separadamente. Em uma aplicação real, o hash seria armazenado em um banco de dados, em vez de ser mostrado no terminal. Um cuidado é que o bcrypt aceita senhas de até 72 bytes na geração do hash, então precisamos considerar esse limite ao receber senhas dos usuários.
Verificando senhas com bcrypt
Depois de armazenar o hash de uma senha, precisamos de uma forma de verificar se a senha informada posteriormente corresponde à original. Para isso, usamos bcrypt.CompareHashAndPassword(), que recebe o hash armazenado e a senha informada, verificando se os valores correspondem.
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
senha := "minhaSenha123"
// Simula o cadastro de uma senha.
hash, err := bcrypt.GenerateFromPassword(
[]byte(senha),
bcrypt.DefaultCost,
)
if err != nil {
fmt.Println("Erro ao gerar hash:", err)
return
}
// Simula uma senha informada durante o login.
senhaInformada := "minhaSenha123"
// Compara a senha informada com o hash armazenado.
err = bcrypt.CompareHashAndPassword(
hash,
[]byte(senhaInformada),
)
if err != nil {
fmt.Println("Senha incorreta")
return
}
fmt.Println("Senha correta")
}
Resultado:
Senha correta
Nesse exemplo, primeiro geramos o hash da senha cadastrada. Depois, usamos CompareHashAndPassword() para verificar se a senha informada corresponde àquela que gerou o hash. Observe que não precisamos descriptografar o hash para verificar a senha, porque o próprio bcrypt faz essa comparação usando as informações armazenadas nele.
Se a senha estiver correta, a função retorna nil. Caso contrário, retorna um erro. Em aplicações reais, o hash normalmente seria recuperado de um banco de dados, enquanto a senha informada seria recebida durante o processo de autenticação.
Gerando valores aleatórios com crypto/rand
O package crypto/rand permite gerar valores aleatórios usando uma fonte de aleatoriedade criptograficamente segura, fornecida pelo sistema operacional. Isso é importante quando precisamos gerar informações que não sejam facilmente previsíveis, como tokens de autenticação, identificadores secretos e chaves criptográficas.
Para esse tipo de operação, não devemos depender de geradores de números pseudoaleatórios comuns, como os encontrados no package math/rand.
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
)
func main() {
// Cria um slice para armazenar 16 bytes aleatórios.
dados := make([]byte, 16)
// Preenche o slice com bytes aleatórios.
_, err := rand.Read(dados)
if err != nil {
fmt.Println("Erro ao gerar dados:", err)
return
}
// Converte os bytes para hexadecimal.
token := hex.EncodeToString(dados)
fmt.Println(token)
}
Resultado possível:
f8a12b6c4e901d73a5b2c08d9e6f3147
Nesse exemplo, rand.Read() preenche o slice com 16 bytes aleatórios, enquanto hex.EncodeToString() converte esses bytes para uma representação hexadecimal. O resultado terá 32 caracteres hexadecimais, porque cada byte é representado por dois caracteres.
Se executarmos o programa novamente, normalmente teremos um resultado diferente. Esse recurso é útil principalmente quando precisamos gerar valores secretos que não possam ser facilmente adivinhados.
Cuidados com informações sensíveis
Quando trabalhamos com senhas, tokens e outras informações sensíveis, precisamos considerar tanto a forma como esses dados são recebidos quanto a maneira como são armazenados. Por exemplo, já vimos que podemos receber senhas sem mostrar os caracteres digitados usando golang.org/x/term. Porém, isso protege apenas a entrada pelo terminal, não significa que os dados estejam automaticamente protegidos na memória ou durante seu armazenamento.
Também devemos evitar receber senhas por argumentos de linha de comando, porque essas informações podem aparecer no histórico do terminal ou nas informações dos processos do sistema operacional. Outro cuidado é não registrar senhas, tokens ou chaves privadas em logs, nem deixar esses valores diretamente no código-fonte.
Quando precisamos armazenar senhas de usuários, podemos usar algoritmos próprios para essa finalidade, como o bcrypt, mantendo apenas o hash gerado. Já para gerar valores aleatórios que precisam ser secretos, devemos usar crypto/rand. O bcrypt não é usado para criptografar e recuperar senhas, mas para gerar e verificar seus hashes. Se precisarmos proteger dados que deverão ser recuperados posteriormente, como o conteúdo de um arquivo, será necessário usar um algoritmo de criptografia reversível, como AES-GCM, com gerenciamento adequado das chaves.