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 === 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 == SecretService Concepts


For reference: 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 (`*Secrets*` are considered "terminating objects" in this model, and contain
actual secret value(s) and metadata). actual secret value(s) and metadata).


@ -79,35 +80,21 @@ So the object hierarchy in *theory* looks kind of like this:
---- ----
Service Service
├─ Session "A" ├─ Session "A"
│ ├─ Collection "A.1" ├─ Session "B"
│ │ ├─ Item "A.1.a" ├─ Collection "A"
│ │ │ ├─ Secret "A_1_a_I" │ ├─ Item "A.1"
│ │ │ └─ Secret "A_1_a_II" │ │ ├─ Secret "A_1_a"
│ │ └─ Item "A.1.b" │ │ └─ Secret "A_1_b"
│ │ ├─ Secret "A_1_b_I" │ └─ Item "A.2"
│ │ └─ Secret "A_1_b_II" │ ├─ Secret "A_2_a"
│ └─ Collection "A.2" │ └─ Secret "A_2_b"
│ ├─ Item "A.2.a" └─ Collection "B"
│ │ ├─ Secret "A_2_a_I" ├─ Item "B.1"
│ │ └─ Secret "A_2_a_II" │ ├─ Secret "B_1_a"
│ └─ Item "A.2.b" │ └─ Secret "B_1_b"
│ ├─ Secret "A_2_b_I" └─ Item "B.2"
│ └─ Secret "A_2_b_II" ├─ Secret "B_2_a"
└─ Session "B" └─ Secret "B_2_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. 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` `strings`
"time" "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. 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. 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, Conn: service.Conn,
Dbus: service.Conn.Object(DbusService, path), Dbus: service.Conn.Object(DbusService, path),
}, },
service: service,
// lastModified: time.Now(), // lastModified: time.Now(),
} }


splitPath = strings.Split(string(coll.Dbus.Path()), "") splitPath = strings.Split(string(coll.Dbus.Path()), "/")


coll.name = splitPath[len(splitPath)-1] coll.name = splitPath[len(splitPath)-1]


@ -58,7 +61,6 @@ func (c *Collection) Items() (items []*Item, err error) {


for idx, path := range paths { for idx, path := range paths {
if item, err = NewItem(c, path); err != nil { if item, err = NewItem(c, path); err != nil {
// return
errs = append(errs, err) errs = append(errs, err)
err = nil err = nil
continue continue
@ -120,22 +122,20 @@ func (c *Collection) SearchItems(profile string) (items []*Item, err error) {
return 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. // 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, secret *Secret, replace bool) (item *Item, err error) { func (c *Collection) CreateItem(label string, attrs map[string]string, secret *Secret, replace bool) (item *Item, err error) {


var prompt *Prompt var prompt *Prompt
var path dbus.ObjectPath var path dbus.ObjectPath
var promptPath dbus.ObjectPath var promptPath dbus.ObjectPath
var variant *dbus.Variant var variant *dbus.Variant
var props map[string]dbus.Variant = make(map[string]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[DbusItemLabel] = dbus.MakeVariant(label)
props["org.freedesktop.Secret.Item.Label"] = dbus.MakeVariant(label) props[DbusItemAttributes] = dbus.MakeVariant(attrs)
props["org.freedesktop.Secret.Item.Attributes"] = dbus.MakeVariant(attrs)


if err = c.Dbus.Call( if err = c.Dbus.Call(
"org.freedesktop.Secret.Collection.CreateItem", 0, props, secret, replace, DbusCollectionCreateItem, 0, props, secret, replace,
).Store(&path, &promptPath); err != nil { ).Store(&path, &promptPath); err != nil {
return return
} }
@ -181,6 +181,10 @@ func (c *Collection) Label() (label string, err error) {


label = variant.Value().(string) label = variant.Value().(string)


if label != c.name {
c.name = label
}

return return
} }



View File

@ -1,11 +1,23 @@
package gosecret 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. // Libsecret/SecretService Dbus interfaces.
const ( const (
// DbusService is the Dbus service bus identifier. // DbusService is the Dbus service bus identifier.
DbusService string = "org.freedesktop.secrets" DbusService string = "org.freedesktop.secrets"
// DbusServiceBase is the base identifier used by interfaces. // DbusServiceBase is the base identifier used by interfaces.
DbusServiceBase string = "org.freedesktop.Secret" 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. // Service interface.
@ -37,10 +49,10 @@ const (
// DbusServiceLockService is [FUNCTION UNKNOWN/UNDOCUMENTED; TODO? NOT IMPLEMENTED.] // DbusServiceLockService is [FUNCTION UNKNOWN/UNDOCUMENTED; TODO? NOT IMPLEMENTED.]
DbusServiceLockService string = DbusInterfaceService + ".LockService" DbusServiceLockService string = DbusInterfaceService + ".LockService"


// DbusServiceOpenSession is used by Service.Open. // DbusServiceOpenSession is used by Service.OpenSession.
DbusServiceOpenSession string = DbusInterfaceService + ".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" DbusServiceReadAlias string = DbusInterfaceService + ".ReadAlias"


// DbusServiceSearchItems is used by Service.SearchItems to get arrays of locked and unlocked Item objects. // 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 is a Dbus boolean for Item.Locked.
DbusItemLocked string = DbusInterfaceItem + ".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" DbusItemAttributes string = DbusInterfaceItem + ".Attributes"


// DbusItemLabel is the name (label) for Item.Label. // DbusItemLabel is the name (label) for Item.Label.
DbusItemLabel string = DbusInterfaceItem + ".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" DbusItemType string = DbusInterfaceItem + ".Type"


// DbusItemCreated is the time an Item was created (in a UNIX Epoch uint64) for Item.Created. // 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/" DbusPromptPrefix string = DbusPath + "/prompt/"
// DbusNewCollectionPath is used to create a new Collection. // DbusNewCollectionPath is used to create a new Collection.
DbusNewCollectionPath string = DbusPath + "/collection/" DbusNewCollectionPath string = DbusPath + "/collection/"
// DbusNewSessionPath is used to create a new Session.
DbusNewSessionPath string = DbusPath + "/session/"
) )


// FLAGS // FLAGS
@ -166,7 +180,7 @@ const (


// SERVICE // SERVICE


// ServiceInitFlag is a flag for Service.Open. // ServiceInitFlag is a flag for Service.OpenSession.
type ServiceInitFlag int type ServiceInitFlag int


const ( const (

54
doc.go
View File

@ -43,13 +43,13 @@ SecretService Concepts


For reference: 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). (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 Service
├─ Session "A" ├─ Session "A"
│ ├─ Collection "A.1" ├─ Session "B"
│ │ ├─ Item "A.1.a" ├─ Collection "A"
│ │ │ ├─ Secret "A_1_a_I" │ ├─ Item "A.1"
│ │ │ └─ Secret "A_1_a_II" │ │ ├─ Secret "A_1_a"
│ │ └─ Item "A.1.b" │ │ └─ Secret "A_1_b"
│ │ ├─ Secret "A_1_b_I" │ └─ Item "A.2"
│ │ └─ Secret "A_1_b_II" │ ├─ Secret "A_2_a"
│ └─ Collection "A.2" │ └─ Secret "A_2_b"
│ ├─ Item "A.2.a" └─ Collection "B"
│ │ ├─ Secret "A_2_a_I" ├─ Item "B.1"
│ │ └─ Secret "A_2_a_II" │ ├─ Secret "B_1_a"
│ └─ Item "A.2.b" │ └─ Secret "B_1_b"
│ ├─ Secret "A_2_b_I" └─ Item "B.2"
│ └─ Secret "A_2_b_II" ├─ Secret "B_2_a"
└─ Session "B" └─ Secret "B_2_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. 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) (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). and a single Collection, named "login" (and aliased to "default", usually).



View File

@ -3,7 +3,7 @@ package gosecret
import ( import (
`strings` `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. // 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 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/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=

View File

@ -1,12 +1,20 @@
package gosecret package gosecret


import ( 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. // 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) { func NewItem(collection *Collection, path dbus.ObjectPath) (item *Item, err error) {


var splitPath []string

if collection == nil { if collection == nil {
err = ErrNoDbusConn err = ErrNoDbusConn
} }
@ -16,12 +24,84 @@ func NewItem(collection *Collection, path dbus.ObjectPath) (item *Item, err erro
} }


item = &Item{ item = &Item{
&DbusObject{ DbusObject: &DbusObject{
Conn: collection.Conn, Conn: collection.Conn,
Dbus: collection.Conn.Object(DbusService, path), 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 return
} }


@ -30,7 +110,7 @@ func (i *Item) Label() (label string, err error) {


var variant dbus.Variant 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 return
} }


@ -39,12 +119,109 @@ func (i *Item) Label() (label string, err error) {
return 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) { func (i *Item) Locked() (isLocked bool, err error) {


var variant dbus.Variant 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 isLocked = true
return return
} }
@ -54,37 +231,52 @@ func (i *Item) Locked() (isLocked bool, err error) {
return return
} }


// GetSecret returns the Secret in an Item using a Session. // Created returns the time.Time of when an Item was created.
func (i *Item) GetSecret(session *Session) (secret *Secret, err error) { func (i *Item) Created() (created time.Time, err error) {


secret = new(Secret) var variant dbus.Variant
var timeInt uint64


if err = i.Dbus.Call( if variant, err = i.Dbus.GetProperty(DbusItemCreated); err != nil {
"org.freedesktop.Secret.Item.GetSecret", 0, session.Path(),
).Store(&secret); err != nil {
return return
} }


timeInt = variant.Value().(uint64)

created = time.Unix(int64(timeInt), 0)

return 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 Note that when calling NewItem, the internal library-tracked modification
var promptPath dbus.ObjectPath 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 return
} }


if isPrompt(promptPath) { timeInt = variant.Value().(uint64)
prompt = NewPrompt(i.Conn, promptPath)


if _, err = prompt.Prompt(); err != nil { modified = time.Unix(int64(timeInt), 0)
return
} 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 return
} }

View File

@ -1,29 +1,22 @@
package gosecret package gosecret


import ( 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. // 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) { func NewPrompt(conn *dbus.Conn, path dbus.ObjectPath) (prompt *Prompt) {


prompt = &Prompt{ prompt = &Prompt{
Conn: conn, DbusObject: &DbusObject{
Dbus: conn.Object(DbusService, path), Conn: conn,
Dbus: conn.Object(DbusService, path),
},
} }


return 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. // Prompt issues/waits for a prompt for unlocking a Locked Collection or Secret / Item.
func (p *Prompt) Prompt() (promptValue *dbus.Variant, err error) { 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) p.Conn.Signal(c)
defer p.Conn.RemoveSignal(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 return
} }


for { for {
if result = <-c; result.Path == p.Path() { if result = <-c; result.Path == p.Dbus.Path() {
*promptValue = result.Body[1].(dbus.Variant) *promptValue = result.Body[1].(dbus.Variant)
return return
} }

View File

@ -4,7 +4,7 @@ package gosecret
func NewSecret(session *Session, params []byte, value []byte, contentType string) (secret *Secret) { func NewSecret(session *Session, params []byte, value []byte, contentType string) (secret *Secret) {


secret = &Secret{ secret = &Secret{
Session: session.Path(), Session: session.Dbus.Path(),
Parameters: params, Parameters: params,
Value: value, Value: value,
ContentType: contentType, 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 package gosecret


import ( 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. // NewService returns a pointer to a new Service connection.
func NewService() (service *Service, err error) { 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)) 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 return
} }


@ -108,40 +115,6 @@ func (s *Service) CreateCollection(label string) (collection *Collection, err er
return 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). GetCollection returns a single Collection based on the name (name can also be an alias).
It's a helper function that avoids needing to make multiple calls in user code. It's a helper function that avoids needing to make multiple calls in user code.
@ -151,7 +124,7 @@ func (s *Service) GetCollection(name string) (c *Collection, err error) {
var colls []*Collection var colls []*Collection


// First check for an alias. // 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 return
} }
if c != nil { 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)) secrets = make(map[dbus.ObjectPath]*Secret, len(itemPaths))


// TODO: trigger a Service.Unlock for any locked items? // 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( if err = s.Dbus.Call(
DbusServiceGetSecrets, 0, itemPaths, DbusServiceGetSecrets, 0, itemPaths,
).Store(&secrets); err != nil { ).Store(&secrets); err != nil {
@ -211,6 +174,17 @@ func (s *Service) GetSecrets(itemPaths ...dbus.ObjectPath) (secrets map[dbus.Obj
return 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. Lock locks an Unlocked Service, Collection, etc.
You can usually get objectPath for the object(s) to unlock via <object>.Dbus.Path(). 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) { 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 locked []dbus.ObjectPath
var prompt *Prompt var prompt *Prompt
var resultPath dbus.ObjectPath var resultPath dbus.ObjectPath
@ -226,12 +202,13 @@ func (s *Service) Lock(objectPaths ...dbus.ObjectPath) (err error) {
objectPaths = []dbus.ObjectPath{s.Dbus.Path()} objectPaths = []dbus.ObjectPath{s.Dbus.Path()}
} }


// TODO: make any errs in here a MultiError instead.
for _, p := range objectPaths { for _, p := range objectPaths {
if err = s.Dbus.Call( if err = s.Dbus.Call(
DbusServiceLock, 0, p, DbusServiceLock, 0, p,
).Store(&locked, &resultPath); err != nil { ).Store(&locked, &resultPath); err != nil {
return errs = append(errs, err)
err = nil
continue
} }


if isPrompt(resultPath) { if isPrompt(resultPath) {
@ -239,28 +216,44 @@ func (s *Service) Lock(objectPaths ...dbus.ObjectPath) (err error) {
prompt = NewPrompt(s.Conn, resultPath) prompt = NewPrompt(s.Conn, resultPath)


if _, err = prompt.Prompt(); err != nil { if _, err = prompt.Prompt(); err != nil {
return errs = append(errs, err)
err = nil
continue
} }
} }
} }


if errs != nil && len(errs) > 0 {
err = NewErrors(errs...)
}

return 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. 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 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)? // In *theory*, SecretService supports multiple "algorithms" for encryption in-transit, but I don't think it's implemented (yet)?
// TODO: confirm this. // TODO: confirm this.
// Possible flags are dbus.Flags consts: https://pkg.go.dev/github.com/godbus/dbus#Flags // Possible flags are dbus.Flags consts: https://pkg.go.dev/github.com/godbus/dbus#Flags
// Oddly, there is no "None" flag. So it's explicitly specified as a null byte. // Oddly, there is no "None" flag. So it's explicitly specified as a null byte.
if err = s.Dbus.Call( if err = s.Dbus.Call(
DbusServiceOpenSession, 0, "plain", dbus.MakeVariant(""), DbusServiceOpenSession, 0, algoVariant, inputVariant,
).Store(&output, &path); err != nil { ).Store(&output, &path); err != nil {
return 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. ReadAlias allows one to fetch a Collection based on an alias name.
TODO: return arrays of Items instead of dbus.ObjectPaths. 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 { if attributes == nil || len(attributes) == 0 {
err = ErrMissingAttrs err = ErrMissingAttrs
@ -283,7 +318,66 @@ func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []dbu


err = s.Dbus.Call( err = s.Dbus.Call(
DbusServiceSearchItems, 0, attributes, 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 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) { func (s *Service) Unlock(objectPaths ...dbus.ObjectPath) (err error) {


var errs []error = make([]error, 0)
var unlocked []dbus.ObjectPath var unlocked []dbus.ObjectPath
var prompt *Prompt var prompt *Prompt
var resultPath dbus.ObjectPath var resultPath dbus.ObjectPath
@ -320,12 +415,13 @@ func (s *Service) Unlock(objectPaths ...dbus.ObjectPath) (err error) {
objectPaths = []dbus.ObjectPath{s.Dbus.Path()} objectPaths = []dbus.ObjectPath{s.Dbus.Path()}
} }


// TODO: make any errs in here a MultiError instead.
for _, p := range objectPaths { for _, p := range objectPaths {
if err = s.Dbus.Call( if err = s.Dbus.Call(
DbusServiceUnlock, 0, p, DbusServiceUnlock, 0, p,
).Store(&unlocked, &resultPath); err != nil { ).Store(&unlocked, &resultPath); err != nil {
return errs = append(errs, err)
err = nil
continue
} }


if isPrompt(resultPath) { if isPrompt(resultPath) {
@ -333,10 +429,16 @@ func (s *Service) Unlock(objectPaths ...dbus.ObjectPath) (err error) {
prompt = NewPrompt(s.Conn, resultPath) prompt = NewPrompt(s.Conn, resultPath)


if _, err = prompt.Prompt(); err != nil { if _, err = prompt.Prompt(); err != nil {
return errs = append(errs, err)
err = nil
continue
} }
} }
} }


if errs != nil && len(errs) > 0 {
err = NewErrors(errs...)
}

return return
} }

View File

@ -1,21 +1,22 @@
package gosecret package gosecret


import ( 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. 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) { func NewSession(service *Service, path dbus.ObjectPath) (session *Session) {


var ssn Session = Session{ var ssn Session = Session{
&DbusObject{ DbusObject: &DbusObject{
Conn: service.Conn, Conn: service.Conn,
}, },
service: service,
} }
session.Dbus = session.Conn.Object(DbusInterfaceSession, path) session.Dbus = session.Conn.Object(DbusInterfaceSession, path)



View File

@ -1,5 +1,7 @@
package gosecret package gosecret


// This currently is not used.

/* /*
TranslateError translates a SecretServiceErrEnum into a SecretServiceError. TranslateError translates a SecretServiceErrEnum into a SecretServiceError.
If a matching error was found, ok will be true and err will be the matching 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 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) { func (e SecretServiceError) Error() (errStr string) {


errStr = e.ErrDesc errStr = e.ErrDesc

View File

@ -3,9 +3,11 @@ package gosecret
import ( import (
"time" "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. 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 { type Service struct {
*DbusObject *DbusObject
// Session is a default Session initiated automatically.
Session *Session `json:"-"` Session *Session `json:"-"`
} }


@ -75,6 +78,8 @@ type Service struct {
*/ */
type Session struct { type Session struct {
*DbusObject *DbusObject
// collection tracks the Service this Session was created from.
service *Service
} }


/* /*
@ -89,8 +94,10 @@ type Collection struct {
lastModified time.Time 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 is unexported; it's only used to determine if this is a first-initialization of the modification time or not.
lastModifiedSet bool lastModifiedSet bool
// name is used for the Collection's name/label so the 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 name string
// service tracks the Service this Collection was created from.
service *Service
} }


/* /*
@ -100,6 +107,30 @@ type Collection struct {
*/ */
type Item struct { type Item struct {
*DbusObject *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 https://specifications.freedesktop.org/secret-service/latest/ch14.html#type-Secret
*/ */
type Secret struct { type Secret struct {
// Session is a Dbus object path for the associated Session. // Session is a Dbus object path for the associated Session (the actual Session is stored in an unexported field).
Session dbus.ObjectPath `json:"-"` Session dbus.ObjectPath `json:"session_path"`
// Parameters are "algorithm dependent parameters for secret value encoding" - likely this will just be an empty byteslice. /*
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"` Parameters []byte `json:"params"`
// Value is the secret's content in []byte format. // 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 is the MIME type of Value.
ContentType string `json:"content_type"` 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