package password import ( "crypto/rand" "crypto/subtle" "encoding/base64" "fmt" "strings" "golang.org/x/crypto/argon2" ) const ( argonTime = 1 argonMemory = 64 * 1024 argonThreads = 4 argonKeyLen = 32 ) // HashPassword creates an argon2id hash of the password func HashPassword(password string) string { salt := make([]byte, 16) rand.Read(salt) hash := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen) b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, argonMemory, argonTime, argonThreads, b64Salt, b64Hash) return encoded } // VerifyPassword checks if a password matches the hash func VerifyPassword(password, encodedHash string) (bool, error) { parts := strings.Split(encodedHash, "$") if len(parts) != 6 { return false, fmt.Errorf("invalid hash format") } if parts[1] != "argon2id" { return false, fmt.Errorf("invalid hash variant") } var version int _, err := fmt.Sscanf(parts[2], "v=%d", &version) if err != nil { return false, err } if version != argon2.Version { return false, fmt.Errorf("incompatible argon2 version") } var m, t, p uint32 _, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p) if err != nil { return false, err } salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false, err } expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false, err } hash := argon2.IDKey([]byte(password), salt, t, m, uint8(p), uint32(len(expectedHash))) if subtle.ConstantTimeCompare(hash, expectedHash) == 1 { return true, nil } return false, nil }