package auth import ( "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "fmt" "strconv" "strings" "golang.org/x/crypto/argon2" ) const ( passwordTime uint32 = 3 passwordMemory uint32 = 64 * 1024 passwordThreads uint8 = 4 passwordKeyLen uint32 = 32 passwordSaltLen = 16 ) func randomBytes(length int) ([]byte, error) { buf := make([]byte, length) if _, err := rand.Read(buf); err != nil { return nil, err } return buf, nil } func randomToken(length int) (string, error) { buf, err := randomBytes(length) if err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(buf), nil } func randomHex(length int) (string, error) { buf, err := randomBytes(length) if err != nil { return "", err } return hex.EncodeToString(buf), nil } func hashSecret(secret string) string { sum := sha256.Sum256([]byte(secret)) return hex.EncodeToString(sum[:]) } func constantTimeEqualHex(a, b string) bool { if len(a) != len(b) { return false } return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 } func hashPassword(password string) (string, error) { salt, err := randomBytes(passwordSaltLen) if err != nil { return "", err } hash := argon2.IDKey([]byte(password), salt, passwordTime, passwordMemory, passwordThreads, passwordKeyLen) return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", passwordMemory, passwordTime, passwordThreads, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(hash), ), nil } func verifyPassword(password, encoded string) (bool, error) { parts := strings.Split(encoded, "$") if len(parts) != 6 || parts[1] != "argon2id" { return false, fmt.Errorf("invalid password hash") } var memory uint64 var timeCost uint64 var threads uint64 for _, param := range strings.Split(parts[3], ",") { keyValue := strings.SplitN(param, "=", 2) if len(keyValue) != 2 { return false, fmt.Errorf("invalid password hash parameters") } value, err := strconv.ParseUint(keyValue[1], 10, 32) if err != nil { return false, fmt.Errorf("parse password hash parameter: %w", err) } switch keyValue[0] { case "m": memory = value case "t": timeCost = value case "p": threads = value } } if memory == 0 || timeCost == 0 || threads == 0 { return false, fmt.Errorf("missing password hash parameters") } salt, err := base64.RawStdEncoding.DecodeString(parts[4]) if err != nil { return false, fmt.Errorf("decode password salt: %w", err) } expected, err := base64.RawStdEncoding.DecodeString(parts[5]) if err != nil { return false, fmt.Errorf("decode password hash: %w", err) } actual := argon2.IDKey([]byte(password), salt, uint32(timeCost), uint32(memory), uint8(threads), uint32(len(expected))) return subtle.ConstantTimeCompare(actual, expected) == 1, nil } func normalizeScopes(scopes []Scope) []Scope { if len(scopes) == 0 { return []Scope{ScopeDocsRead, ScopeDocsWrite, ScopeSyncRead, ScopeSyncWrite} } seen := make(map[Scope]struct{}, len(scopes)) var normalized []Scope for _, scope := range scopes { switch scope { case ScopeDocsRead, ScopeDocsWrite, ScopeSyncRead, ScopeSyncWrite, ScopeAdmin: if _, ok := seen[scope]; !ok { seen[scope] = struct{}{} normalized = append(normalized, scope) } } } if len(normalized) == 0 { return []Scope{ScopeDocsRead} } return normalized } func scopesToString(scopes []Scope) string { normalized := normalizeScopes(scopes) parts := make([]string, 0, len(normalized)) for _, scope := range normalized { parts = append(parts, string(scope)) } return strings.Join(parts, ",") } func scopesFromString(raw string) []Scope { if raw == "" { return nil } parts := strings.Split(raw, ",") scopes := make([]Scope, 0, len(parts)) for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed != "" { scopes = append(scopes, Scope(trimmed)) } } return normalizeScopes(scopes) } func hasScope(scopes []Scope, required Scope) bool { for _, scope := range scopes { if scope == ScopeAdmin || scope == required { return true } } return false }