fix: file keyring fails to add/import/export keys when input is not stdin (fix #9566) (#9821) (#9881)

## Description

Add a test case to reproduce the issue described in #9566. The test currently fails, and I've pointed some possible solutions over https://github.com/cosmos/cosmos-sdk/issues/9566#issuecomment-889281861. But I feel this needs more works in order to provide a more robust solution. I'll keep poking at better options, but taking any pointers if anyone has ideas.

(cherry picked from commit f479b515a8)

Co-authored-by: daeMOn <flavien.binet@gmail.com>
This commit is contained in:
mergify[bot] 2021-08-12 13:41:53 +02:00 committed by GitHub
parent a804449134
commit 4b2b306097
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 225 additions and 73 deletions

View File

@ -1,6 +1,7 @@
package client
import (
"bufio"
"encoding/json"
"io"
"os"
@ -70,7 +71,10 @@ func (ctx Context) WithKeyringOptions(opts ...keyring.Option) Context {
// WithInput returns a copy of the context with an updated input.
func (ctx Context) WithInput(r io.Reader) Context {
ctx.Input = r
// convert to a bufio.Reader to have a shared buffer between the keyring and the
// the Commands, ensuring a read from one advance the read pointer for the other.
// see https://github.com/cosmos/cosmos-sdk/issues/9566.
ctx.Input = bufio.NewReader(r)
return ctx
}

View File

@ -82,12 +82,12 @@ Example:
}
func runAddCmdPrepare(cmd *cobra.Command, args []string) error {
buf := bufio.NewReader(cmd.InOrStdin())
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
buf := bufio.NewReader(clientCtx.Input)
return runAddCmd(clientCtx, cmd, args, buf)
}

View File

@ -18,6 +18,7 @@ import (
"github.com/cosmos/cosmos-sdk/simapp"
"github.com/cosmos/cosmos-sdk/testutil"
sdk "github.com/cosmos/cosmos-sdk/types"
bip39 "github.com/cosmos/go-bip39"
)
func Test_runAddCmdBasic(t *testing.T) {
@ -30,7 +31,7 @@ func Test_runAddCmdBasic(t *testing.T) {
kb, err := keyring.New(sdk.KeyringServiceName(), keyring.BackendTest, kbHome, mockIn)
require.NoError(t, err)
clientCtx := client.Context{}.WithKeyringDir(kbHome)
clientCtx := client.Context{}.WithKeyringDir(kbHome).WithInput(mockIn)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)
t.Cleanup(func() {
@ -227,3 +228,47 @@ func Test_runAddCmdDryRun(t *testing.T) {
})
}
}
func TestAddRecoverFileBackend(t *testing.T) {
cmd := AddKeyCommand()
cmd.Flags().AddFlagSet(Commands("home").PersistentFlags())
mockIn := testutil.ApplyMockIODiscardOutErr(cmd)
kbHome := t.TempDir()
clientCtx := client.Context{}.WithKeyringDir(kbHome).WithInput(mockIn)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)
cmd.SetArgs([]string{
"keyname1",
fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome),
fmt.Sprintf("--%s=%s", cli.OutputFlag, OutputFormatText),
fmt.Sprintf("--%s=%s", flags.FlagKeyAlgorithm, string(hd.Secp256k1Type)),
fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, keyring.BackendFile),
fmt.Sprintf("--%s", flagRecover),
})
keyringPassword := "12345678"
entropySeed, err := bip39.NewEntropy(mnemonicEntropySize)
require.NoError(t, err)
mnemonic, err := bip39.NewMnemonic(entropySeed)
require.NoError(t, err)
mockIn.Reset(fmt.Sprintf("%s\n%s\n%s\n", mnemonic, keyringPassword, keyringPassword))
require.NoError(t, cmd.ExecuteContext(ctx))
kb, err := keyring.New(sdk.KeyringServiceName(), keyring.BackendFile, kbHome, mockIn)
require.NoError(t, err)
t.Cleanup(func() {
mockIn.Reset(fmt.Sprintf("%s\n%s\n", keyringPassword, keyringPassword))
_ = kb.Delete("keyname1")
})
mockIn.Reset(fmt.Sprintf("%s\n%s\n", keyringPassword, keyringPassword))
info, err := kb.Key("keyname1")
require.NoError(t, err)
require.Equal(t, "keyname1", info.GetName())
}

View File

@ -31,11 +31,11 @@ FULLY AWARE OF THE RISKS. If you are unsure, you may want to do some research
and export your keys in ASCII-armored encrypted format.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
buf := bufio.NewReader(cmd.InOrStdin())
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
buf := bufio.NewReader(clientCtx.Input)
unarmored, _ := cmd.Flags().GetBool(flagUnarmoredHex)
unsafe, _ := cmd.Flags().GetBool(flagUnsafe)

View File

@ -1,6 +1,7 @@
package keys
import (
"bufio"
"context"
"fmt"
"testing"
@ -17,55 +18,95 @@ import (
)
func Test_runExportCmd(t *testing.T) {
cmd := ExportKeyCommand()
cmd.Flags().AddFlagSet(Commands("home").PersistentFlags())
mockIn := testutil.ApplyMockIODiscardOutErr(cmd)
// Now add a temporary keybase
kbHome := t.TempDir()
// create a key
kb, err := keyring.New(sdk.KeyringServiceName(), keyring.BackendTest, kbHome, mockIn)
require.NoError(t, err)
t.Cleanup(func() {
kb.Delete("keyname1") // nolint:errcheck
})
path := sdk.GetConfig().GetFullBIP44Path()
_, err = kb.NewAccount("keyname1", testutil.TestMnemonic, "", path, hd.Secp256k1)
require.NoError(t, err)
// Now enter password
args := []string{
"keyname1",
fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome),
fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, keyring.BackendTest),
testCases := []struct {
name string
keyringBackend string
extraArgs []string
userInput string
mustFail bool
expectedOutput string
}{
{
name: "--unsafe only must fail",
keyringBackend: keyring.BackendTest,
extraArgs: []string{"--unsafe"},
mustFail: true,
},
{
name: "--unarmored-hex must fail",
keyringBackend: keyring.BackendTest,
extraArgs: []string{"--unarmored-hex"},
mustFail: true,
},
{
name: "--unsafe --unarmored-hex fail with no user confirmation",
keyringBackend: keyring.BackendTest,
extraArgs: []string{"--unsafe", "--unarmored-hex"},
userInput: "",
mustFail: true,
expectedOutput: "",
},
{
name: "--unsafe --unarmored-hex succeed",
keyringBackend: keyring.BackendTest,
extraArgs: []string{"--unsafe", "--unarmored-hex"},
userInput: "y\n",
mustFail: false,
expectedOutput: "2485e33678db4175dc0ecef2d6e1fc493d4a0d7f7ce83324b6ed70afe77f3485\n",
},
{
name: "file keyring backend properly read password and user confirmation",
keyringBackend: keyring.BackendFile,
extraArgs: []string{"--unsafe", "--unarmored-hex"},
// first 2 pass for creating the key, then unsafe export confirmation, then unlock keyring pass
userInput: "12345678\n12345678\ny\n12345678\n",
mustFail: false,
expectedOutput: "2485e33678db4175dc0ecef2d6e1fc493d4a0d7f7ce83324b6ed70afe77f3485\n",
},
}
mockIn.Reset("123456789\n123456789\n")
cmd.SetArgs(args)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
kbHome := t.TempDir()
defaultArgs := []string{
"keyname1",
fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome),
fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, tc.keyringBackend),
}
clientCtx := client.Context{}.
WithKeyringDir(kbHome).
WithKeyring(kb)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)
cmd := ExportKeyCommand()
cmd.Flags().AddFlagSet(Commands("home").PersistentFlags())
require.NoError(t, cmd.ExecuteContext(ctx))
cmd.SetArgs(append(defaultArgs, tc.extraArgs...))
mockIn, mockOut := testutil.ApplyMockIO(cmd)
argsUnsafeOnly := append(args, "--unsafe")
cmd.SetArgs(argsUnsafeOnly)
require.Error(t, cmd.ExecuteContext(ctx))
mockIn.Reset(tc.userInput)
mockInBuf := bufio.NewReader(mockIn)
argsUnarmoredHexOnly := append(args, "--unarmored-hex")
cmd.SetArgs(argsUnarmoredHexOnly)
require.Error(t, cmd.ExecuteContext(ctx))
// create a key
kb, err := keyring.New(sdk.KeyringServiceName(), tc.keyringBackend, kbHome, bufio.NewReader(mockInBuf))
require.NoError(t, err)
t.Cleanup(func() {
kb.Delete("keyname1") // nolint:errcheck
})
argsUnsafeUnarmoredHex := append(args, "--unsafe", "--unarmored-hex")
cmd.SetArgs(argsUnsafeUnarmoredHex)
require.Error(t, cmd.ExecuteContext(ctx))
path := sdk.GetConfig().GetFullBIP44Path()
_, err = kb.NewAccount("keyname1", testutil.TestMnemonic, "", path, hd.Secp256k1)
require.NoError(t, err)
mockIn, mockOut := testutil.ApplyMockIO(cmd)
mockIn.Reset("y\n")
require.NoError(t, cmd.ExecuteContext(ctx))
require.Equal(t, "2485e33678db4175dc0ecef2d6e1fc493d4a0d7f7ce83324b6ed70afe77f3485\n", mockOut.String())
clientCtx := client.Context{}.
WithKeyringDir(kbHome).
WithKeyring(kb).
WithInput(mockInBuf)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)
err = cmd.ExecuteContext(ctx)
if tc.mustFail {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tc.expectedOutput, mockOut.String())
}
})
}
}

View File

@ -18,11 +18,11 @@ func ImportKeyCommand() *cobra.Command {
Long: "Import a ASCII armored private key into the local keybase.",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
buf := bufio.NewReader(cmd.InOrStdin())
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
buf := bufio.NewReader(clientCtx.Input)
bz, err := ioutil.ReadFile(args[1])
if err != nil {

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
@ -17,25 +18,50 @@ import (
)
func Test_runImportCmd(t *testing.T) {
cmd := ImportKeyCommand()
cmd.Flags().AddFlagSet(Commands("home").PersistentFlags())
mockIn := testutil.ApplyMockIODiscardOutErr(cmd)
testCases := []struct {
name string
keyringBackend string
userInput string
expectError bool
}{
{
name: "test backend success",
keyringBackend: keyring.BackendTest,
// key armor passphrase
userInput: "123456789\n",
},
{
name: "test backend fail with wrong armor pass",
keyringBackend: keyring.BackendTest,
userInput: "987654321\n",
expectError: true,
},
{
name: "file backend success",
keyringBackend: keyring.BackendFile,
// key armor passphrase + keyring password x2
userInput: "123456789\n12345678\n12345678\n",
},
{
name: "file backend fail with wrong armor pass",
keyringBackend: keyring.BackendFile,
userInput: "987654321\n12345678\n12345678\n",
expectError: true,
},
{
name: "file backend fail with wrong keyring pass",
keyringBackend: keyring.BackendFile,
userInput: "123465789\n12345678\n87654321\n",
expectError: true,
},
{
name: "file backend fail with no keyring pass",
keyringBackend: keyring.BackendFile,
userInput: "123465789\n",
expectError: true,
},
}
// Now add a temporary keybase
kbHome := t.TempDir()
kb, err := keyring.New(sdk.KeyringServiceName(), keyring.BackendTest, kbHome, mockIn)
clientCtx := client.Context{}.
WithKeyringDir(kbHome).
WithKeyring(kb)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)
require.NoError(t, err)
t.Cleanup(func() {
kb.Delete("keyname1") // nolint:errcheck
})
keyfile := filepath.Join(kbHome, "key.asc")
armoredKey := `-----BEGIN TENDERMINT PRIVATE KEY-----
salt: A790BB721D1C094260EA84F5E5B72289
kdf: bcrypt
@ -45,12 +71,48 @@ HbP+c6JmeJy9JXe2rbbF1QtCX1gLqGcDQPBXiCtFvP7/8wTZtVOPj8vREzhZ9ElO
=f3l4
-----END TENDERMINT PRIVATE KEY-----
`
require.NoError(t, ioutil.WriteFile(keyfile, []byte(armoredKey), 0644))
mockIn.Reset("123456789\n")
cmd.SetArgs([]string{
"keyname1", keyfile,
fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, keyring.BackendTest),
})
require.NoError(t, cmd.ExecuteContext(ctx))
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cmd := ImportKeyCommand()
cmd.Flags().AddFlagSet(Commands("home").PersistentFlags())
mockIn := testutil.ApplyMockIODiscardOutErr(cmd)
// Now add a temporary keybase
kbHome := t.TempDir()
kb, err := keyring.New(sdk.KeyringServiceName(), tc.keyringBackend, kbHome, nil)
clientCtx := client.Context{}.
WithKeyringDir(kbHome).
WithKeyring(kb).
WithInput(mockIn)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)
require.NoError(t, err)
t.Cleanup(func() {
kb.Delete("keyname1") // nolint:errcheck
})
keyfile := filepath.Join(kbHome, "key.asc")
require.NoError(t, ioutil.WriteFile(keyfile, []byte(armoredKey), 0644))
defer func() {
_ = os.RemoveAll(kbHome)
}()
mockIn.Reset(tc.userInput)
cmd.SetArgs([]string{
"keyname1", keyfile,
fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, tc.keyringBackend),
})
err = cmd.ExecuteContext(ctx)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}