// Package auth provides cryptographic core for authentication. package auth import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strconv" "strings" "golang.org/x/crypto/argon2" ) // argon2Memory, argon2Time, argon2Threads, argon2KeyLen, and saltLen are parameters // recommended by OWASP for Argon2id (memory=65536 KiB, time=3 iterations, parallelism=2). const ( argon2Memory = 65536 argon2Time = 3 argon2Threads = 2 argon2KeyLen = 32 saltLen = 16 ) // HashPassword generates an Argon2id hash of the provided password and returns it // in PHC string format: $argon2id$v=19$m=...,t=...,p=...$$. func HashPassword(password string) (string, error) { salt := make([]byte, saltLen) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("auth: generate salt: %w", err) } hash := argon2.IDKey([]byte(password), salt, argon2Time, argon2Memory, argon2Threads, argon2KeyLen) b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Hash := base64.RawStdEncoding.EncodeToString(hash) phc := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, argon2Memory, argon2Time, argon2Threads, b64Salt, b64Hash) return phc, nil } // VerifyPassword checks whether the provided password matches the given Argon2id PHC hash. // It returns (true, nil) on match, (false, nil) on mismatch, or (false, error) on invalid format. func VerifyPassword(phc, password string) (bool, error) { parts := strings.Split(phc, "$") if len(parts) != 6 || parts[1] != "argon2id" { return false, errors.New("auth: invalid hash format") } // Version vPart := parts[2] if !strings.HasPrefix(vPart, "v=") { return false, errors.New("auth: invalid version format") } v, err := strconv.Atoi(strings.TrimPrefix(vPart, "v=")) if err != nil { return false, fmt.Errorf("auth: parse version: %w", err) } if v != argon2.Version { return false, errors.New("auth: incompatible version") } // Parameters mem, t, p := uint32(0), uint32(0), uint32(0) for _, kv := range strings.Split(parts[3], ",") { pair := strings.SplitN(kv, "=", 2) if len(pair) != 2 { return false, errors.New("auth: invalid parameter format") } key, val := pair[0], pair[1] switch key { case "m": x, err := strconv.ParseUint(val, 10, 32) if err != nil { return false, fmt.Errorf("auth: parse memory: %w", err) } mem = uint32(x) case "t": x, err := strconv.ParseUint(val, 10, 32) if err != nil { return false, fmt.Errorf("auth: parse time: %w", err) } t = uint32(x) case "p": x, err := strconv.ParseUint(val, 10, 32) if err != nil { return false, fmt.Errorf("auth: parse threads: %w", err) } p = uint32(x) default: return false, fmt.Errorf("auth: unknown parameter %q", key) } } salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false, fmt.Errorf("auth: decode salt: %w", err) } expected, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false, fmt.Errorf("auth: decode hash: %w", err) } computed := argon2.IDKey([]byte(password), salt, t, mem, uint8(p), uint32(len(expected))) if subtle.ConstantTimeCompare(computed, expected) == 1 { return true, nil } return false, nil }