3 Commits
v1.0.0 ... V0

Author SHA1 Message Date
df86089517 ready for new release; docs update. 2021-12-13 00:18:36 -05:00
55e740793a updating docs 2021-11-27 02:30:36 -05:00
b96f17fb19 updating docs back into v0 2021-11-26 00:08:30 -05:00
27 changed files with 272 additions and 2600 deletions

39
.gitignore vendored
View File

@@ -1,39 +0,0 @@
*.7z
*.bak
*.deb
*.jar
*.rar
*.run
*.sig
*.tar
*.tar.bz2
*.tar.gz
*.tar.xz
*.tbz
*.tbz2
*.tgz
*.txz
*.zip
.*.swp
.editix
# https://github.com/github/gitignore/blob/master/Go.gitignore
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# But DO include the actual tests.
!_test.go
!*_test.go
!*_test/
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/

4
.idea/.gitignore generated vendored
View File

@@ -1,8 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

33
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,33 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
</value>
</option>
<GoCodeStyleSettings>
<option name="USE_BACK_QUOTES_FOR_IMPORTS" value="true" />
<option name="ADD_PARENTHESES_FOR_SINGLE_IMPORT" value="true" />
<option name="REMOVE_REDUNDANT_IMPORT_ALIASES" value="true" />
<option name="ADD_LEADING_SPACE_TO_COMMENTS" value="true" />
<option name="MOVE_ALL_STDLIB_IMPORTS_IN_ONE_GROUP" value="true" />
<option name="GROUP_STDLIB_IMPORTS" value="true" />
<option name="WRAP_COMP_LIT" value="5" />
<option name="WRAP_FUNC_PARAMS" value="5" />
<option name="WRAP_FUNC_RESULT" value="5" />
</GoCodeStyleSettings>
<XML>
<option name="XML_KEEP_WHITESPACES" value="true" />
<option name="XML_KEEP_WHITE_SPACES_INSIDE_CDATA" value="true" />
</XML>
<codeStyleSettings language="XML">
<option name="WRAP_ON_TYPING" value="1" />
</codeStyleSettings>
<codeStyleSettings language="go">
<option name="RIGHT_MARGIN" value="180" />
<option name="CALL_PARAMETERS_WRAP" value="5" />
<option name="WRAP_ON_TYPING" value="1" />
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="custom" />
</state>
</component>

2
.idea/discord.xml generated
View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="show" value="PROJECT" />
<option name="description" value="" />
</component>
</project>

View File

@@ -1,14 +0,0 @@
https://specifications.freedesktop.org/secret-service/latest/index.html
https://developer-old.gnome.org/libsecret/unstable/
https://developer-old.gnome.org/libsecret/0.18/
https://people.gnome.org/~stefw/libsecret-docs/index.html
https://freedesktop.org/wiki/Specifications/secret-storage-spec/secrets-api-0.1.html#eggdbus-interface-org.freedesktop.Secrets.Collection
Quick reference URLs:
Dbus paths: https://specifications.freedesktop.org/secret-service/latest/ch12.html
Message types: https://dbus.freedesktop.org/doc/dbus-specification.html#type-system
Use this for reference for errors like:
Type of message, “(ao)”, does not match expected type “(aoo)”
See dbus_types subdir for Golang structs that can be used in the future to improve these error messages.

View File

@@ -1,116 +0,0 @@
package dbus_types
// https://dbus.freedesktop.org/doc/dbus-specification.html#type-system
type DbusType struct {
TypeName string
Symbol rune
Desc string
ZeroValue interface{}
}
/*
BASIC TYPES
*/
var DbusByte DbusType = DbusType{
TypeName: "BYTE",
Symbol: 'y',
Desc: "Unsigned 8-bit integer",
ZeroValue: byte(0x0),
}
var DbusBoolean DbusType = DbusType{
TypeName: "BOOLEAN",
Symbol: 'b',
Desc: "Boolean value: 0 is false, 1 is true, any other value allowed by the marshalling format is invalid",
ZeroValue: false,
}
var DbusInt16 DbusType = DbusType{
TypeName: "INT16",
Symbol: 'n',
Desc: "Signed (two's complement) 16-bit integer",
ZeroValue: int16(0),
}
var DbusUint16 DbusType = DbusType{
TypeName: "UINT16",
Symbol: 'q',
Desc: "Unsigned 16-bit integer",
ZeroValue: uint16(0),
}
var DbusInt32 DbusType = DbusType{
TypeName: "INT32",
Symbol: 'i',
Desc: "Signed (two's complement) 32-bit integer",
ZeroValue: int32(0),
}
var DbusUint32 DbusType = DbusType{
TypeName: "UINT32",
Symbol: 'u',
Desc: "Unsigned 32-bit integer",
ZeroValue: uint32(0),
}
var DbusInt64 DbusType = DbusType{
TypeName: "INT64",
Symbol: 'x',
Desc: "Signed (two's complement) 64-bit integer (mnemonic: x and t are the first characters in \"sixty\" not already used for something more common)",
ZeroValue: int64(0),
}
var DbusUint64 DbusType = DbusType{
TypeName: "UINT64",
Symbol: 't',
Desc: "Unsigned 64-bit integer",
ZeroValue: uint64(0),
}
var DbusDoubleFloat DbusType = DbusType{
TypeName: "DOUBLE",
Symbol: 'd',
Desc: "IEEE 754 double-precision floating point",
ZeroValue: float64(0),
}
var DbusUnixFD DbusType = DbusType{
TypeName: "UNIX_FD",
Symbol: 'h',
Desc: "Unsigned 32-bit integer representing an index into an out-of-band array of file descriptors, transferred via some platform-specific mechanism (mnemonic: h for handle)",
ZeroValue: uint32(0), // See https://pkg.go.dev/github.com/godbus/dbus#UnixFDIndex
}
var DbusString DbusType = DbusType{
TypeName: "STRING",
Symbol: 'o',
Desc: "No extra constraints",
ZeroValue: "",
}
var DbusObjectPath DbusType = DbusType{
TypeName: "OBJECT_PATH",
Symbol: 'o',
Desc: "A syntactically valid Path for Dbus",
ZeroValue: nil, // ???
}
var DbusSignature DbusType = DbusType{
TypeName: "SIGNATURE",
Symbol: 'g',
Desc: "0 or more single complete types", // ???
ZeroValue: nil, // ???
}
/*
CONTAINER TYPES
*/
/*
TODO: not sure how to struct this natively, but:
Dbus Struct: (<symbol(s)...>) // Note: structs can be nested e.g. (i(ii))
Dbus Array: a<symbol> // The symbol can be any type (even nested arrays, e.g. aai), but only one type is allowed. Arrays are like Golang slices; no fixed size.
Dbus Variant: v<symbol> // Dbus equivalent of interface{}, more or less. See https://dbus.freedesktop.org/doc/dbus-specification.html#container-types
Dbus Dict: [kv] // Where k is the key's type and v is the value's type.
*/

12
TODO
View File

@@ -1,11 +1 @@
- TEST CASES
-- https://pkg.go.dev/testing
-- https://go.dev/doc/tutorial/add-a-test
-- https://gobyexample.com/testing
-- https://blog.alexellis.io/golang-writing-unit-tests/
- Benchmarking?
- 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)
- tests for V0

View File

@@ -1,106 +1,57 @@
package gosecret
package libsecret
import (
`strings`
"time"
`github.com/godbus/dbus/v5`
`github.com/godbus/dbus`
)
/*
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.
*/
func NewCollection(service *Service, path dbus.ObjectPath) (coll *Collection, err error) {
var splitPath []string
if service == nil {
err = ErrNoDbusConn
}
if _, err = validConnPath(service.Conn, path); err != nil {
return
}
// NewCollection returns a pointer to a new Collection based on a Dbus connection and a Dbus path.
func NewCollection(conn *dbus.Conn, path dbus.ObjectPath) (coll *Collection) {
coll = &Collection{
DbusObject: &DbusObject{
Conn: service.Conn,
Dbus: service.Conn.Object(DbusService, path),
},
service: service,
// lastModified: time.Now(),
Conn: conn,
Dbus: conn.Object(DBusServiceName, path),
}
splitPath = strings.Split(string(coll.Dbus.Path()), "/")
coll.name = splitPath[len(splitPath)-1]
_, _, err = coll.Modified()
return
}
/*
CreateItem returns a pointer to an Item based on a label, some attributes, a Secret,
whether any existing secret with the same label should be replaced or not, and the optional itemType.
// Path returns the dbus.ObjectPath of the Collection.
func (c Collection) Path() (coll dbus.ObjectPath) {
itemType is optional; if specified, it should be a Dbus interface (only the first element is used).
If not specified, the default DbusDefaultItemType will be used.
*/
func (c *Collection) CreateItem(label string, attrs map[string]string, secret *Secret, replace bool, itemType ...string) (item *Item, err error) {
// Remove this method in V1. It's bloat since we now have an exported Dbus.
coll = c.Dbus.Path()
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 typeString string
return
}
if itemType != nil && len(itemType) > 0 {
typeString = itemType[0]
} else {
typeString = DbusDefaultItemType
}
// Items returns a slice of Item items in the Collection.
func (c *Collection) Items() (items []Item, err error) {
props[DbusItemLabel] = dbus.MakeVariant(label)
props[DbusItemType] = dbus.MakeVariant(typeString)
props[DbusItemAttributes] = dbus.MakeVariant(attrs)
var variant dbus.Variant
var paths []dbus.ObjectPath
if err = c.Dbus.Call(
DbusCollectionCreateItem, 0, props, secret, replace,
).Store(&path, &promptPath); err != nil {
if variant, err = c.Dbus.GetProperty("org.freedesktop.Secret.Collection.Items"); err != nil {
return
}
if isPrompt(promptPath) {
prompt = NewPrompt(c.Conn, promptPath)
paths = variant.Value().([]dbus.ObjectPath)
if variant, err = prompt.Prompt(); err != nil {
return
}
items = make([]Item, len(paths))
path = variant.Value().(dbus.ObjectPath)
for idx, path := range paths {
items[idx] = *NewItem(c.Conn, path)
}
item, err = NewItem(c, path)
return
}
/*
Delete removes a Collection.
While *technically* not necessary, it is recommended that you iterate through
Collection.Items and do an Item.Delete for each item *before* calling Collection.Delete;
the item paths are cached as "orphaned paths" in Dbus otherwise if not deleted before deleting
their Collection. They should clear on a reboot or restart of Dbus (but rebooting Dbus on a system in use is... troublesome).
*/
// Delete removes a Collection.
func (c *Collection) Delete() (err error) {
var promptPath dbus.ObjectPath
var prompt *Prompt
if err = c.Dbus.Call(DbusCollectionDelete, 0).Store(&promptPath); err != nil {
if err = c.Dbus.Call("org.freedesktop.Secret.Collection.Delete", 0).Store(&promptPath); err != nil {
return
}
@@ -115,59 +66,68 @@ func (c *Collection) Delete() (err error) {
return
}
// Items returns a slice of Item pointers in the Collection.
func (c *Collection) Items() (items []*Item, err error) {
// SearchItems searches a Collection for a matching profile string.
func (c *Collection) SearchItems(profile string) (items []Item, err error) {
var paths []dbus.ObjectPath
var item *Item
var variant dbus.Variant
var errs []error = make([]error, 0)
var attrs map[string]string = make(map[string]string, 0)
if variant, err = c.Dbus.GetProperty(DbusCollectionItems); err != nil {
attrs["profile"] = profile
if err = c.Dbus.Call("org.freedesktop.Secret.Collection.SearchItems", 0, attrs).Store(&paths); err != nil {
return
}
paths = variant.Value().([]dbus.ObjectPath)
items = make([]*Item, len(paths))
items = make([]Item, len(paths))
for idx, path := range paths {
if item, err = NewItem(c, path); err != nil {
errs = append(errs, err)
err = nil
continue
}
items[idx] = item
items[idx] = *NewItem(c.Conn, path)
}
err = NewErrors(err)
return
}
// Label returns the Collection label (name).
func (c *Collection) Label() (label string, err error) {
// CreateItem returns a pointer to an Item based on a label, a Secret, and whether any existing secret should be replaced or not.
func (c *Collection) CreateItem(label string, secret *Secret, replace bool) (item *Item, err error) {
var variant dbus.Variant
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)
if variant, err = c.Dbus.GetProperty(DbusCollectionLabel); err != nil {
attrs["profile"] = label
props["org.freedesktop.Secret.Item.Label"] = dbus.MakeVariant(label)
props["org.freedesktop.Secret.Item.Attributes"] = dbus.MakeVariant(attrs)
if err = c.Dbus.Call(
"org.freedesktop.Secret.Collection.CreateItem", 0, props, secret, replace,
).Store(&path, &promptPath); err != nil {
return
}
label = variant.Value().(string)
if isPrompt(promptPath) {
prompt = NewPrompt(c.Conn, promptPath)
if label != c.name {
c.name = label
if variant, err = prompt.Prompt(); err != nil {
return
}
path = variant.Value().(dbus.ObjectPath)
}
item = NewItem(c.Conn, path)
return
}
// Locked indicates if a Collection is locked (true) or unlocked (false).
// Locked indicates that a Collection is locked (true) or unlocked (false).
func (c *Collection) Locked() (isLocked bool, err error) {
var variant dbus.Variant
if variant, err = c.Dbus.GetProperty(DbusCollectionLocked); err != nil {
if variant, err = c.Dbus.GetProperty("org.freedesktop.Secret.Collection.Locked"); err != nil {
isLocked = true
return
}
@@ -176,128 +136,3 @@ func (c *Collection) Locked() (isLocked bool, err error) {
return
}
// Relabel modifies the Collection's label in Dbus.
func (c *Collection) Relabel(newLabel string) (err error) {
var variant dbus.Variant = dbus.MakeVariant(newLabel)
if err = c.Dbus.SetProperty(DbusCollectionLabel, variant); err != nil {
return
}
return
}
/*
SearchItems searches a Collection for a matching profile string.
It's mostly a carry-over from go-libsecret, and is here for convenience. IT MAY BE REMOVED IN THE FUTURE.
I promise it's not useful for any other implementation/storage of SecretService whatsoever.
Deprecated: Use Service.SearchItems instead.
*/
func (c *Collection) SearchItems(profile string) (items []*Item, err error) {
var paths []dbus.ObjectPath
var errs []error = make([]error, 0)
var attrs map[string]string = make(map[string]string, 0)
attrs["profile"] = profile
if err = c.Dbus.Call(
DbusCollectionSearchItems, 0, attrs,
).Store(&paths); err != nil {
return
}
items = make([]*Item, len(paths))
for idx, path := range paths {
if items[idx], err = NewItem(c, path); err != nil {
errs = append(errs, err)
err = nil
continue
}
}
err = NewErrors(err)
return
}
// Created returns the time.Time of when a Collection was created.
func (c *Collection) Created() (created time.Time, err error) {
var variant dbus.Variant
var timeInt uint64
if variant, err = c.Dbus.GetProperty(DbusCollectionCreated); err != nil {
return
}
timeInt = variant.Value().(uint64)
created = time.Unix(int64(timeInt), 0)
return
}
/*
Modified returns the time.Time of when a Collection was last modified along with a boolean
that indicates if the collection has changed since the last call of Collection.Modified.
Note that when calling NewCollection, the internal library-tracked modification
time (Collection.lastModified) will be set to the latest modification time of the Collection
itself as reported by Dbus rather than the time that NewCollection was called.
*/
func (c *Collection) Modified() (modified time.Time, isChanged bool, err error) {
var variant dbus.Variant
var timeInt uint64
if variant, err = c.Dbus.GetProperty(DbusCollectionModified); err != nil {
return
}
timeInt = variant.Value().(uint64)
modified = time.Unix(int64(timeInt), 0)
if !c.lastModifiedSet {
// It's "nil", so set it to modified. We can't check for a zero-value in case Dbus has it as a zero-value.
c.lastModified = modified
c.lastModifiedSet = true
}
isChanged = modified.After(c.lastModified)
c.lastModified = modified
return
}
/*
PathName returns the "real" name of a Collection.
In some cases, the Collection.Label may not be the actual *name* of the collection
(i.e. the label is different from the name used in the Dbus path).
This is a thin wrapper around simply extracting the last item from
the Collection.Dbus.Path().
*/
func (c *Collection) PathName() (realName string) {
var pathSplit []string = strings.Split(string(c.Dbus.Path()), "/")
realName = pathSplit[len(pathSplit)-1]
return
}
/*
setModify updates the Collection's modification time (as specified by Collection.Modified).
It seems that this does not update automatically.
*/
func (c *Collection) setModify() (err error) {
err = c.Dbus.SetProperty(DbusCollectionModified, uint64(time.Now().Unix()))
return
}

View File

@@ -1,286 +0,0 @@
package gosecret
import (
`testing`
`github.com/godbus/dbus/v5`
)
// Some functions are covered in the Service tests.
/*
TestNewCollection tests the following internal functions/methods via nested calls:
(all calls in TestNewService)
NewService
NewCollection
*/
func TestNewCollection(t *testing.T) {
var svc *Service
var collection *Collection
var err error
if svc, err = NewService(); err != nil {
t.Fatalf("NewService failed: %v", err.Error())
}
if collection, err = NewCollection(svc, dbus.ObjectPath(dbusDefaultCollectionPath)); err != nil {
t.Errorf(
"TestNewCollection failed when fetching collection at '%v': %v",
dbusDefaultCollectionPath, err.Error(),
)
}
_ = collection
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}
/*
TestCollection_Items tests the following internal functions/methods via nested calls:
(all calls in TestNewCollection)
Service.GetCollection
Collection.Items
NewSecret
Collection.CreateItem
Collection.SearchItems
Item.Delete
*/
func TestCollection_Items(t *testing.T) {
var svc *Service
var collection *Collection
var items []*Item
var item *Item
var searchItemResults []*Item
var secret *Secret
var err error
if svc, err = NewService(); err != nil {
t.Fatalf("NewService failed: %v", err.Error())
}
if collection, err = svc.GetCollection(defaultCollection); err != nil {
t.Errorf("failed when fetching collection '%v': %v",
defaultCollection, err.Error(),
)
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
}
if items, err = collection.Items(); err != nil {
t.Errorf(
"failed fetching items for '%v' at '%v': %v",
defaultCollection, string(collection.Dbus.Path()), err.Error(),
)
} else {
t.Logf("found %v items in collection '%v' at '%v'", len(items), defaultCollection, string(collection.Dbus.Path()))
}
/* This is almost always going to trigger the warning. See Item.idx for details why.
var label string
for idx, i := range items {
if label, err = i.Label(); err != nil {
t.Errorf("failed to get label of item '%v' in collection '%v': %v", string(i.Dbus.Path()), collectionName.String(), err.Error())
continue
}
if i.idx != idx {
t.Logf(
"WARN: item '%v' ('%v') in collection '%v' internal IDX ('%v') does NOT match native slice IDX ('%v')",
string(i.Dbus.Path()), label, collectionName.String(), i.idx, idx,
)
}
}
*/
secret = NewSecret(svc.Session, []byte{}, []byte(testSecretContent), "text/plain")
if item, err = collection.CreateItem(testItemLabel, itemAttrs, secret, false); err != nil {
t.Errorf(
"could not create item '%v' in collection '%v': %v",
testItemLabel, defaultCollection, err.Error(),
)
} else {
if searchItemResults, err = collection.SearchItems(testItemLabel); err != nil {
t.Errorf("failed to find item '%v' via Collection.SearchItems: %v", string(item.Dbus.Path()), err.Error())
} else if len(searchItemResults) == 0 {
t.Errorf("failed to find item '%v' via Collection.SearchItems, returned 0 results (should be at least 1)", testItemLabel)
} else {
t.Logf("found %v results for Collection.SearchItems", len(searchItemResults))
}
if err = item.Delete(); err != nil {
t.Errorf("failed to delete created item '%v': %v", string(item.Dbus.Path()), err.Error())
}
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}
/*
TestCollection_Label tests the following internal functions/methods via nested calls:
(all calls in TestNewCollection)
Service.GetCollection
Collection.Label
Collection.PathName
*/
func TestCollection_Label(t *testing.T) {
var svc *Service
var collection *Collection
var collLabel string
var err error
if svc, err = NewService(); err != nil {
t.Fatalf("NewService failed: %v", err.Error())
}
if collection, err = svc.GetCollection(defaultCollectionLabel); err != nil {
t.Errorf(
"failed when fetching collection '%v': %v",
defaultCollectionLabel, err.Error(),
)
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
}
if collLabel, err = collection.Label(); err != nil {
t.Errorf("cannot fetch label for '%v': %v", string(collection.Dbus.Path()), err.Error())
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
}
if defaultCollectionLabel != collLabel {
t.Errorf("fetched collection ('%v') does not match fetched collection label ('%v')", collLabel, defaultCollectionLabel)
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}
/*
TestCollection_Locked tests the following internal functions/methods via nested calls:
(all calls in TestNewCollection)
Collection.Locked
*/
func TestCollection_Locked(t *testing.T) {
var svc *Service
var collection *Collection
var isLocked bool
var err error
if svc, err = NewService(); err != nil {
t.Fatalf("NewService failed: %v", err.Error())
}
if collection, err = svc.GetCollection(defaultCollection); err != nil {
t.Errorf(
"failed when fetching collection '%v': %v",
defaultCollectionLabel, err.Error(),
)
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
}
if isLocked, err = collection.Locked(); err != nil {
t.Errorf("failed to get lock status for collection '%v': %v", collection.PathName(), err.Error())
} else {
t.Logf("collection '%v' lock status: %v", collection.PathName(), isLocked)
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}
/*
TestCollection_Relabel tests the following internal functions/methods via nested calls:
(all calls in TestNewCollection)
Service.CreateCollection
Collection.Relabel
*/
func TestCollection_Relabel(t *testing.T) {
var svc *Service
var collection *Collection
var collLabel string
var newCollLabel string = collectionAlias.String()
var err error
if svc, err = NewService(); err != nil {
t.Fatalf("NewService failed: %v", err.Error())
}
if collection, err = svc.CreateCollection(collectionName.String()); err != nil {
t.Errorf("could not create collection '%v': %v", collectionName.String(), err.Error())
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
} else {
t.Logf("created collection '%v' at path '%v' successfully", collectionName.String(), string(collection.Dbus.Path()))
}
if collLabel, err = collection.Label(); err != nil {
t.Errorf("could not fetch label for collection '%v': %v", string(collection.Dbus.Path()), err.Error())
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
}
if err = collection.Relabel(newCollLabel); err != nil {
t.Errorf("failed to relabel collection '%v' to '%v': %v", collLabel, newCollLabel, err.Error())
} else {
t.Logf("relabeled collection '%v' to '%v'", collLabel, newCollLabel)
}
if collLabel, err = collection.Label(); err != nil {
t.Errorf("could not fetch label for collection '%v': %v", string(collection.Dbus.Path()), err.Error())
if err = collection.Delete(); err != nil {
t.Errorf("failed to delete collection '%v': %v", string(collection.Dbus.Path()), err.Error())
}
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
} else {
if collLabel != newCollLabel {
t.Errorf("collection did not relabel; new label '%v', actual label '%v'", newCollLabel, collLabel)
}
}
if err = collection.Delete(); err != nil {
t.Errorf("failed to delete collection '%v': %v", string(collection.Dbus.Path()), err.Error())
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}

257
consts.go
View File

@@ -1,256 +1,7 @@
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"
/*
DbusDefaultItemType is the default type to use for Item.Type.
I've only ever seen "org.gnome.keyring.NetworkPassword" in the wild
aside from the below. It may be legacy (gnome-keyring is obsoleted by SecretService).
If in doubt, the below is considered the "proper" interface.
*/
DbusDefaultItemType string = DbusServiceBase + ".Generic"
)
// Service interface.
const (
/*
DbusInterfaceService is the Dbus interface for working with a Service.
Found at /org/freedesktop/secrets/(DbusInterfaceService)
*/
DbusInterfaceService string = DbusServiceBase + ".Service"
// Methods
/*
DbusServiceChangeLock has some references in the SecretService Dbus API but
it seems to be obsolete - undocumented, at the least.
So we don't implement it.
*/
// DbusServiceChangeLock string = DbusInterfaceService + ".ChangeLock"
// DbusServiceCreateCollection is used to create a new Collection via Service.CreateCollection.
DbusServiceCreateCollection string = DbusInterfaceService + ".CreateCollection"
// DbusServiceGetSecrets is used to fetch multiple Secret values from multiple Item items in a given Collection (via Service.GetSecrets).
DbusServiceGetSecrets string = DbusInterfaceService + ".GetSecrets"
// DbusServiceLock is used by Service.Lock.
DbusServiceLock string = DbusInterfaceService + ".Lock"
// DbusServiceLockService is [FUNCTION UNKNOWN/UNDOCUMENTED; TODO? NOT IMPLEMENTED.]
// DbusServiceLockService string = DbusInterfaceService + ".LockService"
// DbusServiceOpenSession is used by Service.OpenSession.
DbusServiceOpenSession string = DbusInterfaceService + ".OpenSession"
// 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.
DbusServiceSearchItems string = DbusInterfaceService + ".SearchItems"
// DbusServiceSetAlias is used by Service.SetAlias to set an alias for a Collection.
DbusServiceSetAlias string = DbusInterfaceService + ".SetAlias"
// DbusServiceUnlock is used by Service.Unlock.
DbusServiceUnlock string = DbusInterfaceService + ".Unlock"
// Properties
// DbusServiceCollections is used to get a Dbus array of Collection items (Service.Collections).
DbusServiceCollections string = DbusInterfaceService + ".Collections"
)
// Session interface.
const (
/*
DbusInterfaceSession is the Dbus interface for working with a Session.
Found at /org/freedesktop/secrets/session/<session ID>/(DbusInterfaceSession)
*/
DbusInterfaceSession = DbusServiceBase + ".Session"
// Methods
// DbusSessionClose is used for Session.Close.
DbusSessionClose string = DbusInterfaceSession + ".Close"
)
// Collection interface.
const (
/*
DbusInterfaceCollection is the Dbus interface for working with a Collection.
Found at /org/freedesktop/secrets/collection/<collection name>/(DbusInterfaceCollection)
*/
DbusInterfaceCollection string = DbusServiceBase + ".Collection"
// Methods
// DbusCollectionCreateItem is used for Collection.CreateItem.
DbusCollectionCreateItem string = DbusInterfaceCollection + ".CreateItem"
// DbusCollectionDelete is used for Collection.Delete.
DbusCollectionDelete string = DbusInterfaceCollection + ".Delete"
// DbusCollectionSearchItems is used for Collection.SearchItems.
DbusCollectionSearchItems string = DbusInterfaceCollection + ".SearchItems"
// Properties
// DbusCollectionItems is a Dbus array of Item.
DbusCollectionItems string = DbusInterfaceCollection + ".Items"
// DbusCollectionLocked is a Dbus boolean for Collection.Locked.
DbusCollectionLocked string = DbusInterfaceCollection + ".Locked"
// DbusCollectionLabel is the name (label) for Collection.Label.
DbusCollectionLabel string = DbusInterfaceCollection + ".Label"
// DbusCollectionCreated is the time a Collection was created (in a UNIX Epoch uint64) for Collection.Created.
DbusCollectionCreated string = DbusInterfaceCollection + ".Created"
// DbusCollectionModified is the time a Collection was last modified (in a UNIX Epoch uint64) for Collection.Modified.
DbusCollectionModified string = DbusInterfaceCollection + ".Modified"
// TODO: Signals?
)
// Item interface.
const (
/*
DbusInterfaceItem is the Dbus interface for working with Item items.
Found at /org/freedesktop/secrets/collection/<collection name>/<item index>/(DbusInterfaceItem)
*/
DbusInterfaceItem string = DbusServiceBase + ".Item"
// Methods
// DbusItemDelete is used by Item.Delete.
DbusItemDelete string = DbusInterfaceItem + ".Delete"
// DbusItemGetSecret is used by Item.GetSecret.
DbusItemGetSecret string = DbusInterfaceItem + ".GetSecret"
// DbusItemSetSecret is used by Item.SetSecret.
DbusItemSetSecret string = DbusInterfaceItem + ".SetSecret"
// Properties
// DbusItemLocked is a Dbus boolean for Item.Locked.
DbusItemLocked string = DbusInterfaceItem + ".Locked"
/*
DbusItemAttributes contains attributes (metadata, schema, etc.) for
Item.Attributes, Item.ReplaceAttributes, and Item.ModifyAttributes.
*/
DbusItemAttributes string = DbusInterfaceItem + ".Attributes"
// DbusItemLabel is the name (label) for Item.Label.
DbusItemLabel string = DbusInterfaceItem + ".Label"
// 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.
DbusItemCreated string = DbusInterfaceItem + ".Created"
// DbusItemModified is the time an Item was last modified (in a UNIX Epoch uint64) for Item.Modified.
DbusItemModified string = DbusInterfaceItem + ".Modified"
)
// Dbus paths.
const (
// DbusPath is the path for DbusService.
DbusPath string = "/org/freedesktop/secrets"
// DbusPromptPrefix is the path used for prompts comparison.
DbusPromptPrefix string = DbusPath + "/prompt/"
// 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
// These are not currently used, but may be in the future.
// SERVICE
// ServiceInitFlag is a flag for Service.OpenSession.
type ServiceInitFlag int
package libsecret
const (
FlagServiceNone ServiceInitFlag = iota
FlagServiceOpenSession
FlagServiceLoadCollections
)
// ServiceSearchFlag is a flag for Service.SearchItems.
type ServiceSearchFlag int
const (
FlagServiceSearchNone ServiceSearchFlag = iota
FlagServiceSearchAll
FlagServiceSearchUnlock
FlagServiceSearchLoadSecrets
)
// COLLECTION
// CollectionInitFlag is a flag for Collection.SearchItems and Collection.Items.
type CollectionInitFlag int
const (
FlagCollectionNone CollectionInitFlag = iota
FlagCollectionLoadItems
)
// ITEM
// ItemInitFlag are flags for Collection.SearchItems and Collection.Items.
type ItemInitFlag int
const (
FlagItemNone ItemInitFlag = iota
FlagItemLoadSecret
)
// ItemSearchFlag are flags for Collection.CreateItem.
type ItemSearchFlag int
const (
FlagItemCreateNone ItemSearchFlag = iota
FlatItemCreateReplace
)
// ERRORS
/*
SecretServiceErrEnum are just constants for the enum'd errors;
see SecretServiceError type and ErrSecretService* vars for what
actually gets returned.
They're used for finding the appropriate matching error.
*/
type SecretServiceErrEnum int
const (
EnumErrProtocol SecretServiceErrEnum = iota
EnumErrIsLocked
EnumErrNoSuchObject
EnumErrAlreadyExists
EnumErrInvalidFileFormat
DBusServiceName string = "org.freedesktop.secrets"
DBusPath string = "/org/freedesktop/secrets"
PromptPrefix string = DBusPath + "/prompt/"
)

View File

@@ -1,32 +0,0 @@
package gosecret
import (
`github.com/google/uuid`
)
// Paths.
const (
dbusCollectionPath string = DbusPath + "/collection"
dbusDefaultCollectionPath string = dbusCollectionPath + "/login"
)
// Strings.
const (
defaultCollectionAlias string = "default" // SHOULD point to a collection named "login" (below); "default" is the alias.
defaultCollection string = "login"
defaultCollectionLabel string = "Login" // a display name; the label is lowercased and normalized for the path (per above).
testAlias string = "GOSECRET_TESTING_ALIAS"
testSecretContent string = "This is a test secret for gosecret."
testItemLabel string = "Gosecret Test Item"
)
// Objects.
var (
collectionName uuid.UUID = uuid.New()
collectionAlias uuid.UUID = uuid.New()
itemAttrs map[string]string = map[string]string{
"GOSECRET": "yes",
"foo": "bar",
"profile": testItemLabel,
}
)

73
errs.go
View File

@@ -1,73 +0,0 @@
package gosecret
import (
"errors"
)
// General errors.
var (
// ErrBadDbusPath indicates an invalid path - either nothing exists at that path or the path is malformed.
ErrBadDbusPath error = errors.New("invalid dbus path")
// ErrInvalidProperty indicates a dbus.Variant is not the "real" type expected.
ErrInvalidProperty error = errors.New("invalid variant type; cannot convert")
// ErrNoDbusConn gets triggered if a connection to Dbus can't be detected.
ErrNoDbusConn error = errors.New("no valid dbus connection")
// ErrMissingPaths gets triggered if one or more Dbus object paths are expected but non/not enough are received.
ErrMissingPaths error = errors.New("one or more Dbus object paths were expected but an insufficient amount were received")
// ErrMissingAttrs gets triggered if attributes were expected but not passed.
ErrMissingAttrs error = errors.New("attributes must not be empty/nil")
// ErrDoesNotExist gets triggered if a Collection, Item, etc. is attempted to be fetched but none exists via the specified identifier.
ErrDoesNotExist error = errors.New("the object under that name/label/alias does not exist")
)
/*
Translated SecretService errors.
See https://developer-old.gnome.org/libsecret/unstable/libsecret-SecretError.html#SecretError.
Used by TranslateError.
*/
var (
ErrUnknownSecretServiceErr error = errors.New("cannot find matching SecretService error")
ErrSecretServiceProto SecretServiceError = SecretServiceError{
ErrCode: EnumErrProtocol,
ErrName: "SECRET_ERROR_PROTOCOL",
ErrDesc: "an invalid message or data was received from SecretService",
}
ErrSecretServiceLocked SecretServiceError = SecretServiceError{
ErrCode: EnumErrIsLocked,
ErrName: "SECRET_ERROR_IS_LOCKED",
ErrDesc: "the item/collection is locked; the specified operation cannot be performed",
}
ErrSecretServiceNoObj SecretServiceError = SecretServiceError{
ErrCode: EnumErrNoSuchObject,
ErrName: "SECRET_ERROR_NO_SUCH_OBJECT",
ErrDesc: "no such item/collection was found in SecretService",
}
ErrSecretServiceExists SecretServiceError = SecretServiceError{
ErrCode: EnumErrAlreadyExists,
ErrName: "SECRET_ERROR_ALREADY_EXISTS",
ErrDesc: "a relevant item/collection already exists",
}
ErrSecretServiceInvalidFormat SecretServiceError = SecretServiceError{
ErrCode: EnumErrInvalidFileFormat,
ErrName: "SECRET_ERROR_INVALID_FILE_FORMAT",
ErrDesc: "the file/content format is invalid",
}
/*
AllSecretServiceErrs provides a slice of these for easier iteration when translating.
TECHNICALLY, because they are indexed in the order of their enums, you could
simplify and optimize translation by just doing e.g.
err = AllSecretServiceErrs[EnumErrProtocol]
But this should be considered UNSTABLE and UNSAFE due to it being potentially unpredictable in the future.
There are only 5 errors currently, so the performance benefits would be negligible compared to iteration.
If SecretService adds more errors, however, this may be more desirable.
*/
AllSecretServiceErrs []SecretServiceError = []SecretServiceError{
ErrSecretServiceProto,
ErrSecretServiceLocked,
ErrSecretServiceNoObj,
ErrSecretServiceExists,
ErrSecretServiceInvalidFormat,
}
)

107
funcs.go
View File

@@ -1,117 +1,16 @@
package gosecret
package libsecret
import (
`strings`
`github.com/godbus/dbus/v5`
`github.com/godbus/dbus`
)
// isPrompt returns a boolean that is true if path is/requires a prompt(ed path) and false if it is/does not.
func isPrompt(path dbus.ObjectPath) (prompt bool) {
prompt = strings.HasPrefix(string(path), DbusPromptPrefix)
prompt = strings.HasPrefix(string(path), PromptPrefix)
return
}
// connIsValid returns a boolean if the dbus.conn named conn is active.
func connIsValid(conn *dbus.Conn) (ok bool, err error) {
// dbus.Conn.Names() will ALWAYS return a []string with at least ONE element.
if conn == nil || (conn.Names() == nil || len(conn.Names()) < 1) {
err = ErrNoDbusConn
return
}
ok = true
return
}
/*
pathIsValid implements path checking for valid Dbus paths. Currently it only checks to make sure path is not a blank string.
The path argument can be either a string or dbus.ObjectPath.
*/
func pathIsValid(path interface{}) (ok bool, err error) {
var realPath string
switch p := path.(type) {
case dbus.ObjectPath:
if !p.IsValid() {
err = ErrBadDbusPath
return
}
realPath = string(p)
case string:
realPath = p
default:
err = ErrBadDbusPath
return
}
if strings.TrimSpace(realPath) == "" {
err = ErrBadDbusPath
return
}
ok = true
return
}
/*
validConnPath condenses the checks for connIsValid and pathIsValid into one func due to how frequently this check is done.
err is a MultiError, which can be treated as an error.error. (See https://pkg.go.dev/builtin#error)
*/
func validConnPath(conn *dbus.Conn, path interface{}) (cr *ConnPathCheckResult, err error) {
var connErr error
var pathErr error
cr = new(ConnPathCheckResult)
cr.ConnOK, connErr = connIsValid(conn)
cr.PathOK, pathErr = pathIsValid(path)
err = NewErrors(connErr, pathErr)
return
}
/*
pathsFromProp returns a slice of dbus.ObjectPath (paths) from a dbus.Variant (prop).
If prop cannot typeswitch to paths, an ErrInvalidProperty will be raised.
*/
func pathsFromProp(prop dbus.Variant) (paths []dbus.ObjectPath, err error) {
switch v := prop.Value().(type) {
case []dbus.ObjectPath:
paths = v
default:
err = ErrInvalidProperty
return
}
return
}
/*
pathsFromPath returns a slice of dbus.ObjectPath based on an object given by path using the dbus.Conn specified by conn.
Internally it uses pathsFromProp.
*/
func pathsFromPath(bus dbus.BusObject, path string) (paths []dbus.ObjectPath, err error) {
var v dbus.Variant
if v, err = bus.GetProperty(path); err != nil {
return
}
if paths, err = pathsFromProp(v); err != nil {
return
}
return
}

5
go.mod
View File

@@ -2,7 +2,4 @@ module r00t2.io/gosecret
go 1.17
require (
github.com/godbus/dbus/v5 v5.0.6
github.com/google/uuid v1.3.0
)
require github.com/godbus/dbus v4.1.0+incompatible

6
go.sum
View File

@@ -1,4 +1,2 @@
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=

View File

@@ -1,103 +1,25 @@
package gosecret
package libsecret
import (
`strconv`
`strings`
`time`
`github.com/godbus/dbus/v5`
`github.com/godbus/dbus`
)
// 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
}
if _, err = validConnPath(collection.Conn, path); err != nil {
return
}
// NewItem returns a pointer to a new Item based on a Dbus connection and a Dbus path.
func NewItem(conn *dbus.Conn, path dbus.ObjectPath) (item *Item) {
item = &Item{
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 returns the Item's attributes from Dbus.
func (i *Item) Attributes() (attrs map[string]string, err error) {
var variant dbus.Variant
if variant, err = i.Dbus.GetProperty(DbusItemAttributes); err != nil {
return
}
attrs = variant.Value().(map[string]string)
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
}
Conn: conn,
Dbus: conn.Object(DBusServiceName, path),
}
return
}
// GetSecret returns the Secret in an Item using a Session.
func (i *Item) GetSecret(session *Session) (secret *Secret, err error) {
// Path returns the path of the underlying Dbus connection.
func (i Item) Path() (path dbus.ObjectPath) {
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
// Remove this method in V1. It's bloat since we now have an exported Dbus.
path = i.Dbus.Path()
return
}
@@ -107,7 +29,7 @@ func (i *Item) Label() (label string, err error) {
var variant dbus.Variant
if variant, err = i.Dbus.GetProperty(DbusItemLabel); err != nil {
if variant, err = i.Dbus.GetProperty("org.freedesktop.Secret.Item.Label"); err != nil {
return
}
@@ -116,112 +38,12 @@ func (i *Item) Label() (label string, err error) {
return
}
/*
ModifyAttributes modifies the Item's attributes 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
}
// Relabel modifies the Item's label in Dbus.
func (i *Item) Relabel(newLabel string) (err error) {
var variant dbus.Variant = dbus.MakeVariant(newLabel)
if err = i.Dbus.SetProperty(DbusItemLabel, variant); err != nil {
return
}
return
}
// ReplaceAttributes replaces the Item's attributes in Dbus.
func (i *Item) ReplaceAttributes(newAttrs map[string]string) (err error) {
var props dbus.Variant
props = dbus.MakeVariant(newAttrs)
if err = i.Dbus.SetProperty(DbusItemAttributes, props); err != nil {
return
}
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
}
itemType = variant.Value().(string)
return
}
// Locked indicates if an Item is locked (true) or unlocked (false).
// Locked indicates that 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(DbusItemLocked); err != nil {
if variant, err = i.Dbus.GetProperty("org.freedesktop.Secret.Item.Locked"); err != nil {
isLocked = true
return
}
@@ -231,52 +53,37 @@ func (i *Item) Locked() (isLocked bool, err error) {
return
}
// Created returns the time.Time of when an Item was created.
func (i *Item) Created() (created time.Time, err error) {
// GetSecret returns the Secret in an Item using a Session.
func (i *Item) GetSecret(session *Session) (secret *Secret, err error) {
var variant dbus.Variant
var timeInt uint64
secret = new(Secret)
if variant, err = i.Dbus.GetProperty(DbusItemCreated); err != nil {
if err = i.Dbus.Call(
"org.freedesktop.Secret.Item.GetSecret", 0, session.Path(),
).Store(&secret); err != nil {
return
}
timeInt = variant.Value().(uint64)
created = time.Unix(int64(timeInt), 0)
return
}
/*
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.
// Delete removes an Item from a Collection.
func (i *Item) Delete() (err error) {
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) {
var prompt *Prompt
var promptPath dbus.ObjectPath
var variant dbus.Variant
var timeInt uint64
if variant, err = i.Dbus.GetProperty(DbusItemModified); err != nil {
if err = i.Dbus.Call("org.freedesktop.Secret.Item.Delete", 0).Store(&promptPath); err != nil {
return
}
timeInt = variant.Value().(uint64)
if isPrompt(promptPath) {
prompt = NewPrompt(i.Conn, promptPath)
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
if _, err = prompt.Prompt(); err != nil {
return
}
}
isChanged = modified.After(i.lastModified)
i.lastModified = modified
return
}

View File

@@ -1,150 +0,0 @@
package gosecret
import (
`reflect`
`testing`
)
// Some functions are covered in the Service tests and Collection tests.
/*
TestItem tests all remaining Item funcs (see Service and Collection funcs for the other tests.
*/
func TestItem(t *testing.T) {
var svc *Service
var collection *Collection
var item *Item
var secret *Secret
var newItemLabel string
var testLabel string
var attrs map[string]string
var modAttrs map[string]string
var newAttrs map[string]string
var newAttrsGnome map[string]string
var typeString string
var err error
// Setup.
if svc, err = NewService(); err != nil {
t.Fatalf("NewService failed: %v", err.Error())
}
if collection, err = svc.CreateCollection(collectionName.String()); err != nil {
t.Errorf("could not create collection '%v': %v", collectionName.String(), err.Error())
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
} else {
t.Logf("created collection '%v' at path '%v' successfully", collectionName.String(), string(collection.Dbus.Path()))
}
// Create an Item/Secret.
secret = NewSecret(svc.Session, []byte{}, []byte(testSecretContent), "text/plain")
if item, err = collection.CreateItem(testItemLabel, itemAttrs, secret, true); err != nil {
t.Errorf("could not create item %v in collection '%v': %v", testItemLabel, collectionName.String(), err.Error())
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
}
// Fetch attributes
if attrs, err = item.Attributes(); err != nil {
t.Errorf("failed to fetch attributes for item %v in collection '%v': %v", testItemLabel, collectionName.String(), err.Error())
} else {
t.Logf(
"Fetch result; original attributes: %#v, fetched attributes: %#v for item '%v' in '%v'",
itemAttrs, attrs, testItemLabel, collectionName.String(),
)
}
newAttrs = map[string]string{
"foo": "bar",
"bar": "baz",
"baz": "quux",
}
// Replace attributes.
if err = item.ReplaceAttributes(newAttrs); err != nil {
t.Errorf("could not replace attributes for item '%v' in collection '%v': %v", testItemLabel, collectionName.String(), err.Error())
} else {
// Modify attributes.
// "flat" modification.
modAttrs = map[string]string{
"foo": "quux",
}
if err = item.ModifyAttributes(modAttrs); err != nil {
t.Errorf(
"could not modify attributes for item '%v' in collection '%v' (%#v => %#v): %v",
testItemLabel, collectionName.String(), newAttrs, modAttrs, err.Error(),
)
}
// "delete" modification.
newAttrs = map[string]string{
"foo": "quux",
"bar": "baz",
}
newAttrsGnome = make(map[string]string, 0)
for k, v := range newAttrs {
newAttrsGnome[k] = v
}
// Added via SecretService automatically? Seahorse? It appears sometimes and others it does not. Cause is unknown.
newAttrsGnome["xdg:schema"] = DbusDefaultItemType
modAttrs = map[string]string{
"baz": ExplicitAttrEmptyValue,
}
if err = item.ModifyAttributes(modAttrs); err != nil {
t.Errorf(
"could not modify (with deletion) attributes for item '%v' in collection '%v' (%#v => %#v): %v",
testItemLabel, collectionName.String(), newAttrs, modAttrs, err.Error(),
)
} else {
if attrs, err = item.Attributes(); err != nil {
t.Errorf("failed to fetch attributes for item %v in collection '%v': %v", testItemLabel, collectionName.String(), err.Error())
}
if !reflect.DeepEqual(attrs, newAttrs) && !reflect.DeepEqual(attrs, newAttrsGnome) {
t.Errorf("newly-modified attributes (%#v) do not match expected attributes (%#v)", attrs, newAttrs)
} else {
t.Logf("modified attributes (%#v) match expected attributes (%#v)", attrs, newAttrs)
}
}
}
// Item.Relabel
newItemLabel = testItemLabel + "_RELABELED"
if err = item.Relabel(newItemLabel); err != nil {
t.Errorf("failed to relabel item '%v' to '%v': %v", testItemLabel, newItemLabel, err.Error())
}
if testLabel, err = item.Label(); err != nil {
t.Errorf("failed to fetch label for '%v': %v", string(item.Dbus.Path()), err.Error())
}
if newItemLabel != testLabel {
t.Errorf("new item label post-relabeling ('%v') does not match explicitly set label ('%v')", testLabel, newItemLabel)
}
// And Item.Type.
if typeString, err = item.Type(); err != nil {
t.Errorf("failed to get Item.Type for '%v': %v", string(item.Dbus.Path()), err.Error())
} else {
if typeString != DbusDefaultItemType {
t.Errorf("Item.Type mismatch for '%v': '%v' (should be '%v')", string(item.Dbus.Path()), typeString, DbusDefaultItemType)
}
t.Logf("item type for '%v': %v", string(item.Dbus.Path()), typeString)
}
// Teardown.
if err = item.Delete(); err != nil {
t.Errorf("failed to delete item '%v': %v", string(item.Dbus.Path()), err.Error())
}
if err = collection.Delete(); err != nil {
t.Errorf("failed to delete collection '%v': %v", collectionName.String(), err.Error())
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}

View File

@@ -1,57 +0,0 @@
package gosecret
import (
`fmt`
)
/*
NewErrors returns a new MultiError based on a slice of error.Error (errs).
Any nil errors are trimmed. If there are no actual errors after trimming, err will be nil.
*/
func NewErrors(errs ...error) (err error) {
if errs == nil || len(errs) == 0 {
return
}
var realErrs []error = make([]error, 0)
for _, e := range errs {
if e == nil {
continue
}
realErrs = append(realErrs, e)
}
if len(realErrs) == 0 {
return
}
err = &MultiError{
Errors: realErrs,
ErrorSep: "\n",
}
return
}
func (e *MultiError) Error() (errStr string) {
var numErrs int
if e == nil || len(e.Errors) == 0 {
return
} else {
numErrs = len(e.Errors)
}
for idx, err := range e.Errors {
if (idx + 1) < numErrs {
errStr += fmt.Sprintf(err.Error(), e.ErrorSep)
} else {
errStr += err.Error()
}
}
return
}

View File

@@ -1,30 +1,35 @@
package gosecret
package libsecret
import (
`github.com/godbus/dbus/v5`
`github.com/godbus/dbus`
)
// 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{
DbusObject: &DbusObject{
Conn: conn,
Dbus: conn.Object(DbusService, path),
},
Conn: conn,
Dbus: conn.Object(DBusServiceName, 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) {
var c chan *dbus.Signal
var result *dbus.Signal
promptValue = new(dbus.Variant)
// Prompts are asynchronous; we connect to the signal and block with a channel until we get a response.
c = make(chan *dbus.Signal, 10)
defer close(c)
@@ -32,14 +37,12 @@ func (p *Prompt) Prompt() (promptValue *dbus.Variant, err error) {
p.Conn.Signal(c)
defer p.Conn.RemoveSignal(c)
if err = p.Dbus.Call(
DbusPrompterInterface, 0, "GoSecret.Prompt", // TODO: This last argument, the string, is for "window ID". I'm unclear what for.
).Store(); err != nil {
if err = p.Dbus.Call("org.freedesktop.Secret.Prompt.Prompt", 0, "").Store(); err != nil {
return
}
for {
if result = <-c; result.Path == p.Dbus.Path() {
if result = <-c; result.Path == p.Path() {
*promptValue = result.Body[1].(dbus.Variant)
return
}

View File

@@ -1,10 +1,10 @@
package gosecret
package libsecret
// NewSecret returns a pointer to a new Secret based on a Session, parameters, (likely an empty byte slice), a value, and the MIME content type.
func NewSecret(session *Session, params []byte, value []byte, contentType string) (secret *Secret) {
secret = &Secret{
Session: session.Dbus.Path(),
Session: session.Path(),
Parameters: params,
Value: value,
ContentType: contentType,

View File

@@ -1,13 +0,0 @@
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,82 +1,74 @@
package gosecret
package libsecret
import (
"errors"
"fmt"
"path/filepath"
"strings"
"github.com/godbus/dbus/v5"
`github.com/godbus/dbus`
)
// NewService returns a pointer to a new Service connection.
// NewService returns a pointer to a new Service.
func NewService() (service *Service, err error) {
var svc Service = Service{
DbusObject: &DbusObject{
Conn: nil,
Dbus: nil,
},
Session: nil,
service = &Service{
Conn: nil,
Dbus: nil,
}
if svc.Conn, err = dbus.SessionBus(); err != nil {
if service.Conn, err = dbus.SessionBus(); err != nil {
return
}
svc.Dbus = svc.Conn.Object(DbusService, dbus.ObjectPath(DbusPath))
if svc.Session, err = svc.GetSession(); err != nil {
return
}
service = &svc
service.Dbus = service.Conn.Object(DBusServiceName, dbus.ObjectPath(DBusPath))
return
}
// Close cleanly closes a Service and all its underlying connections (e.g. Service.Session).
func (s *Service) Close() (err error) {
// Path returns the path of the underlying Dbus connection.
func (s Service) Path() (path dbus.ObjectPath) {
err = s.Session.Close()
// Remove this method in V1. It's bloat since we now have an exported Dbus.
path = s.Dbus.Path()
return
}
// Collections returns a slice of Collection items accessible to this Service.
func (s *Service) Collections() (collections []*Collection, err error) {
// Open returns a pointer to a Session from the Service.
func (s *Service) Open() (session *Session, err error) {
var output dbus.Variant
var path dbus.ObjectPath
if err = s.Dbus.Call(
"org.freedesktop.Secret.Service.OpenSession", 0, "plain", dbus.MakeVariant(""),
).Store(&output, &path); err != nil {
return
}
session = NewSession(s.Conn, path)
return
}
// Collections returns a slice of Collection keyrings accessible to this Service.
func (s *Service) Collections() (collections []Collection, err error) {
var paths []dbus.ObjectPath
var variant dbus.Variant
var coll *Collection
var errs []error = make([]error, 0)
if variant, err = s.Dbus.GetProperty(DbusServiceCollections); err != nil {
if variant, err = s.Dbus.GetProperty("org.freedesktop.Secret.Service.Collections"); err != nil {
return
}
paths = variant.Value().([]dbus.ObjectPath)
collections = make([]*Collection, len(paths))
collections = make([]Collection, len(paths))
for idx, path := range paths {
if coll, err = NewCollection(s, path); err != nil {
// return
errs = append(errs, err)
err = nil
continue
}
collections[idx] = coll
collections[idx] = *NewCollection(s.Conn, path)
}
err = NewErrors(err)
return
}
/*
CreateAliasedCollection creates a new Collection (keyring) via a Service with the name specified by label,
aliased to the name specified by alias, and returns the new Collection.
*/
func (s *Service) CreateAliasedCollection(label, alias string) (collection *Collection, err error) {
// CreateCollection creates a new Collection (keyring) via a Service with the name specified by label.
func (s *Service) CreateCollection(label string) (collection *Collection, err error) {
var variant *dbus.Variant
var path dbus.ObjectPath
@@ -84,17 +76,16 @@ func (s *Service) CreateAliasedCollection(label, alias string) (collection *Coll
var prompt *Prompt
var props map[string]dbus.Variant = make(map[string]dbus.Variant)
props[DbusCollectionLabel] = dbus.MakeVariant(label)
props["org.freedesktop.Secret.Collection.Label"] = dbus.MakeVariant(label)
if err = s.Dbus.Call(
DbusServiceCreateCollection, 0, props, alias,
).Store(&path, &promptPath); err != nil {
if err = s.Dbus.Call("org.freedesktop.Secret.Service.CreateCollection", 0, props, "").Store(&path, &promptPath); err != nil {
return
}
if isPrompt(promptPath) {
prompt = NewPrompt(s.Conn, promptPath)
if variant, err = prompt.Prompt(); err != nil {
return
}
@@ -102,149 +93,20 @@ func (s *Service) CreateAliasedCollection(label, alias string) (collection *Coll
path = variant.Value().(dbus.ObjectPath)
}
collection, err = NewCollection(s, path)
collection = NewCollection(s.Conn, path)
return
}
/*
CreateCollection creates a new Collection (keyring) via a Service with the name specified by label and returns the new Collection.
It is a *very* thin wrapper around Service.CreateAliasedCollection, but with a blank alias.
*/
func (s *Service) CreateCollection(label string) (collection *Collection, err error) {
// Unlock unlocks a Locked Service.
func (s *Service) Unlock(object DBusObject) (err error) {
collection, err = s.CreateAliasedCollection(label, "")
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.
*/
func (s *Service) GetCollection(name string) (c *Collection, err error) {
var errs []error
var colls []*Collection
var collLabel string
// First check for an alias.
if c, err = s.ReadAlias(name); err != nil && err != ErrDoesNotExist {
return
}
if c != nil {
return
} else {
c = nil
}
// We didn't get it by alias, so let's try by name...
if colls, err = s.Collections(); err != nil {
return
}
for _, i := range colls {
if i.name == name {
c = i
return
}
}
// Still nothing? Try by label.
for _, i := range colls {
if collLabel, err = i.Label(); err != nil {
errs = append(errs, err)
err = nil
continue
}
if collLabel == name {
c = i
return
}
}
// Couldn't find it by the given name.
if errs != nil || len(errs) > 0 {
errs = append([]error{ErrDoesNotExist}, errs...)
err = NewErrors(errs...)
} else {
err = ErrDoesNotExist
}
return
}
/*
GetSecrets allows you to fetch values (Secret) from multiple Item object paths using this Service's Session.
An ErrMissingPaths will be returned for err if itemPaths is nil or empty.
The returned secrets is a map with itemPaths as the keys and their corresponding Secret as the value.
*/
func (s *Service) GetSecrets(itemPaths ...dbus.ObjectPath) (secrets map[dbus.ObjectPath]*Secret, err error) {
/*
Results are in the form of a map with the value consisting of:
[]interface {}{
"/org/freedesktop/secrets/session/sNNN", // 0, session path
[]uint8{}, // 1, "params"
[]uint8{0x0}, // 2, value
"text/plain", // 3, content type
}
*/
var results map[dbus.ObjectPath][]interface{}
if itemPaths == nil || len(itemPaths) == 0 {
err = ErrMissingPaths
return
}
secrets = make(map[dbus.ObjectPath]*Secret, len(itemPaths))
results = make(map[dbus.ObjectPath][]interface{}, len(itemPaths))
// TODO: trigger a Service.Unlock for any locked items?
if err = s.Dbus.Call(
DbusServiceGetSecrets, 0, itemPaths, s.Session.Dbus.Path(),
).Store(&results); err != nil {
return
}
for p, r := range results {
secrets[p] = NewSecret(
s.Session, r[1].([]byte), r[2].([]byte), r[3].(string),
)
}
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().
If objectPaths is nil or empty, the Service's own path will be used.
*/
func (s *Service) Lock(objectPaths ...dbus.ObjectPath) (err error) {
// We only use these as destinations.
var locked []dbus.ObjectPath
var unlocked []dbus.ObjectPath
var prompt *Prompt
var promptPath dbus.ObjectPath
var paths []dbus.ObjectPath = []dbus.ObjectPath{object.Path()}
if objectPaths == nil || len(objectPaths) == 0 {
objectPaths = []dbus.ObjectPath{s.Dbus.Path()}
}
if err = s.Dbus.Call(
DbusServiceLock, 0, objectPaths,
).Store(&locked, &promptPath); err != nil {
if err = s.Dbus.Call("org.freedesktop.Secret.Service.Unlock", 0, paths).Store(&unlocked, &promptPath); err != nil {
return
}
@@ -260,196 +122,21 @@ func (s *Service) Lock(objectPaths ...dbus.ObjectPath) (err error) {
return
}
/*
OpenSession returns a pointer to a Session from the Service.
It's a convenience function around NewSession.
*/
func (s *Service) OpenSession(algo, input string) (session *Session, output dbus.Variant, err error) {
var path dbus.ObjectPath
var inputVariant dbus.Variant
if strings.TrimSpace(algo) == "" {
algo = "plain"
}
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, algo, inputVariant,
).Store(&output, &path); err != nil {
return
}
session, err = NewSession(s, path)
return
}
/*
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) 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) {
// Lock locks an Unlocked Service.
func (s *Service) Lock(object DBusObject) (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
return
}
err = s.Dbus.Call(
DbusServiceSearchItems, 0, attributes,
).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
}
/*
SetAlias sets an alias for an existing Collection.
To remove an alias, set objectPath to dbus.ObjectPath("/").
*/
func (s *Service) SetAlias(alias string, objectPath dbus.ObjectPath) (err error) {
var c *dbus.Call
c = s.Dbus.Call(
DbusServiceSetAlias, 0, alias, objectPath,
)
err = c.Err
return
}
/*
Unlock unlocks a Locked Service, Collection, etc.
You can usually get objectPath for the object(s) to unlock via <object>.Dbus.Path().
If objectPaths is nil or empty, the Service's own path will be used.
*/
func (s *Service) Unlock(objectPaths ...dbus.ObjectPath) (err error) {
var unlocked []dbus.ObjectPath
var prompt *Prompt
var resultPath dbus.ObjectPath
var promptPath dbus.ObjectPath
var paths []dbus.ObjectPath = []dbus.ObjectPath{object.Path()}
if objectPaths == nil || len(objectPaths) == 0 {
objectPaths = []dbus.ObjectPath{s.Dbus.Path()}
}
if err = s.Dbus.Call(
DbusServiceUnlock, 0, objectPaths,
).Store(&unlocked, &resultPath); err != nil {
if err = s.Dbus.Call("org.freedesktop.Secret.Service.Lock", 0, paths).Store(&locked, &promptPath); err != nil {
return
}
if isPrompt(resultPath) {
if isPrompt(promptPath) {
prompt = NewPrompt(s.Conn, resultPath)
prompt = NewPrompt(s.Conn, promptPath)
if _, err = prompt.Prompt(); err != nil {
return

View File

@@ -1,442 +0,0 @@
package gosecret
import (
"testing"
`time`
`github.com/godbus/dbus/v5`
)
/*
TestNewService tests the following internal functions/methods via nested calls:
NewService
Service.GetSession
Service.OpenSession
NewSession
validConnPath
connIsValid
pathIsValid
Service.Close
Session.Close
*/
func TestNewService(t *testing.T) {
var err error
var svc *Service
if svc, err = NewService(); err != nil {
t.Fatalf("could not get new Service via NewService: %v", err.Error())
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}
/*
TestService_Collections tests the following internal functions/methods via nested calls:
(all calls in TestNewService)
Service.Collections
NewCollection
Collection.Modified
NewErrors
Collection.Created
*/
func TestService_Collections(t *testing.T) {
var err error
var svc *Service
var colls []*Collection
var collLabel string
var created time.Time
var modified time.Time
if svc, err = NewService(); err != nil {
t.Fatalf("could not get new Service via NewService: %v", err.Error())
}
if colls, err = svc.Collections(); err != nil {
t.Errorf("could not get Service.Collections: %v", err.Error())
} else {
t.Logf("found %v collections via Service.Collections", len(colls))
for idx, c := range colls {
if collLabel, err = c.Label(); err != nil {
t.Errorf(
"failed to get label for collection '%v': %v",
string(c.Dbus.Path()), err.Error(),
)
}
if created, err = c.Created(); err != nil {
t.Errorf(
"failed to get created time for collection '%v': %v",
string(c.Dbus.Path()), err.Error(),
)
}
if modified, _, err = c.Modified(); err != nil {
t.Errorf(
"failed to get modified time for collection '%v': %v",
string(c.Dbus.Path()), err.Error(),
)
}
t.Logf(
"collection #%v (name '%v', label '%v'): created %v, last modified %v",
idx, c.PathName(), collLabel, created, modified,
)
}
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}
/*
TestService_CreateAliasedCollection tests the following internal functions/methods via nested calls:
(all calls in TestNewService)
Service.CreateAliasedCollection
NewCollection
Collection.Modified
Collection.Delete
Service.SetAlias
(By extension, Service.CreateCollection is also tested as it's a very thin wrapper
around Service.CreateAliasedCollection).
*/
/* DISABLED. Currently (as of 0.20.4), *only* the alias "default" is allowed. TODO: revisit in future?
func TestService_CreateAliasedCollection(t *testing.T) {
var err error
var svc *Service
var collection *Collection
if svc, err = NewService(); err != nil {
t.Fatalf("could not get new Service via NewService: %v", err.Error())
}
if collection, err = svc.CreateAliasedCollection(collectionName.String(), collectionAlias.String()); err != nil {
t.Errorf(
"error when creating aliased collection '%v' with alias '%v': %v",
collectionName.String(), collectionAlias.String(), err.Error(),
)
} else {
if err = svc.SetAlias(testAlias, collection.Dbus.Path()); err != nil {
t.Errorf(
"error when setting an alias '%v' for aliased collection '%v' (original alias '%v')",
testAlias, collectionName.String(), collectionAlias.String(),
)
}
if err = collection.Delete(); err != nil {
t.Errorf(
"error when deleting aliased collection '%v' with alias '%v': %v",
collectionName.String(), collectionAlias.String(), err.Error(),
)
}
t.Logf("created collection '%v' at path '%v' successfully", collectionName.String(), string(collection.Dbus.Path()))
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}
*/
/*
TestService_GetCollection tests the following internal functions/methods via nested calls:
(all calls in TestService_CreateAliasedCollection)
(all calls in TestService_Collections)
NewErrors
Collection.Created
Service.GetCollection
NewCollection
Collection.Modified
Service.ReadAlias
The default collection (login) is fetched instead of creating one as this collection should exist,
and thus this function tests fetching existing collections instead of newly-created ones.
*/
func TestService_GetCollection(t *testing.T) {
var err error
var svc *Service
var coll *Collection
if svc, err = NewService(); err != nil {
t.Fatalf("could not get new Service via NewService: %v", err.Error())
}
if coll, err = svc.GetCollection(defaultCollection); err != nil {
t.Errorf("failed to get collection '%v' via Service.GetCollection: %v", defaultCollection, err.Error())
} else {
t.Logf("got collection '%v' via reference '%v'", coll.name, defaultCollection)
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}
/*
TestService_Secrets tests the following internal functions/methods via nested calls:
(all calls in TestNewService)
(all calls in TestService_CreateAliasedCollection)
Service.CreateCollection
Service.SearchItems
NewItem
NewErrors
Service.GetSecrets
NewSecret
Collection.CreateItem
Item.Label
Item.Delete
*/
func TestService_Secrets(t *testing.T) {
var err error
var svc *Service
var collection *Collection
var itemResultsUnlocked []*Item
var itemResultsLocked []*Item
var itemName string
var resultItemName string
var testItem *Item
var testSecret *Secret
var secretsResult map[dbus.ObjectPath]*Secret
var itemPaths []dbus.ObjectPath
var created time.Time
var modified time.Time
var newModified time.Time
var wasModified bool
var isModified bool
if svc, err = NewService(); err != nil {
t.Fatalf("could not get new Service via NewService: %v", err.Error())
}
if collection, err = svc.CreateCollection(collectionName.String()); err != nil {
t.Errorf("could not create collection '%v': %v", collectionName.String(), err.Error())
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
} else {
t.Logf("created collection '%v' at path '%v' successfully", collectionName.String(), string(collection.Dbus.Path()))
}
if created, err = collection.Created(); err != nil {
t.Errorf(
"failed to get created time for '%v': %v",
collectionName.String(), err.Error(),
)
}
if modified, wasModified, err = collection.Modified(); err != nil {
t.Errorf(
"failed to get modified time for '%v': %v",
collectionName.String(), err.Error(),
)
}
t.Logf(
"%v: collection '%v': created at %v, last modified at %v; was changed: %v",
time.Now(), collectionName.String(), created, modified, wasModified,
)
// Create a secret
testSecret = NewSecret(svc.Session, nil, []byte(testSecretContent), "text/plain")
if testItem, err = collection.CreateItem(testItemLabel, itemAttrs, testSecret, true); err != nil {
t.Errorf("could not create Item in collection '%v': %v", collectionName.String(), err.Error())
if err = svc.Close(); err != nil {
t.Fatalf("could not close Service.Session: %v", err.Error())
}
return
}
if itemName, err = testItem.Label(); err != nil {
t.Errorf(
"could not get label for newly-created item '%v' in collection '%v': %v",
string(testItem.Dbus.Path()), collectionName.String(), err.Error(),
)
itemName = testItemLabel
}
// Search items
if itemResultsUnlocked, itemResultsLocked, err = svc.SearchItems(itemAttrs); err != nil {
t.Errorf("did not find Item '%v' in collection '%v' SearchItems: %v", itemName, collectionName.String(), err.Error())
} else {
if len(itemResultsLocked) != 0 && itemResultsUnlocked != nil {
t.Errorf("at least one locked item in collection '%v'", collectionName.String())
}
if len(itemResultsUnlocked) != 1 {
t.Errorf("number of unlocked items in collection '%v' is not equal to 1", collectionName.String())
}
if resultItemName, err = itemResultsUnlocked[0].Label(); err != nil {
t.Errorf("cannot fetch test Item name from collection '%v' in SearchItems: %v", collectionName.String(), err.Error())
} else {
if resultItemName != itemName {
t.Errorf("seem to have fetched an improper Item from collection '%v' in SearchItems", collectionName.String())
}
}
}
// Fetch secrets
itemPaths = make([]dbus.ObjectPath, len(itemResultsUnlocked))
if len(itemResultsUnlocked) >= 1 {
itemPaths[0] = itemResultsUnlocked[0].Dbus.Path()
if secretsResult, err = svc.GetSecrets(itemPaths...); err != nil {
t.Errorf("failed to fetch Item path '%v' via Service.GetSecrets: %v", string(itemPaths[0]), err.Error())
} else if len(secretsResult) != 1 {
t.Errorf("received %v secrets from Service.GetSecrets instead of 1", len(secretsResult))
}
}
// Confirm the modification information changed.
_ = newModified
_ = isModified
/* TODO: Disabled for now; it *seems* the collection modification time doesn't auto-update? See collection.setModify if not.
if newModified, isModified, err = collection.Modified(); err != nil {
t.Errorf(
"failed to get modified time for '%v': %v",
collectionName.String(), err.Error(),
)
}
t.Logf(
"%v: (post-change) collection '%v': last modified at %v; was changed: %v",
time.Now(), collectionName.String(), newModified, isModified,
)
if !isModified {
t.Errorf(
"modification tracking for collection '%v' failed; expected true but got false",
collectionName.String(),
)
} else {
t.Logf("(modification check passed)")
}
if !newModified.After(modified) {
t.Errorf(
"modification timestamp update for '%v' failed: old %v, new %v",
collectionName.String(), modified, newModified,
)
}
*/
/*
Delete the item and collection to clean up.
*Technically* the item is deleted if the collection is, but its path is held in memory if not
manually removed, leading to a "ghost" item.
*/
if err = testItem.Delete(); err != nil {
t.Errorf("could not delete test item '%v' in collection '%v': %v",
string(testItem.Dbus.Path()), collectionName.String(), err.Error(),
)
}
if err = collection.Delete(); err != nil {
t.Errorf(
"error when deleting collection '%v' when testing Service: %v",
collectionName.String(), err.Error(),
)
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}
/*
TestService_Locking tests the following internal functions/methods via nested calls:
(all calls in TestNewService)
(all calls in TestService_CreateAliasedCollection)
Service.Lock
Service.Unlock
Collection.Locked
*/
func TestService_Locking(t *testing.T) {
var err error
var isLocked bool
var stateChangeLock bool
var svc *Service
var collection *Collection
if svc, err = NewService(); err != nil {
t.Fatalf("could not get new Service via NewService: %v", err.Error())
}
if collection, err = svc.CreateCollection(collectionName.String()); err != nil {
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
t.Errorf("could not create collection '%v': %v", collectionName.String(), err.Error())
} else {
t.Logf("created collection '%v' at path '%v' successfully", collectionName.String(), string(collection.Dbus.Path()))
}
if isLocked, err = collection.Locked(); err != nil {
t.Errorf("received error when checking collection '%v' lock status: %v", collectionName.String(), err.Error())
if err = collection.Delete(); err != nil {
t.Errorf(
"error when deleting collection '%v' when testing Service: %v",
collectionName.String(), err.Error(),
)
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
return
} else {
t.Logf("collection '%v' original lock status: %v", collectionName.String(), isLocked)
}
// Change the state.
if isLocked {
if err = svc.Unlock(collection.Dbus.Path()); err != nil {
t.Errorf("could not unlock collection '%v': %v", collectionName.String(), err.Error())
}
if stateChangeLock, err = collection.Locked(); err != nil {
t.Errorf("received error when checking collection '%v' lock status: %v", collectionName.String(), err.Error())
}
if err = svc.Lock(collection.Dbus.Path()); err != nil {
t.Errorf("could not lock collection '%v': %v", collectionName.String(), err.Error())
}
} else {
if err = svc.Lock(collection.Dbus.Path()); err != nil {
t.Errorf("could not lock collection '%v': %v", collectionName.String(), err.Error())
}
if stateChangeLock, err = collection.Locked(); err != nil {
t.Errorf("received error when checking collection '%v' lock status: %v", collectionName.String(), err.Error())
}
if err = svc.Unlock(collection.Dbus.Path()); err != nil {
t.Errorf("could not unlock collection '%v': %v", collectionName.String(), err.Error())
}
}
if stateChangeLock != !isLocked {
t.Errorf(
"flipped lock state for collection '%v' (locked: %v) is not opposite of original lock state (locked: %v)",
collectionName.String(), stateChangeLock, isLocked,
)
}
// Delete the collection to clean up.
if err = collection.Delete(); err != nil {
t.Errorf(
"error when deleting collection '%v' when testing Service: %v",
collectionName.String(), err.Error(),
)
}
if err = svc.Close(); err != nil {
t.Errorf("could not close Service.Session: %v", err.Error())
}
}

View File

@@ -1,44 +1,25 @@
package gosecret
package libsecret
import (
"github.com/godbus/dbus/v5"
`github.com/godbus/dbus`
)
// 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 Dbus connection and a Dbus path.
func NewSession(conn *dbus.Conn, path dbus.ObjectPath) (session *Session) {
/*
NewSession returns a pointer to a new Session based on a Service and a dbus.ObjectPath.
You will almost always want to use Service.GetSession or Service.OpenSession instead.
*/
func NewSession(service *Service, path dbus.ObjectPath) (session *Session, err error) {
if _, err = validConnPath(service.Conn, path); err != nil {
return
session = &Session{
Conn: conn,
Dbus: conn.Object(DBusServiceName, path),
}
var ssn Session = Session{
DbusObject: &DbusObject{
Conn: service.Conn,
},
service: service,
}
ssn.Dbus = ssn.Conn.Object(DbusInterfaceSession, path)
session = &ssn
return
}
// Close cleanly closes a Session.
func (s *Session) Close() (err error) {
// Path returns the path of the underlying Dbus connection.
func (s Session) Path() (path dbus.ObjectPath) {
var c *dbus.Call
c = s.Dbus.Call(
DbusSessionClose, 0,
)
_ = c
// Remove this method in V1. It's bloat since we now have an exported Dbus.
path = s.Dbus.Path()
return
}

View File

@@ -1,31 +0,0 @@
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.
If no matching error was found, however, then ok will be false and err will be ErrUnknownSecretServiceErr.
*/
func TranslateError(ssErr SecretServiceErrEnum) (ok bool, err error) {
err = ErrUnknownSecretServiceErr
for _, e := range AllSecretServiceErrs {
if e.ErrCode == ssErr {
ok = true
err = e
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) {
errStr = e.ErrDesc
return
}

169
types.go
View File

@@ -1,83 +1,12 @@
package gosecret
package libsecret
import (
"time"
"github.com/godbus/dbus/v5"
`github.com/godbus/dbus`
)
/*
MultiError is a type of error.Error that can contain multiple error.Errors. Confused? Don't worry about it.
*/
type MultiError struct {
// Errors is a slice of errors to combine/concatenate when .Error() is called.
Errors []error `json:"errors"`
// ErrorSep is a string to use to separate errors for .Error(). The default is "\n".
ErrorSep string `json:"separator"`
}
/*
SecretServiceError is a translated error from SecretService API.
See https://developer-old.gnome.org/libsecret/unstable/libsecret-SecretError.html#SecretError and
ErrSecretService* errors.
*/
type SecretServiceError struct {
// ErrCode is the SecretService API's enum value.
ErrCode SecretServiceErrEnum `json:"code"`
// ErrName is the SecretService API's error name.
ErrName string `json:"name"`
/*
ErrDesc is the actual error description/text.
This is what should be displayed to users, and is returned by SecretServiceError.Error.
*/
ErrDesc string `json:"desc"`
}
// ConnPathCheckResult contains the result of validConnPath.
type ConnPathCheckResult struct {
// ConnOK is true if the dbus.Conn is valid.
ConnOK bool `json:"conn"`
// PathOK is true if the Dbus path given is a valid type and value.
PathOK bool `json:"path"`
}
// DbusObject is a base struct type to be anonymized by other types.
type DbusObject struct {
// Conn is an active connection to the Dbus.
Conn *dbus.Conn `json:"-"`
// Dbus is the Dbus bus object.
Dbus dbus.BusObject `json:"-"`
}
/*
Prompt is an interface to handling unlocking prompts.
https://developer-old.gnome.org/libsecret/0.18/SecretPrompt.html
https://specifications.freedesktop.org/secret-service/latest/ch09.html
*/
type Prompt struct {
*DbusObject
}
/*
Service is a general SecretService interface, sort of handler for Dbus - it's used for fetching a Session, Collections, etc.
https://developer-old.gnome.org/libsecret/0.18/SecretService.html
https://specifications.freedesktop.org/secret-service/latest/re01.html
*/
type Service struct {
*DbusObject
// Session is a default Session initiated automatically.
Session *Session `json:"-"`
}
/*
Session is a session/instance/connection to SecretService.
https://developer-old.gnome.org/libsecret/0.18/SecretService.html
https://specifications.freedesktop.org/secret-service/latest/ch06.html
*/
type Session struct {
*DbusObject
// collection tracks the Service this Session was created from.
service *Service
// DBusObject is any type that has a Path method that returns a dbus.ObjectPath.
type DBusObject interface {
Path() dbus.ObjectPath
}
/*
@@ -87,15 +16,10 @@ type Session struct {
https://specifications.freedesktop.org/secret-service/latest/ch03.html
*/
type Collection struct {
*DbusObject
// 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
// 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
// Conn is an active connection to the Dbus.
Conn *dbus.Conn
// Dbus is the Dbus bus object.
Dbus dbus.BusObject
}
/*
@@ -104,21 +28,22 @@ type Collection struct {
https://specifications.freedesktop.org/secret-service/latest/re03.html
*/
type Item struct {
*DbusObject
// Secret is the corresponding Secret object.
Secret *Secret `json:"secret"`
// 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 is almost guaranteed to not match the index in Collection.Items (unless you have like, only one item)
as those indices are static and do not determine the order that Dbus returns the list of item paths.
*/
idx int
// collection tracks the Collection this Item is in.
collection *Collection
// Conn is an active connection to the Dbus.
Conn *dbus.Conn
// Dbus is the Dbus bus object.
Dbus dbus.BusObject
}
/*
Prompt is an interface to handling unlocking prompts.
https://developer-old.gnome.org/libsecret/0.18/SecretPrompt.html
https://specifications.freedesktop.org/secret-service/latest/ch09.html
*/
type Prompt struct {
// Conn is an active connection to the Dbus.
Conn *dbus.Conn
// Dbus is the Dbus bus object.
Dbus dbus.BusObject
}
/*
@@ -128,22 +53,36 @@ 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 (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"`
// Session is a Dbus object path for the associated Session.
Session dbus.ObjectPath
// Parameters are "algorithm dependent parameters for secret value encoding" - likely this will just be an empty byteslice.
Parameters []byte
// Value is the secret's content in []byte format.
Value SecretValue `json:"value"`
Value []byte
// 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
ContentType string
}
// SecretValue is a custom type that handles JSON encoding/decoding a little more easily.
type SecretValue []byte
/*
Service is a general SecretService interface, sort of handler for Dbus - it's used for fetching a Session, Collections, etc.
https://developer-old.gnome.org/libsecret/0.18/SecretService.html
https://specifications.freedesktop.org/secret-service/latest/re01.html
*/
type Service struct {
// Conn is an active connection to the Dbus.
Conn *dbus.Conn
// Dbus is the Dbus bus object.
Dbus dbus.BusObject
}
/*
Session is a session/instance/connection to SecretService.
https://developer-old.gnome.org/libsecret/0.18/SecretService.html
https://specifications.freedesktop.org/secret-service/latest/ch06.html
*/
type Session struct {
// Conn is an active connection to the Dbus.
Conn *dbus.Conn
// Dbus is the Dbus bus object.
Dbus dbus.BusObject
}