Compare commits
37 Commits
FORK_ORIGI
...
master
Author | SHA1 | Date | |
---|---|---|---|
aa8aef4ccf | |||
d13b263222 | |||
b4419a6f8c | |||
bb85cb8b52 | |||
9d3299c9dc | |||
3f4db5e82c | |||
fa3061ee7a | |||
5b3328f2b9 | |||
16e972c148 | |||
0767e9c0c1 | |||
6dba963608 | |||
851cc327e5 | |||
09f3c9b73e | |||
b9f529ad56 | |||
dcc8499efa | |||
e79d530e47 | |||
b02aa9877c | |||
d81452a92c | |||
cf354c3fa9 | |||
56ba974dec | |||
142c0ba74f | |||
94ae20829e | |||
eda1777431 | |||
b6ba0f9736 | |||
0fc0e0c269 | |||
1d093627f6 | |||
c0a1e4a281 | |||
1d9145bcf2 | |||
3cab98e584 | |||
cf24035c85 | |||
a5b479ae4e | |||
dbc0962e46 | |||
![]() |
644ac13451 | ||
![]() |
bdee0ea110 | ||
![]() |
e298633935 | ||
![]() |
5975b5ed5c | ||
![]() |
809c6d6f97 |
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
*.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/
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
7
.idea/discord.xml
generated
Normal file
7
.idea/discord.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
9
.idea/gosecret.iml
generated
Normal file
9
.idea/gosecret.iml
generated
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/gosecret.iml" filepath="$PROJECT_DIR$/.idea/gosecret.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
14
.ref/URLs
Normal file
14
.ref/URLs
Normal file
@ -0,0 +1,14 @@
|
||||
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.
|
116
.ref/dbus_types/structs.go
Normal file
116
.ref/dbus_types/structs.go
Normal file
@ -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: (<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.
|
||||
*/
|
3
LICENSE
3
LICENSE
@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Goran Sterjov
|
||||
Copyright (c) 2021 Brent Saner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
242
README.adoc
Normal file
242
README.adoc
Normal file
@ -0,0 +1,242 @@
|
||||
= libsecret/gosecret
|
||||
Brent Saner <bts@square-r00t.net>
|
||||
:doctype: book
|
||||
:docinfo: shared
|
||||
:data-uri:
|
||||
:imagesdir: images
|
||||
:sectlinks:
|
||||
:sectnums:
|
||||
:sectnumlevels: 7
|
||||
:toc: preamble
|
||||
:toc2: left
|
||||
:idprefix:
|
||||
:toclevels: 7
|
||||
:source-highlighter: rouge
|
||||
|
||||
image::https://pkg.go.dev/badge/r00t2.io/gosecret.svg[link="https://pkg.go.dev/r00t2.io/gosecret"]
|
||||
|
||||
This project is originally forked from https://github.com/gsterjov/go-libsecret[go-libsecret^] due to:
|
||||
|
||||
* Lack of response from the developer
|
||||
* Complete lack of documentation
|
||||
* Poor, ineffecient, or just plain antipattern design
|
||||
* Missing functionality
|
||||
|
||||
and as such, hopefully this library should serve as a more effective libsecret/SecretService interface.
|
||||
|
||||
== Backwards Compatability/Drop-In Replacement Support
|
||||
|
||||
Version series `v0.X.X` of this library promises full and non-breaking backwards support of API interaction with the original project. The only changes should be internal optimizations, adding documentation, some file reorganizing, adding Golang module support, etc. -- all transparent from the library API itself.
|
||||
|
||||
To use this library as a replacement without significantly modifying your code, you can simply use a `replace` directive:
|
||||
|
||||
// TODO: did I do this correctly? I never really use replacements so someone PR if this is incorrect.
|
||||
.go.mod
|
||||
[source]
|
||||
----
|
||||
// ...
|
||||
replace (
|
||||
github.com/gsterjov/go-libsecret dev => r00t2.io/gosecret v0
|
||||
)
|
||||
----
|
||||
|
||||
and then run `go mod tidy`.
|
||||
|
||||
== New Developer API
|
||||
|
||||
Starting from `v1.0.0` onwards, entirely breaking changes can be assumed from the original project.
|
||||
|
||||
To use the new version,
|
||||
|
||||
[source,go]
|
||||
----
|
||||
import (
|
||||
`r00t2.io/gosecret/v1`
|
||||
)
|
||||
----
|
||||
|
||||
To reflect the absolute breaking changes, the module name changes as well from `libsecret` to `gosecret`.
|
||||
|
||||
=== Status
|
||||
|
||||
The new API is underway, and all functionality in V0 is present. However, it's not "complete". https://github.com/johnnybubonic/gosecret/pulls[PRs^] welcome, of course, but this will be an ongoing effort for a bit of time.
|
||||
|
||||
== SecretService Concepts
|
||||
|
||||
For reference:
|
||||
|
||||
* A `*Service*` allows one to retrieve and operate on/with `*Session*` and `*Collection*` objects.
|
||||
* A `*Session*` allows one to operate on/with `*Item*` objects (e.g. parsing/decoding/decrypting them).
|
||||
* A `*Collection*` allows one to retrieve and operate on/with `*Item*` objects.
|
||||
* An `*Item*` allows one to retrieve and operate on/with `*Secret*` objects.
|
||||
|
||||
(`*Secrets*` are considered "terminating objects" in this model, and contain
|
||||
actual secret value(s) and metadata).
|
||||
|
||||
Various interactions are handled by `*Prompts*`.
|
||||
|
||||
So the object hierarchy in *theory* looks kind of like this:
|
||||
|
||||
----
|
||||
Service
|
||||
├─ Session "A"
|
||||
├─ Session "B"
|
||||
├─ Collection "A"
|
||||
│ ├─ Item "A.1"
|
||||
│ │ ├─ Secret "A_1_a"
|
||||
│ │ └─ Secret "A_1_b"
|
||||
│ └─ Item "A.2"
|
||||
│ ├─ Secret "A_2_a"
|
||||
│ └─ Secret "A_2_b"
|
||||
└─ Collection "B"
|
||||
├─ Item "B.1"
|
||||
│ ├─ Secret "B_1_a"
|
||||
│ └─ Secret "B_1_b"
|
||||
└─ Item "B.2"
|
||||
├─ Secret "B_2_a"
|
||||
└─ Secret "B_2_b"
|
||||
----
|
||||
|
||||
And so on.
|
||||
|
||||
In *practice*, however, most users will only have two ``Collection``s:
|
||||
|
||||
* a default "system" one named `login` (usually unlocked upon login), and
|
||||
* a temporary one that may or may not exist, running in memory for the current login session named `session`
|
||||
|
||||
== Usage
|
||||
|
||||
Full documentation can be found via inline documentation. Either via the https://pkg.go.dev/r00t2.io/gosecret[pkg.go.dev documentation^] or https://pkg.go.dev/golang.org/x/tools/cmd/godoc[`godoc`^] (or `go doc`) in the source root.
|
||||
|
||||
However, here's a quick demonstration.
|
||||
|
||||
[source,go]
|
||||
----
|
||||
package main
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`log`
|
||||
|
||||
// "github.com/johnnybubonic/gosecret" // GitHub mirror
|
||||
"r00t2.io/gosecret" // real upstream; recommended
|
||||
)
|
||||
|
||||
const (
|
||||
// The default collection; it should be available on all SecretService implementations.
|
||||
collectionName string = "login"
|
||||
// A label for an Item used in examples below.
|
||||
exampleLabel string = "Some Website Credentials"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var service *gosecret.Service
|
||||
var collection *gosecret.Collection
|
||||
var item *gosecret.Item
|
||||
var itemAttrs map[string]string
|
||||
var itemLabel string
|
||||
var secret *gosecret.Secret
|
||||
|
||||
// All interactions with SecretService start with initiating a Service connection.
|
||||
if service, err = gosecret.NewService(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
// And unless operating directly on a Service via its methods, you probably need a Collection as well.
|
||||
if collection, err = service.GetCollection(collectionName); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
/*
|
||||
Create a Secret which gets stored in an Item which gets stored in a Collection.
|
||||
See the documentation for details.
|
||||
*/
|
||||
// Incidentally, I believe this is the only exported function/method that does not return an error returner.
|
||||
secret = gosecret.NewSecret(
|
||||
service.Session, // The session associated with this Secret. You're likely fine with the automatically-created *(Service).Session.
|
||||
[]byte{}, // The "parameters". Likely this is an empty byteslice.
|
||||
[]byte("a super secret password"), // The actual secret value.
|
||||
"text/plain", // The content type (MIME type/media type). See https://www.iana.org/assignments/media-types/media-types.xhtml.
|
||||
)
|
||||
|
||||
/*
|
||||
Item attributes are a map[string]string of *metadata* about a Secret/Item.
|
||||
Do *NOT* store sensitive information in these.
|
||||
They're primarily used for searching for Items.
|
||||
*/
|
||||
itemAttrs = map[string]string{
|
||||
"Use": "an example secret",
|
||||
"note": "These keys can be anything you want!",
|
||||
"url": "https://somewebsite.tld/login",
|
||||
"username": "user.name",
|
||||
}
|
||||
|
||||
// And create the Item (and add it to SecretService).
|
||||
if item, err = collection.CreateItem(
|
||||
exampleLabel, // The label of the item. This should also be considered not secret.
|
||||
itemAttrs, // Attributes for the item; see above.
|
||||
secret, // The actual secret.
|
||||
true, // Whether to replace an existing item with the same label or not.
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
/*
|
||||
Now let's fetch the same Item via its attributes.
|
||||
The results are split into locked items and unlocked items.
|
||||
*/
|
||||
var unlockedItems []*gosecret.Item
|
||||
var lockedItems []*gosecret.Item
|
||||
|
||||
if unlockedItems, lockedItems, err = service.SearchItems(itemAttrs); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
// We should only have one Item that matches the search attributes, and unless the item or collection is locked, ...
|
||||
item = unlockedItems[0]
|
||||
if itemLabel, err = item.Label(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
fmt.Printf("Found item: %v\n", itemLabel)
|
||||
|
||||
// Alternatively if you are unsure of the attributes but know the label of the item you want, you can iterate through them.
|
||||
var itemResults []*gosecret.Item
|
||||
|
||||
if itemResults, err = collection.Items(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
for idx, i := range itemResults {
|
||||
if itemLabel, err = i.Label(); err != nil {
|
||||
fmt.Printf("Cannot read label for item at path '%v'\n", i.Dbus.Path())
|
||||
continue
|
||||
}
|
||||
if itemLabel != exampleLabel { // Matching against a desired label - exampleLabel, in this case.
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Found item labeled '%v'! Index number %v at path '%v'\n", itemLabel, idx, i.Dbus.Path())
|
||||
fmt.Printf("Password: %v\n", string(i.Secret.Value))
|
||||
break
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Note that many functions/methods may return a https://pkg.go.dev/r00t2.io/goutils/multierr#MultiError[`(r00t2.io/goutils/)multierr.MultiError`^], which you may attempt to typeswitch to receive the original errors in their native error format. The functions/methods which may return a MultiError are noted as such in their individual documentation.
|
||||
|
||||
== Library Hacking
|
||||
|
||||
=== Reference
|
||||
Aside from the above (INCREDIBLY brief and perhaps slightly inaccurate) introduction to SecretService concepts, it is recommended to see the `.ref/` directory in git. Notably, the `URLS` file profides several excellent resources for understanding SecretService further. The Dbus specification (first URL in the file) is highly recommended if you are unfamiliar with SecretService internals.
|
||||
|
||||
=== Tests
|
||||
|
||||
Many functions are consolidated into a single test due to how dependent certain processes are on other objects. However, all functionality should be covered by test cases and the error string will always be passed through the stack to `go test -v` output.
|
||||
|
||||
Obviously since this library interacts directly with Dbus (and I don't want to spend the time to mock up an entire Dbus-like interface to test), all tests are integration tests rather than unit tests. Therefore in the event of a failed run, you will need to open e.g. Seahorse or d-feet or some other Dbus/SecretService browser and manually delete the created Secret Service collection. It/they should be easily identified; they use a generated UUID4 string as the collection name and it is highly unlikely that you will see any other collections named as such. If running `go test` with the verbose flag (`-v`), the name and path of the collection will be printed out. If all tests pass, the test collection should be removed automatically.
|
||||
|
||||
The same UUID is used for all tests in a test run.
|
||||
|
||||
You may be prompted during a test run for a password; you can simply use a blank password for this as it is the password used to protect a collection. This prompt pops up during the creation of a Collection.
|
124
collection.go
124
collection.go
@ -1,124 +0,0 @@
|
||||
package libsecret
|
||||
|
||||
import "github.com/godbus/dbus"
|
||||
|
||||
|
||||
type Collection struct {
|
||||
conn *dbus.Conn
|
||||
dbus dbus.BusObject
|
||||
}
|
||||
|
||||
|
||||
func NewCollection(conn *dbus.Conn, path dbus.ObjectPath) *Collection {
|
||||
return &Collection{
|
||||
conn: conn,
|
||||
dbus: conn.Object(DBusServiceName, path),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (collection Collection) Path() dbus.ObjectPath {
|
||||
return collection.dbus.Path()
|
||||
}
|
||||
|
||||
|
||||
// READ Array<ObjectPath> Items;
|
||||
func (collection *Collection) Items() ([]Item, error) {
|
||||
val, err := collection.dbus.GetProperty("org.freedesktop.Secret.Collection.Items")
|
||||
if err != nil {
|
||||
return []Item{}, err
|
||||
}
|
||||
|
||||
items := []Item{}
|
||||
for _, path := range val.Value().([]dbus.ObjectPath) {
|
||||
items = append(items, *NewItem(collection.conn, path))
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
|
||||
// Delete (OUT ObjectPath prompt);
|
||||
func (collection *Collection) Delete() error {
|
||||
var prompt dbus.ObjectPath
|
||||
|
||||
err := collection.dbus.Call("org.freedesktop.Secret.Collection.Delete", 0).Store(&prompt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isPrompt(prompt) {
|
||||
prompt := NewPrompt(collection.conn, prompt)
|
||||
|
||||
_, err := prompt.Prompt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// SearchItems (IN Dict<String,String> attributes, OUT Array<ObjectPath> results);
|
||||
func (collection *Collection) SearchItems(profile string) ([]Item, error) {
|
||||
attributes := make(map[string]string)
|
||||
attributes["profile"] = profile
|
||||
|
||||
var paths []dbus.ObjectPath
|
||||
|
||||
err := collection.dbus.Call("org.freedesktop.Secret.Collection.SearchItems", 0, attributes).Store(&paths)
|
||||
if err != nil {
|
||||
return []Item{}, err
|
||||
}
|
||||
|
||||
items := []Item{}
|
||||
for _, path := range paths {
|
||||
items = append(items, *NewItem(collection.conn, path))
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
|
||||
// CreateItem (IN Dict<String,Variant> properties, IN Secret secret, IN Boolean replace, OUT ObjectPath item, OUT ObjectPath prompt);
|
||||
func (collection *Collection) CreateItem(label string, secret *Secret, replace bool) (*Item, error) {
|
||||
properties := make(map[string]dbus.Variant)
|
||||
attributes := make(map[string]string)
|
||||
|
||||
attributes["profile"] = label
|
||||
properties["org.freedesktop.Secret.Item.Label"] = dbus.MakeVariant(label)
|
||||
properties["org.freedesktop.Secret.Item.Attributes"] = dbus.MakeVariant(attributes)
|
||||
|
||||
var path dbus.ObjectPath
|
||||
var prompt dbus.ObjectPath
|
||||
|
||||
err := collection.dbus.Call("org.freedesktop.Secret.Collection.CreateItem", 0, properties, secret, replace).Store(&path, &prompt)
|
||||
if err != nil {
|
||||
return &Item{}, err
|
||||
}
|
||||
|
||||
if isPrompt(prompt) {
|
||||
prompt := NewPrompt(collection.conn, prompt)
|
||||
|
||||
result, err := prompt.Prompt()
|
||||
if err != nil {
|
||||
return &Item{}, err
|
||||
}
|
||||
|
||||
path = result.Value().(dbus.ObjectPath)
|
||||
}
|
||||
|
||||
return NewItem(collection.conn, path), nil
|
||||
}
|
||||
|
||||
|
||||
// READ Boolean Locked;
|
||||
func (collection *Collection) Locked() (bool, error) {
|
||||
val, err := collection.dbus.GetProperty("org.freedesktop.Secret.Collection.Locked")
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
return val.Value().(bool), nil
|
||||
}
|
406
collection_funcs.go
Normal file
406
collection_funcs.go
Normal file
@ -0,0 +1,406 @@
|
||||
package gosecret
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"r00t2.io/goutils/multierr"
|
||||
)
|
||||
|
||||
/*
|
||||
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) {
|
||||
|
||||
if service == nil {
|
||||
err = ErrNoDbusConn
|
||||
}
|
||||
|
||||
if _, err = validConnPath(service.Conn, path); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
coll = &Collection{
|
||||
DbusObject: &DbusObject{
|
||||
Conn: service.Conn,
|
||||
Dbus: service.Conn.Object(DbusService, path),
|
||||
},
|
||||
service: service,
|
||||
// LastModified: time.Now(),
|
||||
}
|
||||
|
||||
// Populate the struct fields...
|
||||
// TODO: use channel for errors; condense into a MultiError and switch to goroutines.
|
||||
if _, err = coll.Locked(); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = coll.Label(); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = coll.Created(); err != nil {
|
||||
return
|
||||
}
|
||||
if _, _, err = coll.Modified(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
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. The most common itemType is DbusDefaultItemType
|
||||
and is the current recommendation.
|
||||
Other types used are:
|
||||
|
||||
org.gnome.keyring.NetworkPassword
|
||||
org.gnome.keyring.Note
|
||||
|
||||
These are libsecret schemas as defined at
|
||||
https://gitlab.gnome.org/GNOME/libsecret/-/blob/master/libsecret/secret-schemas.c (and bundled in with libsecret).
|
||||
Support for adding custom schemas MAY come in the future but is unsupported currently.
|
||||
*/
|
||||
func (c *Collection) CreateItem(label string, attrs map[string]string, secret *Secret, replace bool, itemType ...string) (item *Item, err error) {
|
||||
|
||||
var call *dbus.Call
|
||||
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
|
||||
|
||||
if itemType != nil && len(itemType) > 0 {
|
||||
typeString = itemType[0]
|
||||
} else {
|
||||
typeString = DbusDefaultItemType
|
||||
}
|
||||
|
||||
props[DbusItemLabel] = dbus.MakeVariant(label)
|
||||
if !c.service.Legacy {
|
||||
props[DbusItemType] = dbus.MakeVariant(typeString)
|
||||
}
|
||||
props[DbusItemAttributes] = dbus.MakeVariant(attrs)
|
||||
props[DbusItemCreated] = dbus.MakeVariant(uint64(time.Now().Unix()))
|
||||
// props[DbusItemModified] = dbus.MakeVariant(uint64(time.Now().Unix()))
|
||||
|
||||
if call = c.Dbus.Call(
|
||||
DbusCollectionCreateItem, 0, props, secret, replace,
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
if err = call.Store(&path, &promptPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if isPrompt(promptPath) {
|
||||
prompt = NewPrompt(c.Conn, promptPath)
|
||||
|
||||
if variant, err = prompt.Prompt(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
path = variant.Value().(dbus.ObjectPath)
|
||||
}
|
||||
|
||||
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).
|
||||
*/
|
||||
func (c *Collection) Delete() (err error) {
|
||||
|
||||
var call *dbus.Call
|
||||
var promptPath dbus.ObjectPath
|
||||
var prompt *Prompt
|
||||
|
||||
if call = c.Dbus.Call(
|
||||
DbusCollectionDelete, 0,
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
if err = call.Store(&promptPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if isPrompt(promptPath) {
|
||||
|
||||
prompt = NewPrompt(c.Conn, promptPath)
|
||||
if _, err = prompt.Prompt(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Items returns a slice of Item pointers in the Collection.
|
||||
err MAY be a *multierr.MultiError.
|
||||
*/
|
||||
func (c *Collection) Items() (items []*Item, err error) {
|
||||
|
||||
var paths []dbus.ObjectPath
|
||||
var item *Item
|
||||
var variant dbus.Variant
|
||||
var errs *multierr.MultiError = multierr.NewMultiError()
|
||||
|
||||
if variant, err = c.Dbus.GetProperty(DbusCollectionItems); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
paths = variant.Value().([]dbus.ObjectPath)
|
||||
|
||||
items = make([]*Item, 0)
|
||||
|
||||
for _, path := range paths {
|
||||
item = nil
|
||||
if item, err = NewItem(c, path); err != nil {
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
if !errs.IsEmpty() {
|
||||
err = errs
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Label returns the Collection label (name).
|
||||
func (c *Collection) Label() (label string, err error) {
|
||||
|
||||
var variant dbus.Variant
|
||||
|
||||
if variant, err = c.Dbus.GetProperty(DbusCollectionLabel); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
label = variant.Value().(string)
|
||||
|
||||
c.LabelName = label
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Lock will lock an unlocked Collection. It will no-op if the Collection is currently locked.
|
||||
func (c *Collection) Lock() (err error) {
|
||||
|
||||
if _, err = c.Locked(); err != nil {
|
||||
return
|
||||
}
|
||||
if c.IsLocked {
|
||||
return
|
||||
}
|
||||
|
||||
if err = c.service.Lock(c); err != nil {
|
||||
return
|
||||
}
|
||||
c.IsLocked = true
|
||||
|
||||
if _, _, err = c.Modified(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Locked indicates if 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 {
|
||||
isLocked = true
|
||||
return
|
||||
}
|
||||
|
||||
isLocked = variant.Value().(bool)
|
||||
c.IsLocked = isLocked
|
||||
|
||||
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
|
||||
}
|
||||
c.LabelName = newLabel
|
||||
|
||||
if _, _, err = c.Modified(); 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.
|
||||
|
||||
err MAY be a *multierr.MultiError.
|
||||
|
||||
Deprecated: Use Service.SearchItems instead.
|
||||
*/
|
||||
func (c *Collection) SearchItems(profile string) (items []*Item, err error) {
|
||||
|
||||
var call *dbus.Call
|
||||
var paths []dbus.ObjectPath
|
||||
var errs *multierr.MultiError = multierr.NewMultiError()
|
||||
var attrs map[string]string = make(map[string]string, 0)
|
||||
var item *Item
|
||||
|
||||
attrs["profile"] = profile
|
||||
|
||||
if call = c.Dbus.Call(
|
||||
DbusCollectionSearchItems, 0, attrs,
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
if err = call.Store(&paths); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
items = make([]*Item, 0)
|
||||
|
||||
for _, path := range paths {
|
||||
item = nil
|
||||
if item, err = NewItem(c, path); err != nil {
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
if !errs.IsEmpty() {
|
||||
err = errs
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetAlias is a thin wrapper/shorthand for Service.SetAlias (but specific to this Collection).
|
||||
func (c *Collection) SetAlias(alias string) (err error) {
|
||||
|
||||
var call *dbus.Call
|
||||
|
||||
if call = c.service.Dbus.Call(
|
||||
DbusServiceSetAlias, 0, alias, c.Dbus.Path(),
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
|
||||
c.Alias = alias
|
||||
|
||||
if _, _, err = c.Modified(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Unlock will unlock a locked Collection. It will no-op if the Collection is currently unlocked.
|
||||
func (c *Collection) Unlock() (err error) {
|
||||
|
||||
if _, err = c.Locked(); err != nil {
|
||||
return
|
||||
}
|
||||
if !c.IsLocked {
|
||||
return
|
||||
}
|
||||
|
||||
if err = c.service.Unlock(c); err != nil {
|
||||
return
|
||||
}
|
||||
c.IsLocked = false
|
||||
|
||||
if _, _, err = c.Modified(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
c.CreatedAt = created
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// path is a *very* thin wrapper around Collection.Dbus.Path(). It is needed for LockableObject interface membership.
|
||||
func (c *Collection) path() (dbusPath dbus.ObjectPath) {
|
||||
|
||||
dbusPath = c.Dbus.Path()
|
||||
|
||||
return
|
||||
}
|
288
collection_funcs_test.go
Normal file
288
collection_funcs_test.go
Normal file
@ -0,0 +1,288 @@
|
||||
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 searchResultsUnlocked []*Item
|
||||
var searchResultsLocked []*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 searchResultsUnlocked, searchResultsLocked, err = collection.service.SearchItems(itemAttrs); err != nil {
|
||||
t.Errorf("failed to find item '%v' via Collection.SearchItems: %v", string(item.Dbus.Path()), err.Error())
|
||||
} else if (len(searchResultsLocked) + len(searchResultsUnlocked)) == 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(searchResultsUnlocked)+len(searchResultsLocked))
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
t.Logf("Attempting to get label of collection: %v", defaultCollectionLabel)
|
||||
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.path(), err.Error())
|
||||
} else {
|
||||
t.Logf("collection '%v' lock status: %v", collection.path(), 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())
|
||||
}
|
||||
}
|
263
consts.go
Normal file
263
consts.go
Normal file
@ -0,0 +1,263 @@
|
||||
package gosecret
|
||||
|
||||
import (
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
// 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/Collection.CreateItem.
|
||||
*/
|
||||
DbusDefaultItemType string = DbusServiceBase + ".Generic"
|
||||
)
|
||||
|
||||
// Libsecret/SecretService special values.
|
||||
var (
|
||||
// DbusRemoveAliasPath is used to remove an alias from a Collection and/or Item.
|
||||
DbusRemoveAliasPath dbus.ObjectPath = dbus.ObjectPath("/")
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
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
|
||||
)
|
32
consts_test.go
Normal file
32
consts_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
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,
|
||||
}
|
||||
)
|
92
doc.go
Normal file
92
doc.go
Normal file
@ -0,0 +1,92 @@
|
||||
// See LICENSE in source root directory for copyright and licensing information.
|
||||
|
||||
/*
|
||||
Package gosecret is(/was originally) a fork of go-libsecret (see https://github.com/gsterjov/go-libsecret
|
||||
and https://pkg.go.dev/github.com/gsterjov/go-libsecret).
|
||||
|
||||
It was forked in order to present bugfixes, actually document the library, conform to more Go-like patterns, and
|
||||
provide missing functionality (as the original seems to be unmaintained).
|
||||
As such, hopefully this library should serve as a more effective libsecret/SecretService interface.
|
||||
|
||||
Backwards Compatibility
|
||||
|
||||
Version series `v0.X.X` of this library promises full and non-breaking backwards compatibility/drop-in
|
||||
support of API interaction with the original project.
|
||||
The only changes should be internal optimizations, adding documentation, some file reorganizing, adding Golang module support,
|
||||
etc. -- all transparent from the library API itself.
|
||||
|
||||
To use this library as a replacement without significantly modifying your code,
|
||||
you can simply use a `replace` directive in your go.mod file:
|
||||
|
||||
// ...
|
||||
replace (
|
||||
github.com/gsterjov/go-libsecret dev => r00t2.io/gosecret v0
|
||||
)
|
||||
|
||||
and then run `go mod tidy`.
|
||||
|
||||
Do NOT use the master branch. For anything. I make no promises on the stability of that branch at any given time.
|
||||
New features will be added to V1 branch, and stable releases will be tagged. V0 branch is reserved only for optimization and bug fixes.
|
||||
|
||||
New Developer API
|
||||
|
||||
Starting from `v1.0.0` onwards, entirely breaking changes can be assumed from the original project.
|
||||
To use the new version,
|
||||
|
||||
import (
|
||||
`r00t2.io/gosecret/v1`
|
||||
)
|
||||
|
||||
To reflect the absolute breaking changes, the module name changes as well from `libsecret` to `gosecret`.
|
||||
|
||||
SecretService Concepts
|
||||
|
||||
For reference:
|
||||
|
||||
- A Service allows one to retrieve and operate on/with Session and Collection objects.
|
||||
|
||||
- A Session allows one to operate on/with Item objects (e.g. parsing/decoding/decrypting them).
|
||||
|
||||
- A Collection allows one to retrieve and operate on/with Item objects.
|
||||
|
||||
- An Item allows one to retrieve and operate on/with Secret objects.
|
||||
|
||||
(Secrets are considered "terminating objects" in this model, and contain actual secret value(s) and metadata).
|
||||
|
||||
Various interactions are handled by Prompts.
|
||||
|
||||
So the object hierarchy in THEORY looks kind of like this:
|
||||
|
||||
Service
|
||||
├─ Session "A"
|
||||
├─ Session "B"
|
||||
├─ Collection "A"
|
||||
│ ├─ Item "A.1"
|
||||
│ │ ├─ Secret "A_1_a"
|
||||
│ │ └─ Secret "A_1_b"
|
||||
│ └─ Item "A.2"
|
||||
│ ├─ Secret "A_2_a"
|
||||
│ └─ Secret "A_2_b"
|
||||
└─ Collection "B"
|
||||
├─ Item "B.1"
|
||||
│ ├─ Secret "B_1_a"
|
||||
│ └─ Secret "B_1_b"
|
||||
└─ Item "B.2"
|
||||
├─ Secret "B_2_a"
|
||||
└─ Secret "B_2_b"
|
||||
|
||||
And so on.
|
||||
In PRACTICE, however, most users will only have two Collection items
|
||||
(a default "system" one named "login", which usually is unlocked upon login,
|
||||
and a temporary one that may or may not exist, running in memory for the current login session named `session`).
|
||||
|
||||
Usage
|
||||
|
||||
Full documentation can be found via inline documentation.
|
||||
Additionally, use either https://pkg.go.dev/r00t2.io/gosecret or https://pkg.go.dev/golang.org/x/tools/cmd/godoc (or `go doc`) in the source root.
|
||||
|
||||
Note that many functions/methods may return a (r00t2.io/goutils/)multierr.MultiError (https://pkg.go.dev/r00t2.io/goutils/multierr#MultiError),
|
||||
which you may attempt to typeswitch back to a *multierr.MultiErr to receive the original errors in their native error format (MultiError.Errors).
|
||||
The functions/methods which may return a MultiError are noted as such in their individual documentation.
|
||||
*/
|
||||
package gosecret
|
75
errs.go
Normal file
75
errs.go
Normal file
@ -0,0 +1,75 @@
|
||||
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 none/not enough are received.
|
||||
ErrMissingPaths error = errors.New("one or more Dbus object paths were expected but an insufficient amount were received")
|
||||
// ErrMissingObj gets triggered if one or more gosecret-native objects are expected but none/not enough are received.
|
||||
ErrMissingObj error = errors.New("one or more objects 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,
|
||||
}
|
||||
)
|
208
funcs.go
Normal file
208
funcs.go
Normal file
@ -0,0 +1,208 @@
|
||||
package gosecret
|
||||
|
||||
import (
|
||||
`strings`
|
||||
|
||||
`github.com/godbus/dbus/v5`
|
||||
`r00t2.io/goutils/multierr`
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
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.
|
||||
|
||||
If err is not nil, it IS a *multierr.MultiError.
|
||||
*/
|
||||
func validConnPath(conn *dbus.Conn, path interface{}) (cr *ConnPathCheckResult, err error) {
|
||||
|
||||
var errs *multierr.MultiError = multierr.NewMultiError()
|
||||
|
||||
cr = new(ConnPathCheckResult)
|
||||
|
||||
if cr.ConnOK, err = connIsValid(conn); err != nil {
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
if cr.PathOK, err = pathIsValid(path); err != nil {
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
|
||||
if !errs.IsEmpty() {
|
||||
err = errs
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
NameFromPath returns an actual name (as it appears in Dbus) from a dbus.ObjectPath.
|
||||
Note that you can get any object's dbus.ObjectPath via <object>.Dbus.Path().
|
||||
path is validated to ensure it is not an empty string.
|
||||
*/
|
||||
func NameFromPath(path dbus.ObjectPath) (name string, err error) {
|
||||
|
||||
var strSplit []string
|
||||
var ok bool
|
||||
|
||||
if ok, err = pathIsValid(path); err != nil {
|
||||
return
|
||||
} else if !ok {
|
||||
err = ErrBadDbusPath
|
||||
return
|
||||
}
|
||||
|
||||
strSplit = strings.Split(string(path), "/")
|
||||
|
||||
if len(strSplit) < 1 {
|
||||
err = ErrBadDbusPath
|
||||
return
|
||||
}
|
||||
|
||||
name = strSplit[len(strSplit)-1]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
CheckErrIsFromLegacy takes an error.Error from e.g.:
|
||||
|
||||
Service.SearchItems
|
||||
Collection.CreateItem
|
||||
NewItem
|
||||
Item.ChangeItemType
|
||||
Item.Type
|
||||
|
||||
and (in order) attempt to typeswitch to a *multierr.MultiError, then iterate through
|
||||
the *multierr.MultiError.Errors, attempt to typeswitch each of them to a Dbus.Error, and then finally
|
||||
check if it is regarding a missing Type property.
|
||||
|
||||
This is *very explicitly* only useful for the above functions/methods. If used anywhere else,
|
||||
it's liable to return an incorrect isLegacy even if parsed == true.
|
||||
|
||||
It is admittedly convoluted and obtuse, but this saves a lot of boilerplate for users.
|
||||
It wouldn't be necessary if projects didn't insist on using the legacy draft SecretService specification.
|
||||
But here we are.
|
||||
|
||||
isLegacy is true if this Service's API destination is legacy spec. Note that this is checking for
|
||||
very explicit conditions; isLegacy may return false but it is in fact running on a legacy API.
|
||||
Don't rely on this too much.
|
||||
|
||||
parsed is true if we found an error type we were able to perform logic of determination on.
|
||||
*/
|
||||
func CheckErrIsFromLegacy(err error) (isLegacy, parsed bool) {
|
||||
|
||||
switch e := err.(type) {
|
||||
case *multierr.MultiError:
|
||||
parsed = true
|
||||
for _, i := range e.Errors {
|
||||
switch e2 := i.(type) {
|
||||
case dbus.Error:
|
||||
if e2.Name == "org.freedesktop.DBus.Error.UnknownProperty" {
|
||||
isLegacy = true
|
||||
return
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
case dbus.Error:
|
||||
parsed = true
|
||||
if e.Name == "org.freedesktop.DBus.Error.UnknownProperty" {
|
||||
isLegacy = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
9
go.mod
Normal file
9
go.mod
Normal file
@ -0,0 +1,9 @@
|
||||
module r00t2.io/gosecret
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/godbus/dbus/v5 v5.0.6
|
||||
github.com/google/uuid v1.3.0
|
||||
r00t2.io/goutils v1.1.2
|
||||
)
|
9
go.sum
Normal file
9
go.sum
Normal file
@ -0,0 +1,9 @@
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
r00t2.io/goutils v1.1.2 h1:zOOqNHQ/HpJVggV5NTXBcd7FQtBP2C/sMLkHw3YvBzU=
|
||||
r00t2.io/goutils v1.1.2/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
|
||||
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=
|
77
item.go
77
item.go
@ -1,77 +0,0 @@
|
||||
package libsecret
|
||||
|
||||
import "github.com/godbus/dbus"
|
||||
|
||||
|
||||
type Item struct {
|
||||
conn *dbus.Conn
|
||||
dbus dbus.BusObject
|
||||
}
|
||||
|
||||
|
||||
func NewItem(conn *dbus.Conn, path dbus.ObjectPath) *Item {
|
||||
return &Item{
|
||||
conn: conn,
|
||||
dbus: conn.Object(DBusServiceName, path),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (item Item) Path() dbus.ObjectPath {
|
||||
return item.dbus.Path()
|
||||
}
|
||||
|
||||
|
||||
// READWRITE String Label;
|
||||
func (item *Item) Label() (string, error) {
|
||||
val, err := item.dbus.GetProperty("org.freedesktop.Secret.Item.Label")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return val.Value().(string), nil
|
||||
}
|
||||
|
||||
|
||||
// READ Boolean Locked;
|
||||
func (item *Item) Locked() (bool, error) {
|
||||
val, err := item.dbus.GetProperty("org.freedesktop.Secret.Item.Locked")
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
return val.Value().(bool), nil
|
||||
}
|
||||
|
||||
|
||||
// GetSecret (IN ObjectPath session, OUT Secret secret);
|
||||
func (item *Item) GetSecret(session *Session) (*Secret, error) {
|
||||
secret := Secret{}
|
||||
|
||||
err := item.dbus.Call("org.freedesktop.Secret.Item.GetSecret", 0, session.Path()).Store(&secret)
|
||||
if err != nil {
|
||||
return &Secret{}, err
|
||||
}
|
||||
|
||||
return &secret, nil
|
||||
}
|
||||
|
||||
|
||||
// Delete (OUT ObjectPath Prompt);
|
||||
func (item *Item) Delete() error {
|
||||
var prompt dbus.ObjectPath
|
||||
|
||||
err := item.dbus.Call("org.freedesktop.Secret.Item.Delete", 0).Store(&prompt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isPrompt(prompt) {
|
||||
prompt := NewPrompt(item.conn, prompt)
|
||||
if _, err := prompt.Prompt(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
422
item_funcs.go
Normal file
422
item_funcs.go
Normal file
@ -0,0 +1,422 @@
|
||||
package gosecret
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Populate the struct fields...
|
||||
// TODO: use channel for errors; condense into a MultiError and switch to goroutines.
|
||||
if _, err = item.GetSecret(collection.service.Session); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = item.Locked(); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = item.Attributes(); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = item.Label(); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = item.Type(); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = item.Created(); err != nil {
|
||||
return
|
||||
}
|
||||
if _, _, err = item.Modified(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
i.Attrs = attrs
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
ChangeItemType changes an Item.Type to newItemType.
|
||||
Note that this is probably a bad idea unless you're also doing Item.SetSecret.
|
||||
It must be a Dbus interface path (e.g. "foo.bar.Baz").
|
||||
If newItemType is an empty string, DbusDefaultItemType will be used.
|
||||
*/
|
||||
func (i *Item) ChangeItemType(newItemType string) (err error) {
|
||||
|
||||
var variant dbus.Variant
|
||||
|
||||
// Legacy spec.
|
||||
if i.collection.service.Legacy {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(newItemType) == "" {
|
||||
newItemType = DbusDefaultItemType
|
||||
}
|
||||
|
||||
variant = dbus.MakeVariant(newItemType)
|
||||
|
||||
if err = i.Dbus.SetProperty(DbusItemType, variant); err != nil {
|
||||
return
|
||||
}
|
||||
i.SecretType = newItemType
|
||||
|
||||
if _, _, err = i.Modified(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Delete removes an Item from a Collection.
|
||||
func (i *Item) Delete() (err error) {
|
||||
|
||||
var call *dbus.Call
|
||||
var promptPath dbus.ObjectPath
|
||||
var prompt *Prompt
|
||||
|
||||
if call = i.Dbus.Call(
|
||||
DbusItemDelete, 0,
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
if err = call.Store(&promptPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if isPrompt(promptPath) {
|
||||
|
||||
prompt = NewPrompt(i.Conn, promptPath)
|
||||
if _, err = prompt.Prompt(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetSecret returns the Secret in an Item using a Session.
|
||||
func (i *Item) GetSecret(session *Session) (secret *Secret, err error) {
|
||||
|
||||
var call *dbus.Call
|
||||
|
||||
if session == nil {
|
||||
err = ErrNoDbusConn
|
||||
}
|
||||
|
||||
if _, err = connIsValid(session.Conn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if call = i.Dbus.Call(
|
||||
DbusItemGetSecret, 0, session.Dbus.Path(),
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
if err = call.Store(&secret); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
secret.session = session
|
||||
secret.item = i
|
||||
i.Secret = secret
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Label returns the label ("name") of an Item.
|
||||
func (i *Item) Label() (label string, err error) {
|
||||
|
||||
var variant dbus.Variant
|
||||
|
||||
if variant, err = i.Dbus.GetProperty(DbusItemLabel); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
label = variant.Value().(string)
|
||||
i.LabelName = label
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if err = i.ReplaceAttributes(currentProps); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
i.LabelName = newLabel
|
||||
|
||||
if _, _, err = i.Modified(); 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
|
||||
}
|
||||
i.Attrs = newAttrs
|
||||
|
||||
if _, _, err = i.Modified(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SetSecret sets the Secret for an Item.
|
||||
func (i *Item) SetSecret(secret *Secret) (err error) {
|
||||
|
||||
var call *dbus.Call
|
||||
|
||||
if call = i.Dbus.Call(
|
||||
DbusItemSetSecret, 0,
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
|
||||
i.Secret = secret
|
||||
|
||||
if _, _, err = i.Modified(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Type updates the Item.ItemType from DBus (and returns it).
|
||||
func (i *Item) Type() (itemType string, err error) {
|
||||
|
||||
var variant dbus.Variant
|
||||
|
||||
// Legacy spec.
|
||||
if i.collection.service.Legacy {
|
||||
return
|
||||
}
|
||||
|
||||
if variant, err = i.Dbus.GetProperty(DbusItemType); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
itemType = variant.Value().(string)
|
||||
i.SecretType = itemType
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Lock will lock an unlocked Item. It will no-op if the Item is currently locked.
|
||||
func (i *Item) Lock() (err error) {
|
||||
|
||||
if _, err = i.Locked(); err != nil {
|
||||
return
|
||||
}
|
||||
if i.IsLocked {
|
||||
return
|
||||
}
|
||||
|
||||
if err = i.collection.service.Lock(i); err != nil {
|
||||
return
|
||||
}
|
||||
i.IsLocked = true
|
||||
|
||||
if _, _, err = i.Modified(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Locked indicates if an Item is locked (true) or unlocked (false).
|
||||
func (i *Item) Locked() (isLocked bool, err error) {
|
||||
|
||||
var variant dbus.Variant
|
||||
|
||||
if variant, err = i.Dbus.GetProperty(DbusItemLocked); err != nil {
|
||||
isLocked = true
|
||||
return
|
||||
}
|
||||
|
||||
isLocked = variant.Value().(bool)
|
||||
i.IsLocked = isLocked
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Unlock will unlock a locked Item. It will no-op if the Item is currently unlocked.
|
||||
func (i *Item) Unlock() (err error) {
|
||||
|
||||
if _, err = i.Locked(); err != nil {
|
||||
return
|
||||
}
|
||||
if !i.IsLocked {
|
||||
return
|
||||
}
|
||||
|
||||
if err = i.collection.service.Unlock(i); err != nil {
|
||||
return
|
||||
}
|
||||
i.IsLocked = false
|
||||
|
||||
if _, _, err = i.Modified(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Created returns the time.Time of when an Item was created.
|
||||
func (i *Item) Created() (created time.Time, err error) {
|
||||
|
||||
var variant dbus.Variant
|
||||
var timeInt uint64
|
||||
|
||||
if variant, err = i.Dbus.GetProperty(DbusItemCreated); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
timeInt = variant.Value().(uint64)
|
||||
|
||||
created = time.Unix(int64(timeInt), 0)
|
||||
i.CreatedAt = created
|
||||
|
||||
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.
|
||||
|
||||
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 variant dbus.Variant
|
||||
var timeInt uint64
|
||||
|
||||
if variant, err = i.Dbus.GetProperty(DbusItemModified); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
timeInt = variant.Value().(uint64)
|
||||
|
||||
modified = time.Unix(int64(timeInt), 0)
|
||||
|
||||
if !i.lastModifiedSet {
|
||||
// It's "nil", so set it to modified. We can't check for a zero-value in case Dbus has it as a zero-value.
|
||||
i.LastModified = modified
|
||||
i.lastModifiedSet = true
|
||||
}
|
||||
|
||||
isChanged = modified.After(i.LastModified)
|
||||
i.LastModified = modified
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// path is a *very* thin wrapper around Item.Dbus.Path(). It is needed for LockableObject membership.
|
||||
func (i *Item) path() (dbusPath dbus.ObjectPath) {
|
||||
|
||||
dbusPath = i.Dbus.Path()
|
||||
|
||||
return
|
||||
}
|
153
item_funcs_test.go
Normal file
153
item_funcs_test.go
Normal file
@ -0,0 +1,153 @@
|
||||
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 = collection.Delete(); err != nil {
|
||||
t.Errorf("could not delete collection '%v': %v", 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())
|
||||
}
|
||||
}
|
55
prompt.go
55
prompt.go
@ -1,55 +0,0 @@
|
||||
package libsecret
|
||||
|
||||
import (
|
||||
"github.com/godbus/dbus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
||||
type Prompt struct {
|
||||
conn *dbus.Conn
|
||||
dbus dbus.BusObject
|
||||
}
|
||||
|
||||
|
||||
func NewPrompt(conn *dbus.Conn, path dbus.ObjectPath) *Prompt {
|
||||
return &Prompt{
|
||||
conn: conn,
|
||||
dbus: conn.Object(DBusServiceName, path),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (prompt Prompt) Path() dbus.ObjectPath {
|
||||
return prompt.dbus.Path()
|
||||
}
|
||||
|
||||
|
||||
func isPrompt(path dbus.ObjectPath) bool {
|
||||
promptPath := DBusPath + "/prompt/"
|
||||
return strings.HasPrefix(string(path), promptPath)
|
||||
}
|
||||
|
||||
|
||||
// Prompt (IN String window-id);
|
||||
func (prompt *Prompt) Prompt() (*dbus.Variant, error) {
|
||||
// prompts are asynchronous so we connect to the signal
|
||||
// and block with a channel until we get a response
|
||||
c := make(chan *dbus.Signal, 10)
|
||||
defer close(c)
|
||||
|
||||
prompt.conn.Signal(c)
|
||||
defer prompt.conn.RemoveSignal(c)
|
||||
|
||||
err := prompt.dbus.Call("org.freedesktop.Secret.Prompt.Prompt", 0, "").Store()
|
||||
if err != nil {
|
||||
return &dbus.Variant{}, err
|
||||
}
|
||||
|
||||
for {
|
||||
if result := <-c; result.Path == prompt.Path() {
|
||||
value := result.Body[1].(dbus.Variant)
|
||||
return &value, nil
|
||||
}
|
||||
}
|
||||
}
|
47
prompt_funcs.go
Normal file
47
prompt_funcs.go
Normal file
@ -0,0 +1,47 @@
|
||||
package gosecret
|
||||
|
||||
import (
|
||||
`github.com/godbus/dbus/v5`
|
||||
)
|
||||
|
||||
// NewPrompt returns a pointer to a new Prompt based on a Dbus connection and a Dbus path.
|
||||
func NewPrompt(conn *dbus.Conn, path dbus.ObjectPath) (prompt *Prompt) {
|
||||
|
||||
prompt = &Prompt{
|
||||
DbusObject: &DbusObject{
|
||||
Conn: conn,
|
||||
Dbus: conn.Object(DbusService, 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)
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
if result = <-c; result.Path == p.Dbus.Path() {
|
||||
*promptValue = result.Body[1].(dbus.Variant)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
21
secret.go
21
secret.go
@ -1,21 +0,0 @@
|
||||
package libsecret
|
||||
|
||||
import "github.com/godbus/dbus"
|
||||
|
||||
|
||||
type Secret struct {
|
||||
Session dbus.ObjectPath
|
||||
Parameters []byte
|
||||
Value []byte
|
||||
ContentType string
|
||||
}
|
||||
|
||||
|
||||
func NewSecret(session *Session, params []byte, value []byte, contentType string) *Secret {
|
||||
return &Secret{
|
||||
Session: session.Path(),
|
||||
Parameters: params,
|
||||
Value: value,
|
||||
ContentType: contentType,
|
||||
}
|
||||
}
|
14
secret_funcs.go
Normal file
14
secret_funcs.go
Normal file
@ -0,0 +1,14 @@
|
||||
package gosecret
|
||||
|
||||
// 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(),
|
||||
Parameters: params,
|
||||
Value: value,
|
||||
ContentType: contentType,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
17
secretvalue_funcs.go
Normal file
17
secretvalue_funcs.go
Normal file
@ -0,0 +1,17 @@
|
||||
package gosecret
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
)
|
||||
|
||||
/*
|
||||
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(fmt.Sprintf("\"%v\"", string(*s)))
|
||||
|
||||
return
|
||||
}
|
141
service.go
141
service.go
@ -1,141 +0,0 @@
|
||||
package libsecret
|
||||
|
||||
import "github.com/godbus/dbus"
|
||||
|
||||
|
||||
const (
|
||||
DBusServiceName = "org.freedesktop.secrets"
|
||||
DBusPath = "/org/freedesktop/secrets"
|
||||
)
|
||||
|
||||
type DBusObject interface {
|
||||
Path() dbus.ObjectPath
|
||||
}
|
||||
|
||||
|
||||
type Service struct {
|
||||
conn *dbus.Conn
|
||||
dbus dbus.BusObject
|
||||
}
|
||||
|
||||
|
||||
func NewService() (*Service, error) {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return &Service{}, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
conn: conn,
|
||||
dbus: conn.Object(DBusServiceName, DBusPath),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
func (service Service) Path() dbus.ObjectPath {
|
||||
return service.dbus.Path()
|
||||
}
|
||||
|
||||
|
||||
// OpenSession (IN String algorithm, IN Variant input, OUT Variant output, OUT ObjectPath result);
|
||||
func (service *Service) Open() (*Session, error) {
|
||||
var output dbus.Variant
|
||||
var path dbus.ObjectPath
|
||||
|
||||
err := service.dbus.Call("org.freedesktop.Secret.Service.OpenSession", 0, "plain", dbus.MakeVariant("")).Store(&output, &path)
|
||||
if err != nil {
|
||||
return &Session{}, err
|
||||
}
|
||||
|
||||
return NewSession(service.conn, path), nil
|
||||
}
|
||||
|
||||
|
||||
// READ Array<ObjectPath> Collections;
|
||||
func (service *Service) Collections() ([]Collection, error) {
|
||||
val, err := service.dbus.GetProperty("org.freedesktop.Secret.Service.Collections")
|
||||
if err != nil {
|
||||
return []Collection{}, err
|
||||
}
|
||||
|
||||
collections := []Collection{}
|
||||
for _, path := range val.Value().([]dbus.ObjectPath) {
|
||||
collections = append(collections, *NewCollection(service.conn, path))
|
||||
}
|
||||
|
||||
return collections, nil
|
||||
}
|
||||
|
||||
|
||||
// CreateCollection (IN Dict<String,Variant> properties, IN String alias, OUT ObjectPath collection, OUT ObjectPath prompt);
|
||||
func (service *Service) CreateCollection(label string) (*Collection, error) {
|
||||
properties := make(map[string]dbus.Variant)
|
||||
properties["org.freedesktop.Secret.Collection.Label"] = dbus.MakeVariant(label)
|
||||
|
||||
var path dbus.ObjectPath
|
||||
var prompt dbus.ObjectPath
|
||||
|
||||
err := service.dbus.Call("org.freedesktop.Secret.Service.CreateCollection", 0, properties, "").Store(&path, &prompt)
|
||||
if err != nil {
|
||||
return &Collection{}, err
|
||||
}
|
||||
|
||||
if isPrompt(prompt) {
|
||||
prompt := NewPrompt(service.conn, prompt)
|
||||
|
||||
result, err := prompt.Prompt()
|
||||
if err != nil {
|
||||
return &Collection{}, err
|
||||
}
|
||||
|
||||
path = result.Value().(dbus.ObjectPath)
|
||||
}
|
||||
|
||||
return NewCollection(service.conn, path), nil
|
||||
}
|
||||
|
||||
|
||||
// Unlock (IN Array<ObjectPath> objects, OUT Array<ObjectPath> unlocked, OUT ObjectPath prompt);
|
||||
func (service *Service) Unlock(object DBusObject) error {
|
||||
objects := []dbus.ObjectPath{object.Path()}
|
||||
|
||||
var unlocked []dbus.ObjectPath
|
||||
var prompt dbus.ObjectPath
|
||||
|
||||
err := service.dbus.Call("org.freedesktop.Secret.Service.Unlock", 0, objects).Store(&unlocked, &prompt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isPrompt(prompt) {
|
||||
prompt := NewPrompt(service.conn, prompt)
|
||||
if _, err := prompt.Prompt(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// Lock (IN Array<ObjectPath> objects, OUT Array<ObjectPath> locked, OUT ObjectPath Prompt);
|
||||
func (service *Service) Lock(object DBusObject) error {
|
||||
objects := []dbus.ObjectPath{object.Path()}
|
||||
|
||||
var locked []dbus.ObjectPath
|
||||
var prompt dbus.ObjectPath
|
||||
|
||||
err := service.dbus.Call("org.freedesktop.Secret.Service.Lock", 0, objects).Store(&locked, &prompt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isPrompt(prompt) {
|
||||
prompt := NewPrompt(service.conn, prompt)
|
||||
if _, err := prompt.Prompt(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
599
service_funcs.go
Normal file
599
service_funcs.go
Normal file
@ -0,0 +1,599 @@
|
||||
package gosecret
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
`r00t2.io/goutils/multierr`
|
||||
)
|
||||
|
||||
// NewService returns a pointer to a new Service connection.
|
||||
func NewService() (service *Service, err error) {
|
||||
|
||||
var svc Service = Service{
|
||||
DbusObject: &DbusObject{
|
||||
Conn: nil,
|
||||
Dbus: nil,
|
||||
},
|
||||
Session: nil,
|
||||
}
|
||||
|
||||
if svc.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
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Close cleanly closes a Service and all its underlying connections (e.g. Service.Session).
|
||||
func (s *Service) Close() (err error) {
|
||||
|
||||
if err = s.Session.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.Conn.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Collections returns a slice of Collection items accessible to this Service.
|
||||
|
||||
err MAY be a *multierr.MultiError.
|
||||
*/
|
||||
func (s *Service) Collections() (collections []*Collection, err error) {
|
||||
|
||||
var paths []dbus.ObjectPath
|
||||
var variant dbus.Variant
|
||||
var coll *Collection
|
||||
var errs *multierr.MultiError = multierr.NewMultiError()
|
||||
|
||||
if variant, err = s.Dbus.GetProperty(DbusServiceCollections); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
paths = variant.Value().([]dbus.ObjectPath)
|
||||
|
||||
collections = make([]*Collection, 0)
|
||||
|
||||
for _, path := range paths {
|
||||
coll = nil
|
||||
if coll, err = NewCollection(s, path); err != nil {
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
collections = append(collections, coll)
|
||||
}
|
||||
|
||||
if !errs.IsEmpty() {
|
||||
err = errs
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
var call *dbus.Call
|
||||
var variant *dbus.Variant
|
||||
var path dbus.ObjectPath
|
||||
var promptPath dbus.ObjectPath
|
||||
var prompt *Prompt
|
||||
var props map[string]dbus.Variant = make(map[string]dbus.Variant)
|
||||
|
||||
props[DbusCollectionLabel] = dbus.MakeVariant(label)
|
||||
props[DbusCollectionCreated] = dbus.MakeVariant(uint64(time.Now().Unix()))
|
||||
props[DbusCollectionModified] = dbus.MakeVariant(uint64(time.Now().Unix()))
|
||||
|
||||
if call = s.Dbus.Call(
|
||||
DbusServiceCreateCollection, 0, props, alias,
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
if err = call.Store(&path, &promptPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if isPrompt(promptPath) {
|
||||
|
||||
prompt = NewPrompt(s.Conn, promptPath)
|
||||
if variant, err = prompt.Prompt(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
path = variant.Value().(dbus.ObjectPath)
|
||||
}
|
||||
|
||||
collection, err = NewCollection(s, 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) {
|
||||
|
||||
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.
|
||||
|
||||
err MAY be a *multierr.MultiError.
|
||||
*/
|
||||
func (s *Service) GetCollection(name string) (c *Collection, err error) {
|
||||
|
||||
var errs *multierr.MultiError = multierr.NewMultiError()
|
||||
var colls []*Collection
|
||||
var pathName string
|
||||
|
||||
// First check for an alias.
|
||||
if c, err = s.ReadAlias(name); err != nil && err != ErrDoesNotExist {
|
||||
c = nil
|
||||
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 pathName, err = NameFromPath(i.Dbus.Path()); err != nil {
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
if pathName == name {
|
||||
c = i
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Still nothing? Try by label.
|
||||
for _, i := range colls {
|
||||
if i.LabelName == name {
|
||||
c = i
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't find it by the given name.
|
||||
if !errs.IsEmpty() {
|
||||
err = 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.
|
||||
If you know which Collection your desired Secret is in, it is recommended to iterate through Collection.Items instead
|
||||
(as Secrets returned here may have missing functionality).
|
||||
*/
|
||||
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{}
|
||||
var call *dbus.Call
|
||||
|
||||
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 call = s.Dbus.Call(
|
||||
DbusServiceGetSecrets, 0, itemPaths, s.Session.Dbus.Path(),
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
if err = call.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
|
||||
}
|
||||
|
||||
// Scrapping this idea for now; it would require introspection on a known Item path.
|
||||
/*
|
||||
IsLegacy indicates with a decent likelihood of accuracy if this Service is
|
||||
connected to a legacy spec Secret Service (true) or if the spec is current (false).
|
||||
|
||||
It also returns a confidence indicator as a float, which indicates how accurate
|
||||
the guess (because it is a guess) may/is likely to be (as a percentage). For example,
|
||||
if confidence is expressed as 0.25, the result of legacyAPI has a 25% of being accurate.
|
||||
*/
|
||||
/*
|
||||
func (s *Service) IsLegacy() (legacyAPI bool, confidence int) {
|
||||
|
||||
var maxCon int
|
||||
|
||||
// Test 1, property introspection on Item. We're looking for a Type property.
|
||||
DbusInterfaceItem
|
||||
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
// Lock locks an Unlocked Collection or Item (LockableObject).
|
||||
func (s *Service) Lock(objects ...LockableObject) (err error) {
|
||||
|
||||
var call *dbus.Call
|
||||
var toLock []dbus.ObjectPath
|
||||
// We only use these as destinations.
|
||||
var locked []dbus.ObjectPath
|
||||
var prompt *Prompt
|
||||
var promptPath dbus.ObjectPath
|
||||
|
||||
if objects == nil || len(objects) == 0 {
|
||||
err = ErrMissingObj
|
||||
return
|
||||
}
|
||||
|
||||
toLock = make([]dbus.ObjectPath, len(objects))
|
||||
|
||||
for idx, o := range objects {
|
||||
toLock[idx] = o.path()
|
||||
}
|
||||
|
||||
if call = s.Dbus.Call(
|
||||
DbusServiceLock, 0, toLock,
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
if err = call.Store(&locked, &promptPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if isPrompt(promptPath) {
|
||||
|
||||
prompt = NewPrompt(s.Conn, promptPath)
|
||||
|
||||
if _, err = prompt.Prompt(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use channels and goroutines here.
|
||||
for _, o := range objects {
|
||||
if _, err = o.Locked(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
OpenSession returns a pointer to a Session from the Service.
|
||||
It's a convenience function around NewSession.
|
||||
However, NewService attaches a Session by default at Service.Session so this is likely unnecessary.
|
||||
*/
|
||||
func (s *Service) OpenSession(algo, input string) (session *Session, output dbus.Variant, err error) {
|
||||
|
||||
var call *dbus.Call
|
||||
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 call = s.Dbus.Call(
|
||||
DbusServiceOpenSession, 0, algo, inputVariant,
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
if err = call.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 call *dbus.Call
|
||||
var objectPath dbus.ObjectPath
|
||||
|
||||
if call = s.Dbus.Call(
|
||||
DbusServiceReadAlias, 0, alias,
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
/*
|
||||
TODO: Confirm that a nonexistent alias will NOT cause an error to return.
|
||||
If it does, alter the below logic.
|
||||
*/
|
||||
if err = call.Store(&objectPath); 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
|
||||
}
|
||||
|
||||
// RemoveAlias is a thin wrapper around Service.SetAlias using the removal method specified there.
|
||||
func (s *Service) RemoveAlias(alias string) (err error) {
|
||||
|
||||
if err = s.SetAlias(alias, DbusRemoveAliasPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
SearchItems searches all Collection objects and returns all matches based on the map of attributes.
|
||||
|
||||
err MAY be a *multierr.MultiError.
|
||||
*/
|
||||
func (s *Service) SearchItems(attributes map[string]string) (unlockedItems []*Item, lockedItems []*Item, err error) {
|
||||
|
||||
var call *dbus.Call
|
||||
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 item *Item
|
||||
var errs *multierr.MultiError = multierr.NewMultiError()
|
||||
|
||||
if attributes == nil || len(attributes) == 0 {
|
||||
err = ErrMissingAttrs
|
||||
return
|
||||
}
|
||||
|
||||
if call = s.Dbus.Call(
|
||||
DbusServiceSearchItems, 0, attributes,
|
||||
); call.Err != nil {
|
||||
}
|
||||
if err = call.Store(&unlocked, &locked); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lockedItems = make([]*Item, 0)
|
||||
unlockedItems = make([]*Item, 0)
|
||||
|
||||
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 _, i := range locked {
|
||||
|
||||
item = nil
|
||||
cPath = dbus.ObjectPath(filepath.Dir(string(i)))
|
||||
|
||||
if c, ok = collections[cPath]; !ok {
|
||||
errs.AddError(errors.New(fmt.Sprintf(
|
||||
"could not find matching Collection for locked item %v", string(i),
|
||||
)))
|
||||
continue
|
||||
}
|
||||
|
||||
if item, err = NewItem(c, i); err != nil {
|
||||
errs.AddError(errors.New(fmt.Sprintf(
|
||||
"could not create Item for locked item %v; error follows", string(i),
|
||||
)))
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
lockedItems = append(lockedItems, item)
|
||||
}
|
||||
|
||||
// Unlocked items
|
||||
for _, i := range unlocked {
|
||||
|
||||
item = nil
|
||||
cPath = dbus.ObjectPath(filepath.Dir(string(i)))
|
||||
|
||||
if c, ok = collections[cPath]; !ok {
|
||||
errs.AddError(errors.New(fmt.Sprintf(
|
||||
"could not find matching Collection for unlocked item %v", string(i),
|
||||
)))
|
||||
continue
|
||||
}
|
||||
|
||||
if item, err = NewItem(c, i); err != nil {
|
||||
errs.AddError(errors.New(fmt.Sprintf(
|
||||
"could not create Item for unlocked item %v; error follows", string(i),
|
||||
)))
|
||||
errs.AddError(err)
|
||||
err = nil
|
||||
continue
|
||||
}
|
||||
unlockedItems = append(unlockedItems, item)
|
||||
}
|
||||
|
||||
if !errs.IsEmpty() {
|
||||
err = errs
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
SetAlias sets an alias for an existing Collection.
|
||||
(You can get its path via <Collection>.Dbus.Path().)
|
||||
To remove an alias, set objectPath to dbus.ObjectPath("/").
|
||||
*/
|
||||
func (s *Service) SetAlias(alias string, objectPath dbus.ObjectPath) (err error) {
|
||||
|
||||
var call *dbus.Call
|
||||
var collection *Collection
|
||||
|
||||
if collection, err = s.GetCollection(alias); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if call = s.Dbus.Call(
|
||||
DbusServiceSetAlias, 0, alias, objectPath,
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
|
||||
if objectPath == DbusRemoveAliasPath {
|
||||
collection.Alias = ""
|
||||
} else {
|
||||
collection.Alias = alias
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Unlock unlocks a locked Collection or Item (LockableObject).
|
||||
func (s *Service) Unlock(objects ...LockableObject) (err error) {
|
||||
|
||||
var call *dbus.Call
|
||||
var toUnlock []dbus.ObjectPath
|
||||
// We only use these as destinations.
|
||||
var unlocked []dbus.ObjectPath
|
||||
var prompt *Prompt
|
||||
var resultPath dbus.ObjectPath
|
||||
|
||||
if objects == nil || len(objects) == 0 {
|
||||
err = ErrMissingObj
|
||||
return
|
||||
}
|
||||
|
||||
toUnlock = make([]dbus.ObjectPath, len(objects))
|
||||
|
||||
for idx, o := range objects {
|
||||
toUnlock[idx] = o.path()
|
||||
}
|
||||
|
||||
if call = s.Dbus.Call(
|
||||
DbusServiceUnlock, 0, toUnlock,
|
||||
); call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
if err = call.Store(&unlocked, &resultPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if isPrompt(resultPath) {
|
||||
|
||||
prompt = NewPrompt(s.Conn, resultPath)
|
||||
|
||||
if _, err = prompt.Prompt(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use channels and goroutines here.
|
||||
for _, o := range objects {
|
||||
if _, err = o.Locked(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// path is a *very* thin wrapper around Service.Dbus.Path().
|
||||
func (s *Service) path() (dbusPath dbus.ObjectPath) {
|
||||
|
||||
dbusPath = s.Dbus.Path()
|
||||
|
||||
return
|
||||
}
|
449
service_funcs_test.go
Normal file
449
service_funcs_test.go
Normal file
@ -0,0 +1,449 @@
|
||||
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.path(), 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.LabelName, 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' (%v) is not equal to 1; items dump pending...",
|
||||
collectionName.String(), len(itemResultsUnlocked),
|
||||
)
|
||||
for idx, i := range itemResultsUnlocked {
|
||||
t.Logf("ITEM #%v IN COLLECTION %v: %v ('%v')", idx, collectionName.String(), i.LabelName, string(i.Dbus.Path()))
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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 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 = collection.Unlock(); 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 = collection.Lock(); err != nil {
|
||||
t.Errorf("could not lock collection '%v': %v", collectionName.String(), err.Error())
|
||||
}
|
||||
} else {
|
||||
if err = collection.Lock(); 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 = collection.Unlock(); 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())
|
||||
}
|
||||
}
|
22
session.go
22
session.go
@ -1,22 +0,0 @@
|
||||
package libsecret
|
||||
|
||||
import "github.com/godbus/dbus"
|
||||
|
||||
|
||||
type Session struct {
|
||||
conn *dbus.Conn
|
||||
dbus dbus.BusObject
|
||||
}
|
||||
|
||||
|
||||
func NewSession(conn *dbus.Conn, path dbus.ObjectPath) *Session {
|
||||
return &Session{
|
||||
conn: conn,
|
||||
dbus: conn.Object(DBusServiceName, path),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (session Session) Path() dbus.ObjectPath {
|
||||
return session.dbus.Path()
|
||||
}
|
62
session_funcs.go
Normal file
62
session_funcs.go
Normal file
@ -0,0 +1,62 @@
|
||||
package gosecret
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
// I'm still not 100% certain what Sessions are used for, aside from getting Secrets from Items.
|
||||
|
||||
/*
|
||||
NewSession returns a pointer to a new Session based on a Service and a dbus.ObjectPath.
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
var call *dbus.Call
|
||||
|
||||
if call = s.Dbus.Call(
|
||||
DbusSessionClose, 0,
|
||||
); call.Err != nil {
|
||||
/*
|
||||
I... still haven't 100% figured out why this happens, but the session DOES seem to close...?
|
||||
PRs or input welcome.
|
||||
TODO: figure out why this error gets triggered.
|
||||
*/
|
||||
if call.Err.Error() != fmt.Sprintf("The name %v was not provided by any .service files", DbusInterfaceSession) {
|
||||
err = call.Err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// path is a *very* thin wrapper around Session.Dbus.Path().
|
||||
func (s *Session) path() (dbusPath dbus.ObjectPath) {
|
||||
|
||||
dbusPath = s.Dbus.Path()
|
||||
|
||||
return
|
||||
}
|
31
sserror_funcs.go
Normal file
31
sserror_funcs.go
Normal file
@ -0,0 +1,31 @@
|
||||
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
|
||||
}
|
188
types.go
Normal file
188
types.go
Normal file
@ -0,0 +1,188 @@
|
||||
package gosecret
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
/*
|
||||
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
|
||||
}
|
||||
|
||||
type LockableObject interface {
|
||||
Locked() (bool, error)
|
||||
path() dbus.ObjectPath
|
||||
}
|
||||
|
||||
/*
|
||||
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:"-"`
|
||||
// IsLocked indicates if the Service is locked or not. Status updated by Service.Locked.
|
||||
IsLocked bool `json:"locked"`
|
||||
/*
|
||||
Legacy indicates that this SecretService implementation breaks current spec
|
||||
by implementing the legacy/obsolete draft spec rather than current libsecret spec
|
||||
for the Dbus API.
|
||||
|
||||
If you're using SecretService with KeePassXC, for instance, or a much older version
|
||||
of Gnome-Keyring *before* libsecret integration(?), or if you are getting strange errors
|
||||
when performing a Service.SearchItems, you probably need to enable this field on the
|
||||
Service returned by NewService. The coverage of this field may expand in the future, but
|
||||
currently it only prevents/suppresses the (non-existent, in legacy spec) Type property
|
||||
from being read or written on Items during e.g.:
|
||||
|
||||
Service.SearchItems
|
||||
Collection.CreateItem
|
||||
NewItem
|
||||
Item.ChangeItemType
|
||||
Item.Type
|
||||
|
||||
It will perform a no-op if enabled in the above contexts to maintain cross-compatability
|
||||
in codebase between legacy and proper current spec systems, avoiding an error return.
|
||||
|
||||
You can use CheckErrIsFromLegacy if Service.Legacy is false and Service.SearchItems returns
|
||||
a non-nil err to determine if this Service is (probably) interfacing with a legacy spec API.
|
||||
*/
|
||||
Legacy bool `json:"is_legacy"`
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
// service tracks the Service this Session was created from.
|
||||
service *Service
|
||||
}
|
||||
|
||||
/*
|
||||
Collection is an accessor for libsecret collections, which contain multiple Secret Item items.
|
||||
Do not change any of these values directly; use the associated methods instead.
|
||||
Reference:
|
||||
https://developer-old.gnome.org/libsecret/0.18/SecretCollection.html
|
||||
https://specifications.freedesktop.org/secret-service/latest/ch03.html
|
||||
*/
|
||||
type Collection struct {
|
||||
*DbusObject
|
||||
// IsLocked indicates if the Collection is locked or not. Status updated by Collection.Locked.
|
||||
IsLocked bool `json:"locked"`
|
||||
// LabelName is the Collection's label (as given by Collection.Label and modified by Collection.Relabel).
|
||||
LabelName string `json:"label"`
|
||||
// CreatedAt is when this Collection was created (used by Collection.Created).
|
||||
CreatedAt time.Time `json:"created"`
|
||||
// LastModified is when this Item was last changed; it's used by Collection.Modified.
|
||||
LastModified time.Time `json:"modified"`
|
||||
// Alias is the Collection's alias (as handled by Service.ReadAlias and Service.SetAlias).
|
||||
Alias string `json:"alias"`
|
||||
// lastModifiedSet is unexported; it's only used to determine if this is a first-initialization of the modification time or not.
|
||||
lastModifiedSet bool
|
||||
// service tracks the Service this Collection was created from.
|
||||
service *Service
|
||||
}
|
||||
|
||||
/*
|
||||
Item is an entry in a Collection that contains a Secret. Do not change any of these values directly; use the associated methods instead.
|
||||
https://developer-old.gnome.org/libsecret/0.18/SecretItem.html
|
||||
https://specifications.freedesktop.org/secret-service/latest/re03.html
|
||||
*/
|
||||
type Item struct {
|
||||
*DbusObject
|
||||
// Secret is the corresponding Secret object.
|
||||
Secret *Secret `json:"secret"`
|
||||
// IsLocked indicates if the Item is locked or not. Status updated by Item.Locked.
|
||||
IsLocked bool
|
||||
// Attrs are the Item's attributes (as would be returned via Item.Attributes).
|
||||
Attrs map[string]string `json:"attributes"`
|
||||
// LabelName is the Item's label (as given by Item.Label and modified by Item.Relabel).
|
||||
LabelName string `json:"label"`
|
||||
// SecretType is the Item's secret type (as returned by Item.Type).
|
||||
SecretType string `json:"type"`
|
||||
// CreatedAt is when this Item was created (used by Item.Created).
|
||||
CreatedAt time.Time `json:"created"`
|
||||
// LastModified is when this Item was last changed; it's used by Item.Modified.
|
||||
LastModified time.Time `json:"modified"`
|
||||
// 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
|
||||
}
|
||||
|
||||
/*
|
||||
Secret is the "Good Stuff" - the actual secret content.
|
||||
https://developer-old.gnome.org/libsecret/0.18/SecretValue.html
|
||||
https://specifications.freedesktop.org/secret-service/latest/re03.html
|
||||
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"`
|
||||
// Value is the secret's content in []byte format.
|
||||
Value SecretValue `json:"value"`
|
||||
// ContentType is the MIME type of Value.
|
||||
ContentType string `json:"content_type"`
|
||||
// item is the Item this Secret belongs to.
|
||||
item *Item
|
||||
// session is the Session used to decode/decrypt this Secret.
|
||||
session *Session
|
||||
}
|
||||
|
||||
// SecretValue is a custom type that handles JSON encoding a little more easily.
|
||||
type SecretValue []byte
|
Loading…
x
Reference in New Issue
Block a user