diff --git a/ioctl/client.go b/ioctl/client.go index 018072d927..9f2ade15c8 100644 --- a/ioctl/client.go +++ b/ioctl/client.go @@ -7,6 +7,7 @@ package ioctl import ( + "bufio" "bytes" "context" "crypto/ecdsa" @@ -28,6 +29,7 @@ import ( "github.com/iotexproject/iotex-core/ioctl/config" "github.com/iotexproject/iotex-core/ioctl/util" "github.com/iotexproject/iotex-core/ioctl/validator" + "github.com/iotexproject/iotex-core/pkg/util/fileutil" ) type ( @@ -75,15 +77,22 @@ type ( IsCryptoSm2() bool // QueryAnalyser sends request to Analyser endpoint QueryAnalyser(interface{}) (*http.Response, error) + // ReadInput reads the input from stdin + ReadInput() (string, error) + // WriteHdWalletConfigFile write encrypting mnemonic into config file + WriteHdWalletConfigFile(string, string) error + // IsHdWalletConfigFileExist return true if config file is existed, false if not existed + IsHdWalletConfigFileExist() bool } client struct { - cfg config.Config - conn *grpc.ClientConn - cryptoSm2 bool - configFilePath string - endpoint string - insecure bool + cfg config.Config + conn *grpc.ClientConn + cryptoSm2 bool + configFilePath string + endpoint string + insecure bool + hdWalletConfigFile string } // Option sets client construction parameter @@ -106,8 +115,9 @@ func EnableCryptoSm2() Option { // NewClient creates a new ioctl client func NewClient(cfg config.Config, configFilePath string, opts ...Option) Client { c := &client{ - cfg: cfg, - configFilePath: configFilePath, + cfg: cfg, + configFilePath: configFilePath, + hdWalletConfigFile: cfg.Wallet + "/hdwallet", } for _, opt := range opts { opt(c) @@ -310,6 +320,32 @@ func (c *client) QueryAnalyser(reqData interface{}) (*http.Response, error) { return resp, nil } +func (c *client) ReadInput() (string, error) { // notest + in := bufio.NewReader(os.Stdin) + line, err := in.ReadString('\n') + if err != nil { + return "", err + } + return line, nil +} + +func (c *client) WriteHdWalletConfigFile(mnemonic string, password string) error { + enctxt := append([]byte(mnemonic), util.HashSHA256([]byte(mnemonic))...) + enckey := util.HashSHA256([]byte(password)) + out, err := util.Encrypt(enctxt, enckey) + if err != nil { + return errors.Wrap(err, "failed to encrypting mnemonic") + } + if err := os.WriteFile(c.hdWalletConfigFile, out, 0600); err != nil { + return errors.Wrapf(err, "failed to write to config file %s", c.hdWalletConfigFile) + } + return nil +} + +func (c *client) IsHdWalletConfigFileExist() bool { // notest + return fileutil.FileExists(c.hdWalletConfigFile) +} + func (m *ConfirmationMessage) String() string { line := fmt.Sprintf("%s\nOptions:", m.Info) for _, option := range m.Options { diff --git a/ioctl/client_test.go b/ioctl/client_test.go index c124f9b097..e6958d0036 100644 --- a/ioctl/client_test.go +++ b/ioctl/client_test.go @@ -343,6 +343,20 @@ func TestDeleteAlias(t *testing.T) { } } +func TestWriteHdWalletConfigFile(t *testing.T) { + r := require.New(t) + testPathWallet, err := os.MkdirTemp(os.TempDir(), "cfgWallet") + r.NoError(err) + defer testutil.CleanupPath(testPathWallet) + + c := NewClient(config.Config{ + Wallet: testPathWallet, + }, testPathWallet+"/config.default") + mnemonic := "lake stove quarter shove dry matrix hire split wide attract argue core" + password := "123" + r.NoError(c.WriteHdWalletConfigFile(mnemonic, password)) +} + func writeTempConfig(t *testing.T, cfg *config.Config) string { r := require.New(t) testPathd, err := os.MkdirTemp(os.TempDir(), "testConfig") diff --git a/ioctl/newcmd/hdwallet/hdwallet.go b/ioctl/newcmd/hdwallet/hdwallet.go index 13ef752821..64e7496f87 100644 --- a/ioctl/newcmd/hdwallet/hdwallet.go +++ b/ioctl/newcmd/hdwallet/hdwallet.go @@ -1,4 +1,4 @@ -// Copyright (c) 2019 IoTeX Foundation +// Copyright (c) 2022 IoTeX Foundation // This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no // warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent // permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache @@ -6,6 +6,17 @@ package hdwallet +import ( + "github.com/pkg/errors" +) + +// Errors +var ( + ErrPasswdNotMatch = errors.New("password doesn't match") +) + // DefaultRootDerivationPath for iotex // https://github.com/satoshilabs/slips/blob/master/slip-0044.md const DefaultRootDerivationPath = "m/44'/304'" + +var _hdWalletConfigFile string diff --git a/ioctl/newcmd/hdwallet/hdwalletimport.go b/ioctl/newcmd/hdwallet/hdwalletimport.go new file mode 100644 index 0000000000..33ed1f08ee --- /dev/null +++ b/ioctl/newcmd/hdwallet/hdwalletimport.go @@ -0,0 +1,81 @@ +// Copyright (c) 2022 IoTeX Foundation +// This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package hdwallet + +import ( + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/tyler-smith/go-bip39" + + "github.com/iotexproject/iotex-core/ioctl" + "github.com/iotexproject/iotex-core/ioctl/config" +) + +// Multi-language support +var ( + _importCmdShorts = map[config.Language]string{ + config.English: "import hdwallet using mnemonic", + config.Chinese: "通过助记词导入钱包", + } + _importCmdUses = map[config.Language]string{ + config.English: "import", + config.Chinese: "import 导入", + } +) + +// NewHdwalletImportCmd represents the hdwallet import command +func NewHdwalletImportCmd(client ioctl.Client) *cobra.Command { + use, _ := client.SelectTranslation(_importCmdUses) + short, _ := client.SelectTranslation(_importCmdShorts) + + return &cobra.Command{ + Use: use, + Short: short, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + if client.IsHdWalletConfigFileExist() { + cmd.Println("Please run 'ioctl hdwallet delete' before import") + return nil + } + + cmd.Println("Enter 12 mnemonic words you saved, separated by space") + + line, err := client.ReadInput() + if err != nil { + return err + } + mnemonic := strings.TrimSpace(line) + if _, err = bip39.MnemonicToByteArray(mnemonic); err != nil { + return err + } + + cmd.Println("Set password") + password, err := client.ReadSecret() + if err != nil { + return errors.Wrap(err, "failed to get password") + } + cmd.Println("Enter password again") + passwordAgain, err := client.ReadSecret() + if err != nil { + return errors.Wrap(err, "failed to get password") + } + if password != passwordAgain { + return ErrPasswdNotMatch + } + + if err := client.WriteHdWalletConfigFile(mnemonic, password); err != nil { + return errors.Wrap(err, "failed to write to config file") + } + cmd.Printf("Mnemonic phrase: %s\n"+ + "It is used to recover your wallet in case you forgot the password. Write them down and store it in a safe place.\n", mnemonic) + return nil + }, + } +} diff --git a/ioctl/newcmd/hdwallet/hdwalletimport_test.go b/ioctl/newcmd/hdwallet/hdwalletimport_test.go new file mode 100644 index 0000000000..e6beeb1605 --- /dev/null +++ b/ioctl/newcmd/hdwallet/hdwalletimport_test.go @@ -0,0 +1,55 @@ +// Copyright (c) 2022 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided 'as is' and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package hdwallet + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/iotexproject/iotex-core/ioctl/config" + "github.com/iotexproject/iotex-core/ioctl/util" + "github.com/iotexproject/iotex-core/test/mock/mock_ioctlclient" +) + +func TestNewHdwalletImportCmd(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + client := mock_ioctlclient.NewMockClient(ctrl) + + mnemonic := "lake stove quarter shove dry matrix hire split wide attract argue core" + password := "123" + + client.EXPECT().SelectTranslation(gomock.Any()).Return("mockTranslationString", config.English).Times(6) + client.EXPECT().IsHdWalletConfigFileExist().Return(false).Times(2) + + t.Run("import hdwallet", func(t *testing.T) { + client.EXPECT().ReadInput().Return(mnemonic, nil) + client.EXPECT().ReadSecret().Return(password, nil) + client.EXPECT().ReadSecret().Return(password, nil) + client.EXPECT().WriteHdWalletConfigFile(gomock.Any(), gomock.Any()).Return(nil) + + cmd := NewHdwalletImportCmd(client) + result, err := util.ExecuteCmd(cmd) + require.NoError(err) + require.Contains(result, mnemonic) + }) + + t.Run("failed to write to config file", func(t *testing.T) { + expectErr := errors.New("failed to write to config file") + client.EXPECT().ReadInput().Return(mnemonic, nil) + client.EXPECT().ReadSecret().Return(password, nil) + client.EXPECT().ReadSecret().Return(password, nil) + client.EXPECT().WriteHdWalletConfigFile(gomock.Any(), gomock.Any()).Return(expectErr) + + cmd := NewHdwalletImportCmd(client) + _, err := util.ExecuteCmd(cmd) + require.Contains(err.Error(), expectErr.Error()) + }) +} diff --git a/test/mock/mock_ioctlclient/mock_ioctlclient.go b/test/mock/mock_ioctlclient/mock_ioctlclient.go index 4d6526f319..fe20c71849 100644 --- a/test/mock/mock_ioctlclient/mock_ioctlclient.go +++ b/test/mock/mock_ioctlclient/mock_ioctlclient.go @@ -183,6 +183,20 @@ func (mr *MockClientMockRecorder) IsCryptoSm2() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCryptoSm2", reflect.TypeOf((*MockClient)(nil).IsCryptoSm2)) } +// IsHdWalletConfigFileExist mocks base method. +func (m *MockClient) IsHdWalletConfigFileExist() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsHdWalletConfigFileExist") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsHdWalletConfigFileExist indicates an expected call of IsHdWalletConfigFileExist. +func (mr *MockClientMockRecorder) IsHdWalletConfigFileExist() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsHdWalletConfigFileExist", reflect.TypeOf((*MockClient)(nil).IsHdWalletConfigFileExist)) +} + // NewKeyStore mocks base method. func (m *MockClient) NewKeyStore() *keystore.KeyStore { m.ctrl.T.Helper() @@ -212,6 +226,21 @@ func (mr *MockClientMockRecorder) QueryAnalyser(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAnalyser", reflect.TypeOf((*MockClient)(nil).QueryAnalyser), arg0) } +// ReadInput mocks base method. +func (m *MockClient) ReadInput() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadInput") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadInput indicates an expected call of ReadInput. +func (mr *MockClientMockRecorder) ReadInput() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadInput", reflect.TypeOf((*MockClient)(nil).ReadInput)) +} + // ReadSecret mocks base method. func (m *MockClient) ReadSecret() (string, error) { m.ctrl.T.Helper() @@ -333,3 +362,17 @@ func (mr *MockClientMockRecorder) WriteConfig() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteConfig", reflect.TypeOf((*MockClient)(nil).WriteConfig)) } + +// WriteHdWalletConfigFile mocks base method. +func (m *MockClient) WriteHdWalletConfigFile(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WriteHdWalletConfigFile", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// WriteHdWalletConfigFile indicates an expected call of WriteHdWalletConfigFile. +func (mr *MockClientMockRecorder) WriteHdWalletConfigFile(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteHdWalletConfigFile", reflect.TypeOf((*MockClient)(nil).WriteHdWalletConfigFile), arg0, arg1) +}