mcp-framework/secretstore/store.go
2026-04-13 15:33:48 +02:00

90 lines
2 KiB
Go

package secretstore
import (
"errors"
"fmt"
"runtime"
"strings"
"github.com/99designs/keyring"
)
var ErrNotFound = errors.New("secret not found")
type Options struct {
ServiceName string
}
type Store interface {
SetSecret(name, label, secret string) error
GetSecret(name string) (string, error)
DeleteSecret(name string) error
}
type keyringStore struct {
ring keyring.Keyring
serviceName string
}
func Open(options Options) (Store, error) {
serviceName := strings.TrimSpace(options.ServiceName)
if serviceName == "" {
return nil, errors.New("service name must not be empty")
}
ring, err := keyring.Open(keyring.Config{
ServiceName: serviceName,
})
if err != nil {
return nil, fmt.Errorf("open OS wallet backend %q for service %q: %w", BackendName(), serviceName, err)
}
return &keyringStore{
ring: ring,
serviceName: serviceName,
}, nil
}
func BackendName() string {
switch runtime.GOOS {
case "darwin":
return "macOS Keychain"
case "windows":
return "Windows Credential Manager"
case "linux":
return "Linux Secret Service or KWallet"
default:
return "system wallet"
}
}
func (s *keyringStore) SetSecret(name, label, secret string) error {
if err := s.ring.Set(keyring.Item{
Key: name,
Label: label,
Data: []byte(secret),
}); err != nil {
return fmt.Errorf("save secret %q in OS wallet for service %q: %w", name, s.serviceName, err)
}
return nil
}
func (s *keyringStore) GetSecret(name string) (string, error) {
item, err := s.ring.Get(name)
if err != nil {
if errors.Is(err, keyring.ErrKeyNotFound) {
return "", ErrNotFound
}
return "", fmt.Errorf("read secret %q from OS wallet for service %q: %w", name, s.serviceName, err)
}
return string(item.Data), nil
}
func (s *keyringStore) DeleteSecret(name string) error {
if err := s.ring.Remove(name); err != nil && !errors.Is(err, keyring.ErrKeyNotFound) {
return fmt.Errorf("delete secret %q from OS wallet for service %q: %w", name, s.serviceName, err)
}
return nil
}