Skip to content

Commit

Permalink
Prepare for the next (minor) release (#9)
Browse files Browse the repository at this point in the history
* Use `lkru` to avoid naming collisions with existing tools.
* Add an 'id' attribute to the secret
* Add test cases and improve test logic
* Avoid hardcoding dbus.content_type
* Improve help/usage output
* Add application and collection flags
* Add automatic base64 processing
* Add a `del` command
* Update README.md

---------

Co-authored-by: Craig Lurey <[email protected]>
  • Loading branch information
amigus and craiglurey authored Aug 27, 2024
1 parent a02707c commit 72a58bf
Show file tree
Hide file tree
Showing 13 changed files with 357 additions and 127 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
GOARCH: amd64
CGO_ENABLED: 0
run: |
go build -o lku-${{ github.ref_name }}_linux_amd64
go build -o lkru-${{ github.ref_name }}_linux_amd64
- name: Create checksums
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.vscode/
linux-keyring-utility
160 changes: 94 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,107 +1,135 @@
# Keeper Security's Linux Keyring Utility
# Linux Keyring Utility

This utility interacts with the native Linux APIs to store and retrieve secrets from the Keyring using [Secret Service](https://specifications.freedesktop.org/secret-service/latest/).
This utility interacts with the native Linux APIs to store and retrieve secrets from the Keyring using [Secret Service](https://specifications.freedesktop.org/secret-service/latest/). It can be used by any integration, plugin, or code base to store and retrieve credentials, secrets, and passwords in the Linux Keyring simply and natively.

While initially developed to help Keeper secure KSM configs, this utility can be used by any integration, plugin, or code base, to store and retrieve credentials, secrets, and passwords in the Linux Keyring simply and natively.
To use this utility, you can deploy the pre-built binary from the releases page, or by importing it into your code base. Both use cases are covered below.

This utility can be used using the pre-build binary from the releases page, or by importing it into your code base. Both use cases are covered below.
For Windows implementations, see the [Windows Credential Utility](https://github.com/Keeper-Security/windows-credential-utility).

## Using the Executable
## Details

Download the latest version from the releases page and optionally add it to PATH to get started.
The Linux Keyring Utility gets and sets _secrets_ in a Linux
[Keyring](http://man7.org/linux/man-pages/man7/keyrings.7.html) using the
[D-Bus](https://dbus.freedesktop.org/doc/dbus-tutorial.html)
[Secret Service](https://specifications.freedesktop.org/secret-service/latest/).

### Usage
It has been tested with
[GNOME Keyring](https://wiki.gnome.org/Projects/GnomeKeyring/) and
[KDE Wallet Manager](https://userbase.kde.org/KDE_Wallet_Manager).
It _should_ work with any implementation of the D-Bus Secrets Service.

The executable supports two commands:
It includes a simple get/set/del(ete) CLI implemented with
[Cobra](https://cobra.dev).

1. `set`
2. `get`
## Usage

Both commands require an application `name` (i.e. the name of the secret in / to be stored in the Keyring) as the first argument.
The Go Language API has offers `Get()`, `Set()` and `Delete()` methods.
The first two accept and return `string` data.

### `set`
### Go API

`set` requires a second argument of the secret to be stored. This can be either a:
The `secret_collection` API is a wrapper object for the function in the `dbus_secrets`.
It unifies the D-Bus _Connection_, _Session_ and _Collection Service_ objects.

1. BASE64 string
2. JSON string
3. Path to an existing JSON file
#### Example (get)

When the secret is saved to the Keyring it is first encoded into a BASE64 format (if not already a BASE64 string). This standardizes the format for both consistent storage and to make it easier to consume by Keeper integrations and products.

> If you need a support for a different format, please submit a feature request. We'd be happy to extend this to support other use cases.
### `get`

`get` returns the stored BASE64 encoded config to `stdout` and exits with a `0` exit code. The requesting integration can capture the output for consumption. Any errors encountered retrieving the config will return a `non-zero` exit code and write to `stderr`.

### Example
```go
package main

```shell
# Save a secret
lku set APPNAME eyJ1c2VybmFtZSI6ICJnb2xsdW0iLCAicGFzc3dvcmQiOiAiTXlQcmVjaW91cyJ9
# or
lku set APPNAME config.json
import (
"os"
sc "github.com/Keeper-Security/linux-keyring-utility/pkg/secret_collection"
)

# Retrieve a secret
lku get APPNAME
func doit() {
if collection, err := sc.DefaultCollection(); err == nil {
if err := collection.Unlock(); err == nil {
if secret, err := collection.Get("myapp", "mysecret"); err == nil {
print(string(secret))
os.Exit(0)
}
}
}
os.Exit(1)
}
```

## Using in Your Code
The `.DefaultCollection()` returns whatever collection the _default_ _alias_ refers to.
It will generate an error if the _default_ alias is not set.
It usually points to the _login_ keyring.
Most Linux Keyring interfaces allow the user to set it.

You can install this utility into your code base using standard `go` commands:
The `.NamedCollection(string)` method provides access to collections by name.

```bash
go get -u github.com/Keeper-Security/linux-keyring-utility@latest
```
#### Example (set)

You can now include the `keyring` package in your application for easy keyring management:
Set takes the data as a parameter and only returns an error.

```go
import (
//...
"github.com/Keeper-Security/linux-keyring-utility/pkg/keyring"
)
if err := collection.Set("myapp", "mysecret", "mysecretdata"); err == nil {
// success
}
```

### Usage
Set accepts _any_ string as secret data.

### `set`
### Binary Interface (CLI)

The `Set()` function of the `SecretProvider` takes two arguments. The first is the name of the secret to be stored, which is usually an application name. The second is the secret itself. This should be either:
The Linux binary supports three subcommands:

1. A BASE64 string
2. A JSON string
3. A path to an existing JSON file
1. `get`
2. `set`
3. `del`

When the secret is saved to the Keyring it is first encoded into a BASE64 format (if not already a BASE64 string). This standardizes the format for both consistent storage and to make it easier to consume by Keeper integrations and products.
_Get_ and _del_ require one parameter; name, which is the secret _Label_ in D-Bus API terms.

```go
provider := keyring.SecretProvider{}
_Del_ accepts one or more secret labels and deletes all of them.
If it generates an error it will stop.

err := provider.Set("MY_APP_NAME", "eyJ1c2VybmFtZSI6InVzZXIiLCAicGFzc3dvcmQiOiJwYXNzIn0=")
if err != nil {
fmt.Println("Unable to set secret:", err)
}
```
_Set_ also requires the data as a _single_ string in the second parameter.
For example, `set foo bar baz` will generate an error but `set foo 'bar baz'` will work.
If the string is `-` then the string is read from standard input.

### `get`
#### Base64 encoding

The `Get()` function of the `SecretProvider` returns the stored BASE64 encoded secret. Pass the name of the secret/application to retrieve the stored value.
_Get_ and _set_ take a `-b` or `--base64` flag that handles base64 automatically.
If used, _Set_ will encode the input before storing it and/or _get_ will decode it before printing.

```go
provider := keyring.SecretProvider{}
Note that calling `get -b` on a secret that is _not_ base64 encoded secret will generate an error.

secret, err := provider.Get("MY_APP_NAME")
if err != nil {
fmt.Println("Unable to get secret:", err)
}
### CLI Examples

fmt.Println(secret) // Prints the BASE64 encoded secret
```shell
# set has no output
lkru set root_cred '{
"username": "root"
"password": "rand0m."
}'
# get prints (to stdout) whatever was set
lku get root_cred
{
"username": "root"
"password": "rand0m."
}
lkru set -b root_cred2 '{"username": "gollum", "password": "MyPrecious"}'
lkru get root_cred2
eyJ1c2VybmFtZSI6ICJnb2xsdW0iLCAicGFzc3dvcmQiOiAiTXlQcmVjaW91cyJ9
lkru get -b root_cred2
{"username": "gollum", "password": "MyPrecious"}
cat ./good_cred.json | lkru set -b root_cred3 -
lkru get root_cred3
ewogICJ1c2VybmFtZSI6ICJhZGFtIiwKICAicGFzc3dvcmQiOiAicGFzc3dvcmQxMjMuIgp9
# errors go to stderr
lkru get root_cred4 2>/dev/null
lkru get root_cred4
Unable to get secret 'root_cred4': Unable to retrieve secret 'root_cred4' for application 'lkru' from collection '/org/freedesktop/secrets/aliases/default': org.freedesktop.Secret.Collection.SearchItems returned nothing
# most errors are obvious
lkru -c missing_wallet get root_cred
Error unlocking the keyring: Unable to unlock collection '/org/freedesktop/secrets/collection/missing_wallet': Object /org/freedesktop/secrets/collection/missing_wallet does not exist
```

## Contributing

Please read and refer to the contribution guide before making your first PR.

For bugs, feature requests, etc., please submit an issue!
For bugs, feature requests, etc., please submit an issue!
20 changes: 11 additions & 9 deletions cmd/del.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import (
)

var delCmd = &cobra.Command{
Use: "del",
Args: cobra.ExactArgs(1),
Short: "Delete a secret from the Linux keyring.",
Long: `Delete a secret from the Linux keyring by it's label.`,
Use: "del [flags] <label> [label...]",
Args: cobra.MinimumNArgs(1),
Short: "Delete secret(s) from the Linux keyring.",
Long: `Delete one or more secrets from the Linux keyring by label.`,
Run: func(cmd *cobra.Command, args []string) {
collection, err := secrets.DefaultCollection()
collection, err := secrets.Collection(collection)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Unable to get the default keyring: %v\n", err)
os.Exit(1)
Expand All @@ -23,10 +23,12 @@ var delCmd = &cobra.Command{
fmt.Fprintf(cmd.ErrOrStderr(), "Error unlocking the keyring: %v\n", err)
os.Exit(1)
} else {
if err := collection.Delete(rootCmd.Name(), args[0]); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Unable to delete secret '%s': %v\n", args[0], err)
os.Exit(1)
for _, label := range args {
if err := collection.Delete(application, label); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Unable to delete secret '%s': %v\n", args[0], err)
os.Exit(1)
}
}
}
},
}
}
20 changes: 15 additions & 5 deletions cmd/get.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"encoding/base64"
"fmt"
"os"

Expand All @@ -9,12 +10,14 @@ import (
)

var getCmd = &cobra.Command{
Use: "get",
Use: "get [flags] <label>",
Args: cobra.ExactArgs(1),
Short: "Gets a secret from the Linux keyring.",
Long: `Get a secret from the Linux keyring by it's label and return the value as a string.`,
Short: "Get a secret from the Linux Keyring.",
Long: `Get a secret from the Linux Keyring by it's label and print the value.
Use -b or --base64 to decode the secret from base64 before printing.
`,
Run: func(cmd *cobra.Command, args []string) {
collection, err := secrets.DefaultCollection()
collection, err := secrets.Collection(collection)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Unable to get the default keyring: %v\n", err)
os.Exit(1)
Expand All @@ -23,10 +26,17 @@ var getCmd = &cobra.Command{
fmt.Fprintf(cmd.ErrOrStderr(), "Error unlocking the keyring: %v\n", err)
os.Exit(1)
} else {
if secret, err := collection.Get(rootCmd.Name(), args[0]); err != nil {
if secret, err := collection.Get(application, args[0]); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Unable to get secret '%s': %v\n", args[0], err)
os.Exit(1)
} else {
if use_base64 {
secret, err = base64.StdEncoding.DecodeString(string(secret))
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Unable to decode base64 secret '%s': %v\n", args[0], err)
os.Exit(1)
}
}
fmt.Println(string(secret))
}
}
Expand Down
18 changes: 15 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ import (
"github.com/spf13/cobra"
)

var application = "lkru"
var collection = "login"
var use_base64 = false

var rootCmd = &cobra.Command{
Use: "kkru",
Short: "Keeper Keyring Utility",
Long: `The Keeper keyring Utility manages secrets using the Linux keychain via the D-Bus Secrets API.`,
Use: "lkru [flags] <get|set|del>",
Short: "Linux Keyring Utility (lkru)",
Long: `lkru is a Linux Keyring Utility.
It manages secrets in a Linux Keyring using the collection interface of the D-Bus Secrets API.
It has a trivial set, get, and delete interface where set always creates and overwrites.
There is no list or search functionality.
`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
cmd.Help()
Expand All @@ -27,6 +35,10 @@ func Execute() {
}

func init() {
rootCmd.PersistentFlags().StringVarP(&application, "application", "a", application, "The application name to use.")
rootCmd.PersistentFlags().StringVarP(&collection, "collection", "c", collection, "The collection name to use.")
getCmd.Flags().BoolVarP(&use_base64, "base64", "b", false, "Decode the secret from base64 before printing.")
setCmd.Flags().BoolVarP(&use_base64, "base64", "b", false, "Encode the secret as base64 before storing.")
rootCmd.AddCommand(setCmd)
rootCmd.AddCommand(getCmd)
rootCmd.AddCommand(delCmd)
Expand Down
35 changes: 31 additions & 4 deletions cmd/set.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,53 @@
package cmd

import (
"bufio"
"encoding/base64"
"fmt"
"os"
"strings"

secrets "github.com/Keeper-Security/linux-keyring-utility/pkg/secret_collection"
"github.com/spf13/cobra"
)

var setCmd = &cobra.Command{
Use: "set",
Use: "set [flags] <label> <secret string>",
Args: cobra.ExactArgs(2),
Short: "Set a secret in the Linux keyring.",
Long: `Set the input string as a secret in the Linux keyring with the corresponding label.`,
Long: `Set secret string as a secret in the Linux keyring with the corresponding label.
If the secret string is "-", lkru reads it from standard input.
Use -b or --base64 to encode the secret string as base64 before storing it.
`,
Run: func(cmd *cobra.Command, args []string) {
collection, err := secrets.DefaultCollection()
collection, err := secrets.Collection(collection)
if err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Unable to get the default keyring: %v\n", err)
os.Exit(1)
}
if err := collection.Unlock(); err == nil {
if err := collection.Set(rootCmd.Name(), args[0], []byte(args[1])); err != nil {
if len(args) == 2 && args[1] == "-" {
scanner := bufio.NewScanner(os.Stdin)

var lines []string
for {
scanner.Scan()
line := scanner.Text()
if len(line) == 0 {
break
}
lines = append(lines, line)
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Unable to read the secret '%s' from standard input: %v\n", args[0], err)
os.Exit(1)
}
args[1] = strings.Join(lines, "\n")
}
if use_base64 {
args[1] = base64.StdEncoding.EncodeToString([]byte(args[1]))
}
if err := collection.Set(application, args[0], []byte(args[1])); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "Unable to create the secret '%s': %v\n", args[0], err)
os.Exit(1)
}
Expand Down
Loading

0 comments on commit 72a58bf

Please sign in to comment.