package crypto // KeyResolver decrypts API keys using the appropriate tier: // - global/team → env-derived key // - personal → user's UEK from cache type KeyResolver struct { envKey []byte // derived from ENCRYPTION_KEY uekCache *UEKCache } // NewKeyResolver creates a resolver. envKey may be nil if ENCRYPTION_KEY is // not set (fresh install with no providers — decryption will fail gracefully). func NewKeyResolver(envKey []byte, cache *UEKCache) *KeyResolver { return &KeyResolver{envKey: envKey, uekCache: cache} } // Decrypt resolves an API key given its encrypted form, nonce, scope, and // the requesting user's ID (needed for personal-scope keys). func (r *KeyResolver) Decrypt(ciphertext, nonce []byte, keyScope, userID string) (string, error) { if len(ciphertext) == 0 { return "", nil // No key stored } switch keyScope { case "global", "team": if r.envKey == nil { return "", ErrNoEncryptionKey } return Decrypt(ciphertext, nonce, r.envKey) case "personal": uek, ok := r.uekCache.Load(userID) if !ok { return "", ErrVaultLocked } return Decrypt(ciphertext, nonce, uek) default: // Fallback: try env key (covers pre-migration or unknown scopes) if r.envKey == nil { return "", ErrNoEncryptionKey } return Decrypt(ciphertext, nonce, r.envKey) } } // EncryptForScope encrypts an API key with the appropriate key for the scope. func (r *KeyResolver) EncryptForScope(plaintext, keyScope, userID string) ([]byte, []byte, error) { if plaintext == "" { return nil, nil, nil } switch keyScope { case "personal": uek, ok := r.uekCache.Load(userID) if !ok { return nil, nil, ErrVaultLocked } return Encrypt(plaintext, uek) default: // global, team if r.envKey == nil { return nil, nil, ErrNoEncryptionKey } return Encrypt(plaintext, r.envKey) } } // HasEnvKey returns true if the platform encryption key is available. func (r *KeyResolver) HasEnvKey() bool { return r.envKey != nil }