checking in - all basic funcs in place; add a few more then v1 merge

This commit is contained in:
brent s. 2021-12-06 03:24:55 -05:00
parent 1d093627f6
commit 0fc0e0c269
Signed by: bts
GPG Key ID: 8C004C2F93481F6B
16 changed files with 541 additions and 193 deletions

View File

@ -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.

10
TODO Normal file
View File

@ -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)

View File

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


View File

@ -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 (

54
doc.go
View File

@ -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).


View File

@ -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.

2
go.mod
View File

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

4
go.sum
View File

@ -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=

View File

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

View File

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

View File

@ -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,

13
secretvalue_funcs.go Normal file
View File

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

View File

@ -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 <object>.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
}

View File

@ -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)


View File

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

View File

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