diff --git a/README.adoc b/README.adoc index c21e9ce..919e4f4 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,5 @@ = libsecret/gosecret Brent Saner -Last updated {localdatetime} :doctype: book :docinfo: shared :data-uri: @@ -26,6 +25,7 @@ This project is originally forked from https://github.com/gsterjov/go-libsecret[ and as such, hopefully this library should serve as a more effective libsecret/SecretService interface. == Backwards Compatability/Drop-In Replacement Support + Version series `v0.X.X` of this library promises full and non-breaking backwards support of API interaction with the original project. The only changes should be internal optimizations, adding documentation, some file reorganizing, adding Golang module support, etc. -- all transparent from the library API itself. To use this library as a replacement without significantly modifying your code, you can simply use a `replace` directive: @@ -36,13 +36,14 @@ To use this library as a replacement without significantly modifying your code, ---- // ... replace ( - github.com/gsterjov/go-libsecret dev => r00t2.io/gosecret v0 + github.com/gsterjov/go-libsecret dev => r00t2.io/gosecret v0 ) ---- and then run `go mod tidy`. == New Developer API + Starting from `v1.0.0` onwards, entirely breaking changes can be assumed from the original project. To use the new version, @@ -56,7 +57,70 @@ import ( To reflect the absolute breaking changes, the module name changes as well from `libsecret` to `gosecret`. +=== Status + +The new API is underway, and all functionality in V0 is present. However, It's not "complete". https://github.com/johnnybubonic/gosecret/pulls[PRs^] welcome, of course, but this will be an ongoing effort for a bit of time. + +== SecretService Concepts + +For reference: + +* A *`Service`* allows one to operate on/with *`Session`* objects. +* A *`Session`* allows one to operate on/with `*Collection*` objects. +* A `*Collection*` allows one to operate on/with `*Item*` objects. +* An `*Item*` allows one to operate on/with `*Secrets*`. +(`*Secrets*` are considered "terminating objects" in this model, and contain +actual secret value(s) and metadata). + +Various interactions are handled by `*Prompts*`. + +So the object hierarchy in *theory* looks kind of like this: + +---- +Service +├─ Session "A" +│ ├─ Collection "A.1" +│ │ ├─ Item "A.1.a" +│ │ │ ├─ Secret "A_1_a_I" +│ │ │ └─ Secret "A_1_a_II" +│ │ └─ Item "A.1.b" +│ │ ├─ Secret "A_1_b_I" +│ │ └─ Secret "A_1_b_II" +│ └─ Collection "A.2" +│ ├─ Item "A.2.a" +│ │ ├─ Secret "A_2_a_I" +│ │ └─ Secret "A_2_a_II" +│ └─ Item "A.2.b" +│ ├─ Secret "A_2_b_I" +│ └─ Secret "A_2_b_II" +└─ Session "B" + ├─ Collection "B.1" + │ ├─ Item "B.1.a" + │ │ ├─ Secret "B_1_a_I" + │ │ └─ Secret "B_1_a_II" + │ └─ Item "B.1.b" + │ ├─ Secret "B_1_b_I" + │ └─ Secret "B_1_b_II" + └─ Collection "B.2"# + ├─ Item "B.2.a" + │ ├─ Secret "B_2_a_I" + │ └─ Secret "B_2_a_II" + └─ Item "B.2.b" + ├─ Secret "B_2_b_I" + └─ Secret "B_2_b_II" +---- + +And so on. + +In *practice*, however, most users will only have two Session types: + +* a default "system" one, and +* a temporary one that may or may not exist, running in memory for the current login session + +and a single Collection, named `login` (and aliased to `default`, usually). + == Usage + Full documentation can be found via inline documentation. Either via the https://pkg.go.dev/r00t2.io/gosecret[pkg.go.dev documentation^] or https://pkg.go.dev/golang.org/x/tools/cmd/godoc[`godoc`^] (or `go doc`) in the source root. //// diff --git a/collection_funcs.go b/collection_funcs.go index 9496946..8f9ffbb 100644 --- a/collection_funcs.go +++ b/collection_funcs.go @@ -1,43 +1,64 @@ package gosecret import ( + `fmt` + `path/filepath` + `strings` + `time` + `github.com/godbus/dbus` ) -// NewCollection returns a pointer to a new Collection based on a Dbus connection and a Dbus path. -func NewCollection(conn *dbus.Conn, path dbus.ObjectPath) (coll *Collection, err error) { +/* + CreateCollection creates a new Collection named `name` using the dbus.BusObject `secretServiceConn`. + `secretServiceConn` should be the same as used for Collection.Dbus (and/or NewCollection). + It will be called by NewCollection if the Collection does not exist in Dbus. - // dbus.Conn.Names() will ALWAYS return a []string with at least ONE element. - if conn == nil || (conn.Names() == nil || len(conn.Names()) < 1) { + Generally speaking, you should probably not use this function directly and instead use NewCollection. +*/ +func CreateCollection(secretServiceConn *dbus.BusObject, name string) (c *Collection, err error) { + + var path dbus.ObjectPath + + if secretServiceConn == nil { err = ErrNoDbusConn return } - if path == "" { - err = ErrBadDbusPath + path = dbus.ObjectPath(strings.Join([]string{DbusPath, ""}, "/")) + + // TODO. + + return +} + +// NewCollection returns a pointer to a new Collection based on a Dbus connection and a Dbus path. +func NewCollection(conn *dbus.Conn, path dbus.ObjectPath) (coll *Collection, err error) { + + if _, err = validConnPath(conn, path); err != nil { return } coll = &Collection{ - Conn: conn, - Dbus: conn.Object(DbusServiceName, path), + Conn: conn, + Dbus: conn.Object(DbusService, path), + // lastModified: time.Now(), } + _, _, err = coll.Modified() + return } // Items returns a slice of Item pointers in the Collection. func (c *Collection) Items() (items []*Item, err error) { - var variant dbus.Variant var paths []dbus.ObjectPath - if variant, err = c.Dbus.GetProperty(DbusItemsID); err != nil { + if paths, err = pathsFromPath(c.Dbus, DbusCollectionItems); err != nil { return } - paths = variant.Value().([]dbus.ObjectPath) - items = make([]*Item, len(paths)) for idx, path := range paths { @@ -124,7 +145,7 @@ func (c *Collection) CreateItem(label string, secret *Secret, replace bool) (ite return } -// Locked indicates that a Collection is locked (true) or unlocked (false). +// Locked indicates if a Collection is locked (true) or unlocked (false). func (c *Collection) Locked() (isLocked bool, err error) { var variant dbus.Variant @@ -138,3 +159,39 @@ func (c *Collection) Locked() (isLocked bool, err error) { return } + +// Label returns the Collection label (name). +func (c *Collection) Label() (label string, err error) { + + // TODO. + + return +} + +// Created returns the time.Time of when a Collection was created. +func (c *Collection) Created() (created time.Time, err error) { + + // TODO. + + return +} + +/* + Modified returns the time.Time of when a Collection was last modified along with a boolean + that indicates if the collection has changed since the last call of Collection.Modified. + + Note that when calling NewCollection, the internal library-tracked modification + time (Collection.lastModified) will be set to the modification time of the Collection + itself as reported by Dbus. +*/ +func (c *Collection) Modified() (modified time.Time, isChanged bool, err error) { + + // TODO. + + if c.lastModified == time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) { + // It's "nil", so set it to modified. + c.lastModified = modified + } + + return +} diff --git a/consts.go b/consts.go index a88578d..b350926 100644 --- a/consts.go +++ b/consts.go @@ -1,19 +1,139 @@ package gosecret -// Libsecret/SecretService/Dbus identifiers. +// Libsecret/SecretService Dbus interfaces. const ( - // DbusServiceName is the "root Dbus path" in identifier format. - DbusServiceName string = "org.freedesktop.secrets" - // DbusItemsID is the Dbus identifier for Item. - DbusItemsID string = "org.freedesktop.Secret.Collection.Items" - // DbusCollectionDelete is the Dbus identifier for Collection.Delete. - DbusCollectionDelete string = "org.freedesktop.Secret.Collection.Delete" + // DbusService is the Dbus service bus identifier. + DbusService string = "org.freedesktop.secrets" + // DbusServiceBase is the base identifier used by interfaces. + DbusServiceBase string = "org.freedesktop.Secret" ) -// Dbus constants and paths. +// Service interface. const ( - // DbusPath is the path version of DbusServiceName. - DbusPath string = "/org/freedesktop/secrets" - // PromptPrefix is the path used for prompts comparison. - PromptPrefix string = DbusPath + "/prompt/" + /* + DbusInterfaceService is the Dbus interface for working with a Service. + Found at /org/freedesktop/secrets/(DbusInterfaceService) + */ + DbusInterfaceService string = DbusServiceBase + ".Service" + + // Methods + + // DbusServiceChangeLock is [FUNCTION UNKNOWN; TODO.] + DbusServiceChangeLock string = DbusInterfaceService + ".ChangeLock" + + // DbusServiceCreateCollection is used to create a new Collection if it doesn't exist in Dbus. + DbusServiceCreateCollection string = DbusInterfaceService + ".CreateCollection" + + /* + DbusServiceGetSecrets is used to fetch all Secret / Item items in a given Collection + (via Service.GetSecrets). + */ + DbusServiceGetSecrets string = DbusInterfaceService + ".GetSecrets" + + // DbusServiceLock is used by Service.Lock. + DbusServiceLock string = DbusInterfaceService + ".Lock" + + // DbusServiceLockService is [FUNCTION UNKNOWN; TODO.] + DbusServiceLockService string = DbusInterfaceService + ".LockService" + + // DbusServiceOpenSession is used by Service.Open. + DbusServiceOpenSession string = DbusInterfaceService + ".OpenSession" + + // DbusServiceReadAlias is used by Service.GetAlias to return a Collection based on its aliased name. + DbusServiceReadAlias string = DbusInterfaceService + ".ReadAlias" + + // DbusServiceSearchItems is used by Service.SearchItems to get arrays of locked and unlocked Item objects. + DbusServiceSearchItems string = DbusInterfaceService + ".SearchItems" + + // DbusServiceSetAlias is used by Service.SetAlias to set an alias for a Collection. + DbusServiceSetAlias string = DbusInterfaceService + ".SetAlias" + + // DbusServiceUnlock is used to unlock a Service. + DbusServiceUnlock string = DbusInterfaceService + ".Unlock" + + // Properties + + // DbusServiceCollections is used to get a Dbus array of Collection items. + DbusServiceCollections string = DbusInterfaceService + ".Collections" +) + +// Session interface. +const ( + /* + DbusInterfaceSession is the Dbus interface for working with a Session. + Found at /org/freedesktop/secrets/session//(DbusInterfaceSession) + */ + DbusInterfaceSession = DbusServiceBase + ".Session" + + // Methods + + // DbusSessionClose is used for Session.Close. + DbusSessionClose string = DbusInterfaceSession + ".Close" +) + +// Collection interface. +const ( + /* + DbusInterfaceCollection is the Dbus interface for working with a Collection. + Found at /org/freedesktop/secrets/collection//(DbusInterfaceCollection) + */ + DbusInterfaceCollection string = DbusServiceBase + ".Collection" + + // Methods + + // DbusCollectionCreateItem is used for Collection.CreateItem. + DbusCollectionCreateItem string = DbusInterfaceCollection + ".CreateItem" + + // DbusCollectionDelete is used for Collection.Delete. + DbusCollectionDelete string = DbusInterfaceCollection + ".Delete" + + // DbusCollectionSearchItems is used for Collection.SearchItems. + DbusCollectionSearchItems string = DbusInterfaceCollection + ".SearchItems" + + // Properties + + // DbusCollectionItems is a Dbus array of Item. + DbusCollectionItems string = DbusInterfaceCollection + ".Items" + + // DbusCollectionLocked is a Dbus boolean for Collection.Locked. + DbusCollectionLocked string = DbusInterfaceCollection + ".Locked" + + // DbusCollectionLabel is the name (label) for Collection.Label. + DbusCollectionLabel string = DbusInterfaceCollection + ".Label" + + // DbusCollectionCreated is the time a Collection was created (in a UNIX Epoch uint64) for Collection.Created. + DbusCollectionCreated string = DbusInterfaceCollection + ".Created" + + // DbusCollectionModified is the time a Collection was last modified (in a UNIX Epoch uint64) for Collection.Modified. + DbusCollectionModified string = DbusInterfaceCollection + ".Modified" + + // TODO: Signals? +) + +// Item interface. +const ( + /* + DbusInterfaceItem is the Dbus interface for working with Item items. + Found at /org/freedesktop/secrets/collection///(DbusInterfaceItem) + */ + DbusInterfaceItem string = DbusServiceBase + ".Item" + + // Methods + + // DbusItemDelete is used by Item.Delete. + DbusItemDelete string = DbusInterfaceItem + ".Delete" + + // DbusItemGetSecret is used by Item.GetSecret. + DbusItemGetSecret string = DbusInterfaceItem + ".GetSecret" + + // DbusItemSetSecret is used by Item.SetSecret. + DbusItemSetSecret string = DbusInterfaceItem + ".SetSecret" +) + +// Dbus paths. +const ( + // DbusPath is the path for DbusService. + DbusPath string = "/org/freedesktop/secrets" + // DbusPromptPrefix is the path used for prompts comparison. + DbusPromptPrefix string = DbusPath + "/prompt/" ) diff --git a/doc.go b/doc.go index 8900972..14c1f66 100644 --- a/doc.go +++ b/doc.go @@ -10,11 +10,13 @@ As such, hopefully this library should serve as a more effective libsecret/Secre Backwards Compatibility -Version series `v0.X.X` of this library promises full and non-breaking backwards compatibility/drop-in support of API interaction with the original project. +Version series `v0.X.X` of this library promises full and non-breaking backwards compatibility/drop-in +support of API interaction with the original project. The only changes should be internal optimizations, adding documentation, some file reorganizing, adding Golang module support, etc. -- all transparent from the library API itself. -To use this library as a replacement without significantly modifying your code, you can simply use a `replace` directive in your go.mod file: +To use this library as a replacement without significantly modifying your code, +you can simply use a `replace` directive in your go.mod file: // ... replace ( @@ -37,6 +39,61 @@ To use the new version, To reflect the absolute breaking changes, the module name changes as well from `libsecret` to `gosecret`. +SecretService Concepts + +For reference: + +- A Service allows one to operate on/with Session objects. + +- A Session allows one to operate on/with Collection objects. + +- A Collection allows one to operate on/with Item objects. + +- An Item allows one to operate on/with Secrets. + +(Secrets are considered "terminating objects" in this model, and contain actual secret value(s) and metadata). + +Various interactions are handled by Prompts. + +So the object hierarchy in THEORY looks kind of like this: + + Service + ├─ Session "A" + │ ├─ Collection "A.1" + │ │ ├─ Item "A.1.a" + │ │ │ ├─ Secret "A_1_a_I" + │ │ │ └─ Secret "A_1_a_II" + │ │ └─ Item "A.1.b" + │ │ ├─ Secret "A_1_b_I" + │ │ └─ Secret "A_1_b_II" + │ └─ Collection "A.2" + │ ├─ Item "A.2.a" + │ │ ├─ Secret "A_2_a_I" + │ │ └─ Secret "A_2_a_II" + │ └─ Item "A.2.b" + │ ├─ Secret "A_2_b_I" + │ └─ Secret "A_2_b_II" + └─ Session "B" + ├─ Collection "B.1" + │ ├─ Item "B.1.a" + │ │ ├─ Secret "B_1_a_I" + │ │ └─ Secret "B_1_a_II" + │ └─ Item "B.1.b" + │ ├─ Secret "B_1_b_I" + │ └─ Secret "B_1_b_II" + └─ Collection "B.2"# + ├─ Item "B.2.a" + │ ├─ Secret "B_2_a_I" + │ └─ Secret "B_2_a_II" + └─ Item "B.2.b" + ├─ Secret "B_2_b_I" + └─ Secret "B_2_b_II" + +And so on. +In PRACTICE, however, most users will only have two Session types +(a default "system" one and a temporary one that may or may not exist, running in memory for the current login session) +and a single Collection, named "login" (and aliased to "default", usually). + Usage Full documentation can be found via inline documentation. diff --git a/errs.go b/errs.go index acf5ca7..6753ca9 100644 --- a/errs.go +++ b/errs.go @@ -4,7 +4,12 @@ import ( `errors` ) +// Errors. var ( - ErrNoDbusConn error = errors.New("no valid dbus connection") + // ErrBadDbusPath indicates an invalid path - either nothing exists at that path or the path is malformed. ErrBadDbusPath error = errors.New("invalid dbus path") + // ErrInvalidProperty indicates a dbus.Variant is not the "real" type expected. + ErrInvalidProperty error = errors.New("invalid variant type; cannot convert") + // ErrNoDbusConn gets triggered if a connection to Dbus can't be detected. + ErrNoDbusConn error = errors.New("no valid dbus connection") ) diff --git a/funcs.go b/funcs.go index 111bd9b..4554094 100644 --- a/funcs.go +++ b/funcs.go @@ -9,8 +9,109 @@ import ( // isPrompt returns a boolean that is true if path is/requires a prompt(ed path) and false if it is/does not. func isPrompt(path dbus.ObjectPath) (prompt bool) { - prompt = strings.HasPrefix(string(path), PromptPrefix) + prompt = strings.HasPrefix(string(path), DbusPromptPrefix) return } + +// connIsValid returns a boolean if the dbus.conn named conn is active. +func connIsValid(conn *dbus.Conn) (ok bool, err error) { + + // dbus.Conn.Names() will ALWAYS return a []string with at least ONE element. + if conn == nil || (conn.Names() == nil || len(conn.Names()) < 1) { + err = ErrNoDbusConn + return + } + + ok = true + + return +} + +/* + pathIsValid implements path checking for valid Dbus paths. Currently it only checks to make sure path is not a blank string. + The path argument can be either a string or dbus.ObjectPath. +*/ +func pathIsValid(path interface{}) (ok bool, err error) { + + var realPath string + + switch p := path.(type) { + case dbus.ObjectPath: + if !p.IsValid() { + err = ErrBadDbusPath + return + } + realPath = string(p) + case string: + realPath = p + default: + err = ErrBadDbusPath + return + } + + if strings.TrimSpace(realPath) == "" { + err = ErrBadDbusPath + return + } + + ok = true + + return +} + +/* + 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) +*/ +func validConnPath(conn *dbus.Conn, path interface{}) (cr *ConnPathCheckResult, err error) { + + var connErr error + var pathErr error + + cr = new(ConnPathCheckResult) + + cr.ConnOK, connErr = connIsValid(conn) + cr.PathOK, pathErr = pathIsValid(path) + + err = NewErrors(connErr, pathErr) + + return +} + +/* + pathsFromProp returns a slice of dbus.ObjectPath (paths) from a dbus.Variant (prop). + If prop cannot typeswitch to paths, an ErrInvalidProperty will be raised. +*/ +func pathsFromProp(prop dbus.Variant) (paths []dbus.ObjectPath, err error) { + + switch v := prop.Value().(type) { + case []dbus.ObjectPath: + paths = v + default: + err = ErrInvalidProperty + return + } + + return +} + +/* + pathsFromPath returns a slice of dbus.ObjectPath based on an object given by path using the dbus.Conn specified by conn. + Internally it uses pathsFromProp. +*/ +func pathsFromPath(bus dbus.BusObject, path string) (paths []dbus.ObjectPath, err error) { + + var v dbus.Variant + + if v, err = bus.GetProperty(path); err != nil { + return + } + + if paths, err = pathsFromProp(v); err != nil { + return + } + + return +} diff --git a/item_funcs.go b/item_funcs.go index b7886b6..1794b8b 100644 --- a/item_funcs.go +++ b/item_funcs.go @@ -9,21 +9,12 @@ func NewItem(conn *dbus.Conn, path dbus.ObjectPath) (item *Item) { item = &Item{ Conn: conn, - Dbus: conn.Object(DbusServiceName, path), + Dbus: conn.Object(DbusService, path), } return } -// Path returns the path of the underlying Dbus connection. -func (i Item) Path() (path dbus.ObjectPath) { - - // Remove this method in V1. It's bloat since we now have an exported Dbus. - path = i.Dbus.Path() - - return -} - // Label returns the label ("name") of an Item. func (i *Item) Label() (label string, err error) { diff --git a/multierr_funcs.go b/multierr_funcs.go new file mode 100644 index 0000000..2bc521c --- /dev/null +++ b/multierr_funcs.go @@ -0,0 +1,57 @@ +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 *MultiError) { + + 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 +} diff --git a/prompt_funcs.go b/prompt_funcs.go index 3e6755e..1804f37 100644 --- a/prompt_funcs.go +++ b/prompt_funcs.go @@ -9,7 +9,7 @@ func NewPrompt(conn *dbus.Conn, path dbus.ObjectPath) (prompt *Prompt) { prompt = &Prompt{ Conn: conn, - Dbus: conn.Object(DbusServiceName, path), + Dbus: conn.Object(DbusService, path), } return diff --git a/service_funcs.go b/service_funcs.go index 09c2d67..1c39cbc 100644 --- a/service_funcs.go +++ b/service_funcs.go @@ -15,7 +15,7 @@ func NewService() (service *Service, err error) { if service.Conn, err = dbus.SessionBus(); err != nil { return } - service.Dbus = service.Conn.Object(DbusServiceName, dbus.ObjectPath(DbusPath)) + service.Dbus = service.Conn.Object(DbusService, dbus.ObjectPath(DbusPath)) return } diff --git a/session_funcs.go b/session_funcs.go index 4ccd66c..02791b9 100644 --- a/session_funcs.go +++ b/session_funcs.go @@ -9,7 +9,7 @@ func NewSession(conn *dbus.Conn, path dbus.ObjectPath) (session *Session) { session = &Session{ Conn: conn, - Dbus: conn.Object(DbusServiceName, path), + Dbus: conn.Object(DbusService, path), } return diff --git a/types.go b/types.go index 23b3f5d..d1dc6a7 100644 --- a/types.go +++ b/types.go @@ -1,9 +1,29 @@ package gosecret import ( + `time` + `github.com/godbus/dbus` ) +/* + 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 + // ErrorSep is a string to use to separate errors for .Error(). The default is "\n". + ErrorSep string +} + +// ConnPathCheckResult contains the result of validConnPath. +type ConnPathCheckResult struct { + // ConnOK is true if the dbus.Conn is valid. + ConnOK bool + // PathOK is true if the Dbus path given is a valid type and value. + PathOK bool +} + // DBusObject is any type that has a Path method that returns a dbus.ObjectPath. type DBusObject interface { Path() dbus.ObjectPath @@ -20,6 +40,8 @@ type Collection struct { Conn *dbus.Conn // Dbus is the Dbus bus object. Dbus dbus.BusObject + // lastModified is unexported because it's important that API users don't change it; it's used by Collection.Modified. + lastModified time.Time } /*