package auth import ( "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "fmt" "os" "strings" "sync" "github.com/pelletier/go-toml/v2" ) type Caller struct { Name string `toml:"name"` Workspace string `toml:"workspace"` KeySHA256 string `toml:"key_sha256"` } type file struct { Caller []Caller `toml:"caller"` } // Store holds hashed keys. Reload on SIGHUP via Load. type Store struct { mu sync.RWMutex path string callers []Caller } func Load(path string) (*Store, error) { s := &Store{path: path} if err := s.reload(); err != nil { return nil, err } return s, nil } func (s *Store) Reload() error { return s.reload() } func (s *Store) reload() error { b, err := os.ReadFile(s.path) if err != nil { return err } var f file if err := toml.Unmarshal(b, &f); err != nil { return err } s.mu.Lock() s.callers = f.Caller s.mu.Unlock() return nil } // LookupBearer returns the caller for a raw bearer token, or nil. func (s *Store) LookupBearer(token string) *Caller { if token == "" || s == nil { return nil } sum := sha256.Sum256([]byte(token)) want := hex.EncodeToString(sum[:]) s.mu.RLock() defer s.mu.RUnlock() var found *Caller for i := range s.callers { got := strings.ToLower(strings.TrimSpace(s.callers[i].KeySHA256)) if len(got) != len(want) { continue } if subtle.ConstantTimeCompare([]byte(got), []byte(want)) == 1 { c := s.callers[i] found = &c } } return found } func HashKey(key string) string { sum := sha256.Sum256([]byte(key)) return hex.EncodeToString(sum[:]) } func GenerateKey() (string, error) { var b [32]byte if _, err := rand.Read(b[:]); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(b[:]), nil } func Add(path, name, workspace, keyOut string) error { if _, err := os.Stat(keyOut); err == nil { return fmt.Errorf("refusing to overwrite %s", keyOut) } key, err := GenerateKey() if err != nil { return err } if err := os.WriteFile(keyOut, []byte(key+"\n"), 0o600); err != nil { return err } var f file if b, err := os.ReadFile(path); err == nil { _ = toml.Unmarshal(b, &f) } f.Caller = append(f.Caller, Caller{Name: name, Workspace: workspace, KeySHA256: HashKey(key)}) out, err := toml.Marshal(f) if err != nil { return err } tmp := path + ".tmp" if err := os.WriteFile(tmp, out, 0o640); err != nil { return err } return os.Rename(tmp, path) } func List(path string) ([]Caller, error) { b, err := os.ReadFile(path) if err != nil { return nil, err } var f file if err := toml.Unmarshal(b, &f); err != nil { return nil, err } return f.Caller, nil } func Revoke(path, name string) error { b, err := os.ReadFile(path) if err != nil { return err } var f file if err := toml.Unmarshal(b, &f); err != nil { return err } out := f.Caller[:0] for _, c := range f.Caller { if c.Name != name { out = append(out, c) } } f.Caller = out raw, err := toml.Marshal(f) if err != nil { return err } return os.WriteFile(path, raw, 0o640) }