diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 0000000..f76475d --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,14 @@ +package errs + +import "runtime" + +const ( + ErrNotFound = KeyringError("secret not found in keyring") + ErrUnsupportedPlatform = KeyringError("Unsupported platform: " + runtime.GOOS) +) + +type KeyringError string + +func (e KeyringError) Error() string { + return string(e) +} diff --git a/keyring.go b/keyring.go index 0b94ad5..f7792eb 100644 --- a/keyring.go +++ b/keyring.go @@ -1,15 +1,15 @@ package keyring -import "fmt" +import errs "github.com/zalando/go-keyring/errors" // provider set in the init function by the relevant os file e.g.: // keyring_linux.go var provider Keyring = fallbackServiceProvider{} -var ( +const ( // ErrNotFound is the expected error if the secret isn't found in the // keyring. - ErrNotFound = fmt.Errorf("secret not found in keyring") + ErrNotFound = errs.ErrNotFound ) // Keyring provides a simple set/get interface for a keyring service. diff --git a/keyring_fallback.go b/keyring_fallback.go index cda7f6a..a195db5 100644 --- a/keyring_fallback.go +++ b/keyring_fallback.go @@ -1,12 +1,13 @@ package keyring import ( - "errors" - "runtime" + errs "github.com/zalando/go-keyring/errors" ) // All of the following methods error out on unsupported platforms -var ErrUnsupportedPlatform = errors.New("Unsupported platform: " + runtime.GOOS) +const ( + ErrUnsupportedPlatform = errs.ErrUnsupportedPlatform +) type fallbackServiceProvider struct{} diff --git a/keyring_linux.go b/keyring_linux.go index 9d65533..80220f2 100644 --- a/keyring_linux.go +++ b/keyring_linux.go @@ -1,120 +1,21 @@ package keyring import ( - "fmt" - dbus "github.com/godbus/dbus/v5" - "github.com/zalando/go-keyring/secret_service" + kw "github.com/zalando/go-keyring/kwallet" + ss "github.com/zalando/go-keyring/secret_service" ) -type secretServiceProvider struct{} - -// Set stores user and pass in the keyring under the defined service -// name. -func (s secretServiceProvider) Set(service, user, pass string) error { - svc, err := ss.NewSecretService() - if err != nil { - return err - } - - // open a session - session, err := svc.OpenSession() - if err != nil { - return err - } - defer svc.Close(session) - - attributes := map[string]string{ - "username": user, - "service": service, - } - - secret := ss.NewSecret(session.Path(), pass) - - collection := svc.GetLoginCollection() - - err = svc.Unlock(collection.Path()) - if err != nil { - return err - } - - err = svc.CreateItem(collection, - fmt.Sprintf("Password for '%s' on '%s'", user, service), - attributes, secret) - if err != nil { - return err - } - - return nil -} - -// findItem looksup an item by service and user. -func (s secretServiceProvider) findItem(svc *ss.SecretService, service, user string) (dbus.ObjectPath, error) { - collection := svc.GetLoginCollection() - - search := map[string]string{ - "username": user, - "service": service, - } - - err := svc.Unlock(collection.Path()) - if err != nil { - return "", err - } - - results, err := svc.SearchItems(collection, search) - if err != nil { - return "", err - } - - if len(results) == 0 { - return "", ErrNotFound - } - - return results[0], nil -} - -// Get gets a secret from the keyring given a service name and a user. -func (s secretServiceProvider) Get(service, user string) (string, error) { - svc, err := ss.NewSecretService() - if err != nil { - return "", err - } - - item, err := s.findItem(svc, service, user) - if err != nil { - return "", err - } - - // open a session - session, err := svc.OpenSession() - if err != nil { - return "", err - } - defer svc.Close(session) - - secret, err := svc.GetSecret(item, session.Path()) - if err != nil { - return "", err - } - - return string(secret.Value), nil -} - -// Delete deletes a secret, identified by service & user, from the keyring. -func (s secretServiceProvider) Delete(service, user string) error { - svc, err := ss.NewSecretService() - if err != nil { - return err - } - - item, err := s.findItem(svc, service, user) - if err != nil { - return err - } - - return svc.Delete(item) -} - func init() { - provider = secretServiceProvider{} + // default to secret service and fall back to kwallet — most systems will only + // have one of the two available anyways + secretService, err := ss.NewSecretService() + if err == nil { + provider = secretService + return + } + kwallet, err := kw.NewKWallet() + if err == nil { + provider = kwallet + return + } } diff --git a/kwallet/kwallet.go b/kwallet/kwallet.go new file mode 100644 index 0000000..0ec6451 --- /dev/null +++ b/kwallet/kwallet.go @@ -0,0 +1,140 @@ +package kw + +import ( + "errors" + "fmt" + + "github.com/godbus/dbus/v5" + errs "github.com/zalando/go-keyring/errors" +) + +const ( + serviceName = "org.kde.kwalletd5" + servicePath = "/modules/kwalletd5" + methodInterface = "org.kde.KWallet" +) + +// KWallet is an interface for the KWallet dbus API. +type KWallet struct { + *dbus.Conn + object dbus.BusObject + walletName string + handle int +} + +// NewKWallet inializes a new NewKwallet object. +func NewKWallet() (*KWallet, error) { + conn, err := dbus.SessionBus() + if err != nil { + return nil, err + } + + kw := &KWallet{ + Conn: conn, + object: conn.Object(serviceName, servicePath), + } + + kw.walletName, err = kw.defaultWallet() + return kw, err +} + +// Set stores user and pass in the keyring under the defined service +// name. +func (k *KWallet) Set(service, user, pass string) error { + if err := k.open(service); err != nil { + return err + } + + var i int + // org.kde.KWallet.writePassword(handle int, folder string, key string, value string, appId string) int + if err := k.object.Call(methodInterface+".writePassword", 0, k.handle, service, user, pass, service).Store(&i); err != nil { + return fmt.Errorf("failed to write password: %w", err) + } + if i < 0 { + return errors.New("Could not write password") + } + return nil +} + +// Get gets a secret from the keyring given a service name and a user. +func (k *KWallet) Get(service, user string) (string, error) { + if err := k.open(service); err != nil { + return "", err + } + if b, err := k.hasEntry(service, user); err != nil { + return "", err + } else if !b { + return "", errs.ErrNotFound + } + + var password string + // org.kde.KWallet.readPassword(handle int, folder string, key string, appId string) string + if err := k.object.Call(methodInterface+".readPassword", 0, k.handle, service, user, service).Store(&password); err != nil { + return "", fmt.Errorf("failed to read password: %w", err) + } + return password, nil +} + +// Delete deletes a secret, identified by service & user, from the keyring. +func (k *KWallet) Delete(service, user string) error { + if err := k.open(service); err != nil { + return err + } + + if b, err := k.hasEntry(service, user); err != nil { + return err + } else if !b { + return errs.ErrNotFound + } + + return k.removeEntry(service, user) +} + +func (k *KWallet) open(service string) error { + var alreadyOpen bool + // org.kde.KWallet.isOpen(wallet string) bool + if err := k.object.Call(methodInterface+".isOpen", 0, k.handle).Store(&alreadyOpen); err != nil { + return fmt.Errorf("failed to check if wallet is open: %w", err) + } + if alreadyOpen { + return nil + } + + // org.kde.KWallet.open(wallet string, wId string, appId string) int + if err := k.object.Call(methodInterface+".open", 0, k.walletName, int64(0), service).Store(&k.handle); err != nil { + return fmt.Errorf("failed to open wallet: %w", err) + } + return nil +} + +func (k *KWallet) defaultWallet() (string, error) { + var wallet string + // org.kde.KWallet.networkWallet() string + if err := k.object.Call(methodInterface+".networkWallet", 0).Store(&wallet); err != nil { + return "", fmt.Errorf("KWallet is not available: %w", err) + } + + return wallet, nil +} + +func (k *KWallet) removeEntry(service, key string) error { + var i int + // org.kde.KWallet.removeEntry(handle int, folder string, key string, appId string) int + if err := k.object.Call(methodInterface+".removeEntry", 0, k.handle, service, key, service).Store(&i); err != nil { + return fmt.Errorf("failed to delete entry: %w", err) + } + if i < 0 { + return errors.New("Could not delete password") + } + + return nil +} + +func (k *KWallet) hasEntry(service, key string) (bool, error) { + var b bool + // org.kde.KWallet.hasEntry(handle int, folder string, key string, appId string) bool + if err := k.object.Call(methodInterface+".hasEntry", 0, k.handle, service, key, service).Store(&b); err != nil { + return b, fmt.Errorf("failed to check if entry exists: %w", err) + } + return b, nil +} diff --git a/secret_service/secret_service.go b/secret_service/secret_service.go index e4644b1..0a18986 100644 --- a/secret_service/secret_service.go +++ b/secret_service/secret_service.go @@ -1,10 +1,11 @@ package ss import ( + "errors" "fmt" - "errors" dbus "github.com/godbus/dbus/v5" + errs "github.com/zalando/go-keyring/errors" ) const ( @@ -52,10 +53,25 @@ func NewSecretService() (*SecretService, error) { return nil, err } - return &SecretService{ + s := &SecretService{ conn, conn.Object(serviceName, servicePath), - }, nil + } + + session, err := s.OpenSession() + if err != nil { + return nil, fmt.Errorf("failed to open secret service session: %w", err) + } + defer s.Close(session) + + // check that the secret service backend is available + collection := s.GetLoginCollection() + err = s.Unlock(collection.Path()) + if err != nil { + return nil, fmt.Errorf("failed to open secret service session: %w", err) + } + + return s, nil } // OpenSession opens a secret service session. @@ -70,6 +86,82 @@ func (s *SecretService) OpenSession() (dbus.BusObject, error) { return s.Object(serviceName, sessionPath), nil } +func (s *SecretService) Set(service, user, password string) error { + // open a session + session, err := s.OpenSession() + if err != nil { + return err + } + defer s.Close(session) + + attributes := map[string]string{ + "username": user, + "service": service, + } + + secret := NewSecret(session.Path(), password) + + collection := s.GetLoginCollection() + + err = s.Unlock(collection.Path()) + if err != nil { + return err + } + + return s.CreateItem( + collection, + fmt.Sprintf("Password for '%s' on '%s'", user, service), + attributes, + secret, + ) +} + +func (s *SecretService) Get(service, user string) (string, error) { + item, err := s.find(service, user) + if err != nil { + return "", err + } + + session, err := s.OpenSession() + if err != nil { + return "", err + } + defer s.Close(session) + + secret, err := s.GetSecret(item, session.Path()) + if err != nil { + return "", err + } + + return string(secret.Value), nil +} + +func (s *SecretService) find(service, user string) (dbus.ObjectPath, error) { + collection := s.GetLoginCollection() + + err := s.Unlock(collection.Path()) + if err != nil { + return "", err + } + + results, err := s.SearchItems( + collection, + map[string]string{ + "username": user, + "service": service, + }, + ) + if err != nil { + return "", err + } + + if len(results) == 0 { + return "", errs.ErrNotFound + } + + return results[0], nil +} + // CheckCollectionPath accepts dbus path and returns nil if the path is found // in the collection interface (and can be used). func (s *SecretService) CheckCollectionPath(path dbus.ObjectPath) error { @@ -229,9 +321,13 @@ func (s *SecretService) GetSecret(itemPath dbus.ObjectPath, session dbus.ObjectP } // Delete deletes an item from the collection. -func (s *SecretService) Delete(itemPath dbus.ObjectPath) error { +func (s *SecretService) Delete(service, user string) error { + itemPath, err := s.find(service, user) + if err != nil { + return err + } var prompt dbus.ObjectPath - err := s.Object(serviceName, itemPath).Call(itemInterface+".Delete", 0).Store(&prompt) + err = s.Object(serviceName, itemPath).Call(itemInterface+".Delete", 0).Store(&prompt) if err != nil { return err }