Compare commits

...

3 Commits

Author SHA1 Message Date
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
d13b263222
wrap errors
Implement error wrapping so we catch all errors into a single error.
2022-01-09 15:53:50 -05:00
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
10 changed files with 191 additions and 103 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

View File

@ -4,6 +4,7 @@ import (
"time" "time"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"r00t2.io/goutils/multierr"
) )
/* /*
@ -80,7 +81,9 @@ func (c *Collection) CreateItem(label string, attrs map[string]string, secret *S
} }
props[DbusItemLabel] = dbus.MakeVariant(label) props[DbusItemLabel] = dbus.MakeVariant(label)
if !c.service.Legacy {
props[DbusItemType] = dbus.MakeVariant(typeString) 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()))
@ -144,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
@ -163,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
} }
@ -246,18 +255,20 @@ 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 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
@ -278,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
} }

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
} }
@ -282,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,58 +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
}
// Error makes a MultiError conform to the error interface.
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

@ -8,6 +8,7 @@ import (
"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.
@ -49,13 +50,17 @@ func (s *Service) Close() (err error) {
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
@ -68,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
} }
@ -136,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
@ -160,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
} }
@ -179,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
} }
@ -249,6 +257,27 @@ 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) {
@ -384,6 +413,8 @@ 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) {
@ -396,7 +427,7 @@ 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
@ -431,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
} }
@ -454,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

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
} }