diff --git a/go.mod b/go.mod index 551d1b3..7252995 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module gitea.zaclys.com/bvaudour/gob go 1.21.1 + +require golang.org/x/crypto v0.13.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8886ba7 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= diff --git a/secret/bcrypt.go b/secret/bcrypt.go new file mode 100644 index 0000000..488e7ef --- /dev/null +++ b/secret/bcrypt.go @@ -0,0 +1,29 @@ +package secret + +import ( + "golang.org/x/crypto/bcrypt" +) + +// Bcrypt permet de calculer un hash en utilisant l’algorithme Bcrypt. +type Bcrypt struct { + cost int +} + +// Bcrypt initialise une fonction de hashage de type Bcrypt et de coût cost. +func NewBcrypt(cost int) Bcrypt { + return Bcrypt{cost: cost} +} + +// Hash retourne le hash des bytes d’entrée. +func (h Bcrypt) Hash(data []byte) []byte { + hashed, _ := bcrypt.GenerateFromPassword(data, h.cost) + + return hashed +} + +// Compare vérifie qu’une donnée d’entrée a bien pour hash la valeur hashed. +func (h Bcrypt) Compare(data []byte, hashed []byte) bool { + err := bcrypt.CompareHashAndPassword(hashed, data) + + return err == nil +} diff --git a/secret/hash.go b/secret/hash.go new file mode 100644 index 0000000..a7bcae8 --- /dev/null +++ b/secret/hash.go @@ -0,0 +1,142 @@ +package secret + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "fmt" + "hash" + + . "gitea.zaclys.com/bvaudour/gob/option" + "gitea.zaclys.com/bvaudour/gob/random" +) + +func errAlgorithm(algorithm string) error { + return fmt.Errorf("unknown algorithm: %s", algorithm) +} + +// Hasher est un utilitaire permettant de hasher une chaîne et de vérifier qu’une chaîne correspond bien à un hashage. +type Hasher interface { + Hash(data []byte) (hashed []byte) + Compare(data []byte, hashed []byte) bool +} + +type hasher struct { + h hash.Hash +} + +func (h hasher) Hash(data []byte) []byte { + h.h.Write(data) + return h.h.Sum(nil) +} + +func (h hasher) Compare(data []byte, hashed []byte) bool { + return string(h.Hash(data)) == string(hashed) +} + +// HashResult est le résultat d’un hash. +type HashResult[T ~[]byte | ~string] struct { + Hash T + Salt T +} + +func hashOf(data []byte, saltLen int, h Hasher) (result HashResult[[]byte]) { + l := len(data) + in := make([]byte, l+saltLen) + + result.Salt = random.BytesKey(saltLen, random.All) + copy(in[:l], data) + copy(in[l:], result.Salt) + result.Hash = h.Hash(in) + + return +} + +// HashOf génère le hash et un sel aléatoire à une chaîne donnée. +func HashOf[T ~[]byte | ~string](data T, saltLen int, h Hasher) (result HashResult[T]) { + rb := hashOf([]byte(data), saltLen, h) + result.Hash, result.Salt = T(rb.Hash), T(rb.Salt) + + return +} + +const ( + MD5 = "md5" + SHA1 = "sha1" + SHA256 = "sha256" + SHA512 = "sha512" + BCRYPT = "bcrypt" +) + +var ( + hashers = map[string]Hasher{ + MD5: hasher{md5.New()}, + SHA1: hasher{sha1.New()}, + SHA256: hasher{sha256.New()}, + SHA512: hasher{sha512.New()}, + BCRYPT: NewBcrypt(14), + } +) + +// AvailableHashAlgorithms retourne la liste des noms des algorithmes de hachage supportés. +func AvailableHashAlgorithms() (out []string) { + for h := range hashers { + out = append(out, h) + } + + return +} + +// IsHashAlgorithmAvailable vérifie si le nom fournit en entrée fait partie des algorithmes de hachage supportés. +func IsHashAlgorithmAvailable(name string) bool { + _, exists := hashers[name] + + return exists +} + +// AddHashAlgorithm ajoute un algorithme de hashage à disposition. +func AddHashAlgorithm(name string, hasher Hasher) { + hashers[name] = hasher +} + +// Hash chiffre la donnée d’entrée en utilisant l’algorithme demandé. +// Si une longueur de sel est fournie, le hachage est créé à partir +// d’un sel défini aléatoirement. +// Les algorithmes supportés par défaut sont : +// - md5 +// - sha1 +// - sha256 +// - sha512 +// - bcrypt +func Hash[T ~[]byte | ~string](data T, algorithm string, saltLen ...int) (result Result[HashResult[T]]) { + h, ok := hashers[algorithm] + if !ok { + return Err[HashResult[T]](errAlgorithm(algorithm)) + } + + sl := 0 + if len(saltLen) > 0 && saltLen[0] > 0 { + sl = saltLen[0] + } + r := HashOf(data, sl, h) + + return Ok(r) +} + +// CheckHash vérifie le hash d’une donnée d’entrée avec un algorithme de hashage. +// Si le hash a été généré avec du sel, la chaîne doit être la concaténation +// de la chaîne d’origine et du sel. +func CheckHash[T ~[]byte | ~string](data T, hash HashResult[T], algorithm string) bool { + h, ok := hashers[algorithm] + if !ok { + return false + } + + ld, ls := len(data), len(hash.Salt) + in := make([]byte, ld+ls) + copy(in[:ld], []byte(data)) + copy(in[ld:], []byte(hash.Salt)) + + return h.Compare(in, []byte(hash.Hash)) +} diff --git a/secret/symetric.go b/secret/symetric.go new file mode 100644 index 0000000..df6a8b1 --- /dev/null +++ b/secret/symetric.go @@ -0,0 +1,146 @@ +package secret + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "io" + "strings" + + . "gitea.zaclys.com/bvaudour/gob/option" + "golang.org/x/crypto/blowfish" +) + +const ( + AES = "aes" + BLOWFISH = "blowfish" +) + +type CipherFunc func([]byte) (cipher.Block, error) + +var ( + encoders = map[string]CipherFunc{ + AES: aes.NewCipher, + BLOWFISH: func(key []byte) (c cipher.Block, err error) { + c, err = blowfish.NewCipher(key) + return + }, + } + blockSize = map[string]int{ + AES: aes.BlockSize, + BLOWFISH: blowfish.BlockSize, + } +) + +func addBase64Padding(value string) string { + m := len(value) % 4 + if m != 0 { + value += strings.Repeat("=", 4-m) + } + + return value +} + +func encodeToBase64[T ~[]byte | ~string](value []byte) T { + encoded := strings.Replace(base64.URLEncoding.EncodeToString(value), "=", "", -1) + return T(encoded) +} + +func decodeFromBase64[T ~[]byte | ~string](value T) Result[[]byte] { + decoded, err := base64.URLEncoding.DecodeString(addBase64Padding(string(value))) + if err != nil { + return Err[[]byte](err) + } + return Ok([]byte(decoded)) +} + +func pad(src []byte, bs int) []byte { + padding := bs - len(src)%bs + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(src, padtext...) +} + +func unpad(src []byte) Result[[]byte] { + length := len(src) + unpadding := int(src[length-1]) + + if unpadding > length { + return Err[[]byte](errors.New("unpad error. This could happen when incorrect encryption key is used")) + } + + return Ok(src[:(length - unpadding)]) +} + +// Encrypt chiffre une donnée d’entrée (avec une clé aléatoire) selon +// l’algorithme demandé. Les algorithmes supportés sont : +// - blowfish +// - aes +func Encrypt[T ~[]byte | ~string](data, key T, algorithm string) Result[T] { + c, ok := encoders[algorithm] + if !ok { + return Err[T](errAlgorithm(algorithm)) + } + + block, err := c([]byte(key)) + if err != nil { + return Err[T](err) + } + + bs := blockSize[algorithm] + msg := pad([]byte(data), bs) + cipherText := make([]byte, bs+len(msg)) + iv := cipherText[:bs] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return Err[T](err) + } + + cfb := cipher.NewCFBEncrypter(block, iv) + cfb.XORKeyStream(cipherText[bs:], msg) + + return Ok(encodeToBase64[T](cipherText)) +} + +// Decrypte déchiffre une donnée d’entrée (avec une clé aléatoire) selon +// l’algorithme demandé. Les algorithmes supportés sont : +// - blowfish +// - aes +func Decrypt[T ~[]byte | ~string](encrypted, key T, algorithm string) Result[T] { + c, ok := encoders[algorithm] + if !ok { + return Err[T](errAlgorithm(algorithm)) + } + + block, err := c([]byte(key)) + if err != nil { + return Err[T](err) + } + + decoded := decodeFromBase64(encrypted) + if err, ok := decoded.Err(); ok { + return Err[T](err) + } + + dec, _ := decoded.Ok() + bs := blockSize[algorithm] + + if (len(dec) % bs) != 0 { + return Err[T](errors.New("blocksize must be multiple of data length")) + } + + iv := dec[:bs] + msg := dec[bs:] + + cfb := cipher.NewCFBDecrypter(block, iv) + cfb.XORKeyStream(msg, msg) + + unpadMsg := unpad(msg) + if err, ok := unpadMsg.Err(); ok { + return Err[T](err) + } + + result, _ := unpadMsg.Ok() + return Ok(T(result)) +}