package auth import ( "crypto/rand" "encoding/base64" "fmt" "log/slog" "sync" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" ) // APIKey manages a bounded, in-memory constant table of API keys, each bound to // a Role so keys can be minted for admin/owner to reach administrative // endpoints. Keys are generated with crypto/rand and stored by their string // form. This suits the current single-binary milestone; production should // persist keys (e.g. hashed) and support revocation. type APIKey struct { mu sync.RWMutex cfg config.AuthConfig keys map[string]Role log *slog.Logger } // NewAPIKey returns an empty key manager. cfg is accepted for constructor // parity and future persistence settings; keys are runtime-generated. func NewAPIKey(cfg config.AuthConfig) *APIKey { return &APIKey{ cfg: cfg, keys: make(map[string]Role), log: logger.WithSource(slog.Default(), source), } } // Generate mints a new random key bound to role and stores it. Returns the // plaintext key exactly once; it must not be logged. func (k *APIKey) Generate(role Role) (string, error) { if !role.Valid() { return "", fmt.Errorf("%w: %q", ErrInvalidRole, role) } raw := make([]byte, keyLength) if _, err := rand.Read(raw); err != nil { return "", fmt.Errorf("auth: generate api key: %w", err) } key := base64.RawURLEncoding.EncodeToString(raw) k.mu.Lock() k.keys[key] = role k.mu.Unlock() return key, nil } // Verify reports whether key is a known, unrevoked API key. func (k *APIKey) Verify(key string) bool { _, ok := k.Lookup(key) return ok } // Lookup returns the role bound to key. func (k *APIKey) Lookup(key string) (Role, bool) { k.mu.RLock() defer k.mu.RUnlock() role, ok := k.keys[key] return role, ok } // Revoke removes key from the constant table. func (k *APIKey) Revoke(key string) { k.mu.Lock() defer k.mu.Unlock() delete(k.keys, key) }