Compare commits

...

4 Commits

Author SHA1 Message Date
brent s. aa8aef4ccf
adding convenience function to check (in a very basic manner) if an API spec is legacy. 2022-01-09 17:11:50 -05:00
brent s. d13b263222
wrap errors
Implement error wrapping so we catch all errors into a single error.
2022-01-09 15:53:50 -05:00
brent s. b4419a6f8c
Workaround for missing Type property
Some poor decisions that have been made made by KeePassXC lead to the case where they diverge from libsecret's implementation and implement their own incompatible API in the same Dbus namespace.

But they still call their API "Secret Service".

 Well then.
2022-01-09 00:56:56 -05:00
brent s. bb85cb8b52
cleanly close, catch Dbus errors 2021-12-25 01:51:49 -05:00
13 changed files with 322 additions and 153 deletions

View File

@ -224,6 +224,8 @@ func main() {
} }
---- ----


Note that many functions/methods may return a https://pkg.go.dev/r00t2.io/goutils/multierr#MultiError[`(r00t2.io/goutils/)multierr.MultiError`^], which you may attempt to typeswitch to receive the original errors in their native error format. The functions/methods which may return a MultiError are noted as such in their individual documentation.

== Library Hacking == Library Hacking


=== Reference === Reference

2
TODO
View File

@ -1 +1,3 @@
- Benchmarking? - Benchmarking?

- call .Close() on dbus.Conns

View File

@ -4,6 +4,7 @@ import (
"time" "time"


"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"r00t2.io/goutils/multierr"
) )


/* /*
@ -65,6 +66,7 @@ func NewCollection(service *Service, path dbus.ObjectPath) (coll *Collection, er
*/ */
func (c *Collection) CreateItem(label string, attrs map[string]string, secret *Secret, replace bool, itemType ...string) (item *Item, err error) { func (c *Collection) CreateItem(label string, attrs map[string]string, secret *Secret, replace bool, itemType ...string) (item *Item, err error) {


var call *dbus.Call
var prompt *Prompt var prompt *Prompt
var path dbus.ObjectPath var path dbus.ObjectPath
var promptPath dbus.ObjectPath var promptPath dbus.ObjectPath
@ -79,14 +81,20 @@ func (c *Collection) CreateItem(label string, attrs map[string]string, secret *S
} }


props[DbusItemLabel] = dbus.MakeVariant(label) props[DbusItemLabel] = dbus.MakeVariant(label)
props[DbusItemType] = dbus.MakeVariant(typeString) if !c.service.Legacy {
props[DbusItemType] = dbus.MakeVariant(typeString)
}
props[DbusItemAttributes] = dbus.MakeVariant(attrs) props[DbusItemAttributes] = dbus.MakeVariant(attrs)
props[DbusItemCreated] = dbus.MakeVariant(uint64(time.Now().Unix())) props[DbusItemCreated] = dbus.MakeVariant(uint64(time.Now().Unix()))
// props[DbusItemModified] = dbus.MakeVariant(uint64(time.Now().Unix())) // props[DbusItemModified] = dbus.MakeVariant(uint64(time.Now().Unix()))


if err = c.Dbus.Call( if call = c.Dbus.Call(
DbusCollectionCreateItem, 0, props, secret, replace, DbusCollectionCreateItem, 0, props, secret, replace,
).Store(&path, &promptPath); err != nil { ); call.Err != nil {
err = call.Err
return
}
if err = call.Store(&path, &promptPath); err != nil {
return return
} }


@ -114,10 +122,17 @@ func (c *Collection) CreateItem(label string, attrs map[string]string, secret *S
*/ */
func (c *Collection) Delete() (err error) { func (c *Collection) Delete() (err error) {


var call *dbus.Call
var promptPath dbus.ObjectPath var promptPath dbus.ObjectPath
var prompt *Prompt var prompt *Prompt


if err = c.Dbus.Call(DbusCollectionDelete, 0).Store(&promptPath); err != nil { if call = c.Dbus.Call(
DbusCollectionDelete, 0,
); call.Err != nil {
err = call.Err
return
}
if err = call.Store(&promptPath); err != nil {
return return
} }


@ -132,13 +147,16 @@ func (c *Collection) Delete() (err error) {
return return
} }


// Items returns a slice of Item pointers in the Collection. /*
Items returns a slice of Item pointers in the Collection.
err MAY be a *multierr.MultiError.
*/
func (c *Collection) Items() (items []*Item, err error) { func (c *Collection) Items() (items []*Item, err error) {


var paths []dbus.ObjectPath var paths []dbus.ObjectPath
var item *Item var item *Item
var variant dbus.Variant var variant dbus.Variant
var errs []error = make([]error, 0) var errs *multierr.MultiError = multierr.NewMultiError()


if variant, err = c.Dbus.GetProperty(DbusCollectionItems); err != nil { if variant, err = c.Dbus.GetProperty(DbusCollectionItems); err != nil {
return return
@ -151,13 +169,16 @@ func (c *Collection) Items() (items []*Item, err error) {
for _, path := range paths { for _, path := range paths {
item = nil item = nil
if item, err = NewItem(c, path); err != nil { if item, err = NewItem(c, path); err != nil {
errs = append(errs, err) errs.AddError(err)
err = nil err = nil
continue continue
} }
items = append(items, item) items = append(items, item)
} }
err = NewErrors(err)
if !errs.IsEmpty() {
err = errs
}


return return
} }
@ -234,25 +255,32 @@ func (c *Collection) Relabel(newLabel string) (err error) {
} }


/* /*
SearchItems searches a Collection for a matching profile string. SearchItems searches a Collection for a matching "profile" string.
It's mostly a carry-over from go-libsecret, and is here for convenience. IT MAY BE REMOVED IN THE FUTURE. It's mostly a carry-over from go-libsecret, and is here for convenience. IT MAY BE REMOVED IN THE FUTURE.


I promise it's not useful for any other implementation/storage of SecretService whatsoever. I promise it's not useful for any other implementation/storage of SecretService whatsoever.


err MAY be a *multierr.MultiError.

Deprecated: Use Service.SearchItems instead. Deprecated: Use Service.SearchItems instead.
*/ */
func (c *Collection) SearchItems(profile string) (items []*Item, err error) { func (c *Collection) SearchItems(profile string) (items []*Item, err error) {


var call *dbus.Call
var paths []dbus.ObjectPath var paths []dbus.ObjectPath
var errs []error = make([]error, 0) var errs *multierr.MultiError = multierr.NewMultiError()
var attrs map[string]string = make(map[string]string, 0) var attrs map[string]string = make(map[string]string, 0)
var item *Item var item *Item


attrs["profile"] = profile attrs["profile"] = profile


if err = c.Dbus.Call( if call = c.Dbus.Call(
DbusCollectionSearchItems, 0, attrs, DbusCollectionSearchItems, 0, attrs,
).Store(&paths); err != nil { ); call.Err != nil {
err = call.Err
return
}
if err = call.Store(&paths); err != nil {
return return
} }


@ -261,13 +289,16 @@ func (c *Collection) SearchItems(profile string) (items []*Item, err error) {
for _, path := range paths { for _, path := range paths {
item = nil item = nil
if item, err = NewItem(c, path); err != nil { if item, err = NewItem(c, path); err != nil {
errs = append(errs, err) errs.AddError(err)
err = nil err = nil
continue continue
} }
items = append(items, item) items = append(items, item)
} }
err = NewErrors(err)
if !errs.IsEmpty() {
err = errs
}


return return
} }
@ -277,11 +308,10 @@ func (c *Collection) SetAlias(alias string) (err error) {


var call *dbus.Call var call *dbus.Call


call = c.service.Dbus.Call( if call = c.service.Dbus.Call(
DbusServiceSetAlias, 0, alias, c.Dbus.Path(), DbusServiceSetAlias, 0, alias, c.Dbus.Path(),
) ); call.Err != nil {

err = call.Err
if err = call.Err; err != nil {
return return
} }



View File

@ -1,9 +1,9 @@
package gosecret package gosecret


import ( import (
`testing` "testing"


`github.com/godbus/dbus/v5` "github.com/godbus/dbus/v5"
) )


// Some functions are covered in the Service tests. // Some functions are covered in the Service tests.
@ -57,7 +57,8 @@ func TestCollection_Items(t *testing.T) {
var collection *Collection var collection *Collection
var items []*Item var items []*Item
var item *Item var item *Item
var searchItemResults []*Item var searchResultsUnlocked []*Item
var searchResultsLocked []*Item
var secret *Secret var secret *Secret
var err error var err error


@ -109,12 +110,12 @@ func TestCollection_Items(t *testing.T) {
) )
} else { } else {


if searchItemResults, err = collection.SearchItems(testItemLabel); err != nil { if searchResultsUnlocked, searchResultsLocked, err = collection.service.SearchItems(itemAttrs); err != nil {
t.Errorf("failed to find item '%v' via Collection.SearchItems: %v", string(item.Dbus.Path()), err.Error()) t.Errorf("failed to find item '%v' via Collection.SearchItems: %v", string(item.Dbus.Path()), err.Error())
} else if len(searchItemResults) == 0 { } else if (len(searchResultsLocked) + len(searchResultsUnlocked)) == 0 {
t.Errorf("failed to find item '%v' via Collection.SearchItems, returned 0 results (should be at least 1)", testItemLabel) t.Errorf("failed to find item '%v' via Collection.SearchItems, returned 0 results (should be at least 1)", testItemLabel)
} else { } else {
t.Logf("found %v results for Collection.SearchItems", len(searchItemResults)) t.Logf("found %v results for Collection.SearchItems", len(searchResultsUnlocked)+len(searchResultsLocked))
} }


if err = item.Delete(); err != nil { if err = item.Delete(); err != nil {

4
doc.go
View File

@ -84,5 +84,9 @@ Usage


Full documentation can be found via inline documentation. Full documentation can be found via inline documentation.
Additionally, use either https://pkg.go.dev/r00t2.io/gosecret or https://pkg.go.dev/golang.org/x/tools/cmd/godoc (or `go doc`) in the source root. Additionally, use either https://pkg.go.dev/r00t2.io/gosecret or https://pkg.go.dev/golang.org/x/tools/cmd/godoc (or `go doc`) in the source root.

Note that many functions/methods may return a (r00t2.io/goutils/)multierr.MultiError (https://pkg.go.dev/r00t2.io/goutils/multierr#MultiError),
which you may attempt to typeswitch back to a *multierr.MultiErr to receive the original errors in their native error format (MultiError.Errors).
The functions/methods which may return a MultiError are noted as such in their individual documentation.
*/ */
package gosecret package gosecret

View File

@ -4,6 +4,7 @@ import (
`strings` `strings`


`github.com/godbus/dbus/v5` `github.com/godbus/dbus/v5`
`r00t2.io/goutils/multierr`
) )


// isPrompt returns a boolean that is true if path is/requires a prompt(ed path) and false if it is/does not. // isPrompt returns a boolean that is true if path is/requires a prompt(ed path) and false if it is/does not.
@ -63,19 +64,27 @@ func pathIsValid(path interface{}) (ok bool, err error) {


/* /*
validConnPath condenses the checks for connIsValid and pathIsValid into one func due to how frequently this check is done. validConnPath condenses the checks for connIsValid and pathIsValid into one func due to how frequently this check is done.
err is a MultiError, which can be treated as an error.error. (See https://pkg.go.dev/builtin#error)
If err is not nil, it IS a *multierr.MultiError.
*/ */
func validConnPath(conn *dbus.Conn, path interface{}) (cr *ConnPathCheckResult, err error) { func validConnPath(conn *dbus.Conn, path interface{}) (cr *ConnPathCheckResult, err error) {


var connErr error var errs *multierr.MultiError = multierr.NewMultiError()
var pathErr error


cr = new(ConnPathCheckResult) cr = new(ConnPathCheckResult)


cr.ConnOK, connErr = connIsValid(conn) if cr.ConnOK, err = connIsValid(conn); err != nil {
cr.PathOK, pathErr = pathIsValid(path) errs.AddError(err)
err = nil
}
if cr.PathOK, err = pathIsValid(path); err != nil {
errs.AddError(err)
err = nil
}


err = NewErrors(connErr, pathErr) if !errs.IsEmpty() {
err = errs
}


return return
} }
@ -144,3 +153,56 @@ func NameFromPath(path dbus.ObjectPath) (name string, err error) {


return return
} }

/*
CheckErrIsFromLegacy takes an error.Error from e.g.:

Service.SearchItems
Collection.CreateItem
NewItem
Item.ChangeItemType
Item.Type

and (in order) attempt to typeswitch to a *multierr.MultiError, then iterate through
the *multierr.MultiError.Errors, attempt to typeswitch each of them to a Dbus.Error, and then finally
check if it is regarding a missing Type property.

This is *very explicitly* only useful for the above functions/methods. If used anywhere else,
it's liable to return an incorrect isLegacy even if parsed == true.

It is admittedly convoluted and obtuse, but this saves a lot of boilerplate for users.
It wouldn't be necessary if projects didn't insist on using the legacy draft SecretService specification.
But here we are.

isLegacy is true if this Service's API destination is legacy spec. Note that this is checking for
very explicit conditions; isLegacy may return false but it is in fact running on a legacy API.
Don't rely on this too much.

parsed is true if we found an error type we were able to perform logic of determination on.
*/
func CheckErrIsFromLegacy(err error) (isLegacy, parsed bool) {

switch e := err.(type) {
case *multierr.MultiError:
parsed = true
for _, i := range e.Errors {
switch e2 := i.(type) {
case dbus.Error:
if e2.Name == "org.freedesktop.DBus.Error.UnknownProperty" {
isLegacy = true
return
}
default:
continue
}
}
case dbus.Error:
parsed = true
if e.Name == "org.freedesktop.DBus.Error.UnknownProperty" {
isLegacy = true
return
}
}

return
}

1
go.mod
View File

@ -5,4 +5,5 @@ go 1.17
require ( require (
github.com/godbus/dbus/v5 v5.0.6 github.com/godbus/dbus/v5 v5.0.6
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
r00t2.io/goutils v1.1.2
) )

5
go.sum
View File

@ -1,4 +1,9 @@
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
r00t2.io/goutils v1.1.2 h1:zOOqNHQ/HpJVggV5NTXBcd7FQtBP2C/sMLkHw3YvBzU=
r00t2.io/goutils v1.1.2/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=

View File

@ -85,6 +85,11 @@ func (i *Item) ChangeItemType(newItemType string) (err error) {


var variant dbus.Variant var variant dbus.Variant


// Legacy spec.
if i.collection.service.Legacy {
return
}

if strings.TrimSpace(newItemType) == "" { if strings.TrimSpace(newItemType) == "" {
newItemType = DbusDefaultItemType newItemType = DbusDefaultItemType
} }
@ -106,10 +111,17 @@ func (i *Item) ChangeItemType(newItemType string) (err error) {
// Delete removes an Item from a Collection. // Delete removes an Item from a Collection.
func (i *Item) Delete() (err error) { func (i *Item) Delete() (err error) {


var call *dbus.Call
var promptPath dbus.ObjectPath var promptPath dbus.ObjectPath
var prompt *Prompt var prompt *Prompt


if err = i.Dbus.Call(DbusItemDelete, 0).Store(&promptPath); err != nil { if call = i.Dbus.Call(
DbusItemDelete, 0,
); call.Err != nil {
err = call.Err
return
}
if err = call.Store(&promptPath); err != nil {
return return
} }


@ -127,6 +139,8 @@ func (i *Item) Delete() (err error) {
// GetSecret returns the Secret in an Item using a Session. // GetSecret returns the Secret in an Item using a Session.
func (i *Item) GetSecret(session *Session) (secret *Secret, err error) { func (i *Item) GetSecret(session *Session) (secret *Secret, err error) {


var call *dbus.Call

if session == nil { if session == nil {
err = ErrNoDbusConn err = ErrNoDbusConn
} }
@ -135,9 +149,13 @@ func (i *Item) GetSecret(session *Session) (secret *Secret, err error) {
return return
} }


if err = i.Dbus.Call( if call = i.Dbus.Call(
DbusItemGetSecret, 0, session.Dbus.Path(), DbusItemGetSecret, 0, session.Dbus.Path(),
).Store(&secret); err != nil { ); call.Err != nil {
err = call.Err
return
}
if err = call.Store(&secret); err != nil {
return return
} }


@ -246,15 +264,15 @@ func (i *Item) ReplaceAttributes(newAttrs map[string]string) (err error) {
// SetSecret sets the Secret for an Item. // SetSecret sets the Secret for an Item.
func (i *Item) SetSecret(secret *Secret) (err error) { func (i *Item) SetSecret(secret *Secret) (err error) {


var c *dbus.Call var call *dbus.Call


c = i.Dbus.Call( if call = i.Dbus.Call(
DbusItemSetSecret, 0, DbusItemSetSecret, 0,
) ); call.Err != nil {
if c.Err != nil { err = call.Err
err = c.Err
return return
} }

i.Secret = secret i.Secret = secret


if _, _, err = i.Modified(); err != nil { if _, _, err = i.Modified(); err != nil {
@ -269,6 +287,11 @@ func (i *Item) Type() (itemType string, err error) {


var variant dbus.Variant var variant dbus.Variant


// Legacy spec.
if i.collection.service.Legacy {
return
}

if variant, err = i.Dbus.GetProperty(DbusItemType); err != nil { if variant, err = i.Dbus.GetProperty(DbusItemType); err != nil {
return return
} }

View File

@ -1,57 +0,0 @@
package gosecret

import (
"fmt"
)

/*
NewErrors returns a new MultiError based on a slice of error.Error (errs).
Any nil errors are trimmed. If there are no actual errors after trimming, err will be nil.
*/
func NewErrors(errs ...error) (err error) {

if errs == nil || len(errs) == 0 {
return
}

var realErrs []error = make([]error, 0)

for _, e := range errs {
if e == nil {
continue
}
realErrs = append(realErrs, e)
}

if len(realErrs) == 0 {
return
}

err = &MultiError{
Errors: realErrs,
ErrorSep: "\n",
}

return
}

func (e *MultiError) Error() (errStr string) {

var numErrs int

if e == nil || len(e.Errors) == 0 {
return
} else {
numErrs = len(e.Errors)
}

for idx, err := range e.Errors {
if (idx + 1) < numErrs {
errStr += fmt.Sprintf("%v%v", err.Error(), e.ErrorSep)
} else {
errStr += err.Error()
}
}

return
}

View File

@ -5,9 +5,10 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings" "strings"
`time` "time"


"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
`r00t2.io/goutils/multierr`
) )


// NewService returns a pointer to a new Service connection. // NewService returns a pointer to a new Service connection.
@ -38,18 +39,28 @@ func NewService() (service *Service, err error) {
// Close cleanly closes a Service and all its underlying connections (e.g. Service.Session). // Close cleanly closes a Service and all its underlying connections (e.g. Service.Session).
func (s *Service) Close() (err error) { func (s *Service) Close() (err error) {


err = s.Session.Close() if err = s.Session.Close(); err != nil {
return
}

if err = s.Conn.Close(); err != nil {
return
}


return return
} }


// Collections returns a slice of Collection items accessible to this Service. /*
Collections returns a slice of Collection items accessible to this Service.

err MAY be a *multierr.MultiError.
*/
func (s *Service) Collections() (collections []*Collection, err error) { func (s *Service) Collections() (collections []*Collection, err error) {


var paths []dbus.ObjectPath var paths []dbus.ObjectPath
var variant dbus.Variant var variant dbus.Variant
var coll *Collection var coll *Collection
var errs []error = make([]error, 0) var errs *multierr.MultiError = multierr.NewMultiError()


if variant, err = s.Dbus.GetProperty(DbusServiceCollections); err != nil { if variant, err = s.Dbus.GetProperty(DbusServiceCollections); err != nil {
return return
@ -62,14 +73,16 @@ func (s *Service) Collections() (collections []*Collection, err error) {
for _, path := range paths { for _, path := range paths {
coll = nil coll = nil
if coll, err = NewCollection(s, path); err != nil { if coll, err = NewCollection(s, path); err != nil {
// return errs.AddError(err)
errs = append(errs, err)
err = nil err = nil
continue continue
} }
collections = append(collections, coll) collections = append(collections, coll)
} }
err = NewErrors(err)
if !errs.IsEmpty() {
err = errs
}


return return
} }
@ -80,6 +93,7 @@ func (s *Service) Collections() (collections []*Collection, err error) {
*/ */
func (s *Service) CreateAliasedCollection(label, alias string) (collection *Collection, err error) { func (s *Service) CreateAliasedCollection(label, alias string) (collection *Collection, err error) {


var call *dbus.Call
var variant *dbus.Variant var variant *dbus.Variant
var path dbus.ObjectPath var path dbus.ObjectPath
var promptPath dbus.ObjectPath var promptPath dbus.ObjectPath
@ -90,9 +104,13 @@ func (s *Service) CreateAliasedCollection(label, alias string) (collection *Coll
props[DbusCollectionCreated] = dbus.MakeVariant(uint64(time.Now().Unix())) props[DbusCollectionCreated] = dbus.MakeVariant(uint64(time.Now().Unix()))
props[DbusCollectionModified] = dbus.MakeVariant(uint64(time.Now().Unix())) props[DbusCollectionModified] = dbus.MakeVariant(uint64(time.Now().Unix()))


if err = s.Dbus.Call( if call = s.Dbus.Call(
DbusServiceCreateCollection, 0, props, alias, DbusServiceCreateCollection, 0, props, alias,
).Store(&path, &promptPath); err != nil { ); call.Err != nil {
err = call.Err
return
}
if err = call.Store(&path, &promptPath); err != nil {
return return
} }


@ -125,10 +143,12 @@ func (s *Service) CreateCollection(label string) (collection *Collection, err er
/* /*
GetCollection returns a single Collection based on the name (name can also be an alias). GetCollection returns a single Collection based on the name (name can also be an alias).
It's a helper function that avoids needing to make multiple calls in user code. It's a helper function that avoids needing to make multiple calls in user code.

err MAY be a *multierr.MultiError.
*/ */
func (s *Service) GetCollection(name string) (c *Collection, err error) { func (s *Service) GetCollection(name string) (c *Collection, err error) {


var errs []error = make([]error, 0) var errs *multierr.MultiError = multierr.NewMultiError()
var colls []*Collection var colls []*Collection
var pathName string var pathName string


@ -149,7 +169,7 @@ func (s *Service) GetCollection(name string) (c *Collection, err error) {
} }
for _, i := range colls { for _, i := range colls {
if pathName, err = NameFromPath(i.Dbus.Path()); err != nil { if pathName, err = NameFromPath(i.Dbus.Path()); err != nil {
errs = append(errs, err) errs.AddError(err)
err = nil err = nil
continue continue
} }
@ -168,9 +188,8 @@ func (s *Service) GetCollection(name string) (c *Collection, err error) {
} }


// Couldn't find it by the given name. // Couldn't find it by the given name.
if errs != nil || len(errs) > 0 { if !errs.IsEmpty() {
errs = append([]error{ErrDoesNotExist}, errs...) err = errs
err = NewErrors(errs...)
} else { } else {
err = ErrDoesNotExist err = ErrDoesNotExist
} }
@ -197,6 +216,7 @@ func (s *Service) GetSecrets(itemPaths ...dbus.ObjectPath) (secrets map[dbus.Obj
} }
*/ */
var results map[dbus.ObjectPath][]interface{} var results map[dbus.ObjectPath][]interface{}
var call *dbus.Call


if itemPaths == nil || len(itemPaths) == 0 { if itemPaths == nil || len(itemPaths) == 0 {
err = ErrMissingPaths err = ErrMissingPaths
@ -207,9 +227,13 @@ func (s *Service) GetSecrets(itemPaths ...dbus.ObjectPath) (secrets map[dbus.Obj
results = make(map[dbus.ObjectPath][]interface{}, len(itemPaths)) results = make(map[dbus.ObjectPath][]interface{}, len(itemPaths))


// TODO: trigger a Service.Unlock for any locked items? // TODO: trigger a Service.Unlock for any locked items?
if err = s.Dbus.Call( if call = s.Dbus.Call(
DbusServiceGetSecrets, 0, itemPaths, s.Session.Dbus.Path(), DbusServiceGetSecrets, 0, itemPaths, s.Session.Dbus.Path(),
).Store(&results); err != nil { ); call.Err != nil {
err = call.Err
return
}
if err = call.Store(&results); err != nil {
return return
} }


@ -233,9 +257,31 @@ func (s *Service) GetSession() (ssn *Session, err error) {
return return
} }


// Scrapping this idea for now; it would require introspection on a known Item path.
/*
IsLegacy indicates with a decent likelihood of accuracy if this Service is
connected to a legacy spec Secret Service (true) or if the spec is current (false).

It also returns a confidence indicator as a float, which indicates how accurate
the guess (because it is a guess) may/is likely to be (as a percentage). For example,
if confidence is expressed as 0.25, the result of legacyAPI has a 25% of being accurate.
*/
/*
func (s *Service) IsLegacy() (legacyAPI bool, confidence int) {

var maxCon int

// Test 1, property introspection on Item. We're looking for a Type property.
DbusInterfaceItem

return
}
*/

// Lock locks an Unlocked Collection or Item (LockableObject). // Lock locks an Unlocked Collection or Item (LockableObject).
func (s *Service) Lock(objects ...LockableObject) (err error) { func (s *Service) Lock(objects ...LockableObject) (err error) {


var call *dbus.Call
var toLock []dbus.ObjectPath var toLock []dbus.ObjectPath
// We only use these as destinations. // We only use these as destinations.
var locked []dbus.ObjectPath var locked []dbus.ObjectPath
@ -253,9 +299,13 @@ func (s *Service) Lock(objects ...LockableObject) (err error) {
toLock[idx] = o.path() toLock[idx] = o.path()
} }


if err = s.Dbus.Call( if call = s.Dbus.Call(
DbusServiceLock, 0, toLock, DbusServiceLock, 0, toLock,
).Store(&locked, &promptPath); err != nil { ); call.Err != nil {
err = call.Err
return
}
if err = call.Store(&locked, &promptPath); err != nil {
return return
} }


@ -285,6 +335,7 @@ func (s *Service) Lock(objects ...LockableObject) (err error) {
*/ */
func (s *Service) OpenSession(algo, input string) (session *Session, output dbus.Variant, err error) { func (s *Service) OpenSession(algo, input string) (session *Session, output dbus.Variant, err error) {


var call *dbus.Call
var path dbus.ObjectPath var path dbus.ObjectPath
var inputVariant dbus.Variant var inputVariant dbus.Variant


@ -298,9 +349,13 @@ func (s *Service) OpenSession(algo, input string) (session *Session, output dbus
// TODO: confirm this. // TODO: confirm this.
// Possible flags are dbus.Flags consts: https://pkg.go.dev/github.com/godbus/dbus#Flags // Possible flags are dbus.Flags consts: https://pkg.go.dev/github.com/godbus/dbus#Flags
// Oddly, there is no "None" flag. So it's explicitly specified as a null byte. // Oddly, there is no "None" flag. So it's explicitly specified as a null byte.
if err = s.Dbus.Call( if call = s.Dbus.Call(
DbusServiceOpenSession, 0, algo, inputVariant, DbusServiceOpenSession, 0, algo, inputVariant,
).Store(&output, &path); err != nil { ); call.Err != nil {
err = call.Err
return
}
if err = call.Store(&output, &path); err != nil {
return return
} }


@ -316,17 +371,20 @@ func (s *Service) OpenSession(algo, input string) (session *Session, output dbus
*/ */
func (s *Service) ReadAlias(alias string) (collection *Collection, err error) { func (s *Service) ReadAlias(alias string) (collection *Collection, err error) {


var call *dbus.Call
var objectPath dbus.ObjectPath var objectPath dbus.ObjectPath


err = s.Dbus.Call( if call = s.Dbus.Call(
DbusServiceReadAlias, 0, alias, DbusServiceReadAlias, 0, alias,
).Store(&objectPath) ); call.Err != nil {

err = call.Err
return
}
/* /*
TODO: Confirm that a nonexistent alias will NOT cause an error to return. TODO: Confirm that a nonexistent alias will NOT cause an error to return.
If it does, alter the below logic. If it does, alter the below logic.
*/ */
if err != nil { if err = call.Store(&objectPath); err != nil {
return return
} }


@ -355,9 +413,12 @@ func (s *Service) RemoveAlias(alias string) (err error) {


/* /*
SearchItems searches all Collection objects and returns all matches based on the map of attributes. SearchItems searches all Collection objects and returns all matches based on the map of attributes.

err MAY be a *multierr.MultiError.
*/ */
func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []*Item, lockedItems []*Item, err error) { func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []*Item, lockedItems []*Item, err error) {


var call *dbus.Call
var locked []dbus.ObjectPath var locked []dbus.ObjectPath
var unlocked []dbus.ObjectPath var unlocked []dbus.ObjectPath
var collectionObjs []*Collection var collectionObjs []*Collection
@ -366,16 +427,20 @@ func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []*It
var c *Collection var c *Collection
var cPath dbus.ObjectPath var cPath dbus.ObjectPath
var item *Item var item *Item
var errs []error = make([]error, 0) var errs *multierr.MultiError = multierr.NewMultiError()


if attributes == nil || len(attributes) == 0 { if attributes == nil || len(attributes) == 0 {
err = ErrMissingAttrs err = ErrMissingAttrs
return return
} }


err = s.Dbus.Call( if call = s.Dbus.Call(
DbusServiceSearchItems, 0, attributes, DbusServiceSearchItems, 0, attributes,
).Store(&unlocked, &locked) ); call.Err != nil {
}
if err = call.Store(&unlocked, &locked); err != nil {
return
}


lockedItems = make([]*Item, 0) lockedItems = make([]*Item, 0)
unlockedItems = make([]*Item, 0) unlockedItems = make([]*Item, 0)
@ -397,16 +462,17 @@ func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []*It
cPath = dbus.ObjectPath(filepath.Dir(string(i))) cPath = dbus.ObjectPath(filepath.Dir(string(i)))


if c, ok = collections[cPath]; !ok { if c, ok = collections[cPath]; !ok {
errs = append(errs, errors.New(fmt.Sprintf( errs.AddError(errors.New(fmt.Sprintf(
"could not find matching Collection for locked item %v", string(i), "could not find matching Collection for locked item %v", string(i),
))) )))
continue continue
} }


if item, err = NewItem(c, i); err != nil { if item, err = NewItem(c, i); err != nil {
errs = append(errs, errors.New(fmt.Sprintf( errs.AddError(errors.New(fmt.Sprintf(
"could not create Item for locked item %v", string(i), "could not create Item for locked item %v; error follows", string(i),
))) )))
errs.AddError(err)
err = nil err = nil
continue continue
} }
@ -420,24 +486,25 @@ func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []*It
cPath = dbus.ObjectPath(filepath.Dir(string(i))) cPath = dbus.ObjectPath(filepath.Dir(string(i)))


if c, ok = collections[cPath]; !ok { if c, ok = collections[cPath]; !ok {
errs = append(errs, errors.New(fmt.Sprintf( errs.AddError(errors.New(fmt.Sprintf(
"could not find matching Collection for unlocked item %v", string(i), "could not find matching Collection for unlocked item %v", string(i),
))) )))
continue continue
} }


if item, err = NewItem(c, i); err != nil { if item, err = NewItem(c, i); err != nil {
errs = append(errs, errors.New(fmt.Sprintf( errs.AddError(errors.New(fmt.Sprintf(
"could not create Item for unlocked item %v", string(i), "could not create Item for unlocked item %v; error follows", string(i),
))) )))
errs.AddError(err)
err = nil err = nil
continue continue
} }
unlockedItems = append(unlockedItems, item) unlockedItems = append(unlockedItems, item)
} }


if errs != nil && len(errs) > 0 { if !errs.IsEmpty() {
err = NewErrors(errs...) err = errs
} }


return return
@ -450,18 +517,17 @@ func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []*It
*/ */
func (s *Service) SetAlias(alias string, objectPath dbus.ObjectPath) (err error) { func (s *Service) SetAlias(alias string, objectPath dbus.ObjectPath) (err error) {


var c *dbus.Call var call *dbus.Call
var collection *Collection var collection *Collection


if collection, err = s.GetCollection(alias); err != nil { if collection, err = s.GetCollection(alias); err != nil {
return return
} }


c = s.Dbus.Call( if call = s.Dbus.Call(
DbusServiceSetAlias, 0, alias, objectPath, DbusServiceSetAlias, 0, alias, objectPath,
) ); call.Err != nil {

err = call.Err
if err = c.Err; err != nil {
return return
} }


@ -477,6 +543,7 @@ func (s *Service) SetAlias(alias string, objectPath dbus.ObjectPath) (err error)
// Unlock unlocks a locked Collection or Item (LockableObject). // Unlock unlocks a locked Collection or Item (LockableObject).
func (s *Service) Unlock(objects ...LockableObject) (err error) { func (s *Service) Unlock(objects ...LockableObject) (err error) {


var call *dbus.Call
var toUnlock []dbus.ObjectPath var toUnlock []dbus.ObjectPath
// We only use these as destinations. // We only use these as destinations.
var unlocked []dbus.ObjectPath var unlocked []dbus.ObjectPath
@ -494,9 +561,13 @@ func (s *Service) Unlock(objects ...LockableObject) (err error) {
toUnlock[idx] = o.path() toUnlock[idx] = o.path()
} }


if err = s.Dbus.Call( if call = s.Dbus.Call(
DbusServiceUnlock, 0, toUnlock, DbusServiceUnlock, 0, toUnlock,
).Store(&unlocked, &resultPath); err != nil { ); call.Err != nil {
err = call.Err
return
}
if err = call.Store(&unlocked, &resultPath); err != nil {
return return
} }



View File

@ -1,6 +1,8 @@
package gosecret package gosecret


import ( import (
"fmt"

"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )


@ -32,13 +34,21 @@ func NewSession(service *Service, path dbus.ObjectPath) (session *Session, err e
// Close cleanly closes a Session. // Close cleanly closes a Session.
func (s *Session) Close() (err error) { func (s *Session) Close() (err error) {


var c *dbus.Call var call *dbus.Call


c = s.Dbus.Call( if call = s.Dbus.Call(
DbusSessionClose, 0, DbusSessionClose, 0,
) ); call.Err != nil {

/*
_ = c I... still haven't 100% figured out why this happens, but the session DOES seem to close...?
PRs or input welcome.
TODO: figure out why this error gets triggered.
*/
if call.Err.Error() != fmt.Sprintf("The name %v was not provided by any .service files", DbusInterfaceSession) {
err = call.Err
return
}
}


return return
} }

View File

@ -6,16 +6,6 @@ import (
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )


/*
MultiError is a type of error.Error that can contain multiple error.Errors. Confused? Don't worry about it.
*/
type MultiError struct {
// Errors is a slice of errors to combine/concatenate when .Error() is called.
Errors []error `json:"errors"`
// ErrorSep is a string to use to separate errors for .Error(). The default is "\n".
ErrorSep string `json:"separator"`
}

/* /*
SecretServiceError is a translated error from SecretService API. SecretServiceError is a translated error from SecretService API.
See https://developer-old.gnome.org/libsecret/unstable/libsecret-SecretError.html#SecretError and See https://developer-old.gnome.org/libsecret/unstable/libsecret-SecretError.html#SecretError and
@ -74,6 +64,31 @@ type Service struct {
Session *Session `json:"-"` Session *Session `json:"-"`
// IsLocked indicates if the Service is locked or not. Status updated by Service.Locked. // IsLocked indicates if the Service is locked or not. Status updated by Service.Locked.
IsLocked bool `json:"locked"` IsLocked bool `json:"locked"`
/*
Legacy indicates that this SecretService implementation breaks current spec
by implementing the legacy/obsolete draft spec rather than current libsecret spec
for the Dbus API.

If you're using SecretService with KeePassXC, for instance, or a much older version
of Gnome-Keyring *before* libsecret integration(?), or if you are getting strange errors
when performing a Service.SearchItems, you probably need to enable this field on the
Service returned by NewService. The coverage of this field may expand in the future, but
currently it only prevents/suppresses the (non-existent, in legacy spec) Type property
from being read or written on Items during e.g.:

Service.SearchItems
Collection.CreateItem
NewItem
Item.ChangeItemType
Item.Type

It will perform a no-op if enabled in the above contexts to maintain cross-compatability
in codebase between legacy and proper current spec systems, avoiding an error return.

You can use CheckErrIsFromLegacy if Service.Legacy is false and Service.SearchItems returns
a non-nil err to determine if this Service is (probably) interfacing with a legacy spec API.
*/
Legacy bool `json:"is_legacy"`
} }


/* /*
@ -83,7 +98,7 @@ type Service struct {
*/ */
type Session struct { type Session struct {
*DbusObject *DbusObject
// collection tracks the Service this Session was created from. // service tracks the Service this Session was created from.
service *Service service *Service
} }