package lib import ( "crypto/rand" "crypto/subtle" "encoding/base32" "encoding/base64" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) var ( ErrInvalidHash = errors.New("Hash has an invalid format") ErrIncompatibleVersion = errors.New("Incompatible version of argon2") ) type Argon2idConfig struct { raw_hash []byte salt []byte hash_len uint32 // bytes salt_len uint32 // bytes mem_cost uint32 // KiB iterations uint32 threads uint8 } func GenerateSalt(length int) ([]byte, error) { secret := make([]byte, length) _, err := rand.Read(secret) if err != nil { return nil, err } return secret, nil } // generates an argon2id hash func GenerateHash(password string) (string, error) { var err error config := &Argon2idConfig{ iterations: 4, mem_cost: 64 * 1024, threads: 4, hash_len: 32, } config.salt, err = GenerateSalt(16) if err != nil { return "", err } config.raw_hash = argon2.IDKey([]byte(password), config.salt, config.iterations, config.mem_cost, config.threads, config.hash_len) encoded_hash := fmt.Sprintf( "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, config.mem_cost, config.iterations, config.threads, base64.RawStdEncoding.EncodeToString(config.salt), base64.RawStdEncoding.EncodeToString(config.raw_hash), ) return encoded_hash, nil } func ValidateHash(password string, encoded_hash string) (match bool, err error) { p, salt, hash, err := DecodeHash(encoded_hash) if err != nil { return false, err } otherHash := argon2.IDKey([]byte(password), salt, p.iterations, p.mem_cost, p.threads, p.hash_len) // subtle.ConstantTimeCompare prevents timing attacks. if subtle.ConstantTimeCompare(hash, otherHash) == 1 { return true, nil } return false, nil } // generates an alphanumeric token with a given length. func AlphanumericToken(length uint) string { randomBytes := make([]byte, length) _, err := rand.Read(randomBytes) if err != nil { panic(err) } // this contraption makes sure to not trim unicode chars, for instance. return string([]rune(base32.StdEncoding.EncodeToString(randomBytes))[:length]) } // generates a base64-encoded token with a given length. func Base64Token(length uint) string { randomBytes := make([]byte, length) _, err := rand.Read(randomBytes) if err != nil { panic(err) } return base64.StdEncoding.EncodeToString(randomBytes)[:length] } func DecodeHash(encodedHash string) (p *Argon2idConfig, salt, hash []byte, err error) { vals := strings.Split(encodedHash, "$") if len(vals) != 6 { return nil, nil, nil, ErrInvalidHash } var version int _, err = fmt.Sscanf(vals[2], "v=%d", &version) if err != nil { return nil, nil, nil, err } if version != argon2.Version { return nil, nil, nil, ErrIncompatibleVersion } p = &Argon2idConfig{} _, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.mem_cost, &p.iterations, &p.threads) if err != nil { return nil, nil, nil, err } salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4]) if err != nil { return nil, nil, nil, err } hash, err = base64.RawStdEncoding.Strict().DecodeString(vals[5]) if err != nil { return nil, nil, nil, err } p.hash_len = uint32(len(hash)) p.salt_len = uint32(len(salt)) return p, salt, hash, nil }