From 0fc0e0c269a911aecd42ae4b9d7dda7fefe8852b Mon Sep 17 00:00:00 2001 From: brent s Date: Mon, 6 Dec 2021 03:24:55 -0500 Subject: [PATCH] checking in - all basic funcs in place; add a few more then v1 merge --- README.adoc | 55 ++++------ TODO | 10 ++ collection_funcs.go | 24 +++-- consts.go | 24 ++++- doc.go | 54 ++++------ funcs.go | 2 +- go.mod | 2 +- go.sum | 4 +- item_funcs.go | 234 +++++++++++++++++++++++++++++++++++++++---- prompt_funcs.go | 23 ++--- secret_funcs.go | 2 +- secretvalue_funcs.go | 13 +++ service_funcs.go | 222 +++++++++++++++++++++++++++++----------- session_funcs.go | 9 +- sserror_funcs.go | 3 + types.go | 53 ++++++++-- 16 files changed, 541 insertions(+), 193 deletions(-) create mode 100644 TODO create mode 100644 secretvalue_funcs.go diff --git a/README.adoc b/README.adoc index 919e4f4..ed61567 100644 --- a/README.adoc +++ b/README.adoc @@ -59,16 +59,17 @@ To reflect the absolute breaking changes, the module name changes as well from ` === 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. +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*`. +* A `*Service*` allows one to retrieve and operate on/with `*Session*` and `*Collection*` objects. +* A `*Session*` allows one to operate on/with `*Item*` objects (e.g. parsing/decoding/decrypting them). +* A `*Collection*` allows one to retrieve and operate on/with `*Item*` objects. +* An `*Item*` allows one to retrieve and operate on/with `*Secret*` objects. + (`*Secrets*` are considered "terminating objects" in this model, and contain actual secret value(s) and metadata). @@ -79,35 +80,21 @@ 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" +├─ Session "B" +├─ Collection "A" +│ ├─ Item "A.1" +│ │ ├─ Secret "A_1_a" +│ │ └─ Secret "A_1_b" +│ └─ Item "A.2" +│ ├─ Secret "A_2_a" +│ └─ Secret "A_2_b" +└─ Collection "B" + ├─ Item "B.1" + │ ├─ Secret "B_1_a" + │ └─ Secret "B_1_b" + └─ Item "B.2" + ├─ Secret "B_2_a" + └─ Secret "B_2_b" ---- And so on. diff --git a/TODO b/TODO new file mode 100644 index 0000000..0cfa9c4 --- /dev/null +++ b/TODO @@ -0,0 +1,10 @@ +- 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/ +- Example usage +- Merge master into V1 +-- and tag release (v1.0.0) +- Merge doc.go and README.adoc to V0 +-- and tag release (v0.1.3) diff --git a/collection_funcs.go b/collection_funcs.go index 11511ee..b6874a5 100644 --- a/collection_funcs.go +++ b/collection_funcs.go @@ -4,9 +4,11 @@ import ( `strings` "time" - `github.com/godbus/dbus` + `github.com/godbus/dbus/v5` ) +// TODO: add method Relabel + /* NewCollection returns a pointer to a Collection based on a Service and a Dbus path. You will almost always want to use Service.GetCollection instead. @@ -28,10 +30,11 @@ func NewCollection(service *Service, path dbus.ObjectPath) (coll *Collection, er Conn: service.Conn, Dbus: service.Conn.Object(DbusService, path), }, + service: service, // lastModified: time.Now(), } - splitPath = strings.Split(string(coll.Dbus.Path()), "") + splitPath = strings.Split(string(coll.Dbus.Path()), "/") coll.name = splitPath[len(splitPath)-1] @@ -58,7 +61,6 @@ func (c *Collection) Items() (items []*Item, err error) { for idx, path := range paths { if item, err = NewItem(c, path); err != nil { - // return errs = append(errs, err) err = nil continue @@ -120,22 +122,20 @@ func (c *Collection) SearchItems(profile string) (items []*Item, err error) { return } -// CreateItem returns a pointer to an Item based on a label, a Secret, and whether any existing secret with the same label should be replaced or not. -func (c *Collection) CreateItem(label string, secret *Secret, replace bool) (item *Item, err error) { +// CreateItem returns a pointer to an Item based on a label, some attributes, a Secret, and whether any existing secret with the same label should be replaced or not. +func (c *Collection) CreateItem(label string, attrs map[string]string, secret *Secret, replace bool) (item *Item, err error) { var prompt *Prompt var path dbus.ObjectPath var promptPath dbus.ObjectPath var variant *dbus.Variant var props map[string]dbus.Variant = make(map[string]dbus.Variant) - var attrs map[string]string = make(map[string]string) - attrs["profile"] = label - props["org.freedesktop.Secret.Item.Label"] = dbus.MakeVariant(label) - props["org.freedesktop.Secret.Item.Attributes"] = dbus.MakeVariant(attrs) + props[DbusItemLabel] = dbus.MakeVariant(label) + props[DbusItemAttributes] = dbus.MakeVariant(attrs) if err = c.Dbus.Call( - "org.freedesktop.Secret.Collection.CreateItem", 0, props, secret, replace, + DbusCollectionCreateItem, 0, props, secret, replace, ).Store(&path, &promptPath); err != nil { return } @@ -181,6 +181,10 @@ func (c *Collection) Label() (label string, err error) { label = variant.Value().(string) + if label != c.name { + c.name = label + } + return } diff --git a/consts.go b/consts.go index f0e349c..7248ba5 100644 --- a/consts.go +++ b/consts.go @@ -1,11 +1,23 @@ package gosecret +// Constants for use with gosecret. +const ( + /* + ExplicitAttrEmptyValue is the constant used in Item.ModifyAttributes to explicitly set a value as empty. + Between the surrounding with %'s, the weird name that includes "gosecret", and the UUID4... + I am fairly confident this is unique enough. + */ + ExplicitAttrEmptyValue string = "%EXPLICIT_GOSECRET_BLANK_VALUE_8A4E3D7D-F30E-4754-8C56-9C172D1400F6%" +) + // Libsecret/SecretService Dbus interfaces. const ( // 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" + // DbusPrompterInterface is an interface for issuing a Prompt. Yes, it should be doubled up like that. + DbusPrompterInterface string = DbusServiceBase + ".Prompt.Prompt" ) // Service interface. @@ -37,10 +49,10 @@ const ( // DbusServiceLockService is [FUNCTION UNKNOWN/UNDOCUMENTED; TODO? NOT IMPLEMENTED.] DbusServiceLockService string = DbusInterfaceService + ".LockService" - // DbusServiceOpenSession is used by Service.Open. + // DbusServiceOpenSession is used by Service.OpenSession. DbusServiceOpenSession string = DbusInterfaceService + ".OpenSession" - // DbusServiceReadAlias is used by Service.GetAlias to return a Collection based on its aliased name. + // DbusServiceReadAlias is used by Service.ReadAlias 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. @@ -135,13 +147,13 @@ const ( // DbusItemLocked is a Dbus boolean for Item.Locked. DbusItemLocked string = DbusInterfaceItem + ".Locked" - // DbusItemAttributes contains attributes (metadata, schema, etc.) for Item.Attributes. + // DbusItemAttributes contains attributes (metadata, schema, etc.) for Item.Attrs. DbusItemAttributes string = DbusInterfaceItem + ".Attributes" // DbusItemLabel is the name (label) for Item.Label. DbusItemLabel string = DbusInterfaceItem + ".Label" - // DbusItemType is the type of an Item (Item.Type). + // DbusItemType is the type of Item (Item.ItemType). DbusItemType string = DbusInterfaceItem + ".Type" // DbusItemCreated is the time an Item was created (in a UNIX Epoch uint64) for Item.Created. @@ -159,6 +171,8 @@ const ( DbusPromptPrefix string = DbusPath + "/prompt/" // DbusNewCollectionPath is used to create a new Collection. DbusNewCollectionPath string = DbusPath + "/collection/" + // DbusNewSessionPath is used to create a new Session. + DbusNewSessionPath string = DbusPath + "/session/" ) // FLAGS @@ -166,7 +180,7 @@ const ( // SERVICE -// ServiceInitFlag is a flag for Service.Open. +// ServiceInitFlag is a flag for Service.OpenSession. type ServiceInitFlag int const ( diff --git a/doc.go b/doc.go index 14c1f66..e7bc876 100644 --- a/doc.go +++ b/doc.go @@ -43,13 +43,13 @@ SecretService Concepts For reference: -- A Service allows one to operate on/with Session objects. +- A Service allows one to retrieve and operate on/with Session and Collection objects. -- A Session allows one to operate on/with Collection objects. +- A Session allows one to operate on/with Item objects (e.g. parsing/decoding/decrypting them). -- A Collection allows one to operate on/with Item objects. +- A Collection allows one to retrieve and operate on/with Item objects. -- An Item allows one to operate on/with Secrets. +- An Item allows one to retrieve and operate on/with Secret objects. (Secrets are considered "terminating objects" in this model, and contain actual secret value(s) and metadata). @@ -59,38 +59,24 @@ 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" + ├─ Session "B" + ├─ Collection "A" + │ ├─ Item "A.1" + │ │ ├─ Secret "A_1_a" + │ │ └─ Secret "A_1_b" + │ └─ Item "A.2" + │ ├─ Secret "A_2_a" + │ └─ Secret "A_2_b" + └─ Collection "B" + ├─ Item "B.1" + │ ├─ Secret "B_1_a" + │ └─ Secret "B_1_b" + └─ Item "B.2" + ├─ Secret "B_2_a" + └─ Secret "B_2_b" And so on. -In PRACTICE, however, most users will only have two Session types +In PRACTICE, however, most users will only have two Session items (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). diff --git a/funcs.go b/funcs.go index 4554094..2f26d6e 100644 --- a/funcs.go +++ b/funcs.go @@ -3,7 +3,7 @@ package gosecret import ( `strings` - `github.com/godbus/dbus` + `github.com/godbus/dbus/v5` ) // isPrompt returns a boolean that is true if path is/requires a prompt(ed path) and false if it is/does not. diff --git a/go.mod b/go.mod index 477c7cb..a3e3af5 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module r00t2.io/gosecret go 1.17 -require github.com/godbus/dbus v4.1.0+incompatible +require github.com/godbus/dbus/v5 v5.0.6 diff --git a/go.sum b/go.sum index 3a4ab68..aeec758 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= -github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= diff --git a/item_funcs.go b/item_funcs.go index acb8162..1829ac4 100644 --- a/item_funcs.go +++ b/item_funcs.go @@ -1,12 +1,20 @@ package gosecret import ( - `github.com/godbus/dbus` + `strconv` + `strings` + `time` + + `github.com/godbus/dbus/v5` ) +// TODO: add method Relabel + // NewItem returns a pointer to an Item based on Collection and a Dbus path. func NewItem(collection *Collection, path dbus.ObjectPath) (item *Item, err error) { + var splitPath []string + if collection == nil { err = ErrNoDbusConn } @@ -16,12 +24,84 @@ func NewItem(collection *Collection, path dbus.ObjectPath) (item *Item, err erro } item = &Item{ - &DbusObject{ + DbusObject: &DbusObject{ Conn: collection.Conn, Dbus: collection.Conn.Object(DbusService, path), }, } + splitPath = strings.Split(string(item.Dbus.Path()), "/") + + item.idx, err = strconv.Atoi(splitPath[len(splitPath)-1]) + item.collection = collection + if _, err = item.Attributes(); err != nil { + return + } + if _, err = item.Type(); err != nil { + return + } + + _, _, err = item.Modified() + + return +} + +// Attributes updates the Item.Attrs from Dbus (and returns them). +func (i *Item) Attributes() (attrs map[string]string, err error) { + + var variant dbus.Variant + + if variant, err = i.Dbus.GetProperty(DbusItemAttributes); err != nil { + return + } + + i.Attrs = variant.Value().(map[string]string) + attrs = i.Attrs + + return +} + +// Delete removes an Item from a Collection. +func (i *Item) Delete() (err error) { + + var promptPath dbus.ObjectPath + var prompt *Prompt + + if err = i.Dbus.Call(DbusItemDelete, 0).Store(&promptPath); err != nil { + return + } + + if isPrompt(promptPath) { + + prompt = NewPrompt(i.Conn, promptPath) + if _, err = prompt.Prompt(); err != nil { + return + } + } + + return +} + +// GetSecret returns the Secret in an Item using a Session. +func (i *Item) GetSecret(session *Session) (secret *Secret, err error) { + + if session == nil { + err = ErrNoDbusConn + } + + if _, err = connIsValid(session.Conn); err != nil { + return + } + + if err = i.Dbus.Call( + DbusItemGetSecret, 0, session.Dbus.Path(), + ).Store(&secret); err != nil { + return + } + + secret.session = session + secret.item = i + return } @@ -30,7 +110,7 @@ func (i *Item) Label() (label string, err error) { var variant dbus.Variant - if variant, err = i.Dbus.GetProperty("org.freedesktop.Secret.Item.Label"); err != nil { + if variant, err = i.Dbus.GetProperty(DbusItemLabel); err != nil { return } @@ -39,12 +119,109 @@ func (i *Item) Label() (label string, err error) { return } -// Locked indicates that an Item is locked (true) or unlocked (false). +/* + ModifyAttributes modifies the Item.Attrs, both in the object and in Dbus. + This is similar to Item.ReplaceAttributes but will only modify the map's given keys so you do not need to provide + the entire attribute map. + If you wish to remove an attribute, use the value "" (empty string). + If you wish to explicitly provide a blank value/empty string, use the constant gosecret.ExplicitAttrEmptyValue. + + This is more or less a convenience/wrapper function around Item.ReplaceAttributes. +*/ +func (i *Item) ModifyAttributes(replaceAttrs map[string]string) (err error) { + + var ok bool + var currentProps map[string]string = make(map[string]string, 0) + var currentVal string + + if replaceAttrs == nil || len(replaceAttrs) == 0 { + err = ErrMissingAttrs + return + } + + if currentProps, err = i.Attributes(); err != nil { + return + } + + for k, v := range replaceAttrs { + if currentVal, ok = currentProps[k]; !ok { // If it isn't in the replacement map, do nothing (i.e. keep it). + continue + } else if v == currentVal { // If the value is the same, do nothing. + continue + } else if v == ExplicitAttrEmptyValue { // If it's the "magic empty value" constant, delete the key/value pair. + delete(currentProps, k) + continue + } else { // Otherwise, replace the value. + currentProps[k] = v + } + } + + err = i.ReplaceAttributes(currentProps) + + return +} + +// ReplaceAttributes replaces the Item.Attrs, both in the object and in Dbus. +func (i *Item) ReplaceAttributes(newAttrs map[string]string) (err error) { + + var label string + var props map[string]dbus.Variant = make(map[string]dbus.Variant, 0) + + if label, err = i.Label(); err != nil { + return + } + + props[DbusItemLabel] = dbus.MakeVariant(label) + props[DbusItemAttributes] = dbus.MakeVariant(newAttrs) + + if err = i.Dbus.SetProperty(DbusItemAttributes, props); err != nil { + return + } + + i.Attrs = newAttrs + + return +} + +// SetSecret sets the Secret for an Item. +func (i *Item) SetSecret(secret *Secret) (err error) { + + var c *dbus.Call + + c = i.Dbus.Call( + DbusItemSetSecret, 0, + ) + if c.Err != nil { + err = c.Err + return + } + + i.Secret = secret + + return +} + +// Type updates the Item.ItemType from DBus (and returns it). +func (i *Item) Type() (itemType string, err error) { + + var variant dbus.Variant + + if variant, err = i.Dbus.GetProperty(DbusItemType); err != nil { + return + } + + i.ItemType = variant.Value().(string) + itemType = i.ItemType + + return +} + +// Locked indicates if an Item is locked (true) or unlocked (false). func (i *Item) Locked() (isLocked bool, err error) { var variant dbus.Variant - if variant, err = i.Dbus.GetProperty("org.freedesktop.Secret.Item.Locked"); err != nil { + if variant, err = i.Dbus.GetProperty(DbusItemLocked); err != nil { isLocked = true return } @@ -54,37 +231,52 @@ func (i *Item) Locked() (isLocked bool, err error) { return } -// GetSecret returns the Secret in an Item using a Session. -func (i *Item) GetSecret(session *Session) (secret *Secret, err error) { +// Created returns the time.Time of when an Item was created. +func (i *Item) Created() (created time.Time, err error) { - secret = new(Secret) + var variant dbus.Variant + var timeInt uint64 - if err = i.Dbus.Call( - "org.freedesktop.Secret.Item.GetSecret", 0, session.Path(), - ).Store(&secret); err != nil { + if variant, err = i.Dbus.GetProperty(DbusItemCreated); err != nil { return } + timeInt = variant.Value().(uint64) + + created = time.Unix(int64(timeInt), 0) + return } -// Delete removes an Item from a Collection. -func (i *Item) Delete() (err error) { +/* + Modified returns the time.Time of when an Item was last modified along with a boolean + that indicates if the collection has changed since the last call of Item.Modified. - var prompt *Prompt - var promptPath dbus.ObjectPath + Note that when calling NewItem, the internal library-tracked modification + 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. +*/ +func (i *Item) Modified() (modified time.Time, isChanged bool, err error) { - if err = i.Dbus.Call("org.freedesktop.Secret.Item.Delete", 0).Store(&promptPath); err != nil { + var variant dbus.Variant + var timeInt uint64 + + if variant, err = i.Dbus.GetProperty(DbusItemModified); err != nil { return } - if isPrompt(promptPath) { - prompt = NewPrompt(i.Conn, promptPath) + timeInt = variant.Value().(uint64) - if _, err = prompt.Prompt(); err != nil { - return - } + modified = time.Unix(int64(timeInt), 0) + + 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. + i.lastModified = modified + i.lastModifiedSet = true } + isChanged = modified.After(i.lastModified) + i.lastModified = modified + return } diff --git a/prompt_funcs.go b/prompt_funcs.go index 1804f37..d63ec96 100644 --- a/prompt_funcs.go +++ b/prompt_funcs.go @@ -1,29 +1,22 @@ package gosecret import ( - `github.com/godbus/dbus` + `github.com/godbus/dbus/v5` ) // NewPrompt returns a pointer to a new Prompt based on a Dbus connection and a Dbus path. func NewPrompt(conn *dbus.Conn, path dbus.ObjectPath) (prompt *Prompt) { prompt = &Prompt{ - Conn: conn, - Dbus: conn.Object(DbusService, path), + DbusObject: &DbusObject{ + Conn: conn, + Dbus: conn.Object(DbusService, path), + }, } return } -// Path returns the path of the underlying Dbus connection. -func (p Prompt) Path() (path dbus.ObjectPath) { - - // Remove this method in V1. It's bloat since we now have an exported Dbus. - path = p.Dbus.Path() - - return -} - // Prompt issues/waits for a prompt for unlocking a Locked Collection or Secret / Item. func (p *Prompt) Prompt() (promptValue *dbus.Variant, err error) { @@ -37,12 +30,14 @@ func (p *Prompt) Prompt() (promptValue *dbus.Variant, err error) { p.Conn.Signal(c) defer p.Conn.RemoveSignal(c) - if err = p.Dbus.Call("org.freedesktop.Secret.Prompt.Prompt", 0, "").Store(); err != nil { + if err = p.Dbus.Call( + DbusPrompterInterface, 0, "", // TODO: This last argument, the string, is for "window ID". I'm unclear what for. + ).Store(); err != nil { return } for { - if result = <-c; result.Path == p.Path() { + if result = <-c; result.Path == p.Dbus.Path() { *promptValue = result.Body[1].(dbus.Variant) return } diff --git a/secret_funcs.go b/secret_funcs.go index c026949..961ef57 100644 --- a/secret_funcs.go +++ b/secret_funcs.go @@ -4,7 +4,7 @@ package gosecret func NewSecret(session *Session, params []byte, value []byte, contentType string) (secret *Secret) { secret = &Secret{ - Session: session.Path(), + Session: session.Dbus.Path(), Parameters: params, Value: value, ContentType: contentType, diff --git a/secretvalue_funcs.go b/secretvalue_funcs.go new file mode 100644 index 0000000..7571968 --- /dev/null +++ b/secretvalue_funcs.go @@ -0,0 +1,13 @@ +package gosecret + +/* + MarshalJSON converts a SecretValue to a JSON representation. + For compat reasons, the MarshalText is left "unmolested" (i.e. renders to a Base64 value). + I don't bother with an UnmarshalJSON because it makes exactly 0 sense to unmarshal due to runtime and unexported fields in Secret. +*/ +func (s *SecretValue) MarshalJSON() (b []byte, err error) { + + b = []byte(string(*s)) + + return +} diff --git a/service_funcs.go b/service_funcs.go index d03b1d4..c11619e 100644 --- a/service_funcs.go +++ b/service_funcs.go @@ -1,9 +1,16 @@ package gosecret import ( - "github.com/godbus/dbus" + `errors` + `fmt` + `path/filepath` + `strings` + + "github.com/godbus/dbus/v5" ) +// TODO: Lock method (DbusServiceLockService)? + // NewService returns a pointer to a new Service connection. func NewService() (service *Service, err error) { @@ -14,7 +21,7 @@ func NewService() (service *Service, err error) { } svc.Dbus = service.Conn.Object(DbusService, dbus.ObjectPath(DbusPath)) - if svc.Session, _, err = svc.Open(); err != nil { + if svc.Session, err = svc.GetSession(); err != nil { return } @@ -108,40 +115,6 @@ func (s *Service) CreateCollection(label string) (collection *Collection, err er return } -/* - GetAlias allows one to fetch a Collection based on an alias name. - An ErrDoesNotExist will be raised if the alias does not exist. - You will almost assuredly want to use Service.GetCollection instead; it works for both alias names and real names. -*/ -func (s *Service) GetAlias(alias string) (collection *Collection, err error) { - - var objectPath dbus.ObjectPath - - err = s.Dbus.Call( - DbusServiceReadAlias, 0, alias, - ).Store(&objectPath) - - /* - TODO: Confirm that a nonexistent alias will NOT cause an error to return. - If it does, alter the below logic. - */ - if err != nil { - return - } - - // If the alias does not exist, objectPath will be dbus.ObjectPath("/"). - if objectPath == dbus.ObjectPath("/") { - err = ErrDoesNotExist - return - } - - if collection, err = NewCollection(s, objectPath); err != nil { - return - } - - return -} - /* 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. @@ -151,7 +124,7 @@ func (s *Service) GetCollection(name string) (c *Collection, err error) { var colls []*Collection // First check for an alias. - if c, err = s.GetAlias(name); err != nil && err != ErrDoesNotExist{ + if c, err = s.ReadAlias(name); err != nil && err != ErrDoesNotExist { return } if c != nil { @@ -192,16 +165,6 @@ func (s *Service) GetSecrets(itemPaths ...dbus.ObjectPath) (secrets map[dbus.Obj secrets = make(map[dbus.ObjectPath]*Secret, len(itemPaths)) // TODO: trigger a Service.Unlock for any locked items? - /* - // TODO: make any errs in here a MultiError instead. - for _, secretPath := range itemPaths { - if err = s.Dbus.Call( - DbusServiceGetSecrets, 0, secretPath, - ).Store(&result); err != nil { - return - } - } - */ if err = s.Dbus.Call( DbusServiceGetSecrets, 0, itemPaths, ).Store(&secrets); err != nil { @@ -211,6 +174,17 @@ func (s *Service) GetSecrets(itemPaths ...dbus.ObjectPath) (secrets map[dbus.Obj return } +/* + GetSession returns a single Session. + It's a helper function that wraps Service.OpenSession. +*/ +func (s *Service) GetSession() (ssn *Session, err error) { + + ssn, _, err = s.OpenSession("", "") + + return +} + /* Lock locks an Unlocked Service, Collection, etc. You can usually get objectPath for the object(s) to unlock via .Dbus.Path(). @@ -218,6 +192,8 @@ func (s *Service) GetSecrets(itemPaths ...dbus.ObjectPath) (secrets map[dbus.Obj */ func (s *Service) Lock(objectPaths ...dbus.ObjectPath) (err error) { + var errs []error = make([]error, 0) + // We only use these as destinations. var locked []dbus.ObjectPath var prompt *Prompt var resultPath dbus.ObjectPath @@ -226,12 +202,13 @@ func (s *Service) Lock(objectPaths ...dbus.ObjectPath) (err error) { objectPaths = []dbus.ObjectPath{s.Dbus.Path()} } - // TODO: make any errs in here a MultiError instead. for _, p := range objectPaths { if err = s.Dbus.Call( DbusServiceLock, 0, p, ).Store(&locked, &resultPath); err != nil { - return + errs = append(errs, err) + err = nil + continue } if isPrompt(resultPath) { @@ -239,28 +216,44 @@ func (s *Service) Lock(objectPaths ...dbus.ObjectPath) (err error) { prompt = NewPrompt(s.Conn, resultPath) if _, err = prompt.Prompt(); err != nil { - return + errs = append(errs, err) + err = nil + continue } } } + if errs != nil && len(errs) > 0 { + err = NewErrors(errs...) + } + return } /* - Open 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. */ -func (s *Service) Open() (session *Session, output dbus.Variant, err error) { +func (s *Service) OpenSession(algo, input string) (session *Session, output dbus.Variant, err error) { var path dbus.ObjectPath + var algoVariant dbus.Variant + var inputVariant dbus.Variant + + if strings.TrimSpace(algo) == "" { + algoVariant = dbus.MakeVariant("plain") + } else { + algoVariant = dbus.MakeVariant(algo) + } + + inputVariant = dbus.MakeVariant(input) // In *theory*, SecretService supports multiple "algorithms" for encryption in-transit, but I don't think it's implemented (yet)? // TODO: confirm this. // 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. if err = s.Dbus.Call( - DbusServiceOpenSession, 0, "plain", dbus.MakeVariant(""), + DbusServiceOpenSession, 0, algoVariant, inputVariant, ).Store(&output, &path); err != nil { return } @@ -271,10 +264,52 @@ func (s *Service) Open() (session *Session, output dbus.Variant, err error) { } /* - SearchItems searches all Collection objects and returns all matches based on the map of attributes. - TODO: return arrays of Items instead of dbus.ObjectPaths. + ReadAlias allows one to fetch a Collection based on an alias name. + An ErrDoesNotExist will be raised if the alias does not exist. + You will almost assuredly want to use Service.GetCollection instead; it works for both alias names and real names. */ -func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []dbus.ObjectPath, lockedItems []dbus.ObjectPath, err error) { +func (s *Service) ReadAlias(alias string) (collection *Collection, err error) { + + var objectPath dbus.ObjectPath + + err = s.Dbus.Call( + DbusServiceReadAlias, 0, alias, + ).Store(&objectPath) + + /* + TODO: Confirm that a nonexistent alias will NOT cause an error to return. + If it does, alter the below logic. + */ + if err != nil { + return + } + + // If the alias does not exist, objectPath will be dbus.ObjectPath("/"). + if objectPath == dbus.ObjectPath("/") { + err = ErrDoesNotExist + return + } + + if collection, err = NewCollection(s, objectPath); err != nil { + return + } + + return +} + +/* + SearchItems searches all Collection objects and returns all matches based on the map of attributes. +*/ +func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []*Item, lockedItems []*Item, err error) { + + var locked []dbus.ObjectPath + var unlocked []dbus.ObjectPath + var collectionObjs []*Collection + var collections map[dbus.ObjectPath]*Collection = make(map[dbus.ObjectPath]*Collection, 0) + var ok bool + var c *Collection + var cPath dbus.ObjectPath + var errs []error = make([]error, 0) if attributes == nil || len(attributes) == 0 { err = ErrMissingAttrs @@ -283,7 +318,66 @@ func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []dbu err = s.Dbus.Call( DbusServiceSearchItems, 0, attributes, - ).Store(&unlockedItems, &lockedItems) + ).Store(&unlocked, &locked) + + lockedItems = make([]*Item, len(locked)) + unlockedItems = make([]*Item, len(unlocked)) + + if collectionObjs, err = s.Collections(); err != nil { + return + } + + for _, c = range collectionObjs { + if _, ok = collections[c.Dbus.Path()]; !ok { + collections[c.Dbus.Path()] = c + } + } + + // Locked items + for idx, i := range locked { + + cPath = dbus.ObjectPath(filepath.Dir(string(i))) + + if c, ok = collections[cPath]; !ok { + errs = append(errs, errors.New(fmt.Sprintf( + "could not find matching Collection for locked item %v", string(i), + ))) + continue + } + + if lockedItems[idx], err = NewItem(c, i); err != nil { + errs = append(errs, errors.New(fmt.Sprintf( + "could not create Item for locked item %v", string(i), + ))) + err = nil + continue + } + } + + // Unlocked items + for idx, i := range unlocked { + + cPath = dbus.ObjectPath(filepath.Dir(string(i))) + + if c, ok = collections[cPath]; !ok { + errs = append(errs, errors.New(fmt.Sprintf( + "could not find matching Collection for unlocked item %v", string(i), + ))) + continue + } + + if unlockedItems[idx], err = NewItem(c, i); err != nil { + errs = append(errs, errors.New(fmt.Sprintf( + "could not create Item for unlocked item %v", string(i), + ))) + err = nil + continue + } + } + + if errs != nil && len(errs) > 0 { + err = NewErrors(errs...) + } return } @@ -312,6 +406,7 @@ func (s *Service) SetAlias(alias string, objectPath dbus.ObjectPath) (err error) */ func (s *Service) Unlock(objectPaths ...dbus.ObjectPath) (err error) { + var errs []error = make([]error, 0) var unlocked []dbus.ObjectPath var prompt *Prompt var resultPath dbus.ObjectPath @@ -320,12 +415,13 @@ func (s *Service) Unlock(objectPaths ...dbus.ObjectPath) (err error) { objectPaths = []dbus.ObjectPath{s.Dbus.Path()} } - // TODO: make any errs in here a MultiError instead. for _, p := range objectPaths { if err = s.Dbus.Call( DbusServiceUnlock, 0, p, ).Store(&unlocked, &resultPath); err != nil { - return + errs = append(errs, err) + err = nil + continue } if isPrompt(resultPath) { @@ -333,10 +429,16 @@ func (s *Service) Unlock(objectPaths ...dbus.ObjectPath) (err error) { prompt = NewPrompt(s.Conn, resultPath) if _, err = prompt.Prompt(); err != nil { - return + errs = append(errs, err) + err = nil + continue } } } + if errs != nil && len(errs) > 0 { + err = NewErrors(errs...) + } + return } diff --git a/session_funcs.go b/session_funcs.go index edc4026..ed1188f 100644 --- a/session_funcs.go +++ b/session_funcs.go @@ -1,21 +1,22 @@ package gosecret import ( - "github.com/godbus/dbus" + "github.com/godbus/dbus/v5" ) -// I'm still not 100% certain what Sessions are used for? +// I'm still not 100% certain what Sessions are used for, aside from getting Secrets from Items. /* NewSession returns a pointer to a new Session based on a Service and a dbus.ObjectPath. - If path is empty (""), the default + You will almost always want to use Service.GetSession or Service.OpenSession instead. */ func NewSession(service *Service, path dbus.ObjectPath) (session *Session) { var ssn Session = Session{ - &DbusObject{ + DbusObject: &DbusObject{ Conn: service.Conn, }, + service: service, } session.Dbus = session.Conn.Object(DbusInterfaceSession, path) diff --git a/sserror_funcs.go b/sserror_funcs.go index ab0d80a..98fa643 100644 --- a/sserror_funcs.go +++ b/sserror_funcs.go @@ -1,5 +1,7 @@ package gosecret +// This currently is not used. + /* TranslateError translates a SecretServiceErrEnum into a SecretServiceError. If a matching error was found, ok will be true and err will be the matching SecretServiceError. @@ -20,6 +22,7 @@ func TranslateError(ssErr SecretServiceErrEnum) (ok bool, err error) { return } +// Error returns the string format of the error; this is necessary to be considered a valid error interface. func (e SecretServiceError) Error() (errStr string) { errStr = e.ErrDesc diff --git a/types.go b/types.go index d783e9f..e1a309b 100644 --- a/types.go +++ b/types.go @@ -3,9 +3,11 @@ package gosecret import ( "time" - "github.com/godbus/dbus" + "github.com/godbus/dbus/v5" ) +// TODO: add label fields to Collection and Item, make their respective Label methods update the field. + /* MultiError is a type of error.Error that can contain multiple error.Errors. Confused? Don't worry about it. */ @@ -65,6 +67,7 @@ type Prompt struct { */ type Service struct { *DbusObject + // Session is a default Session initiated automatically. Session *Session `json:"-"` } @@ -75,6 +78,8 @@ type Service struct { */ type Session struct { *DbusObject + // collection tracks the Service this Session was created from. + service *Service } /* @@ -89,8 +94,10 @@ type Collection struct { lastModified time.Time // lastModifiedSet is unexported; it's only used to determine if this is a first-initialization of the modification time or not. lastModifiedSet bool - // name is used for the Collection's name/label so the Dnus path doesn't need to be parsed all the time. + // 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 *Service } /* @@ -100,6 +107,30 @@ type Collection struct { */ type Item struct { *DbusObject + /* + Attrs are the attributes to assign to this Item. + They should be considered non-secret; they're primarily used to *look up* an Item. + *Do NOT put secret/sensitive data in an Item's Attrs!* + */ + Attrs map[string]string `json:"attributes"` + // Secret is the corresponding Secret object. + Secret *Secret `json:"secret"` + /* + ItemType is the type of this Item as a Dbus interface name. + e.g. org.gnome.keyring.NetworkPassword, org.freedesktop.Secret.Generic, org.remmina.Password, etc. + */ + ItemType string `json:"dbus_type"` + // lastModified is unexported because it's important that API users don't change it; it's used by Collection.Modified. + lastModified time.Time + // lastModifiedSet is unexported; it's only used to determine if this is a first-initialization of the modification time or not. + lastModifiedSet bool + /* + idx is the index identifier of the Item. + It SHOULD correlate to indices in Collection.Items, but don't rely on this. + */ + idx int + // collection tracks the Collection this Item is in. + collection *Collection } /* @@ -109,12 +140,22 @@ type Item struct { https://specifications.freedesktop.org/secret-service/latest/ch14.html#type-Secret */ type Secret struct { - // Session is a Dbus object path for the associated Session. - Session dbus.ObjectPath `json:"-"` - // Parameters are "algorithm dependent parameters for secret value encoding" - likely this will just be an empty byteslice. + // Session is a Dbus object path for the associated Session (the actual Session is stored in an unexported field). + Session dbus.ObjectPath `json:"session_path"` + /* + Parameters are "algorithm dependent parameters for secret value encoding" - likely this will just be an empty byteslice. + Refer to Session for more information. + */ Parameters []byte `json:"params"` // Value is the secret's content in []byte format. - Value []byte `json:"value"` + Value SecretValue `json:"value"` // ContentType is the MIME type of Value. ContentType string `json:"content_type"` + // item is the Item this Secret belongs to. + item *Item + // session is the Session used to decode/decrypt this Secret. + session *Session } + +// SecretValue is a custom type that handles JSON encoding/decoding a little more easily. +type SecretValue []byte