// Package auth handles password hashing and session tokens. package auth import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) // Argon2id parameters, taken from the RFC 9106 "low memory" recommendation // (2nd choice) as a reasonable default for a server handling interactive // logins without dedicated hardware. const ( argonTime = 2 argonMemory = 19 * 1024 // KiB argonThreads = 1 argonKeyLen = 32 saltLen = 16 ) // HashPassword returns a self-describing hash string: algorithm, parameters, // salt and key are all encoded together so the parameters can change later // without invalidating hashes already stored. func HashPassword(password string) (string, error) { salt := make([]byte, saltLen) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("generate salt: %w", err) } key := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen) b64Salt := base64.RawStdEncoding.EncodeToString(salt) b64Key := base64.RawStdEncoding.EncodeToString(key) return fmt.Sprintf("argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", argonMemory, argonTime, argonThreads, b64Salt, b64Key), nil } // VerifyPassword checks password against an encoded hash produced by // HashPassword. It returns an error only on malformed input; a wrong // password is reported via the bool return. func VerifyPassword(password, encoded string) (bool, error) { parts := strings.Split(encoded, "$") if len(parts) != 5 || parts[0] != "argon2id" { return false, errors.New("unrecognized hash format") } var memory, time uint32 var threads uint8 if _, err := fmt.Sscanf(parts[2], "m=%d,t=%d,p=%d", &memory, &time, &threads); err != nil { return false, fmt.Errorf("parse hash params: %w", err) } salt, err := base64.RawStdEncoding.DecodeString(parts[3]) if err != nil { return false, fmt.Errorf("decode salt: %w", err) } wantKey, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false, fmt.Errorf("decode key: %w", err) } gotKey := argon2.IDKey([]byte(password), salt, time, memory, threads, uint32(len(wantKey))) return subtle.ConstantTimeCompare(gotKey, wantKey) == 1, nil }