github.com/klaytn/klaytn@v1.12.1/cmd/utils/nodecmd/accountcmd.go (about)

     1  // Modifications Copyright 2018 The klaytn Authors
     2  // Copyright 2016 The go-ethereum Authors
     3  // This file is part of go-ethereum.
     4  //
     5  // go-ethereum is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // go-ethereum is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    13  // GNU General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU General Public License
    16  // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
    17  //
    18  // This file is derived from cmd/geth/accountcmd.go (2018/06/04).
    19  // Modified and improved for the klaytn development.
    20  
    21  package nodecmd
    22  
    23  import (
    24  	"encoding/hex"
    25  	"encoding/json"
    26  	"errors"
    27  	"fmt"
    28  	"io/fs"
    29  	"os"
    30  	"path/filepath"
    31  
    32  	"github.com/klaytn/klaytn/accounts"
    33  	"github.com/klaytn/klaytn/accounts/keystore"
    34  	"github.com/klaytn/klaytn/api/debug"
    35  	"github.com/klaytn/klaytn/cmd/utils"
    36  	"github.com/klaytn/klaytn/console"
    37  	"github.com/klaytn/klaytn/crypto"
    38  	"github.com/klaytn/klaytn/crypto/bls"
    39  	"github.com/klaytn/klaytn/log"
    40  	"github.com/klaytn/klaytn/node"
    41  	"github.com/urfave/cli/v2"
    42  )
    43  
    44  var AccountCommand = &cli.Command{
    45  	Name:     "account",
    46  	Usage:    "Manage accounts",
    47  	Category: "ACCOUNT COMMANDS",
    48  	Description: `
    49  Manage accounts, list all existing accounts, import a private key into a new
    50  account, create a new account or update an existing account.
    51  
    52  It supports interactive mode, when you are prompted for password as well as
    53  non-interactive mode where passwords are supplied via a given password file.
    54  Non-interactive mode is only meant for scripted use on test networks or known
    55  safe environments.
    56  
    57  Make sure you remember the password you gave when creating a new account (with
    58  either new or import). Without it you are not able to unlock your account.
    59  
    60  Note that exporting your key in unencrypted format is NOT supported.
    61  
    62  Keys are stored under <DATADIR>/keystore.
    63  It is safe to transfer the entire directory or the individual keys therein
    64  between klay nodes by simply copying.
    65  
    66  Make sure you backup your keys regularly.`,
    67  	Before: beforeAccountCmd,
    68  	Subcommands: []*cli.Command{
    69  		{
    70  			Name:   "list",
    71  			Usage:  "Print summary of existing accounts",
    72  			Action: accountList,
    73  			Flags: []cli.Flag{
    74  				utils.DataDirFlag,
    75  				utils.KeyStoreDirFlag,
    76  			},
    77  			Description: `
    78  Print a short summary of all accounts`,
    79  		},
    80  		{
    81  			Name:   "new",
    82  			Usage:  "Create a new account",
    83  			Action: accountCreate,
    84  			Flags: []cli.Flag{
    85  				utils.DataDirFlag,
    86  				utils.KeyStoreDirFlag,
    87  				utils.PasswordFileFlag,
    88  				utils.LightKDFFlag,
    89  			},
    90  			Description: `
    91  Creates a new account and prints the address.
    92  
    93  The account is saved in encrypted format, you are prompted for a passphrase.
    94  
    95  You must remember this passphrase to unlock your account in the future.
    96  
    97  For non-interactive use the passphrase can be specified with the --password flag:
    98  
    99  Note, this is meant to be used for testing only, it is a bad idea to save your
   100  password to file or expose in any other way.
   101  `,
   102  		},
   103  		{
   104  			Name:      "update",
   105  			Usage:     "Update an existing account",
   106  			Action:    accountUpdate,
   107  			ArgsUsage: "<address>",
   108  			Flags: []cli.Flag{
   109  				utils.DataDirFlag,
   110  				utils.KeyStoreDirFlag,
   111  				utils.LightKDFFlag,
   112  			},
   113  			Description: `
   114  Update an existing account.
   115  
   116  The account is saved in the newest version in encrypted format, you are prompted
   117  for a passphrase to unlock the account and another to save the updated file.
   118  
   119  This same command can therefore be used to migrate an account of a deprecated
   120  format to the newest format or change the password for an account.
   121  
   122  For non-interactive use the passphrase can be specified with the --password flag:
   123  
   124  Since only one password can be given, only format update can be performed,
   125  changing your password is only possible interactively.
   126  `,
   127  		},
   128  		{
   129  			Name:   "import",
   130  			Usage:  "Import a private key into a new account",
   131  			Action: accountImport,
   132  			Flags: []cli.Flag{
   133  				utils.DataDirFlag,
   134  				utils.KeyStoreDirFlag,
   135  				utils.PasswordFileFlag,
   136  				utils.LightKDFFlag,
   137  			},
   138  			ArgsUsage: "<keyFile>",
   139  			Description: `
   140  Imports an unencrypted private key from <keyfile> and creates a new account.
   141  Prints the address.
   142  
   143  The keyfile is assumed to contain an unencrypted private key in hexadecimal format.
   144  
   145  The account is saved in encrypted format, you are prompted for a passphrase.
   146  
   147  You must remember this passphrase to unlock your account in the future.
   148  
   149  For non-interactive use the passphrase can be specified with the --password flag:
   150  
   151  Note, as you can directly copy your encrypted accounts to another klay instance,
   152  this import mechanism is not needed when you transfer an account between
   153  nodes.
   154  `,
   155  		},
   156  		{
   157  			Name:      "bls-info",
   158  			Usage:     "Fetch BLS public key info of the running node",
   159  			ArgsUsage: "[endpoint]",
   160  			Action:    accountBlsInfo,
   161  			Flags: []cli.Flag{
   162  				utils.DataDirFlag,
   163  			},
   164  			Description: `
   165  Calculate BLS public key info (the public key and proof-of-possession)
   166  then saves to bls-publicinfo-NODEID.json.
   167  
   168  EXAMPLES
   169  
   170  # From the local node, by attaching to the default IPC endpoint DATADIR/klay.ipc.
   171  kcn account bls-info
   172  `,
   173  		},
   174  		{
   175  			Name:   "bls-import",
   176  			Usage:  "Import a BLS private key from an EIP-2335 keystore JSON",
   177  			Action: accountBlsImport,
   178  			Flags: []cli.Flag{
   179  				utils.DataDirFlag,
   180  				utils.BlsNodeKeystoreFileFlag,
   181  				utils.PasswordFileFlag,
   182  			},
   183  			Description: `
   184  Decrypt an EIP-2335 keystore and save the BLS secret key to default location (DATADIR/bls-nodekey).
   185  
   186  EXAMPLES
   187  
   188  kcn account bls-import --bls-nodekeystore bls-keystore.json
   189  `,
   190  		},
   191  		{
   192  			Name:   "bls-export",
   193  			Usage:  "Export a BLS private key to an EIP-2335 keystore JSON",
   194  			Action: accountBlsExport,
   195  			Flags: []cli.Flag{
   196  				utils.DataDirFlag,
   197  				utils.PasswordFileFlag,
   198  				utils.LightKDFFlag,
   199  			},
   200  			Description: `
   201  Export the BLS secret key from the default location (DATADIR/bls-nodekey)
   202  to an EIP-2335 keystore bls-keystore-NODEID.json.
   203  
   204  EXAMPLES
   205  
   206  kcn account bls-export
   207  `,
   208  		},
   209  	},
   210  }
   211  
   212  func beforeAccountCmd(ctx *cli.Context) error {
   213  	// Silence INFO logs from MakeConfigNode() or SetNodeConfig()
   214  	// Account commands are almost independent from the regular node operation,
   215  	// so INFO logs about networking or chain config are not necessary.
   216  	if glogger, err := debug.GetGlogger(); err == nil {
   217  		log.ChangeGlobalLogLevel(glogger, log.Lvl(log.LvlWarn))
   218  	}
   219  	return nil
   220  }
   221  
   222  func accountList(ctx *cli.Context) error {
   223  	stack, _ := utils.MakeConfigNode(ctx)
   224  	var index int
   225  	for _, wallet := range stack.AccountManager().Wallets() {
   226  		for _, account := range wallet.Accounts() {
   227  			fmt.Printf("Account #%d: {%x} %s\n", index, account.Address, &account.URL)
   228  			index++
   229  		}
   230  	}
   231  	return nil
   232  }
   233  
   234  // tries unlocking the specified account a few times.
   235  func UnlockAccount(ctx *cli.Context, ks *keystore.KeyStore, address string, i int, passwords []string) (accounts.Account, string) {
   236  	account, err := utils.MakeAddress(ks, address)
   237  	if err != nil {
   238  		log.Fatalf("Could not list accounts: %v", err)
   239  	}
   240  	for trials := 0; trials < 3; trials++ {
   241  		prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3)
   242  		password := getPassPhrase(prompt, false, i, passwords)
   243  		err = ks.Unlock(account, password)
   244  		if err == nil {
   245  			logger.Info("Unlocked account", "address", account.Address.Hex())
   246  			return account, password
   247  		}
   248  		if err, ok := err.(*keystore.AmbiguousAddrError); ok {
   249  			logger.Info("Unlocked account", "address", account.Address.Hex())
   250  			return ambiguousAddrRecovery(ks, err, password), password
   251  		}
   252  		if err != keystore.ErrDecrypt {
   253  			// No need to prompt again if the error is not decryption-related.
   254  			break
   255  		}
   256  	}
   257  	// All trials expended to unlock account, bail out
   258  	log.Fatalf("Failed to unlock account %s (%v)", address, err)
   259  
   260  	return accounts.Account{}, ""
   261  }
   262  
   263  // getPassPhrase retrieves the password associated with an account, either fetched
   264  // from a list of preloaded passphrases, or requested interactively from the user.
   265  func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) string {
   266  	// If a list of passwords was supplied, retrieve from them
   267  	if len(passwords) > 0 {
   268  		if i < len(passwords) {
   269  			return passwords[i]
   270  		}
   271  		return passwords[len(passwords)-1]
   272  	}
   273  	// Otherwise prompt the user for the password
   274  	if prompt != "" {
   275  		fmt.Println(prompt)
   276  	}
   277  	password, err := console.Stdin.PromptPassword("Passphrase: ")
   278  	if err != nil {
   279  		log.Fatalf("Failed to read passphrase: %v", err)
   280  	}
   281  	if confirmation {
   282  		confirm, err := console.Stdin.PromptPassword("Repeat passphrase: ")
   283  		if err != nil {
   284  			log.Fatalf("Failed to read passphrase confirmation: %v", err)
   285  		}
   286  		if password != confirm {
   287  			log.Fatalf("Passphrases do not match")
   288  		}
   289  	}
   290  	return password
   291  }
   292  
   293  func ambiguousAddrRecovery(ks *keystore.KeyStore, err *keystore.AmbiguousAddrError, auth string) accounts.Account {
   294  	fmt.Printf("Multiple key files exist for address %x:\n", err.Addr)
   295  	for _, a := range err.Matches {
   296  		fmt.Println("  ", a.URL)
   297  	}
   298  	fmt.Println("Testing your passphrase against all of them...")
   299  	var match *accounts.Account
   300  	for _, a := range err.Matches {
   301  		if err := ks.Unlock(a, auth); err == nil {
   302  			match = &a
   303  			break
   304  		}
   305  	}
   306  	if match == nil {
   307  		log.Fatalf("None of the listed files could be unlocked.")
   308  	}
   309  	fmt.Printf("Your passphrase unlocked %s\n", match.URL)
   310  	fmt.Println("In order to avoid this warning, you need to remove the following duplicate key files:")
   311  	for _, a := range err.Matches {
   312  		if a != *match {
   313  			fmt.Println("  ", a.URL)
   314  		}
   315  	}
   316  	return *match
   317  }
   318  
   319  // accountCreate creates a new account into the keystore defined by the CLI flags.
   320  func accountCreate(ctx *cli.Context) error {
   321  	cfg := utils.KlayConfig{Node: utils.DefaultNodeConfig()}
   322  	// Load config file.
   323  	if file := ctx.String(utils.ConfigFileFlag.Name); file != "" {
   324  		if err := utils.LoadConfig(file, &cfg); err != nil {
   325  			log.Fatalf("%v", err)
   326  		}
   327  	}
   328  	cfg.SetNodeConfig(ctx)
   329  	scryptN, scryptP, keydir, err := cfg.Node.AccountConfig()
   330  	if err != nil {
   331  		log.Fatalf("Failed to read configuration: %v", err)
   332  	}
   333  
   334  	password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
   335  
   336  	address, err := keystore.StoreKey(keydir, password, scryptN, scryptP)
   337  	if err != nil {
   338  		log.Fatalf("Failed to create account: %v", err)
   339  	}
   340  	fmt.Printf("Address: {%x}\n", address)
   341  	return nil
   342  }
   343  
   344  // accountUpdate transitions an account from a previous format to the current
   345  // one, also providing the possibility to change the pass-phrase.
   346  func accountUpdate(ctx *cli.Context) error {
   347  	if ctx.Args().Len() == 0 {
   348  		log.Fatalf("No accounts specified to update")
   349  	}
   350  	stack, _ := utils.MakeConfigNode(ctx)
   351  	ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
   352  
   353  	for _, addr := range ctx.Args().Slice() {
   354  		account, oldPassword := UnlockAccount(ctx, ks, addr, 0, nil)
   355  		newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil)
   356  		if err := ks.Update(account, oldPassword, newPassword); err != nil {
   357  			log.Fatalf("Could not update the account: %v", err)
   358  		}
   359  	}
   360  	return nil
   361  }
   362  
   363  func accountImport(ctx *cli.Context) error {
   364  	keyfile := ctx.Args().First()
   365  	if len(keyfile) == 0 {
   366  		log.Fatalf("keyfile must be given as argument")
   367  	}
   368  	key, err := crypto.LoadECDSA(keyfile)
   369  	if err != nil {
   370  		log.Fatalf("Failed to load the private key: %v", err)
   371  	}
   372  	stack, _ := utils.MakeConfigNode(ctx)
   373  	passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx))
   374  
   375  	ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore)
   376  	acct, err := ks.ImportECDSA(key, passphrase)
   377  	if err != nil {
   378  		log.Fatalf("Could not create the account: %v", err)
   379  	}
   380  	fmt.Printf("Address: {%x}\n", acct.Address)
   381  	if _acct, err := ks.Find(acct); err == nil {
   382  		fmt.Println("Your account is imported at", _acct.URL.Path)
   383  	}
   384  	return nil
   385  }
   386  
   387  func accountBlsInfo(ctx *cli.Context) error {
   388  	endpoint := rpcEndpoint(ctx)
   389  	client, err := dialRPC(endpoint)
   390  	if err != nil {
   391  		log.Fatalf("Unable to attach to remote node: %v", err)
   392  	}
   393  
   394  	var nodeInfo node.NodeInfoOutput
   395  	err = client.Call(&nodeInfo, "admin_nodeInfo", nil)
   396  	if err != nil {
   397  		log.Fatalf("Unable to fetch node info: %v", err)
   398  	}
   399  
   400  	infoJson, err := json.MarshalIndent(map[string]interface{}{
   401  		"address":          nodeInfo.NodeAddress,
   402  		"blsPublicKeyInfo": nodeInfo.BlsPublicKeyInfo,
   403  	}, "", "  ")
   404  	infoJson = append(infoJson, '\n')
   405  	if err != nil {
   406  		return err
   407  	}
   408  
   409  	name := fmt.Sprintf("bls-publicinfo-%s.json", nodeInfo.NodeAddress)
   410  	writeFile(name, infoJson, 0o644) // Ordinary non-secret text file permission.
   411  	return nil
   412  }
   413  
   414  func accountBlsImport(ctx *cli.Context) error {
   415  	// Load from keystore.json
   416  	if !ctx.IsSet(utils.BlsNodeKeystoreFileFlag.Name) {
   417  		return errors.New("no BLS keystore file specified")
   418  	}
   419  	blsPriv, err := loadBlsKeystore(ctx)
   420  	if err != nil {
   421  		return err
   422  	}
   423  	fmt.Printf("Importing BLS key: pub=%x\n", blsPriv.PublicKey().Marshal())
   424  
   425  	// Parse CLI arguments in the same way as running a node.
   426  	_, cfg := utils.MakeConfigNode(ctx)
   427  	path := cfg.Node.ResolvePath(node.DatadirBlsSecretKey)
   428  
   429  	// Write DATADIR/bls-nodekey
   430  	// Not using bls.SaveKey to prevent overwriting the existing file.
   431  	b := []byte(hex.EncodeToString(blsPriv.Marshal()))
   432  	writeFile(path, b, 0o600) // Secret file permission.
   433  	return nil
   434  }
   435  
   436  func accountBlsExport(ctx *cli.Context) error {
   437  	// Parse CLI arguments in the same way as running a node.
   438  	var (
   439  		_, cfg                 = utils.MakeConfigNode(ctx)
   440  		nodeKey                = cfg.Node.NodeKey()
   441  		blsPriv                = cfg.Node.BlsNodeKey()
   442  		scryptN, scryptP, _, _ = cfg.Node.AccountConfig()
   443  	)
   444  	fmt.Printf("Exporting BLS key: pub=%x\n", blsPriv.PublicKey().Marshal())
   445  
   446  	// Calculate filename from node address.
   447  	nodeAddr := crypto.PubkeyToAddress(nodeKey.PublicKey)
   448  	name := fmt.Sprintf("bls-keystore-%s.json", nodeAddr.Hex())
   449  
   450  	// Write bls-keystore-*.json
   451  	keystoreJson, err := makeBlsKeystore(ctx, blsPriv, scryptN, scryptP)
   452  	if err != nil {
   453  		return err
   454  	}
   455  	writeFile(name, keystoreJson, 0o600) // Secret file permission.
   456  	return nil
   457  }
   458  
   459  func loadBlsKeystore(ctx *cli.Context) (bls.SecretKey, error) {
   460  	file := ctx.String(utils.BlsNodeKeystoreFileFlag.Name)
   461  	content, err := os.ReadFile(file)
   462  	if err != nil {
   463  		return nil, err
   464  	}
   465  
   466  	storedPasswords := utils.MakePasswordList(ctx)
   467  	password := getPassPhrase("Enter the password",
   468  		false, 0, storedPasswords)
   469  
   470  	plainKeystore, err := keystore.DecryptKeyEIP2335(content, password)
   471  	if err != nil {
   472  		return nil, err
   473  	}
   474  	return plainKeystore.SecretKey, nil
   475  }
   476  
   477  func makeBlsKeystore(ctx *cli.Context, sk bls.SecretKey, scryptN, scryptP int) ([]byte, error) {
   478  	storedPasswords := utils.MakePasswordList(ctx)
   479  	password := getPassPhrase("Please give a new password. Do not forget this password.",
   480  		true, 0, storedPasswords)
   481  
   482  	plainKeystore := keystore.NewKeyEIP2335(sk)
   483  	return keystore.EncryptKeyEIP2335(plainKeystore, password, scryptN, scryptP)
   484  }
   485  
   486  func writeFile(path string, content []byte, perm fs.FileMode) {
   487  	if _, err := os.Stat(path); err == nil {
   488  		log.Fatalf("file '%s' already exists", path)
   489  	}
   490  	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
   491  		log.Fatalf("cannot write '%s': %v", path, err)
   492  	}
   493  	if err := os.WriteFile(path, content, perm); err != nil {
   494  		log.Fatalf("cannot write '%s': %v", path, err)
   495  	}
   496  	fmt.Printf("Successfully wrote '%s'\n", path)
   497  }