github.com/iotexproject/iotex-core@v1.14.1-rc1/ioctl/newcmd/account/account.go (about)

     1  // Copyright (c) 2019 IoTeX Foundation
     2  // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability
     3  // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed.
     4  // This source code is governed by Apache License 2.0 that can be found in the LICENSE file.
     5  
     6  package account
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"crypto/ecdsa"
    12  	"encoding/hex"
    13  	"fmt"
    14  	"os"
    15  	"path/filepath"
    16  	"strings"
    17  
    18  	ecrypto "github.com/ethereum/go-ethereum/crypto"
    19  	"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
    20  	"github.com/iotexproject/go-pkgs/crypto"
    21  	"github.com/iotexproject/go-pkgs/hash"
    22  	"github.com/iotexproject/iotex-address/address"
    23  	"github.com/iotexproject/iotex-proto/golang/iotexapi"
    24  	"github.com/iotexproject/iotex-proto/golang/iotextypes"
    25  	"github.com/pkg/errors"
    26  	"github.com/spf13/cobra"
    27  	"google.golang.org/grpc/codes"
    28  	"google.golang.org/grpc/status"
    29  
    30  	"github.com/iotexproject/iotex-core/ioctl"
    31  	"github.com/iotexproject/iotex-core/ioctl/config"
    32  	"github.com/iotexproject/iotex-core/ioctl/newcmd/hdwallet"
    33  	"github.com/iotexproject/iotex-core/ioctl/util"
    34  	"github.com/iotexproject/iotex-core/ioctl/validator"
    35  )
    36  
    37  // Multi-language support
    38  var (
    39  	_accountCmdShorts = map[config.Language]string{
    40  		config.English: "Manage accounts of IoTeX blockchain",
    41  		config.Chinese: "管理IoTeX区块链上的账号",
    42  	}
    43  )
    44  
    45  // Errors
    46  var (
    47  	// ErrPasswdNotMatch indicates that the second input password is different from the first
    48  	ErrPasswdNotMatch = errors.New("password doesn't match")
    49  )
    50  
    51  // NewAccountCmd represents the account command
    52  func NewAccountCmd(client ioctl.Client) *cobra.Command {
    53  	accountShorts, _ := client.SelectTranslation(_accountCmdShorts)
    54  
    55  	ac := &cobra.Command{
    56  		Use:   "account",
    57  		Short: accountShorts,
    58  	}
    59  	ac.AddCommand(NewAccountCreate(client))
    60  	ac.AddCommand(NewAccountCreateAdd(client))
    61  	ac.AddCommand(NewAccountDelete(client))
    62  	ac.AddCommand(NewAccountNonce(client))
    63  	ac.AddCommand(NewAccountList(client))
    64  	ac.AddCommand(NewAccountSign(client))
    65  	ac.AddCommand(NewAccountUpdate(client))
    66  	ac.AddCommand(NewAccountInfo(client))
    67  	ac.AddCommand(NewAccountVerify(client))
    68  	ac.AddCommand(NewAccountEthAddr(client))
    69  	ac.AddCommand(NewAccountExportPublic(client))
    70  	ac.AddCommand(NewAccountExport(client))
    71  	ac.AddCommand(NewAccountImportCmd(client))
    72  	ac.AddCommand(NewAccountBalance(client))
    73  
    74  	client.SetEndpointWithFlag(ac.PersistentFlags().StringVar)
    75  	client.SetInsecureWithFlag(ac.PersistentFlags().BoolVar)
    76  	return ac
    77  }
    78  
    79  // Sign sign message with signer
    80  func Sign(client ioctl.Client, cmd *cobra.Command, signer, password, message string) (string, error) {
    81  	pri, err := PrivateKeyFromSigner(client, cmd, signer, password)
    82  	if err != nil {
    83  		return "", err
    84  	}
    85  	mes := message
    86  	head := message[:2]
    87  	if strings.EqualFold(head, "0x") {
    88  		mes = message[2:]
    89  	}
    90  	b, err := hex.DecodeString(mes)
    91  	if err != nil {
    92  		return "", err
    93  	}
    94  	prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(b))
    95  	msg := append([]byte(prefix), b...)
    96  	mesToSign := hash.Hash256b(msg)
    97  	ret, err := pri.Sign(mesToSign[:])
    98  	if err != nil {
    99  		return "", err
   100  	}
   101  	return hex.EncodeToString(ret), nil
   102  }
   103  
   104  // keyStoreAccountToPrivateKey generates our PrivateKey interface from Keystore account
   105  func keyStoreAccountToPrivateKey(client ioctl.Client, signer, password string) (crypto.PrivateKey, error) {
   106  	addrString, err := client.Address(signer)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	addr, err := address.FromString(addrString)
   111  	if err != nil {
   112  		return nil, fmt.Errorf("invalid account #%s, addr %s", signer, addrString)
   113  	}
   114  
   115  	if client.IsCryptoSm2() {
   116  		// find the account in pem files
   117  		pemFilePath := sm2KeyPath(client, addr)
   118  		prvKey, err := crypto.ReadPrivateKeyFromPem(pemFilePath, password)
   119  		if err == nil {
   120  			return prvKey, nil
   121  		}
   122  	} else {
   123  		// find the account in keystore
   124  		ks := client.NewKeyStore()
   125  		for _, account := range ks.Accounts() {
   126  			if bytes.Equal(addr.Bytes(), account.Address.Bytes()) {
   127  				return crypto.KeystoreToPrivateKey(account, password)
   128  			}
   129  		}
   130  	}
   131  	return nil, fmt.Errorf("account #%s does not match all local keys", signer)
   132  }
   133  
   134  // PrivateKeyFromSigner returns private key from signer
   135  func PrivateKeyFromSigner(client ioctl.Client, cmd *cobra.Command, signer, password string) (crypto.PrivateKey, error) {
   136  	var (
   137  		prvKey crypto.PrivateKey
   138  		err    error
   139  	)
   140  
   141  	if !IsSignerExist(client, signer) && !util.AliasIsHdwalletKey(signer) {
   142  		return nil, fmt.Errorf("invalid address #%s", signer)
   143  	}
   144  
   145  	if password == "" {
   146  		cmd.Printf("Enter password for #%s:\n", signer)
   147  		password, err = client.ReadSecret()
   148  		if err != nil {
   149  			return nil, errors.Wrap(err, "failed to get password")
   150  		}
   151  	}
   152  
   153  	if util.AliasIsHdwalletKey(signer) {
   154  		account, change, index, err := util.ParseHdwPath(signer)
   155  		if err != nil {
   156  			return nil, errors.Wrap(err, "invalid HDWallet key format")
   157  		}
   158  		_, prvKey, err = hdwallet.DeriveKey(client, account, change, index, password)
   159  		if err != nil {
   160  			return nil, errors.Wrap(err, "failed to derive key from HDWallet")
   161  		}
   162  		return prvKey, nil
   163  	}
   164  	return keyStoreAccountToPrivateKey(client, signer, password)
   165  }
   166  
   167  // Meta gets account metadata
   168  func Meta(client ioctl.Client, addr string) (*iotextypes.AccountMeta, error) {
   169  	apiServiceClient, err := client.APIServiceClient()
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  
   174  	ctx := context.Background()
   175  	jwtMD, err := util.JwtAuth()
   176  	if err == nil {
   177  		ctx = metautils.NiceMD(jwtMD).ToOutgoing(ctx)
   178  	}
   179  
   180  	response, err := apiServiceClient.GetAccount(ctx, &iotexapi.GetAccountRequest{Address: addr})
   181  	if err != nil {
   182  		if sta, ok := status.FromError(err); ok {
   183  			if sta.Code() == codes.Unavailable {
   184  				return nil, ioctl.ErrInvalidEndpointOrInsecure
   185  			}
   186  			return nil, errors.New(sta.Message())
   187  		}
   188  		return nil, errors.Wrap(err, "failed to invoke GetAccount api")
   189  	}
   190  	return response.AccountMeta, nil
   191  }
   192  
   193  // IsSignerExist checks whether signer account is existed
   194  func IsSignerExist(client ioctl.Client, signer string) bool {
   195  	addr, err := address.FromString(signer)
   196  	if err != nil {
   197  		return false
   198  	}
   199  
   200  	if client.IsCryptoSm2() {
   201  		// find the account in pem files
   202  		_, err = findSm2PemFile(client, addr)
   203  		return err == nil
   204  	}
   205  
   206  	// find the account in keystore
   207  	ks := client.NewKeyStore()
   208  	for _, ksAccount := range ks.Accounts() {
   209  		if address.Equal(addr, ksAccount.Address) {
   210  			return true
   211  		}
   212  	}
   213  
   214  	return false
   215  }
   216  
   217  func newAccount(client ioctl.Client, cmd *cobra.Command, alias string) (string, error) {
   218  	cmd.Printf("#%s: Set password\n", alias)
   219  	password, err := client.ReadSecret()
   220  	if err != nil {
   221  		return "", errors.Wrap(err, "failed to get password")
   222  	}
   223  	cmd.Printf("#%s: Enter password again\n", alias)
   224  	passwordAgain, err := client.ReadSecret()
   225  	if err != nil {
   226  		return "", errors.Wrap(err, "failed to get password")
   227  	}
   228  	if password != passwordAgain {
   229  		return "", ErrPasswdNotMatch
   230  	}
   231  	ks := client.NewKeyStore()
   232  	account, err := ks.NewAccount(password)
   233  	if err != nil {
   234  		return "", errors.Wrap(err, "failed to create new keystore")
   235  	}
   236  	addr, err := address.FromBytes(account.Address.Bytes())
   237  	if err != nil {
   238  		return "", errors.Wrap(err, "failed to convert bytes into address")
   239  	}
   240  	return addr.String(), nil
   241  }
   242  
   243  func newAccountSm2(client ioctl.Client, cmd *cobra.Command, alias string) (string, error) {
   244  	cmd.Printf("#%s: Set password\n", alias)
   245  	password, err := client.ReadSecret()
   246  	if err != nil {
   247  		return "", errors.Wrap(err, "failed to get password")
   248  	}
   249  	cmd.Printf("#%s: Enter password again\n", alias)
   250  	passwordAgain, err := client.ReadSecret()
   251  	if err != nil {
   252  		return "", errors.Wrap(err, "failed to get password")
   253  	}
   254  	if password != passwordAgain {
   255  		return "", ErrPasswdNotMatch
   256  	}
   257  	priKey, err := crypto.GenerateKeySm2()
   258  	if err != nil {
   259  		return "", errors.Wrap(err, "failed to generate sm2 private key")
   260  	}
   261  
   262  	addr := priKey.PublicKey().Address()
   263  	if addr == nil {
   264  		return "", errors.New("failed to convert bytes into address")
   265  	}
   266  
   267  	pemFilePath := sm2KeyPath(client, addr)
   268  	if err := crypto.WritePrivateKeyToPem(pemFilePath, priKey.(*crypto.P256sm2PrvKey), password); err != nil {
   269  		return "", errors.Wrap(err, "failed to save private key into pem file ")
   270  	}
   271  
   272  	return addr.String(), nil
   273  }
   274  
   275  func newAccountByKey(client ioctl.Client, cmd *cobra.Command, alias string, privateKey string) (string, error) {
   276  	cmd.Printf("#%s: Set password\n", alias)
   277  	password, err := client.ReadSecret()
   278  	if err != nil {
   279  		return "", errors.Wrap(err, "failed to get password")
   280  	}
   281  	cmd.Printf("#%s: Enter password again\n", alias)
   282  	passwordAgain, err := client.ReadSecret()
   283  	if err != nil {
   284  		return "", errors.Wrap(err, "failed to get password")
   285  	}
   286  	if password != passwordAgain {
   287  		return "", ErrPasswdNotMatch
   288  	}
   289  
   290  	return storeKey(client, privateKey, password)
   291  }
   292  
   293  func newAccountByKeyStore(client ioctl.Client, cmd *cobra.Command, alias, passwordOfKeyStore, keyStorePath string) (string, error) {
   294  	privateKey, err := client.DecryptPrivateKey(passwordOfKeyStore, keyStorePath)
   295  	if err != nil {
   296  		return "", err
   297  	}
   298  	return newAccountByKey(client, cmd, alias, hex.EncodeToString(ecrypto.FromECDSA(privateKey)))
   299  }
   300  
   301  func newAccountByPem(client ioctl.Client, cmd *cobra.Command, alias, passwordOfPem, pemFilePath string) (string, error) {
   302  	prvKey, err := crypto.ReadPrivateKeyFromPem(pemFilePath, passwordOfPem)
   303  	if err != nil {
   304  		return "", errors.Wrap(err, "failed to read private key from pem file")
   305  	}
   306  
   307  	return newAccountByKey(client, cmd, alias, prvKey.HexString())
   308  }
   309  
   310  func storeKey(client ioctl.Client, privateKey, password string) (string, error) {
   311  	priKey, err := crypto.HexStringToPrivateKey(privateKey)
   312  	if err != nil {
   313  		return "", errors.Wrap(err, "failed to generate private key from hex string ")
   314  	}
   315  	defer priKey.Zero()
   316  
   317  	addr := priKey.PublicKey().Address()
   318  	if addr == nil {
   319  		return "", errors.New("failed to convert bytes into address")
   320  	}
   321  
   322  	switch sk := priKey.EcdsaPrivateKey().(type) {
   323  	case *ecdsa.PrivateKey:
   324  		ks := client.NewKeyStore()
   325  		if _, err := ks.ImportECDSA(sk, password); err != nil {
   326  			return "", errors.Wrap(err, "failed to import private key into keystore ")
   327  		}
   328  	case *crypto.P256sm2PrvKey:
   329  		pemFilePath := sm2KeyPath(client, addr)
   330  		if err := crypto.WritePrivateKeyToPem(pemFilePath, sk, password); err != nil {
   331  			return "", errors.Wrap(err, "failed to save private key into pem file ")
   332  		}
   333  	default:
   334  		return "", errors.New("invalid private key")
   335  	}
   336  
   337  	return addr.String(), nil
   338  }
   339  
   340  func sm2KeyPath(client ioctl.Client, addr address.Address) string {
   341  	return filepath.Join(client.Config().Wallet, "sm2sk-"+addr.String()+".pem")
   342  }
   343  
   344  func findSm2PemFile(client ioctl.Client, addr address.Address) (string, error) {
   345  	filePath := sm2KeyPath(client, addr)
   346  	_, err := os.Stat(filePath)
   347  	if err != nil {
   348  		return "", errors.Wrap(err, "crypto file not found")
   349  	}
   350  	return filePath, nil
   351  }
   352  
   353  func listSm2Account(client ioctl.Client) ([]string, error) {
   354  	sm2Accounts := make([]string, 0)
   355  	files, err := os.ReadDir(client.Config().Wallet)
   356  	if err != nil {
   357  		return nil, errors.Wrap(err, "failed to read files in wallet")
   358  	}
   359  	for _, f := range files {
   360  		if !f.IsDir() {
   361  			if strings.HasSuffix(f.Name(), ".pem") {
   362  				addr := strings.TrimSuffix(strings.TrimPrefix(f.Name(), "sm2sk-"), ".pem")
   363  				if err := validator.ValidateAddress(addr); err == nil {
   364  					sm2Accounts = append(sm2Accounts, addr)
   365  				}
   366  			}
   367  		}
   368  	}
   369  	return sm2Accounts, nil
   370  }