package auth import ( "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/hex" "fmt" "strings" "sync" "git.warky.dev/wdevs/amcs/internal/config" ) type Keyring struct { mu sync.RWMutex keys []config.APIKey tenantsByKeyID map[string]string managed map[string]managedKey } type managedKey struct { secretHash string enabled bool } func NewKeyring(keys []config.APIKey) (*Keyring, error) { if len(keys) == 0 { return nil, fmt.Errorf("keyring requires at least one key") } tenantsByKeyID := make(map[string]string) for _, key := range keys { if tenantID := strings.TrimSpace(key.TenantID); tenantID != "" { tenantsByKeyID[key.ID] = tenantID } } return &Keyring{keys: append([]config.APIKey(nil), keys...), tenantsByKeyID: tenantsByKeyID, managed: make(map[string]managedKey)}, nil } // NewManagedKeyring is used when API credentials are administered in the // database rather than supplied through static configuration. func NewManagedKeyring() *Keyring { return &Keyring{tenantsByKeyID: make(map[string]string), managed: make(map[string]managedKey)} } func (k *Keyring) Lookup(value string) (string, bool) { k.mu.RLock() defer k.mu.RUnlock() for _, key := range k.keys { if subtle.ConstantTimeCompare([]byte(key.Value), []byte(value)) == 1 { return key.ID, true } } hash := secretHash(value) for keyID, key := range k.managed { if key.enabled && subtle.ConstantTimeCompare([]byte(key.secretHash), []byte(hash)) == 1 { return keyID, true } } return "", false } // TenantForKey returns the tenant boundary assigned to keyID. Unassigned // configured keys retain the historical key-ID boundary for compatibility. func (k *Keyring) TenantForKey(keyID string) string { k.mu.RLock() defer k.mu.RUnlock() if tenantID := k.tenantsByKeyID[keyID]; tenantID != "" { return tenantID } return keyID } func (k *Keyring) AssignTenant(keyID, tenantID string) { k.mu.Lock() defer k.mu.Unlock() if tenantID == "" { delete(k.tenantsByKeyID, keyID) return } k.tenantsByKeyID[keyID] = tenantID } func (k *Keyring) AddManaged(keyID, secretHash string, enabled bool) { k.mu.Lock() defer k.mu.Unlock() k.managed[keyID] = managedKey{secretHash: secretHash, enabled: enabled} } func (k *Keyring) ConfiguredKeys() []config.APIKey { k.mu.RLock() defer k.mu.RUnlock() return append([]config.APIKey(nil), k.keys...) } func (k *Keyring) IsConfigured(keyID string) bool { k.mu.RLock() defer k.mu.RUnlock() for _, key := range k.keys { if key.ID == keyID { return true } } return false } func (k *Keyring) IsSuperadmin(keyID string) bool { k.mu.RLock() defer k.mu.RUnlock() for _, key := range k.keys { if key.ID == keyID { return key.Superadmin } } return false } func (k *Keyring) SetManagedEnabled(keyID string, enabled bool) { k.mu.Lock() defer k.mu.Unlock() if key, ok := k.managed[keyID]; ok { key.enabled = enabled k.managed[keyID] = key } } func GenerateSecret() (string, string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", "", err } secret := "amcs_" + hex.EncodeToString(b) return secret, secretHash(secret), nil } func secretHash(secret string) string { sum := sha256.Sum256([]byte(secret)) return hex.EncodeToString(sum[:]) }