Compare commits

..

14 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
bb85cb8b52
cleanly close, catch Dbus errors 2021-12-25 01:51:49 -05:00
9d3299c9dc
multierror update, fix output 2021-12-24 05:50:31 -05:00
3f4db5e82c
fix - suppress nil/empty items from being added to slices 2021-12-18 00:57:41 -05:00
fa3061ee7a
docs update; clarification 2021-12-15 02:27:20 -05:00
5b3328f2b9
fix Item.Label; it was not updating Item.LabelName. 2021-12-14 04:36:53 -05:00
16e972c148
typo 2021-12-13 05:50:05 -05:00
0767e9c0c1
fixing various race conditions and errors after refactoring 2021-12-13 05:34:53 -05:00
6dba963608
fixing some race conditions 2021-12-13 04:33:43 -05:00
851cc327e5
fix JSON marshaling for SecretValue. 2021-12-13 04:25:45 -05:00
09f3c9b73e
adding more convenience functions, improve some argument receivers 2021-12-13 04:04:03 -05:00
b9f529ad56
updating TODO 2021-12-13 01:01:03 -05:00
18 changed files with 765 additions and 266 deletions

View File

@ -224,8 +224,13 @@ 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
Aside from the above (INCREDIBLY brief and perhaps slightly inaccurate) introduction to SecretService concepts, it is recommended to see the `.ref/` directory in git. Notably, the `URLS` file profides several excellent resources for understanding SecretService further. The Dbus specification (first URL in the file) is highly recommended if you are unfamiliar with SecretService internals.
=== Tests === Tests
Many functions are consolidated into a single test due to how dependent certain processes are on other objects. However, all functionality should be covered by test cases and the error string will always be passed through the stack to `go test -v` output. Many functions are consolidated into a single test due to how dependent certain processes are on other objects. However, all functionality should be covered by test cases and the error string will always be passed through the stack to `go test -v` output.

12
TODO
View File

@ -1,11 +1,3 @@
- TEST CASES
-- https://pkg.go.dev/testing
-- https://go.dev/doc/tutorial/add-a-test
-- https://gobyexample.com/testing
-- https://blog.alexellis.io/golang-writing-unit-tests/
- Benchmarking? - Benchmarking?
- Example usage
- Merge master into V1 - call .Close() on dbus.Conns
-- and tag release (v1.0.0)
- Merge doc.go and README.adoc to V0
-- and tag release (v0.1.3)

View File

@ -1,10 +1,10 @@
package gosecret package gosecret
import ( import (
"strings"
"time" "time"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"r00t2.io/goutils/multierr"
) )
/* /*
@ -13,8 +13,6 @@ import (
*/ */
func NewCollection(service *Service, path dbus.ObjectPath) (coll *Collection, err error) { func NewCollection(service *Service, path dbus.ObjectPath) (coll *Collection, err error) {
var splitPath []string
if service == nil { if service == nil {
err = ErrNoDbusConn err = ErrNoDbusConn
} }
@ -29,14 +27,23 @@ func NewCollection(service *Service, path dbus.ObjectPath) (coll *Collection, er
Dbus: service.Conn.Object(DbusService, path), Dbus: service.Conn.Object(DbusService, path),
}, },
service: service, service: service,
// lastModified: time.Now(), // LastModified: time.Now(),
} }
splitPath = strings.Split(string(coll.Dbus.Path()), "/") // Populate the struct fields...
// TODO: use channel for errors; condense into a MultiError and switch to goroutines.
coll.name = splitPath[len(splitPath)-1] if _, err = coll.Locked(); err != nil {
return
_, _, err = coll.Modified() }
if _, err = coll.Label(); err != nil {
return
}
if _, err = coll.Created(); err != nil {
return
}
if _, _, err = coll.Modified(); err != nil {
return
}
return return
} }
@ -59,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
@ -73,12 +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[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
} }
@ -106,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
} }
@ -124,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
@ -138,17 +164,21 @@ func (c *Collection) Items() (items []*Item, err error) {
paths = variant.Value().([]dbus.ObjectPath) paths = variant.Value().([]dbus.ObjectPath)
items = make([]*Item, len(paths)) items = make([]*Item, 0)
for idx, path := range paths { for _, path := range paths {
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[idx] = item items = append(items, item)
}
if !errs.IsEmpty() {
err = errs
} }
err = NewErrors(err)
return return
} }
@ -164,8 +194,28 @@ func (c *Collection) Label() (label string, err error) {
label = variant.Value().(string) label = variant.Value().(string)
if label != c.name { c.LabelName = label
c.name = label
return
}
// Lock will lock an unlocked Collection. It will no-op if the Collection is currently locked.
func (c *Collection) Lock() (err error) {
if _, err = c.Locked(); err != nil {
return
}
if c.IsLocked {
return
}
if err = c.service.Lock(c); err != nil {
return
}
c.IsLocked = true
if _, _, err = c.Modified(); err != nil {
return
} }
return return
@ -182,6 +232,7 @@ func (c *Collection) Locked() (isLocked bool, err error) {
} }
isLocked = variant.Value().(bool) isLocked = variant.Value().(bool)
c.IsLocked = isLocked
return return
} }
@ -194,42 +245,103 @@ func (c *Collection) Relabel(newLabel string) (err error) {
if err = c.Dbus.SetProperty(DbusCollectionLabel, variant); err != nil { if err = c.Dbus.SetProperty(DbusCollectionLabel, variant); err != nil {
return return
} }
c.LabelName = newLabel
if _, _, err = c.Modified(); err != nil {
return
}
return return
} }
/* /*
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
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
} }
items = make([]*Item, len(paths)) items = make([]*Item, 0)
for idx, path := range paths { for _, path := range paths {
if items[idx], err = NewItem(c, path); err != nil { item = nil
errs = append(errs, err) if item, err = NewItem(c, path); err != nil {
errs.AddError(err)
err = nil err = nil
continue continue
} }
items = append(items, item)
}
if !errs.IsEmpty() {
err = errs
}
return
}
// SetAlias is a thin wrapper/shorthand for Service.SetAlias (but specific to this Collection).
func (c *Collection) SetAlias(alias string) (err error) {
var call *dbus.Call
if call = c.service.Dbus.Call(
DbusServiceSetAlias, 0, alias, c.Dbus.Path(),
); call.Err != nil {
err = call.Err
return
}
c.Alias = alias
if _, _, err = c.Modified(); err != nil {
return
}
return
}
// Unlock will unlock a locked Collection. It will no-op if the Collection is currently unlocked.
func (c *Collection) Unlock() (err error) {
if _, err = c.Locked(); err != nil {
return
}
if !c.IsLocked {
return
}
if err = c.service.Unlock(c); err != nil {
return
}
c.IsLocked = false
if _, _, err = c.Modified(); err != nil {
return
} }
err = NewErrors(err)
return return
} }
@ -247,6 +359,7 @@ func (c *Collection) Created() (created time.Time, err error) {
timeInt = variant.Value().(uint64) timeInt = variant.Value().(uint64)
created = time.Unix(int64(timeInt), 0) created = time.Unix(int64(timeInt), 0)
c.CreatedAt = created
return return
} }
@ -256,7 +369,7 @@ func (c *Collection) Created() (created time.Time, err error) {
that indicates if the collection has changed since the last call of Collection.Modified. that indicates if the collection has changed since the last call of Collection.Modified.
Note that when calling NewCollection, the internal library-tracked modification Note that when calling NewCollection, the internal library-tracked modification
time (Collection.lastModified) will be set to the latest modification time of the Collection time (Collection.LastModified) will be set to the latest modification time of the Collection
itself as reported by Dbus rather than the time that NewCollection was called. itself as reported by Dbus rather than the time that NewCollection was called.
*/ */
func (c *Collection) Modified() (modified time.Time, isChanged bool, err error) { func (c *Collection) Modified() (modified time.Time, isChanged bool, err error) {
@ -274,39 +387,20 @@ func (c *Collection) Modified() (modified time.Time, isChanged bool, err error)
if !c.lastModifiedSet { if !c.lastModifiedSet {
// It's "nil", so set it to modified. We can't check for a zero-value in case Dbus has it as a zero-value. // It's "nil", so set it to modified. We can't check for a zero-value in case Dbus has it as a zero-value.
c.lastModified = modified c.LastModified = modified
c.lastModifiedSet = true c.lastModifiedSet = true
} }
isChanged = modified.After(c.lastModified) isChanged = modified.After(c.LastModified)
c.lastModified = modified c.LastModified = modified
return return
} }
/* // path is a *very* thin wrapper around Collection.Dbus.Path(). It is needed for LockableObject interface membership.
PathName returns the "real" name of a Collection. func (c *Collection) path() (dbusPath dbus.ObjectPath) {
In some cases, the Collection.Label may not be the actual *name* of the collection
(i.e. the label is different from the name used in the Dbus path).
This is a thin wrapper around simply extracting the last item from
the Collection.Dbus.Path().
*/
func (c *Collection) PathName() (realName string) {
var pathSplit []string = strings.Split(string(c.Dbus.Path()), "/") dbusPath = c.Dbus.Path()
realName = pathSplit[len(pathSplit)-1]
return
}
/*
setModify updates the Collection's modification time (as specified by Collection.Modified).
It seems that this does not update automatically.
*/
func (c *Collection) setModify() (err error) {
err = c.Dbus.SetProperty(DbusCollectionModified, uint64(time.Now().Unix()))
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 {
@ -148,6 +149,7 @@ func TestCollection_Label(t *testing.T) {
t.Fatalf("NewService failed: %v", err.Error()) t.Fatalf("NewService failed: %v", err.Error())
} }
t.Logf("Attempting to get label of collection: %v", defaultCollectionLabel)
if collection, err = svc.GetCollection(defaultCollectionLabel); err != nil { if collection, err = svc.GetCollection(defaultCollectionLabel); err != nil {
t.Errorf( t.Errorf(
"failed when fetching collection '%v': %v", "failed when fetching collection '%v': %v",
@ -207,9 +209,9 @@ func TestCollection_Locked(t *testing.T) {
} }
if isLocked, err = collection.Locked(); err != nil { if isLocked, err = collection.Locked(); err != nil {
t.Errorf("failed to get lock status for collection '%v': %v", collection.PathName(), err.Error()) t.Errorf("failed to get lock status for collection '%v': %v", collection.path(), err.Error())
} else { } else {
t.Logf("collection '%v' lock status: %v", collection.PathName(), isLocked) t.Logf("collection '%v' lock status: %v", collection.path(), isLocked)
} }
if err = svc.Close(); err != nil { if err = svc.Close(); err != nil {

View File

@ -1,5 +1,9 @@
package gosecret package gosecret
import (
"github.com/godbus/dbus/v5"
)
// Constants for use with gosecret. // Constants for use with gosecret.
const ( const (
/* /*
@ -24,6 +28,12 @@ const (
DbusDefaultItemType string = DbusServiceBase + ".Generic" DbusDefaultItemType string = DbusServiceBase + ".Generic"
) )
// Libsecret/SecretService special values.
var (
// DbusRemoveAliasPath is used to remove an alias from a Collection and/or Item.
DbusRemoveAliasPath dbus.ObjectPath = dbus.ObjectPath("/")
)
// Service interface. // Service interface.
const ( const (
/* /*

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

@ -12,8 +12,10 @@ var (
ErrInvalidProperty error = errors.New("invalid variant type; cannot convert") ErrInvalidProperty error = errors.New("invalid variant type; cannot convert")
// ErrNoDbusConn gets triggered if a connection to Dbus can't be detected. // ErrNoDbusConn gets triggered if a connection to Dbus can't be detected.
ErrNoDbusConn error = errors.New("no valid dbus connection") ErrNoDbusConn error = errors.New("no valid dbus connection")
// ErrMissingPaths gets triggered if one or more Dbus object paths are expected but non/not enough are received. // ErrMissingPaths gets triggered if one or more Dbus object paths are expected but none/not enough are received.
ErrMissingPaths error = errors.New("one or more Dbus object paths were expected but an insufficient amount were received") ErrMissingPaths error = errors.New("one or more Dbus object paths were expected but an insufficient amount were received")
// ErrMissingObj gets triggered if one or more gosecret-native objects are expected but none/not enough are received.
ErrMissingObj error = errors.New("one or more objects were expected but an insufficient amount were received")
// ErrMissingAttrs gets triggered if attributes were expected but not passed. // ErrMissingAttrs gets triggered if attributes were expected but not passed.
ErrMissingAttrs error = errors.New("attributes must not be empty/nil") ErrMissingAttrs error = errors.New("attributes must not be empty/nil")
// ErrDoesNotExist gets triggered if a Collection, Item, etc. is attempted to be fetched but none exists via the specified identifier. // ErrDoesNotExist gets triggered if a Collection, Item, etc. is attempted to be fetched but none exists via the specified identifier.

103
funcs.go
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
} }
@ -115,3 +124,85 @@ func pathsFromPath(bus dbus.BusObject, path string) (paths []dbus.ObjectPath, er
return return
} }
/*
NameFromPath returns an actual name (as it appears in Dbus) from a dbus.ObjectPath.
Note that you can get any object's dbus.ObjectPath via <object>.Dbus.Path().
path is validated to ensure it is not an empty string.
*/
func NameFromPath(path dbus.ObjectPath) (name string, err error) {
var strSplit []string
var ok bool
if ok, err = pathIsValid(path); err != nil {
return
} else if !ok {
err = ErrBadDbusPath
return
}
strSplit = strings.Split(string(path), "/")
if len(strSplit) < 1 {
err = ErrBadDbusPath
return
}
name = strSplit[len(strSplit)-1]
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

@ -1,11 +1,11 @@
package gosecret package gosecret
import ( import (
`strconv` "strconv"
`strings` "strings"
`time` "time"
`github.com/godbus/dbus/v5` "github.com/godbus/dbus/v5"
) )
// NewItem returns a pointer to an Item based on Collection and a Dbus path. // NewItem returns a pointer to an Item based on Collection and a Dbus path.
@ -32,14 +32,30 @@ func NewItem(collection *Collection, path dbus.ObjectPath) (item *Item, err erro
item.idx, err = strconv.Atoi(splitPath[len(splitPath)-1]) item.idx, err = strconv.Atoi(splitPath[len(splitPath)-1])
item.collection = collection item.collection = collection
// Populate the struct fields...
// TODO: use channel for errors; condense into a MultiError and switch to goroutines.
if _, err = item.GetSecret(collection.service.Session); err != nil {
return
}
if _, err = item.Locked(); err != nil {
return
}
if _, err = item.Attributes(); err != nil { if _, err = item.Attributes(); err != nil {
return return
} }
if _, err = item.Label(); err != nil {
return
}
if _, err = item.Type(); err != nil { if _, err = item.Type(); err != nil {
return return
} }
if _, err = item.Created(); err != nil {
_, _, err = item.Modified() return
}
if _, _, err = item.Modified(); err != nil {
return
}
return return
} }
@ -54,6 +70,40 @@ func (i *Item) Attributes() (attrs map[string]string, err error) {
} }
attrs = variant.Value().(map[string]string) attrs = variant.Value().(map[string]string)
i.Attrs = attrs
return
}
/*
ChangeItemType changes an Item.Type to newItemType.
Note that this is probably a bad idea unless you're also doing Item.SetSecret.
It must be a Dbus interface path (e.g. "foo.bar.Baz").
If newItemType is an empty string, DbusDefaultItemType will be used.
*/
func (i *Item) ChangeItemType(newItemType string) (err error) {
var variant dbus.Variant
// Legacy spec.
if i.collection.service.Legacy {
return
}
if strings.TrimSpace(newItemType) == "" {
newItemType = DbusDefaultItemType
}
variant = dbus.MakeVariant(newItemType)
if err = i.Dbus.SetProperty(DbusItemType, variant); err != nil {
return
}
i.SecretType = newItemType
if _, _, err = i.Modified(); err != nil {
return
}
return return
} }
@ -61,10 +111,17 @@ func (i *Item) Attributes() (attrs map[string]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
} }
@ -82,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
} }
@ -90,14 +149,19 @@ 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
} }
secret.session = session secret.session = session
secret.item = i secret.item = i
i.Secret = secret
return return
} }
@ -112,6 +176,7 @@ func (i *Item) Label() (label string, err error) {
} }
label = variant.Value().(string) label = variant.Value().(string)
i.LabelName = label
return return
} }
@ -153,7 +218,9 @@ func (i *Item) ModifyAttributes(replaceAttrs map[string]string) (err error) {
} }
} }
err = i.ReplaceAttributes(currentProps) if err = i.ReplaceAttributes(currentProps); err != nil {
return
}
return return
} }
@ -166,6 +233,11 @@ func (i *Item) Relabel(newLabel string) (err error) {
if err = i.Dbus.SetProperty(DbusItemLabel, variant); err != nil { if err = i.Dbus.SetProperty(DbusItemLabel, variant); err != nil {
return return
} }
i.LabelName = newLabel
if _, _, err = i.Modified(); err != nil {
return
}
return return
} }
@ -180,6 +252,11 @@ func (i *Item) ReplaceAttributes(newAttrs map[string]string) (err error) {
if err = i.Dbus.SetProperty(DbusItemAttributes, props); err != nil { if err = i.Dbus.SetProperty(DbusItemAttributes, props); err != nil {
return return
} }
i.Attrs = newAttrs
if _, _, err = i.Modified(); err != nil {
return
}
return return
} }
@ -187,18 +264,21 @@ 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 {
return
}
return return
} }
@ -207,11 +287,39 @@ 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
} }
itemType = variant.Value().(string) itemType = variant.Value().(string)
i.SecretType = itemType
return
}
// Lock will lock an unlocked Item. It will no-op if the Item is currently locked.
func (i *Item) Lock() (err error) {
if _, err = i.Locked(); err != nil {
return
}
if i.IsLocked {
return
}
if err = i.collection.service.Lock(i); err != nil {
return
}
i.IsLocked = true
if _, _, err = i.Modified(); err != nil {
return
}
return return
} }
@ -227,6 +335,29 @@ func (i *Item) Locked() (isLocked bool, err error) {
} }
isLocked = variant.Value().(bool) isLocked = variant.Value().(bool)
i.IsLocked = isLocked
return
}
// Unlock will unlock a locked Item. It will no-op if the Item is currently unlocked.
func (i *Item) Unlock() (err error) {
if _, err = i.Locked(); err != nil {
return
}
if !i.IsLocked {
return
}
if err = i.collection.service.Unlock(i); err != nil {
return
}
i.IsLocked = false
if _, _, err = i.Modified(); err != nil {
return
}
return return
} }
@ -244,6 +375,7 @@ func (i *Item) Created() (created time.Time, err error) {
timeInt = variant.Value().(uint64) timeInt = variant.Value().(uint64)
created = time.Unix(int64(timeInt), 0) created = time.Unix(int64(timeInt), 0)
i.CreatedAt = created
return return
} }
@ -253,7 +385,7 @@ func (i *Item) Created() (created time.Time, err error) {
that indicates if the collection has changed since the last call of Item.Modified. that indicates if the collection has changed since the last call of Item.Modified.
Note that when calling NewItem, the internal library-tracked modification Note that when calling NewItem, the internal library-tracked modification
time (Item.lastModified) will be set to the latest modification time of the Item time (Item.LastModified) will be set to the latest modification time of the Item
itself as reported by Dbus rather than the time that NewItem was called. itself as reported by Dbus rather than the time that NewItem was called.
*/ */
func (i *Item) Modified() (modified time.Time, isChanged bool, err error) { func (i *Item) Modified() (modified time.Time, isChanged bool, err error) {
@ -271,12 +403,20 @@ func (i *Item) Modified() (modified time.Time, isChanged bool, err error) {
if !i.lastModifiedSet { if !i.lastModifiedSet {
// It's "nil", so set it to modified. We can't check for a zero-value in case Dbus has it as a zero-value. // It's "nil", so set it to modified. We can't check for a zero-value in case Dbus has it as a zero-value.
i.lastModified = modified i.LastModified = modified
i.lastModifiedSet = true i.lastModifiedSet = true
} }
isChanged = modified.After(i.lastModified) isChanged = modified.After(i.LastModified)
i.lastModified = modified i.LastModified = modified
return
}
// path is a *very* thin wrapper around Item.Dbus.Path(). It is needed for LockableObject membership.
func (i *Item) path() (dbusPath dbus.ObjectPath) {
dbusPath = i.Dbus.Path()
return return
} }

View File

@ -46,6 +46,9 @@ func TestItem(t *testing.T) {
if item, err = collection.CreateItem(testItemLabel, itemAttrs, secret, true); err != nil { if item, err = collection.CreateItem(testItemLabel, itemAttrs, secret, true); err != nil {
t.Errorf("could not create item %v in collection '%v': %v", testItemLabel, collectionName.String(), err.Error()) t.Errorf("could not create item %v in collection '%v': %v", testItemLabel, collectionName.String(), err.Error())
if err = collection.Delete(); err != nil {
t.Errorf("could not delete collection '%v': %v", collectionName.String(), err.Error())
}
if err = svc.Close(); err != nil { if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error()) t.Fatalf("could not close Service.Session: %v", err.Error())
} }

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(err.Error(), e.ErrorSep)
} else {
errStr += err.Error()
}
}
return
}

View File

@ -1,5 +1,9 @@
package gosecret package gosecret
import (
`fmt`
)
/* /*
MarshalJSON converts a SecretValue to a JSON representation. MarshalJSON converts a SecretValue to a JSON representation.
For compat reasons, the MarshalText is left "unmolested" (i.e. renders to a Base64 value). For compat reasons, the MarshalText is left "unmolested" (i.e. renders to a Base64 value).
@ -7,7 +11,7 @@ package gosecret
*/ */
func (s *SecretValue) MarshalJSON() (b []byte, err error) { func (s *SecretValue) MarshalJSON() (b []byte, err error) {
b = []byte(string(*s)) b = []byte(fmt.Sprintf("\"%v\"", string(*s)))
return return
} }

View File

@ -5,8 +5,10 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings" "strings"
"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.
@ -37,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
@ -56,18 +68,21 @@ func (s *Service) Collections() (collections []*Collection, err error) {
paths = variant.Value().([]dbus.ObjectPath) paths = variant.Value().([]dbus.ObjectPath)
collections = make([]*Collection, len(paths)) collections = make([]*Collection, 0)
for idx, path := range paths { for _, path := range paths {
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[idx] = coll collections = append(collections, coll)
}
if !errs.IsEmpty() {
err = errs
} }
err = NewErrors(err)
return return
} }
@ -78,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
@ -85,10 +101,16 @@ func (s *Service) CreateAliasedCollection(label, alias string) (collection *Coll
var props map[string]dbus.Variant = make(map[string]dbus.Variant) var props map[string]dbus.Variant = make(map[string]dbus.Variant)
props[DbusCollectionLabel] = dbus.MakeVariant(label) props[DbusCollectionLabel] = dbus.MakeVariant(label)
props[DbusCollectionCreated] = 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
} }
@ -121,15 +143,18 @@ 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 var errs *multierr.MultiError = multierr.NewMultiError()
var colls []*Collection var colls []*Collection
var collLabel string var pathName string
// First check for an alias. // First check for an alias.
if c, err = s.ReadAlias(name); err != nil && err != ErrDoesNotExist { if c, err = s.ReadAlias(name); err != nil && err != ErrDoesNotExist {
c = nil
return return
} }
if c != nil { if c != nil {
@ -143,7 +168,12 @@ func (s *Service) GetCollection(name string) (c *Collection, err error) {
return return
} }
for _, i := range colls { for _, i := range colls {
if i.name == name { if pathName, err = NameFromPath(i.Dbus.Path()); err != nil {
errs.AddError(err)
err = nil
continue
}
if pathName == name {
c = i c = i
return return
} }
@ -151,21 +181,15 @@ func (s *Service) GetCollection(name string) (c *Collection, err error) {
// Still nothing? Try by label. // Still nothing? Try by label.
for _, i := range colls { for _, i := range colls {
if collLabel, err = i.Label(); err != nil { if i.LabelName == name {
errs = append(errs, err)
err = nil
continue
}
if collLabel == name {
c = i c = i
return return
} }
} }
// 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
} }
@ -177,6 +201,8 @@ func (s *Service) GetCollection(name string) (c *Collection, err error) {
GetSecrets allows you to fetch values (Secret) from multiple Item object paths using this Service's Session. GetSecrets allows you to fetch values (Secret) from multiple Item object paths using this Service's Session.
An ErrMissingPaths will be returned for err if itemPaths is nil or empty. An ErrMissingPaths will be returned for err if itemPaths is nil or empty.
The returned secrets is a map with itemPaths as the keys and their corresponding Secret as the value. The returned secrets is a map with itemPaths as the keys and their corresponding Secret as the value.
If you know which Collection your desired Secret is in, it is recommended to iterate through Collection.Items instead
(as Secrets returned here may have missing functionality).
*/ */
func (s *Service) GetSecrets(itemPaths ...dbus.ObjectPath) (secrets map[dbus.ObjectPath]*Secret, err error) { func (s *Service) GetSecrets(itemPaths ...dbus.ObjectPath) (secrets map[dbus.ObjectPath]*Secret, err error) {
@ -190,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
@ -200,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
} }
@ -226,25 +257,55 @@ func (s *Service) GetSession() (ssn *Session, err error) {
return return
} }
// Scrapping this idea for now; it would require introspection on a known Item path.
/* /*
Lock locks an Unlocked Service, Collection, etc. IsLegacy indicates with a decent likelihood of accuracy if this Service is
You can usually get objectPath for the object(s) to unlock via <object>.Dbus.Path(). connected to a legacy spec Secret Service (true) or if the spec is current (false).
If objectPaths is nil or empty, the Service's own path will be used.
*/
func (s *Service) Lock(objectPaths ...dbus.ObjectPath) (err error) {
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).
func (s *Service) Lock(objects ...LockableObject) (err error) {
var call *dbus.Call
var toLock []dbus.ObjectPath
// We only use these as destinations. // We only use these as destinations.
var locked []dbus.ObjectPath var locked []dbus.ObjectPath
var prompt *Prompt var prompt *Prompt
var promptPath dbus.ObjectPath var promptPath dbus.ObjectPath
if objectPaths == nil || len(objectPaths) == 0 { if objects == nil || len(objects) == 0 {
objectPaths = []dbus.ObjectPath{s.Dbus.Path()} err = ErrMissingObj
return
} }
if err = s.Dbus.Call( toLock = make([]dbus.ObjectPath, len(objects))
DbusServiceLock, 0, objectPaths,
).Store(&locked, &promptPath); err != nil { for idx, o := range objects {
toLock[idx] = o.path()
}
if call = s.Dbus.Call(
DbusServiceLock, 0, toLock,
); call.Err != nil {
err = call.Err
return
}
if err = call.Store(&locked, &promptPath); err != nil {
return return
} }
@ -257,15 +318,24 @@ func (s *Service) Lock(objectPaths ...dbus.ObjectPath) (err error) {
} }
} }
// TODO: use channels and goroutines here.
for _, o := range objects {
if _, err = o.Locked(); err != nil {
return
}
}
return return
} }
/* /*
OpenSession returns a pointer to a Session from the Service. OpenSession returns a pointer to a Session from the Service.
It's a convenience function around NewSession. It's a convenience function around NewSession.
However, NewService attaches a Session by default at Service.Session so this is likely unnecessary.
*/ */
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
@ -279,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
} }
@ -297,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
} }
@ -324,11 +401,24 @@ func (s *Service) ReadAlias(alias string) (collection *Collection, err error) {
return return
} }
// RemoveAlias is a thin wrapper around Service.SetAlias using the removal method specified there.
func (s *Service) RemoveAlias(alias string) (err error) {
if err = s.SetAlias(alias, DbusRemoveAliasPath); err != nil {
return
}
return
}
/* /*
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
@ -336,19 +426,24 @@ func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []*It
var ok bool var ok bool
var c *Collection var c *Collection
var cPath dbus.ObjectPath var cPath dbus.ObjectPath
var errs []error = make([]error, 0) var item *Item
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, len(locked)) lockedItems = make([]*Item, 0)
unlockedItems = make([]*Item, len(unlocked)) unlockedItems = make([]*Item, 0)
if collectionObjs, err = s.Collections(); err != nil { if collectionObjs, err = s.Collections(); err != nil {
return return
@ -361,49 +456,55 @@ func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []*It
} }
// Locked items // Locked items
for idx, i := range locked { for _, i := range locked {
item = nil
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 lockedItems[idx], 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
} }
lockedItems = append(lockedItems, item)
} }
// Unlocked items // Unlocked items
for idx, i := range unlocked { for _, i := range unlocked {
item = nil
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 unlockedItems[idx], 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)
} }
if errs != nil && len(errs) > 0 { if !errs.IsEmpty() {
err = NewErrors(errs...) err = errs
} }
return return
@ -411,39 +512,62 @@ func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []*It
/* /*
SetAlias sets an alias for an existing Collection. SetAlias sets an alias for an existing Collection.
(You can get its path via <Collection>.Dbus.Path().)
To remove an alias, set objectPath to dbus.ObjectPath("/"). To remove an alias, set objectPath to dbus.ObjectPath("/").
*/ */
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
c = s.Dbus.Call( if collection, err = s.GetCollection(alias); err != nil {
return
}
if call = s.Dbus.Call(
DbusServiceSetAlias, 0, alias, objectPath, DbusServiceSetAlias, 0, alias, objectPath,
) ); call.Err != nil {
err = call.Err
return
}
err = c.Err if objectPath == DbusRemoveAliasPath {
collection.Alias = ""
} else {
collection.Alias = alias
}
return return
} }
/* // Unlock unlocks a locked Collection or Item (LockableObject).
Unlock unlocks a Locked Service, Collection, etc. func (s *Service) Unlock(objects ...LockableObject) (err error) {
You can usually get objectPath for the object(s) to unlock via <object>.Dbus.Path().
If objectPaths is nil or empty, the Service's own path will be used.
*/
func (s *Service) Unlock(objectPaths ...dbus.ObjectPath) (err error) {
var call *dbus.Call
var toUnlock []dbus.ObjectPath
// We only use these as destinations.
var unlocked []dbus.ObjectPath var unlocked []dbus.ObjectPath
var prompt *Prompt var prompt *Prompt
var resultPath dbus.ObjectPath var resultPath dbus.ObjectPath
if objectPaths == nil || len(objectPaths) == 0 { if objects == nil || len(objects) == 0 {
objectPaths = []dbus.ObjectPath{s.Dbus.Path()} err = ErrMissingObj
return
} }
if err = s.Dbus.Call( toUnlock = make([]dbus.ObjectPath, len(objects))
DbusServiceUnlock, 0, objectPaths,
).Store(&unlocked, &resultPath); err != nil { for idx, o := range objects {
toUnlock[idx] = o.path()
}
if call = s.Dbus.Call(
DbusServiceUnlock, 0, toUnlock,
); call.Err != nil {
err = call.Err
return
}
if err = call.Store(&unlocked, &resultPath); err != nil {
return return
} }
@ -456,5 +580,20 @@ func (s *Service) Unlock(objectPaths ...dbus.ObjectPath) (err error) {
} }
} }
// TODO: use channels and goroutines here.
for _, o := range objects {
if _, err = o.Locked(); err != nil {
return
}
}
return
}
// path is a *very* thin wrapper around Service.Dbus.Path().
func (s *Service) path() (dbusPath dbus.ObjectPath) {
dbusPath = s.Dbus.Path()
return return
} }

View File

@ -85,7 +85,7 @@ func TestService_Collections(t *testing.T) {
} }
t.Logf( t.Logf(
"collection #%v (name '%v', label '%v'): created %v, last modified %v", "collection #%v (name '%v', label '%v'): created %v, last modified %v",
idx, c.PathName(), collLabel, created, modified, idx, c.path(), collLabel, created, modified,
) )
} }
} }
@ -177,7 +177,7 @@ func TestService_GetCollection(t *testing.T) {
if coll, err = svc.GetCollection(defaultCollection); err != nil { if coll, err = svc.GetCollection(defaultCollection); err != nil {
t.Errorf("failed to get collection '%v' via Service.GetCollection: %v", defaultCollection, err.Error()) t.Errorf("failed to get collection '%v' via Service.GetCollection: %v", defaultCollection, err.Error())
} else { } else {
t.Logf("got collection '%v' via reference '%v'", coll.name, defaultCollection) t.Logf("got collection '%v' via reference '%v'", coll.LabelName, defaultCollection)
} }
if err = svc.Close(); err != nil { if err = svc.Close(); err != nil {
@ -277,7 +277,13 @@ func TestService_Secrets(t *testing.T) {
t.Errorf("at least one locked item in collection '%v'", collectionName.String()) t.Errorf("at least one locked item in collection '%v'", collectionName.String())
} }
if len(itemResultsUnlocked) != 1 { if len(itemResultsUnlocked) != 1 {
t.Errorf("number of unlocked items in collection '%v' is not equal to 1", collectionName.String()) t.Errorf(
"number of unlocked items in collection '%v' (%v) is not equal to 1; items dump pending...",
collectionName.String(), len(itemResultsUnlocked),
)
for idx, i := range itemResultsUnlocked {
t.Logf("ITEM #%v IN COLLECTION %v: %v ('%v')", idx, collectionName.String(), i.LabelName, string(i.Dbus.Path()))
}
} }
if resultItemName, err = itemResultsUnlocked[0].Label(); err != nil { if resultItemName, err = itemResultsUnlocked[0].Label(); err != nil {
t.Errorf("cannot fetch test Item name from collection '%v' in SearchItems: %v", collectionName.String(), err.Error()) t.Errorf("cannot fetch test Item name from collection '%v' in SearchItems: %v", collectionName.String(), err.Error())
@ -374,10 +380,11 @@ func TestService_Locking(t *testing.T) {
} }
if collection, err = svc.CreateCollection(collectionName.String()); err != nil { if collection, err = svc.CreateCollection(collectionName.String()); err != nil {
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
t.Errorf("could not create collection '%v': %v", collectionName.String(), err.Error()) t.Errorf("could not create collection '%v': %v", collectionName.String(), err.Error())
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
} else { } else {
t.Logf("created collection '%v' at path '%v' successfully", collectionName.String(), string(collection.Dbus.Path())) t.Logf("created collection '%v' at path '%v' successfully", collectionName.String(), string(collection.Dbus.Path()))
} }
@ -400,23 +407,23 @@ func TestService_Locking(t *testing.T) {
// Change the state. // Change the state.
if isLocked { if isLocked {
if err = svc.Unlock(collection.Dbus.Path()); err != nil { if err = collection.Unlock(); err != nil {
t.Errorf("could not unlock collection '%v': %v", collectionName.String(), err.Error()) t.Errorf("could not unlock collection '%v': %v", collectionName.String(), err.Error())
} }
if stateChangeLock, err = collection.Locked(); err != nil { if stateChangeLock, err = collection.Locked(); err != nil {
t.Errorf("received error when checking collection '%v' lock status: %v", collectionName.String(), err.Error()) t.Errorf("received error when checking collection '%v' lock status: %v", collectionName.String(), err.Error())
} }
if err = svc.Lock(collection.Dbus.Path()); err != nil { if err = collection.Lock(); err != nil {
t.Errorf("could not lock collection '%v': %v", collectionName.String(), err.Error()) t.Errorf("could not lock collection '%v': %v", collectionName.String(), err.Error())
} }
} else { } else {
if err = svc.Lock(collection.Dbus.Path()); err != nil { if err = collection.Lock(); err != nil {
t.Errorf("could not lock collection '%v': %v", collectionName.String(), err.Error()) t.Errorf("could not lock collection '%v': %v", collectionName.String(), err.Error())
} }
if stateChangeLock, err = collection.Locked(); err != nil { if stateChangeLock, err = collection.Locked(); err != nil {
t.Errorf("received error when checking collection '%v' lock status: %v", collectionName.String(), err.Error()) t.Errorf("received error when checking collection '%v' lock status: %v", collectionName.String(), err.Error())
} }
if err = svc.Unlock(collection.Dbus.Path()); err != nil { if err = collection.Unlock(); err != nil {
t.Errorf("could not unlock collection '%v': %v", collectionName.String(), err.Error()) t.Errorf("could not unlock collection '%v': %v", collectionName.String(), err.Error())
} }
} }

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,29 @@ 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
}
// path is a *very* thin wrapper around Session.Dbus.Path().
func (s *Session) path() (dbusPath dbus.ObjectPath) {
dbusPath = s.Dbus.Path()
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
@ -58,6 +48,11 @@ type Prompt struct {
*DbusObject *DbusObject
} }
type LockableObject interface {
Locked() (bool, error)
path() dbus.ObjectPath
}
/* /*
Service is a general SecretService interface, sort of handler for Dbus - it's used for fetching a Session, Collections, etc. Service is a general SecretService interface, sort of handler for Dbus - it's used for fetching a Session, Collections, etc.
https://developer-old.gnome.org/libsecret/0.18/SecretService.html https://developer-old.gnome.org/libsecret/0.18/SecretService.html
@ -67,6 +62,33 @@ type Service struct {
*DbusObject *DbusObject
// Session is a default Session initiated automatically. // Session is a default Session initiated automatically.
Session *Session `json:"-"` Session *Session `json:"-"`
// IsLocked indicates if the Service is locked or not. Status updated by Service.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"`
} }
/* /*
@ -76,30 +98,37 @@ 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
} }
/* /*
Collection is an accessor for libsecret collections, which contain multiple Secret Item items. Collection is an accessor for libsecret collections, which contain multiple Secret Item items.
Do not change any of these values directly; use the associated methods instead.
Reference: Reference:
https://developer-old.gnome.org/libsecret/0.18/SecretCollection.html https://developer-old.gnome.org/libsecret/0.18/SecretCollection.html
https://specifications.freedesktop.org/secret-service/latest/ch03.html https://specifications.freedesktop.org/secret-service/latest/ch03.html
*/ */
type Collection struct { type Collection struct {
*DbusObject *DbusObject
// lastModified is unexported because it's important that API users don't change it; it's used by Collection.Modified. // IsLocked indicates if the Collection is locked or not. Status updated by Collection.Locked.
lastModified time.Time IsLocked bool `json:"locked"`
// LabelName is the Collection's label (as given by Collection.Label and modified by Collection.Relabel).
LabelName string `json:"label"`
// CreatedAt is when this Collection was created (used by Collection.Created).
CreatedAt time.Time `json:"created"`
// LastModified is when this Item was last changed; it's used by Collection.Modified.
LastModified time.Time `json:"modified"`
// Alias is the Collection's alias (as handled by Service.ReadAlias and Service.SetAlias).
Alias string `json:"alias"`
// lastModifiedSet is unexported; it's only used to determine if this is a first-initialization of the modification time or not. // lastModifiedSet is unexported; it's only used to determine if this is a first-initialization of the modification time or not.
lastModifiedSet bool lastModifiedSet bool
// name is used for the Collection's name/label so the Dbus path doesn't need to be parsed all the time.
name string
// service tracks the Service this Collection was created from. // service tracks the Service this Collection was created from.
service *Service service *Service
} }
/* /*
Item is an entry in a Collection that contains a Secret. Item is an entry in a Collection that contains a Secret. Do not change any of these values directly; use the associated methods instead.
https://developer-old.gnome.org/libsecret/0.18/SecretItem.html https://developer-old.gnome.org/libsecret/0.18/SecretItem.html
https://specifications.freedesktop.org/secret-service/latest/re03.html https://specifications.freedesktop.org/secret-service/latest/re03.html
*/ */
@ -107,8 +136,18 @@ type Item struct {
*DbusObject *DbusObject
// Secret is the corresponding Secret object. // Secret is the corresponding Secret object.
Secret *Secret `json:"secret"` Secret *Secret `json:"secret"`
// lastModified is unexported because it's important that API users don't change it; it's used by Collection.Modified. // IsLocked indicates if the Item is locked or not. Status updated by Item.Locked.
lastModified time.Time IsLocked bool
// Attrs are the Item's attributes (as would be returned via Item.Attributes).
Attrs map[string]string `json:"attributes"`
// LabelName is the Item's label (as given by Item.Label and modified by Item.Relabel).
LabelName string `json:"label"`
// SecretType is the Item's secret type (as returned by Item.Type).
SecretType string `json:"type"`
// CreatedAt is when this Item was created (used by Item.Created).
CreatedAt time.Time `json:"created"`
// LastModified is when this Item was last changed; it's used by Item.Modified.
LastModified time.Time `json:"modified"`
// lastModifiedSet is unexported; it's only used to determine if this is a first-initialization of the modification time or not. // lastModifiedSet is unexported; it's only used to determine if this is a first-initialization of the modification time or not.
lastModifiedSet bool lastModifiedSet bool
/* /*
@ -145,5 +184,5 @@ type Secret struct {
session *Session session *Session
} }
// SecretValue is a custom type that handles JSON encoding/decoding a little more easily. // SecretValue is a custom type that handles JSON encoding a little more easily.
type SecretValue []byte type SecretValue []byte