Files
sift/internal/auth/auth.go
T
kyle 9444a35322 p1: /v2/scrape, /v1/fetch, auth, CLI, contract tests
Single Fetcher.Fetch on the Guard that holds DenyPrefixes. skipTlsVerification
is ignored. No 502. keys add writes the secret only to --out. Release binary
has no -test-allow-prefix.
2026-09-22 21:54:01 -07:00

156 lines
3.0 KiB
Go

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)
}