From a91136a19cfdf4460e53e067baa39d37967861f8 Mon Sep 17 00:00:00 2001 From: Fred Carle Date: Tue, 17 Sep 2024 19:37:21 -0400 Subject: [PATCH] make keyring non-interactive --- README.md | 6 ++- cli/config.go | 5 +++ cli/errors.go | 13 +------ cli/keyring_export.go | 3 ++ cli/keyring_export_test.go | 7 ++-- cli/keyring_generate.go | 3 ++ cli/keyring_generate_test.go | 23 +++++------ cli/keyring_import.go | 3 ++ cli/keyring_import_test.go | 7 ++-- cli/start.go | 37 +++++++++++++++--- cli/utils.go | 19 +++------- .../references/cli/defradb_keyring_export.md | 3 ++ .../cli/defradb_keyring_generate.md | 3 ++ .../references/cli/defradb_keyring_import.md | 3 ++ docs/website/references/cli/defradb_start.md | 1 + go.mod | 3 +- go.sum | 2 + keyring/file.go | 38 +++---------------- keyring/file_test.go | 6 +-- tests/integration/acp.go | 4 +- 20 files changed, 93 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index f61ec5d34d..0c4d4cbe83 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,11 @@ The following keys are loaded from the keyring on start: - `peer-key` Ed25519 private key (required) - `encryption-key` AES-128, AES-192, or AES-256 key (optional) -To randomly generate the required keys, run the following command: +A secret to unlock the keyring is required on start and must be provided via the `DEFRADB_KEYRING_SECRET` environment variable. If a `.env` file is available at the root of the project, the secret can be stored there. + +The keys will be randomly generated on the inital start of the node if they are not found. + +Alternatively, to randomly generate the required keys, run the following command: ``` defradb keyring generate diff --git a/cli/config.go b/cli/config.go index 9a0290eb04..738e817611 100644 --- a/cli/config.go +++ b/cli/config.go @@ -16,6 +16,7 @@ import ( "path/filepath" "strings" + "github.com/joho/godotenv" "github.com/sourcenetwork/corelog" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -63,6 +64,7 @@ var configFlags = map[string]string{ "keyring-backend": "keyring.backend", "keyring-path": "keyring.path", "no-keyring": "keyring.disabled", + "no-encryption-key": "keyring.noencryptionkey", "source-hub-address": "acp.sourceHub.address", } @@ -94,6 +96,9 @@ var configDefaults = map[string]any{ // defaultConfig returns a new config with default values. func defaultConfig() *viper.Viper { + // load environment variables from .env file if one exists + _ = godotenv.Load() + cfg := viper.New() cfg.AutomaticEnv() diff --git a/cli/errors.go b/cli/errors.go index 504cb9ca25..884ded82b3 100644 --- a/cli/errors.go +++ b/cli/errors.go @@ -16,14 +16,6 @@ import ( "github.com/sourcenetwork/defradb/errors" ) -const errKeyringHelp = `%w - -Did you forget to initialize the keyring? - -Use the following command to generate the required keys: - defradb keyring generate -` - const ( errInvalidLensConfig string = "invalid lens configuration" errSchemaVersionNotOfSchema string = "the given schema version is from a different schema" @@ -40,6 +32,7 @@ var ( ErrSchemaVersionNotOfSchema = errors.New(errSchemaVersionNotOfSchema) ErrViewAddMissingArgs = errors.New("please provide a base query and output SDL for this view") ErrPolicyFileArgCanNotBeEmpty = errors.New("policy file argument can not be empty") + ErrMissingKeyringSecret = errors.New("missing keyring secret") ) func NewErrRequiredFlagEmpty(longName string, shortName string) error { @@ -61,7 +54,3 @@ func NewErrSchemaVersionNotOfSchema(schemaRoot string, schemaVersionID string) e errors.NewKV("SchemaVersionID", schemaVersionID), ) } - -func NewErrKeyringHelp(inner error) error { - return fmt.Errorf(errKeyringHelp, inner) -} diff --git a/cli/keyring_export.go b/cli/keyring_export.go index 775672fc8a..b81d117ea0 100644 --- a/cli/keyring_export.go +++ b/cli/keyring_export.go @@ -21,6 +21,9 @@ func MakeKeyringExportCommand() *cobra.Command { Long: `Export a private key. Prints the hexadecimal representation of a private key. +The DEFRA_KEYRING_SECRET environment variable must be set to unlock the keyring. +This can also be done with a .env file in the root directory. + Example: defradb keyring export encryption-key`, Args: cobra.ExactArgs(1), diff --git a/cli/keyring_export_test.go b/cli/keyring_export_test.go index 8631ff70ab..15d1ebd5bd 100644 --- a/cli/keyring_export_test.go +++ b/cli/keyring_export_test.go @@ -13,21 +13,20 @@ package cli import ( "bytes" "encoding/hex" + "os" "strings" "testing" "github.com/sourcenetwork/defradb/crypto" - "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestKeyringExport(t *testing.T) { rootdir := t.TempDir() - readPassword = func(_ *cobra.Command, _ string) ([]byte, error) { - return []byte("secret"), nil - } + err := os.Setenv("DEFRA_KEYRING_SECRET", "password") + require.NoError(t, err) keyBytes, err := crypto.GenerateAES256() require.NoError(t, err) diff --git a/cli/keyring_generate.go b/cli/keyring_generate.go index 34209671a5..8bbb491dbf 100644 --- a/cli/keyring_generate.go +++ b/cli/keyring_generate.go @@ -26,6 +26,9 @@ func MakeKeyringGenerateCommand() *cobra.Command { Randomly generate and store private keys in the keyring. By default peer and encryption keys will be generated. +The DEFRA_KEYRING_SECRET environment variable must be set to unlock the keyring. +This can also be done with a .env file in the root directory. + WARNING: This will overwrite existing keys in the keyring. Example: diff --git a/cli/keyring_generate_test.go b/cli/keyring_generate_test.go index b29446bd15..d4cc7b2404 100644 --- a/cli/keyring_generate_test.go +++ b/cli/keyring_generate_test.go @@ -11,24 +11,23 @@ package cli import ( + "os" "path/filepath" "testing" - "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestKeyringGenerate(t *testing.T) { rootdir := t.TempDir() - readPassword = func(_ *cobra.Command, _ string) ([]byte, error) { - return []byte("secret"), nil - } + err := os.Setenv("DEFRA_KEYRING_SECRET", "password") + require.NoError(t, err) cmd := NewDefraCommand() cmd.SetArgs([]string{"keyring", "generate", "--rootdir", rootdir}) - err := cmd.Execute() + err = cmd.Execute() require.NoError(t, err) assert.FileExists(t, filepath.Join(rootdir, "keys", encryptionKeyName)) @@ -37,14 +36,13 @@ func TestKeyringGenerate(t *testing.T) { func TestKeyringGenerateNoEncryptionKey(t *testing.T) { rootdir := t.TempDir() - readPassword = func(_ *cobra.Command, _ string) ([]byte, error) { - return []byte("secret"), nil - } + err := os.Setenv("DEFRA_KEYRING_SECRET", "password") + require.NoError(t, err) cmd := NewDefraCommand() cmd.SetArgs([]string{"keyring", "generate", "--no-encryption-key", "--rootdir", rootdir}) - err := cmd.Execute() + err = cmd.Execute() require.NoError(t, err) assert.NoFileExists(t, filepath.Join(rootdir, "keys", encryptionKeyName)) @@ -53,14 +51,13 @@ func TestKeyringGenerateNoEncryptionKey(t *testing.T) { func TestKeyringGenerateNoPeerKey(t *testing.T) { rootdir := t.TempDir() - readPassword = func(_ *cobra.Command, _ string) ([]byte, error) { - return []byte("secret"), nil - } + err := os.Setenv("DEFRA_KEYRING_SECRET", "password") + require.NoError(t, err) cmd := NewDefraCommand() cmd.SetArgs([]string{"keyring", "generate", "--no-peer-key", "--rootdir", rootdir}) - err := cmd.Execute() + err = cmd.Execute() require.NoError(t, err) assert.FileExists(t, filepath.Join(rootdir, "keys", encryptionKeyName)) diff --git a/cli/keyring_import.go b/cli/keyring_import.go index 61f80f12a1..06baae816b 100644 --- a/cli/keyring_import.go +++ b/cli/keyring_import.go @@ -23,6 +23,9 @@ func MakeKeyringImportCommand() *cobra.Command { Long: `Import a private key. Store an externally generated key 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 root directory. + Example: defradb keyring import encryption-key 0000000000000000`, Args: cobra.ExactArgs(2), diff --git a/cli/keyring_import_test.go b/cli/keyring_import_test.go index dac907e000..d0d51a2ce8 100644 --- a/cli/keyring_import_test.go +++ b/cli/keyring_import_test.go @@ -12,21 +12,20 @@ package cli import ( "encoding/hex" + "os" "path/filepath" "testing" "github.com/sourcenetwork/defradb/crypto" - "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestKeyringImport(t *testing.T) { rootdir := t.TempDir() - readPassword = func(_ *cobra.Command, _ string) ([]byte, error) { - return []byte("secret"), nil - } + err := os.Setenv("DEFRA_KEYRING_SECRET", "password") + require.NoError(t, err) keyBytes, err := crypto.GenerateAES256() require.NoError(t, err) diff --git a/cli/start.go b/cli/start.go index 651360ab83..110cfe2131 100644 --- a/cli/start.go +++ b/cli/start.go @@ -18,6 +18,7 @@ import ( "github.com/sourcenetwork/immutable" "github.com/spf13/cobra" + "github.com/sourcenetwork/defradb/crypto" "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/http" "github.com/sourcenetwork/defradb/internal/db" @@ -78,20 +79,40 @@ func MakeStartCommand() *cobra.Command { if !cfg.GetBool("keyring.disabled") { kr, err := openKeyring(cmd) if err != nil { - return NewErrKeyringHelp(err) + return err } - // load the required peer key + // load the required peer key or generate one if it doesn't exist peerKey, err := kr.Get(peerKeyName) - if err != nil { - return NewErrKeyringHelp(err) + if err != nil && errors.Is(err, keyring.ErrNotFound) { + peerKey, err = crypto.GenerateEd25519() + if err != nil { + return err + } + err = kr.Set(peerKeyName, peerKey) + if err != nil { + return err + } + log.Info("generated peer key") + } else if err != nil { + return err } opts = append(opts, net.WithPrivateKey(peerKey)) + // load the optional encryption key encryptionKey, err := kr.Get(encryptionKeyName) - if err != nil && !errors.Is(err, keyring.ErrNotFound) { + if err != nil && errors.Is(err, keyring.ErrNotFound) && !cfg.GetBool("keyring.noencryptionkey") { + encryptionKey, err = crypto.GenerateAES256() + if err != nil { + return err + } + err = kr.Set(encryptionKeyName, encryptionKey) + if err != nil { + return err + } + log.Info("generated encryption key") + } else if err != nil && !errors.Is(err, keyring.ErrNotFound) { return err } - opts = append(opts, node.WithBadgerEncryptionKey(encryptionKey)) sourceHubKeyName := cfg.GetString("acp.sourceHub.KeyName") @@ -185,5 +206,9 @@ func MakeStartCommand() *cobra.Command { cfg.GetString(configFlags["privkeypath"]), "Path to the private key for tls", ) + cmd.Flags().Bool( + "no-encryption-key", + cfg.GetBool(configFlags["no-encryption-key"]), + "Skip generating an encryption key. Encryption at rest will be disabled. WARNING: This cannot be undone.") return cmd } diff --git a/cli/utils.go b/cli/utils.go index f0bd6a8098..845cea671b 100644 --- a/cli/utils.go +++ b/cli/utils.go @@ -16,14 +16,12 @@ import ( "encoding/json" "os" "path/filepath" - "syscall" "time" "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/sourcenetwork/immutable" "github.com/spf13/cobra" "github.com/spf13/viper" - "golang.org/x/term" acpIdentity "github.com/sourcenetwork/defradb/acp/identity" "github.com/sourcenetwork/defradb/client" @@ -58,14 +56,6 @@ const ( authTokenExpiration = time.Minute * 15 ) -// readPassword reads a user input password without echoing it to the terminal. -var readPassword = func(cmd *cobra.Command, msg string) ([]byte, error) { - cmd.Print(msg) - pass, err := term.ReadPassword(int(syscall.Stdin)) - cmd.Println("") - return pass, err -} - // mustGetContextDB returns the db for the current command context. // // If a db is not set in the current context this function panics. @@ -214,10 +204,11 @@ func openKeyring(cmd *cobra.Command) (keyring.Keyring, error) { if err := os.MkdirAll(path, 0755); err != nil { return nil, err } - prompt := keyring.PromptFunc(func(s string) ([]byte, error) { - return readPassword(cmd, s) - }) - return keyring.OpenFileKeyring(path, prompt) + secret := []byte(cfg.GetString("keyring.secret")) + if len(secret) == 0 { + return nil, ErrMissingKeyringSecret + } + return keyring.OpenFileKeyring(path, secret) } func writeJSON(cmd *cobra.Command, out any) error { diff --git a/docs/website/references/cli/defradb_keyring_export.md b/docs/website/references/cli/defradb_keyring_export.md index 15029b484a..44890293e1 100644 --- a/docs/website/references/cli/defradb_keyring_export.md +++ b/docs/website/references/cli/defradb_keyring_export.md @@ -7,6 +7,9 @@ Export a private key Export a private key. Prints the hexadecimal representation of a private key. +The DEFRA_KEYRING_SECRET environment variable must be set to unlock the keyring. +This can also be done with a .env file in the root directory. + Example: defradb keyring export encryption-key diff --git a/docs/website/references/cli/defradb_keyring_generate.md b/docs/website/references/cli/defradb_keyring_generate.md index 3651479823..1da38fa72c 100644 --- a/docs/website/references/cli/defradb_keyring_generate.md +++ b/docs/website/references/cli/defradb_keyring_generate.md @@ -8,6 +8,9 @@ Generate private keys. Randomly generate and store private keys in the keyring. By default peer and encryption keys will be generated. +The DEFRA_KEYRING_SECRET environment variable must be set to unlock the keyring. +This can also be done with a .env file in the root directory. + WARNING: This will overwrite existing keys in the keyring. Example: diff --git a/docs/website/references/cli/defradb_keyring_import.md b/docs/website/references/cli/defradb_keyring_import.md index fe5df3f4ff..08da00e78e 100644 --- a/docs/website/references/cli/defradb_keyring_import.md +++ b/docs/website/references/cli/defradb_keyring_import.md @@ -7,6 +7,9 @@ Import a private key Import a private key. Store an externally generated key 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 root directory. + Example: defradb keyring import encryption-key 0000000000000000 diff --git a/docs/website/references/cli/defradb_start.md b/docs/website/references/cli/defradb_start.md index d71dfb14e4..3150ffa7bb 100644 --- a/docs/website/references/cli/defradb_start.md +++ b/docs/website/references/cli/defradb_start.md @@ -16,6 +16,7 @@ defradb start [flags] --allowed-origins stringArray List of origins to allow for CORS requests -h, --help help for start --max-txn-retries int Specify the maximum number of retries per transaction (default 5) + --no-encryption-key Skip generating an encryption key. Encryption at rest will be disabled. WARNING: This cannot be undone. --no-p2p Disable the peer-to-peer network synchronization system --p2paddr strings Listen addresses for the p2p network (formatted as a libp2p MultiAddr) (default [/ip4/127.0.0.1/tcp/9171]) --peers stringArray List of peers to connect to diff --git a/go.mod b/go.mod index a23a90d299..4be484b96e 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20240322071758-198d7dba8fb8 github.com/ipld/go-ipld-prime/storage/bsrvadapter v0.0.0-20240322071758-198d7dba8fb8 github.com/jbenet/goprocess v0.1.4 + github.com/joho/godotenv v1.5.1 github.com/lens-vm/lens/host-go v0.0.0-20231127204031-8d858ed2926c github.com/lestrrat-go/jwx/v2 v2.1.1 github.com/libp2p/go-libp2p v0.36.3 @@ -62,7 +63,6 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.30.0 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa - golang.org/x/term v0.24.0 google.golang.org/grpc v1.66.2 google.golang.org/protobuf v1.34.2 ) @@ -365,6 +365,7 @@ require ( golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect + golang.org/x/term v0.24.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.24.0 // indirect diff --git a/go.sum b/go.sum index cdd939b06e..eaf23755d5 100644 --- a/go.sum +++ b/go.sum @@ -917,6 +917,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jorrizza/ed2curve25519 v0.1.0 h1:P58ZEiVKW4vknYuGyOXuskMm82rTJyGhgRGrMRcCE8E= github.com/jorrizza/ed2curve25519 v0.1.0/go.mod h1:27VPNk2FnNqLQNvvVymiX41VE/nokPyn5HHP7gtfYlo= diff --git a/keyring/file.go b/keyring/file.go index 61191b3285..a8f7532274 100644 --- a/keyring/file.go +++ b/keyring/file.go @@ -28,30 +28,21 @@ type fileKeyring struct { dir string // password is the user defined password used to generate encryption keys password []byte - // prompt func is used to retrieve the user password - prompt PromptFunc } -// PromptFunc is a callback used to retrieve the user's password. -type PromptFunc func(s string) ([]byte, error) - // OpenFileKeyring opens the keyring in the given directory. -func OpenFileKeyring(dir string, prompt PromptFunc) (*fileKeyring, error) { +func OpenFileKeyring(dir string, password []byte) (*fileKeyring, error) { if err := os.MkdirAll(dir, 0755); err != nil { return nil, err } return &fileKeyring{ - dir: dir, - prompt: prompt, + dir: dir, + password: password, }, nil } func (f *fileKeyring) Set(name string, key []byte) error { - password, err := f.promptPassword() - if err != nil { - return err - } - cipher, err := jwe.Encrypt(key, jwe.WithKey(keyEncryptionAlgorithm, password)) + cipher, err := jwe.Encrypt(key, jwe.WithKey(keyEncryptionAlgorithm, f.password)) if err != nil { return err } @@ -63,11 +54,7 @@ func (f *fileKeyring) Get(name string) ([]byte, error) { if os.IsNotExist(err) { return nil, ErrNotFound } - password, err := f.promptPassword() - if err != nil { - return nil, err - } - return jwe.Decrypt(cipher, jwe.WithKey(keyEncryptionAlgorithm, password)) + return jwe.Decrypt(cipher, jwe.WithKey(keyEncryptionAlgorithm, f.password)) } func (f *fileKeyring) Delete(user string) error { @@ -77,18 +64,3 @@ func (f *fileKeyring) Delete(user string) error { } return err } - -// promptPassword returns the password from the user. -// -// If the password has been previously prompted it will be remembered. -func (f *fileKeyring) promptPassword() ([]byte, error) { - if len(f.password) > 0 { - return f.password, nil - } - password, err := f.prompt("Enter keystore password:") - if err != nil { - return nil, err - } - f.password = password - return password, nil -} diff --git a/keyring/file_test.go b/keyring/file_test.go index f3aa3529b1..5be5663cef 100644 --- a/keyring/file_test.go +++ b/keyring/file_test.go @@ -19,11 +19,7 @@ import ( ) func TestFileKeyring(t *testing.T) { - prompt := PromptFunc(func(s string) ([]byte, error) { - return []byte("secret"), nil - }) - - kr, err := OpenFileKeyring(t.TempDir(), prompt) + kr, err := OpenFileKeyring(t.TempDir(), []byte("secret")) require.NoError(t, err) err = kr.Set("peer_key", []byte("abc")) diff --git a/tests/integration/acp.go b/tests/integration/acp.go index 9242a266fc..44ac023bce 100644 --- a/tests/integration/acp.go +++ b/tests/integration/acp.go @@ -141,9 +141,7 @@ func setupSourceHub(s *state) ([]node.ACPOpt, error) { kr, err := keyring.OpenFileKeyring( directory, - keyring.PromptFunc(func(s string) ([]byte, error) { - return []byte("secret"), nil - }), + []byte("secret"), ) if err != nil { return nil, err