// src: https://gist.github.com/alexedwards/34277fae0f48abe36822b375f0f6a621 package user import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) var ( errInvalidHash = errors.New("provided hash is wrong format") errHashesNotEqual = errors.New("secret is not equal to encoded hash") errIncorrectVersion = errors.New("incorrect version of Argon2") ) type hashconf struct { memory uint32 iterations uint32 parallelism uint8 keyLength uint32 saltLength uint32 } func hashSecret(secret string) (string, error) { hc := &hashconf{ memory: 64 * 1024, iterations: 3, parallelism: 2, keyLength: 12, saltLength: 16, } salt := make([]byte, hc.saltLength) _, err := rand.Read(salt) if err != nil { return "", err } hash := hashArgon2(secret, salt, hc) b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) encodedHash := fmt.Sprintf( "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, hc.memory, hc.iterations, hc.parallelism, b64Salt, b64Hash, ) return encodedHash, nil } func compareSecretToHash(secret, encoded string) error { // decode the encoded hash hc, salt, comparehash, err := decodeHash(encoded) if err != nil { return err } // encode the secret verifyhash := hashArgon2(secret, salt, hc) // compare the hashes using constant time comparison // to prevent timing attacks. if not equal, then return false if subtle.ConstantTimeCompare(comparehash, verifyhash) != 1 { return errHashesNotEqual } return nil } func hashArgon2(secret string, salt []byte, hc *hashconf) []byte { hash := argon2.IDKey( []byte(secret), salt, hc.iterations, hc.memory, hc.parallelism, hc.keyLength, ) return hash } func decodeHash(encoded string) (hc *hashconf, salt, decodedhash []byte, err error) { params := strings.Split(encoded, "$") // check we have enough params if len(params) != 6 { return nil, nil, nil, errInvalidHash } // check the argon2 version matches var version int _, err = fmt.Sscanf(params[2], "v=%d", &version) if err != nil { return nil, nil, nil, err } if version != argon2.Version { return nil, nil, nil, errIncorrectVersion } // parse hashconf params to be returned hc = &hashconf{} _, err = fmt.Sscanf(params[3], "m=%d,t=%d,p=%d", &hc.memory, &hc.iterations, &hc.parallelism) if err != nil { return nil, nil, nil, err } // decode the salt salt, err = base64.RawStdEncoding.Strict().DecodeString(params[4]) if err != nil { return nil, nil, nil, err } hc.saltLength = uint32(len(salt)) // decode the hash decodedhash, err = base64.RawStdEncoding.Strict().DecodeString(params[5]) if err != nil { return nil, nil, nil, err } hc.keyLength = uint32(len(decodedhash)) return hc, salt, decodedhash, nil }