From 94ae20829e0d783e25b17c599f0e0fe1344f84ad Mon Sep 17 00:00:00 2001 From: brent s Date: Fri, 10 Dec 2021 02:50:30 -0500 Subject: [PATCH] adding ref mat'l, Service tests done --- .ref => .ref/URLs | 4 + .ref/dbus_types/structs.go | 116 +++++++++++++++++++++++ conts_test.go | 18 ++++ multierr_funcs.go | 4 +- prompt_funcs.go | 4 +- service_funcs.go | 91 +++++++++--------- service_funcs_test.go | 184 ++++++++++++++++++++++++++++++++----- 7 files changed, 348 insertions(+), 73 deletions(-) rename .ref => .ref/URLs (52%) create mode 100644 .ref/dbus_types/structs.go diff --git a/.ref b/.ref/URLs similarity index 52% rename from .ref rename to .ref/URLs index c39a67a..8ef6d77 100644 --- a/.ref +++ b/.ref/URLs @@ -7,3 +7,7 @@ https://people.gnome.org/~stefw/libsecret-docs/index.html 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. diff --git a/.ref/dbus_types/structs.go b/.ref/dbus_types/structs.go new file mode 100644 index 0000000..8b311b1 --- /dev/null +++ b/.ref/dbus_types/structs.go @@ -0,0 +1,116 @@ +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: () // Note: structs can be nested e.g. (i(ii)) + Dbus Array: a // 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 // 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. +*/ diff --git a/conts_test.go b/conts_test.go index 109b0a3..307bb78 100644 --- a/conts_test.go +++ b/conts_test.go @@ -1,6 +1,24 @@ package gosecret +import ( + `github.com/google/uuid` +) + // Paths. const ( DbusDefaultCollectionPath string = DbusPath + "/collections/login" ) + +// Strings. +const ( + defaultCollection string = "default" // SHOULD point to a collection named "login"; "default" is the alias. + 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() +) diff --git a/multierr_funcs.go b/multierr_funcs.go index 2bc521c..b58f475 100644 --- a/multierr_funcs.go +++ b/multierr_funcs.go @@ -8,7 +8,7 @@ import ( NewErrors returns a new MultiError based on a slice of error.Error (errs). Any nil errors are trimmed. If there are no actual errors after trimming, err will be nil. */ -func NewErrors(errs ...error) (err *MultiError) { +func NewErrors(errs ...error) (err error) { if errs == nil || len(errs) == 0 { return @@ -46,7 +46,7 @@ func (e *MultiError) Error() (errStr string) { } for idx, err := range e.Errors { - if (idx +1 ) < numErrs { + if (idx + 1) < numErrs { errStr += fmt.Sprintf(err.Error(), e.ErrorSep) } else { errStr += err.Error() diff --git a/prompt_funcs.go b/prompt_funcs.go index d63ec96..9caedfc 100644 --- a/prompt_funcs.go +++ b/prompt_funcs.go @@ -23,6 +23,8 @@ 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) @@ -31,7 +33,7 @@ func (p *Prompt) Prompt() (promptValue *dbus.Variant, err error) { defer p.Conn.RemoveSignal(c) if err = p.Dbus.Call( - DbusPrompterInterface, 0, "", // TODO: This last argument, the string, is for "window ID". I'm unclear what for. + DbusPrompterInterface, 0, "GoSecret.Prompt", // TODO: This last argument, the string, is for "window ID". I'm unclear what for. ).Store(); err != nil { return } diff --git a/service_funcs.go b/service_funcs.go index ece7742..a341eee 100644 --- a/service_funcs.go +++ b/service_funcs.go @@ -95,7 +95,6 @@ func (s *Service) CreateAliasedCollection(label, alias string) (collection *Coll if isPrompt(promptPath) { prompt = NewPrompt(s.Conn, promptPath) - if variant, err = prompt.Prompt(); err != nil { return } @@ -161,20 +160,38 @@ func (s *Service) GetCollection(name string) (c *Collection, err error) { */ 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, - ).Store(&secrets); err != nil { + 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 } @@ -196,39 +213,28 @@ func (s *Service) GetSession() (ssn *Session, 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 prompt *Prompt - var resultPath dbus.ObjectPath + var promptPath dbus.ObjectPath if objectPaths == nil || len(objectPaths) == 0 { objectPaths = []dbus.ObjectPath{s.Dbus.Path()} } - for _, p := range objectPaths { - if err = s.Dbus.Call( - DbusServiceLock, 0, p, - ).Store(&locked, &resultPath); err != nil { - errs = append(errs, err) - err = nil - continue - } - - if isPrompt(resultPath) { - - prompt = NewPrompt(s.Conn, resultPath) - - if _, err = prompt.Prompt(); err != nil { - errs = append(errs, err) - err = nil - continue - } - } + if err = s.Dbus.Call( + DbusServiceLock, 0, objectPaths, + ).Store(&locked, &promptPath); err != nil { + return } - if errs != nil && len(errs) > 0 { - err = NewErrors(errs...) + if isPrompt(promptPath) { + + prompt = NewPrompt(s.Conn, promptPath) + + if _, err = prompt.Prompt(); err != nil { + return + } } return @@ -407,7 +413,6 @@ func (s *Service) SetAlias(alias string, objectPath dbus.ObjectPath) (err error) */ func (s *Service) Unlock(objectPaths ...dbus.ObjectPath) (err error) { - var errs []error = make([]error, 0) var unlocked []dbus.ObjectPath var prompt *Prompt var resultPath dbus.ObjectPath @@ -416,29 +421,19 @@ func (s *Service) Unlock(objectPaths ...dbus.ObjectPath) (err error) { objectPaths = []dbus.ObjectPath{s.Dbus.Path()} } - for _, p := range objectPaths { - if err = s.Dbus.Call( - DbusServiceUnlock, 0, p, - ).Store(&unlocked, &resultPath); err != nil { - errs = append(errs, err) - err = nil - continue - } - - if isPrompt(resultPath) { - - prompt = NewPrompt(s.Conn, resultPath) - - if _, err = prompt.Prompt(); err != nil { - errs = append(errs, err) - err = nil - continue - } - } + if err = s.Dbus.Call( + DbusServiceUnlock, 0, objectPaths, + ).Store(&unlocked, &resultPath); err != nil { + return } - if errs != nil && len(errs) > 0 { - err = NewErrors(errs...) + if isPrompt(resultPath) { + + prompt = NewPrompt(s.Conn, resultPath) + + if _, err = prompt.Prompt(); err != nil { + return + } } return diff --git a/service_funcs_test.go b/service_funcs_test.go index d5e98c0..b826f38 100644 --- a/service_funcs_test.go +++ b/service_funcs_test.go @@ -3,17 +3,7 @@ package gosecret import ( "testing" - "github.com/google/uuid" -) - -var ( - collectionName uuid.UUID = uuid.New() - collectionAlias uuid.UUID = uuid.New() -) - -const ( - defaultCollection string = "Login" - testAlias string = "GOSECRET_TESTING_ALIAS" + `github.com/godbus/dbus/v5` ) /* @@ -64,7 +54,7 @@ func TestService_Collections(t *testing.T) { t.Fatalf("could not get new Service via NewService: %v", err.Error()) } - if _, err = svc.Collections(); err != nil { + 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)) @@ -89,6 +79,7 @@ func TestService_Collections(t *testing.T) { (By extension, Service.CreateCollection is also tested as it's a very thin wrapper around Service.CreateAliasedCollection). */ +/* DISABLED. Currently, *only* the alias "default" is allowed. TODO: revisit in future? func TestService_CreateAliasedCollection(t *testing.T) { var err error @@ -105,6 +96,12 @@ func TestService_CreateAliasedCollection(t *testing.T) { 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", @@ -113,17 +110,11 @@ func TestService_CreateAliasedCollection(t *testing.T) { } } - 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 = 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: @@ -133,6 +124,8 @@ func TestService_CreateAliasedCollection(t *testing.T) { (all calls in TestService_Collections) Service.ReadAlias + The default collection (login) is fetched instead of creating one as this collection should exist, + and tests fetching existing collections instead of newly-created ones. */ func TestService_GetCollection(t *testing.T) { @@ -162,29 +155,176 @@ func TestService_GetCollection(t *testing.T) { (all calls in TestService_CreateAliasedCollection) Service.CreateCollection Service.SearchItems + NewItem + NewErrors Service.GetSecrets + NewSecret + Collection.CreateItem + Item.Label */ -/* TODO: left off on this. 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 itemAttrs map[string]string = map[string]string{ + "GOSECRET": "yes", + } 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.Errorf("could not close Service.Session: %v", err.Error()) + } + t.Fatalf("could not create collection '%v': %v", collectionName.String(), err.Error()) + } + + // Create a secret + testSecret = NewSecret(svc.Session, nil, []byte(testSecretContent), "text/plain") + if testItem, err = collection.CreateItem(testItemLabel, itemAttrs, testSecret, true); err != nil { + if err = svc.Close(); err != nil { + t.Errorf("could not close Service.Session: %v", err.Error()) + } + t.Fatalf("could not create Item in collection '%v': %v", collectionName.String(), err.Error()) + } + + 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)) + } + } + + // 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()) } } -*/ // service.Lock & service.Unlock + +/* + 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()) + } + + 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, + ) + } +}