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 }