diff --git a/cli/cli.go b/cli/cli.go index 8f9d3fcbd1..faeaa26d1f 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -137,6 +137,7 @@ func NewDefraCommand() *cobra.Command { MakeKeyringGenerateCommand(), MakeKeyringImportCommand(), MakeKeyringExportCommand(), + MakeKeyringListCommand(), ) identity := MakeIdentityCommand() diff --git a/cli/keyring_list.go b/cli/keyring_list.go new file mode 100644 index 0000000000..35e2a2482e --- /dev/null +++ b/cli/keyring_list.go @@ -0,0 +1,54 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import ( + "github.com/spf13/cobra" +) + +// MakeKeyringListCommand creates a new command to list all keys in the keyring. +func MakeKeyringListCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "list", + Short: "List all keys in the keyring", + Long: `List all keys in the keyring. +The DEFRA_KEYRING_SECRET environment variable must be set to unlock the keyring. +This can also be done with a .env file in the working directory or at a path +defined with the --secret-file flag. + +Example: + defradb keyring list`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + keyring, err := openKeyring(cmd) + if err != nil { + return err + } + + keyNames, err := keyring.List() + if err != nil { + return err + } + + if len(keyNames) == 0 { + cmd.Println("No keys found in the keyring.") + return nil + } + + cmd.Println("Keys in the keyring:") + for _, keyName := range keyNames { + cmd.Println("- " + keyName) + } + return nil + }, + } + return cmd +} diff --git a/cli/keyring_list_test.go b/cli/keyring_list_test.go new file mode 100644 index 0000000000..b7e88ecf93 --- /dev/null +++ b/cli/keyring_list_test.go @@ -0,0 +1,66 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import ( + "bytes" + "encoding/hex" + "os" + "regexp" + "testing" + + "github.com/sourcenetwork/defradb/crypto" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestKeyringList(t *testing.T) { + rootdir := t.TempDir() + + err := os.Setenv("DEFRA_KEYRING_SECRET", "password") + require.NoError(t, err) + + keyNames := []string{"keyname1", "keyname2", "keyname3"} + + // Insert the keys into the keyring + for _, keyName := range keyNames { + keyBytes, err := crypto.GenerateAES256() + require.NoError(t, err) + keyHex := hex.EncodeToString(keyBytes) + cmd := NewDefraCommand() + cmd.SetArgs([]string{"keyring", "import", "--rootdir", rootdir, keyName, keyHex}) + err = cmd.Execute() + require.NoError(t, err) + } + + // Run the 'keyring list' command, and require no error on the output + var output bytes.Buffer + cmd := NewDefraCommand() + cmd.SetOut(&output) + cmd.SetArgs([]string{"keyring", "list", "--rootdir", rootdir}) + err = cmd.Execute() + require.NoError(t, err) + + outputString := output.String() + + // Use regex to extract the keys, and compare with the expected values + // We know what the format the output should be, which is: + // "Keys in the keyring:\n- keyname1\n- keyname2\n- keyname3\n" + re := regexp.MustCompile(`-\s([^\n]+)`) + matches := re.FindAllStringSubmatch(outputString, -1) + var extractedKeys []string + for _, match := range matches { + extractedKeys = append(extractedKeys, match[1]) + } + + assert.ElementsMatch(t, keyNames, extractedKeys, "The listed keys do not match the expected keys.") +} diff --git a/docs/website/references/cli/defradb_keyring.md b/docs/website/references/cli/defradb_keyring.md index 8dad08b542..f2d3048e71 100644 --- a/docs/website/references/cli/defradb_keyring.md +++ b/docs/website/references/cli/defradb_keyring.md @@ -53,4 +53,5 @@ To learn more about the available options: * [defradb keyring export](defradb_keyring_export.md) - Export a private key * [defradb keyring generate](defradb_keyring_generate.md) - Generate private keys * [defradb keyring import](defradb_keyring_import.md) - Import a private key +* [defradb keyring list](defradb_keyring_list.md) - List all keys in the keyring diff --git a/docs/website/references/cli/defradb_keyring_list.md b/docs/website/references/cli/defradb_keyring_list.md new file mode 100644 index 0000000000..fb43aef05c --- /dev/null +++ b/docs/website/references/cli/defradb_keyring_list.md @@ -0,0 +1,48 @@ +## defradb keyring list + +List all keys in the keyring + +### Synopsis + +List all keys in the keyring. +The DEFRA_KEYRING_SECRET environment variable must be set to unlock the keyring. +This can also be done with a .env file in the working directory or at a path +defined with the --secret-file flag. + +Example: + defradb keyring list + +``` +defradb keyring list [flags] +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + --keyring-backend string Keyring backend to use. Options are file or system (default "file") + --keyring-namespace string Service name to use when using the system backend (default "defradb") + --keyring-path string Path to store encrypted keys when using the file backend (default "keys") + --log-format string Log format to use. Options are text or json (default "text") + --log-level string Log level to use. Options are debug, info, error, fatal (default "info") + --log-output string Log output path. Options are stderr or stdout. (default "stderr") + --log-overrides string Logger config overrides. Format ,=,...;,... + --log-source Include source location in logs + --log-stacktrace Include stacktrace in error and fatal logs + --no-keyring Disable the keyring and generate ephemeral keys + --no-log-color Disable colored log output + --rootdir string Directory for persistent data (default: $HOME/.defradb) + --secret-file string Path to the file containing secrets (default ".env") + --source-hub-address string The SourceHub address authorized by the client to make SourceHub transactions on behalf of the actor + --url string URL of HTTP endpoint to listen on or connect to (default "127.0.0.1:9181") +``` + +### SEE ALSO + +* [defradb keyring](defradb_keyring.md) - Manage DefraDB private keys + diff --git a/keyring/errors.go b/keyring/errors.go index 724d36612c..45280daa1c 100644 --- a/keyring/errors.go +++ b/keyring/errors.go @@ -10,7 +10,14 @@ package keyring -import "github.com/zalando/go-keyring" +import ( + "github.com/zalando/go-keyring" -// ErrNotFound is returned when a keyring item is not found. -var ErrNotFound = keyring.ErrNotFound + "github.com/sourcenetwork/defradb/errors" +) + +var ( + // ErrNotFound is returned when a keyring item is not found. + ErrNotFound = keyring.ErrNotFound + ErrSystemKeyringListInvoked = errors.New("listing keys is not supported by OS keyring") +) diff --git a/keyring/file.go b/keyring/file.go index a8f7532274..d78fe1322c 100644 --- a/keyring/file.go +++ b/keyring/file.go @@ -64,3 +64,20 @@ func (f *fileKeyring) Delete(user string) error { } return err } + +func (f *fileKeyring) List() ([]string, error) { + files, err := os.ReadDir(f.dir) + if err != nil { + return nil, err + } + + // File names are key names + var keyNames []string + for _, file := range files { + if !file.IsDir() { + keyNames = append(keyNames, file.Name()) + } + } + + return keyNames, nil +} diff --git a/keyring/keyring.go b/keyring/keyring.go index 603c25bb78..f7fa5961a1 100644 --- a/keyring/keyring.go +++ b/keyring/keyring.go @@ -24,4 +24,6 @@ type Keyring interface { // // If a key with that name does not exist `ErrNotFound` is returned. Delete(name string) error + // List returns a list of all keys in the keyring, used by the CLI 'keyring list' command + List() ([]string, error) } diff --git a/keyring/system.go b/keyring/system.go index 81575b5501..a2140ec7bb 100644 --- a/keyring/system.go +++ b/keyring/system.go @@ -53,3 +53,10 @@ func (s *systemKeyring) Get(name string) ([]byte, error) { func (s *systemKeyring) Delete(user string) error { return keyring.Delete(s.service, user) } + +func (s *systemKeyring) List() ([]string, error) { + // The OS keyring does not support listing keys + // This function is a stub for now because the Keyring interface requires it + // Currently, the 'defradb keyring list' command uses only fileKeyring + return nil, ErrSystemKeyringListInvoked +} diff --git a/keyring/system_keyring_list_test.go b/keyring/system_keyring_list_test.go new file mode 100644 index 0000000000..443b099c01 --- /dev/null +++ b/keyring/system_keyring_list_test.go @@ -0,0 +1,27 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package keyring + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSystemKeyringListThrowsError(t *testing.T) { + service := "test-service" + systemKeyring := OpenSystemKeyring(service) + + keys, err := systemKeyring.List() + + require.Nil(t, keys, "keys should be nil when List is called") + require.ErrorIs(t, err, ErrSystemKeyringListInvoked, "function should throw ErrSystemKeyringListInvoked error") +}