From 4426c3279d7c3c74fcd06bcbc4e43914d514be6b Mon Sep 17 00:00:00 2001 From: Jim McDonald Date: Wed, 23 Mar 2022 22:06:48 +0000 Subject: [PATCH] Allow account import from keystores. --- CHANGELOG.md | 3 ++ cmd/account/import/input.go | 70 ++++++++++++++++++++++++++++------- cmd/account/import/process.go | 56 +++++++++++++++++++++++++++- cmd/accountimport.go | 8 ++++ cmd/version.go | 2 +- docs/usage.md | 7 ++++ 6 files changed, 131 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df5530e..0e23636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +1.19.1: + - add the ability to import keystores to ethdo wallets + 1.19.0: - add "epoch summary" diff --git a/cmd/account/import/input.go b/cmd/account/import/input.go index cceb40f..125f425 100644 --- a/cmd/account/import/input.go +++ b/cmd/account/import/input.go @@ -1,4 +1,4 @@ -// Copyright © 2019, 2020 Weald Technology Trading +// Copyright © 2019 - 2022 Weald Technology Trading. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -16,6 +16,7 @@ package accountimport import ( "context" "encoding/hex" + "io/ioutil" "strings" "time" @@ -27,12 +28,14 @@ import ( ) type dataIn struct { - timeout time.Duration - wallet e2wtypes.Wallet - key []byte - accountName string - passphrase string - walletPassphrase string + timeout time.Duration + wallet e2wtypes.Wallet + key []byte + accountName string + passphrase string + walletPassphrase string + keystore []byte + keystorePassphrase []byte } func input(ctx context.Context) (*dataIn, error) { @@ -74,14 +77,55 @@ func input(ctx context.Context) (*dataIn, error) { // Wallet passphrase. data.walletPassphrase = util.GetWalletPassphrase() - // Key. - if viper.GetString("key") == "" { - return nil, errors.New("key is required") + if viper.GetString("key") == "" && viper.GetString("keystore") == "" { + return nil, errors.New("key or keystore is required") } - data.key, err = hex.DecodeString(strings.TrimPrefix(viper.GetString("key"), "0x")) - if err != nil { - return nil, errors.Wrap(err, "key is malformed") + if viper.GetString("key") != "" && viper.GetString("keystore") != "" { + return nil, errors.New("only one of key and keystore is required") + } + + if viper.GetString("key") != "" { + data.key, err = hex.DecodeString(strings.TrimPrefix(viper.GetString("key"), "0x")) + if err != nil { + return nil, errors.Wrap(err, "key is malformed") + } + } + + if viper.GetString("keystore") != "" { + data.keystorePassphrase = []byte(viper.GetString("keystore-passphrase")) + if len(data.keystorePassphrase) == 0 { + return nil, errors.New("must supply keystore passphrase with keystore-passphrase when supplying keystore") + } + data.keystore, err = obtainKeystore(viper.GetString("keystore")) + if err != nil { + return nil, errors.Wrap(err, "invalid keystore") + } } return data, nil } + +// obtainKeystore obtains keystore from an input, could be JSON itself or a path to JSON. +func obtainKeystore(input string) ([]byte, error) { + var err error + var data []byte + // Input could be JSON or a path to JSON + if strings.HasPrefix(input, "{") { + // Looks like JSON + data = []byte(input) + } else { + // Assume it's a path to JSON + data, err = ioutil.ReadFile(input) + if err != nil { + return nil, errors.Wrap(err, "failed to find deposit data file") + } + } + return data, nil + // exitData := &util.ValidatorExitData{} + // err = json.Unmarshal(data, exitData) + // if err != nil { + // return nil, errors.Wrap(err, "data is not valid JSON") + // } + + // return exitData, nil +} diff --git a/cmd/account/import/process.go b/cmd/account/import/process.go index a4e5c64..e8189b3 100644 --- a/cmd/account/import/process.go +++ b/cmd/account/import/process.go @@ -1,4 +1,4 @@ -// Copyright © 2019, 2020 Weald Technology Trading +// Copyright © 2019 -2022 Weald Technology Trading // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -15,9 +15,14 @@ package accountimport import ( "context" + "fmt" "github.com/pkg/errors" "github.com/wealdtech/ethdo/util" + "github.com/wealdtech/go-ecodec" + keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" + nd "github.com/wealdtech/go-eth2-wallet-nd/v2" + scratch "github.com/wealdtech/go-eth2-wallet-store-scratch" e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2" ) @@ -43,6 +48,16 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) { }() } + if len(data.key) > 0 { + return processFromKey(ctx, data) + } + if len(data.keystore) > 0 { + return processFromKeystore(ctx, data) + } + return nil, errors.New("unsupported import mechanism") +} + +func processFromKey(ctx context.Context, data *dataIn) (*dataOut, error) { results := &dataOut{} account, err := data.wallet.(e2wtypes.WalletAccountImporter).ImportAccount(ctx, data.accountName, data.key, []byte(data.passphrase)) @@ -53,3 +68,42 @@ func process(ctx context.Context, data *dataIn) (*dataOut, error) { return results, nil } + +func processFromKeystore(ctx context.Context, data *dataIn) (*dataOut, error) { + // Need to import the keystore in to a temporary wallet to fetch the private key. + store := scratch.New() + encryptor := keystorev4.New() + + // Need to add a couple of fields to the keystore to make it compliant. + keystoreData := fmt.Sprintf(`{"name":"Import","encryptor":"keystore",%s`, string(data.keystore[1:])) + walletData := fmt.Sprintf(`{"wallet":{"name":"ImportTest","type":"non-deterministic","uuid":"e1526407-1dc7-4f3f-9d05-ab696f40707c","version":1},"accounts":[%s]}`, keystoreData) + encryptedData, err := ecodec.Encrypt([]byte(walletData), data.keystorePassphrase) + if err != nil { + return nil, err + } + wallet, err := nd.Import(ctx, encryptedData, data.keystorePassphrase, store, encryptor) + if err != nil { + return nil, errors.Wrap(err, "failed to import wallet") + } + + var account e2wtypes.Account + for account = range wallet.Accounts(ctx) { + // Only one account. + } + privateKeyProvider, isPrivateKeyProvider := account.(e2wtypes.AccountPrivateKeyProvider) + if !isPrivateKeyProvider { + return nil, errors.New("account does not provide its private key") + } + if locker, isLocker := account.(e2wtypes.AccountLocker); isLocker { + if err = locker.Unlock(ctx, data.keystorePassphrase); err != nil { + return nil, errors.Wrap(err, "failed to unlock account") + } + } + key, err := privateKeyProvider.PrivateKey(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain private key") + } + data.key = key.Marshal() + // We have the key from the keystore; import it. + return processFromKey(ctx, data) +} diff --git a/cmd/accountimport.go b/cmd/accountimport.go index 457b6f6..14ff9e1 100644 --- a/cmd/accountimport.go +++ b/cmd/accountimport.go @@ -48,10 +48,18 @@ func init() { accountCmd.AddCommand(accountImportCmd) accountFlags(accountImportCmd) accountImportCmd.Flags().String("key", "", "Private key of the account to import (0x...)") + accountImportCmd.Flags().String("keystore", "", "Keystore, or path to keystore ") + accountImportCmd.Flags().String("keystore-passphrase", "", "Passphrase of keystore") } func accountImportBindings() { if err := viper.BindPFlag("key", accountImportCmd.Flags().Lookup("key")); err != nil { panic(err) } + if err := viper.BindPFlag("keystore", accountImportCmd.Flags().Lookup("keystore")); err != nil { + panic(err) + } + if err := viper.BindPFlag("keystore-passphrase", accountImportCmd.Flags().Lookup("keystore-passphrase")); err != nil { + panic(err) + } } diff --git a/cmd/version.go b/cmd/version.go index fd7d96e..462c7cd 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -24,7 +24,7 @@ import ( // ReleaseVersion is the release version of the codebase. // Usually overridden by tag names when building binaries. -var ReleaseVersion = "local build (latest release 1.19.0)" +var ReleaseVersion = "local build (latest release 1.19.1)" // versionCmd represents the version command var versionCmd = &cobra.Command{ diff --git a/docs/usage.md b/docs/usage.md index 42820ab..b03f09f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -174,6 +174,13 @@ Public key: 0x99b1f1d84d76185466d86c34bde1101316afddae76217aa86cd066979b19858c2c $ ethdo account import --account=Validators/123 --key=6dd12d588d1c05ba40e80880ac7e894aa20babdbf16da52eae26b3f267d68032 --passphrase="my account secret" ``` +You can also import from an existing keystore such as those generated by the deposit CLI. For this you need the keystore and the keystore passphrase. For example: + +```sh +$ ethdo account import --account=Validators/123 --keystore=/path/to/keystore.json --keystore-passphrase="the keystore secret" --passphrase="my account secret" +``` +`--keystore` can either be the path to the keystore file, or the contents of the keystore file. + #### `info` `ethdo account info` provides information about the given account. Options include: