//go:build (dragonfly && cgo) || (freebsd && cgo) || linux || netbsd || openbsd package secret import ( "fmt" "github.com/godbus/dbus/v5" ) const ( dbusServiceName = "org.freedesktop.secrets" dbusServicePath = "/org/freedesktop/secrets" dbusServiceInterface = "org.freedesktop.Secret.Service" dbusServiceClose = dbusServiceInterface + ".Close" dbusServiceUnlock = dbusServiceInterface + ".Unlock" dbusCollectionBasePath = "/org/freedesktop/secrets/collection/" dbusCollectionLoginPath = dbusCollectionBasePath + "login" dbusCollectionLoginAliasPath = "/org/freedesktop/secrets/aliases/default" dbusCollectionInterface = "org.freedesktop.Secret.Collection" dbusCollectionGetSecret = dbusCollectionInterface + ".GetSecret" dbusCollectionSearchItems = dbusCollectionInterface + ".SearchItems" dbusPromptInterface = "org.freedesktop.Secret.Prompt" dbusPrompt = dbusPromptInterface + ".Prompt" dbusPromptCompleted = dbusPromptInterface + ".Completed" ) type keyring struct { service string session *dbus.Conn object dbus.BusObject } type keyringSecret struct { Session dbus.ObjectPath Parameters []byte Value []byte ContentType string `dbus:"content_type"` } func keyringProvider(service string) (Provider, error) { session, err := dbus.SessionBus() if err != nil { return nil, err } return &keyring{ service: service, session: session, object: session.Object(dbusServiceName, dbusServicePath), }, nil } func (p *keyring) Close() error { return p.object.Call(dbusServiceClose, 0).Err } func (p *keyring) GetSecret(key string) (value []byte, err error) { collection := p.getLoginCollection() if err = p.unlock(collection.Path()); err != nil { return } query := map[string]string{ "service": p.service, "username": key, } var results []dbus.ObjectPath if results, err = p.search(collection, query); err != nil { return } else if len(results) == 0 { return nil, NotFound{Key: key} } var secret *keyringSecret if secret, err = p.getSecret(results[0]); err != nil { return } return secret.Value, nil } func (p keyring) checkCollectionPath(here dbus.ObjectPath) bool { obj := p.session.Object(dbusServiceName, dbusServicePath) val, err := obj.GetProperty(dbusCollectionInterface) if err != nil { return false } paths := val.Value().([]dbus.ObjectPath) for _, p := range paths { if p == here { return true } } return false } func (p keyring) getLoginCollection() dbus.BusObject { here := dbus.ObjectPath(dbusCollectionLoginPath) if !p.checkCollectionPath(here) { here = dbus.ObjectPath(dbusCollectionLoginAliasPath) } return p.session.Object(dbusServiceName, here) } func (p *keyring) search(collection dbus.BusObject, query interface{}) (results []dbus.ObjectPath, err error) { err = collection.Call(dbusCollectionSearchItems, 0, query).Store(&results) return } func (p *keyring) getSecret(here dbus.ObjectPath) (secret *keyringSecret, err error) { secret = new(keyringSecret) err = p.session.Object(dbusServiceName, here).Call(dbusCollectionGetSecret, 0).Store(secret) return } func (p *keyring) unlock(here dbus.ObjectPath) (err error) { var ( unlocked []dbus.ObjectPath prompt dbus.ObjectPath ) if err = p.object.Call(dbusServiceUnlock, 0, []dbus.ObjectPath{here}).Store(&unlocked, &prompt); err != nil { return } var variant dbus.Variant if _, variant, err = p.handlePrompt(prompt); err != nil { return } collections := variant.Value() switch t := collections.(type) { case []dbus.ObjectPath: unlocked = append(unlocked, t...) } if len(unlocked) != 1 || (here != dbusCollectionLoginAliasPath && unlocked[0] != here) { return fmt.Errorf("secret: failed to unlock keyring collection %q", here) } return } func (s *keyring) handlePrompt(prompt dbus.ObjectPath) (bool, dbus.Variant, error) { if prompt != dbus.ObjectPath("/") { err := s.session.AddMatchSignal(dbus.WithMatchObjectPath(prompt), dbus.WithMatchInterface(dbusPromptInterface), ) if err != nil { return false, dbus.MakeVariant(""), err } defer func(s *keyring, options ...dbus.MatchOption) { _ = s.session.RemoveMatchSignal(options...) }(s, dbus.WithMatchObjectPath(prompt), dbus.WithMatchInterface(dbusPromptInterface)) promptSignal := make(chan *dbus.Signal, 1) s.session.Signal(promptSignal) err = s.session.Object(dbusServiceName, prompt).Call(dbusPrompt, 0, "").Err if err != nil { return false, dbus.MakeVariant(""), err } signal := <-promptSignal switch signal.Name { case dbusPromptCompleted: dismissed := signal.Body[0].(bool) result := signal.Body[1].(dbus.Variant) return dismissed, result, nil } } return false, dbus.MakeVariant(""), nil }