// Package auth implements local authentication: Argon2id passwords, opaque // sessions, CSRF tokens, login throttling, roles and HTTP middleware // (ADR-0006). package auth import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) // Argon2id parameters: 64 MiB, 3 passes, 2 lanes. Roughly 100 ms on the // development machine, which throttles offline guessing without hurting login. const ( argonTime = 3 argonMemory = 64 * 1024 argonThreads = 2 argonKeyLen = 32 saltLen = 16 ) // MinPasswordLength is the shortest password accepted anywhere. const MinPasswordLength = 12 // HashPassword returns a PHC-formatted Argon2id hash. func HashPassword(password string) (string, error) { if len(password) < MinPasswordLength { return "", fmt.Errorf("password must be at least %d characters", MinPasswordLength) } salt := make([]byte, saltLen) if _, err := rand.Read(salt); err != nil { return "", err } key := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen) return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", argonMemory, argonTime, argonThreads, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(key)), nil } // VerifyPassword reports whether password matches the PHC hash. It parses the // parameters from the hash so they can be raised later without invalidating // existing users. func VerifyPassword(hash, password string) (bool, error) { parts := strings.Split(hash, "$") if len(parts) != 6 || parts[1] != "argon2id" { return false, errors.New("unsupported hash format") } var m uint32 var t uint32 var p uint8 if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p); err != nil { return false, fmt.Errorf("bad hash params: %w", err) } salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false, err } want, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false, err } got := argon2.IDKey([]byte(password), salt, t, m, p, uint32(len(want))) return subtle.ConstantTimeCompare(got, want) == 1, nil } // dummyHash is verified against when a username does not exist so that the // response time does not reveal whether the account exists. var dummyHash, _ = HashPassword("noema-timing-equalisation-password")