diff --git a/pkg/operator/keys.go b/pkg/operator/keys.go index 021f8e8..a0d1b9b 100644 --- a/pkg/operator/keys.go +++ b/pkg/operator/keys.go @@ -14,6 +14,7 @@ func KeysCmd(p utils.Prompter) *cli.Command { keys.CreateCmd(p), keys.ListCmd(), keys.ImportCmd(p), + keys.ExportCmd(p), }, } diff --git a/pkg/operator/keys/export.go b/pkg/operator/keys/export.go new file mode 100644 index 0000000..635580a --- /dev/null +++ b/pkg/operator/keys/export.go @@ -0,0 +1,134 @@ +package keys + +import ( + "encoding/hex" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/Layr-Labs/eigenlayer-cli/pkg/utils" + "github.com/Layr-Labs/eigensdk-go/crypto/bls" + "github.com/Layr-Labs/eigensdk-go/crypto/ecdsa" + "github.com/urfave/cli/v2" +) + +func ExportCmd(p utils.Prompter) *cli.Command { + exportCmd := &cli.Command{ + Name: "export", + Usage: "Used to export existing keys from local keystore", + UsageText: "export --key-type [flags] [keyname]", + Description: `Used to export ecdsa and bls key from local keystore + +keyname - This will be the name of the key to be imported. If the path of keys is +different from default path created by "create"/"import" command, then provide the +full path using --key-path flag. + +If both keyname is provided and --key-path flag is provided, then keyname will be used. + +use --key-type ecdsa/bls to export ecdsa/bls key. +- ecdsa - exported key should be plaintext hex encoded private key +- bls - exported key should be plaintext bls private key + +It will prompt for password to encrypt the key. + +This command will import keys from $HOME/.eigenlayer/operator_keys/ location + +But if you want it to export from a different location, use --key-path flag`, + + Flags: []cli.Flag{ + &KeyTypeFlag, + &KeyPathFlag, + }, + Action: func(c *cli.Context) error { + keyType := c.String(KeyTypeFlag.Name) + + keyName := c.Args().Get(0) + if err := validateKeyName(keyName); err != nil { + return err + } + + keyPath := c.String(KeyPathFlag.Name) + if len(keyPath) == 0 && len(keyName) == 0 { + return errors.New("one of keyname or --key-path is required") + } + + if len(keyPath) > 0 && len(keyName) > 0 { + return errors.New("keyname and --key-path both are provided. Please provide only one") + } + + filePath, err := getKeyPath(keyPath, keyName, keyType) + if err != nil { + return err + } + + confirm, err := p.Confirm("This will show your private key. Are you sure you want to export?") + if err != nil { + return err + } + if !confirm { + return nil + } + + password, err := p.InputHiddenString("Enter password to decrypt the key", "", func(s string) error { + return nil + }) + if err != nil { + return err + } + fmt.Println("exporting key from: ", filePath) + + privateKey, err := getPrivateKey(keyType, filePath, password) + if err != nil { + return err + } + fmt.Println("Private key: ", privateKey) + return nil + }, + } + + return exportCmd +} + +func getPrivateKey(keyType string, filePath string, password string) (string, error) { + switch keyType { + case KeyTypeECDSA: + key, err := ecdsa.ReadKey(filePath, password) + if err != nil { + return "", err + } + return hex.EncodeToString(key.D.Bytes()), nil + case KeyTypeBLS: + key, err := bls.ReadPrivateKeyFromFile(filePath, password) + if err != nil { + return "", err + } + return key.PrivKey.String(), nil + default: + return "", ErrInvalidKeyType + } +} + +func getKeyPath(keyPath string, keyName string, keyType string) (string, error) { + homePath, err := os.UserHomeDir() + if err != nil { + return "", err + } + + var filePath string + if len(keyName) > 0 { + switch keyType { + case KeyTypeECDSA: + filePath = filepath.Join(homePath, OperatorKeystoreSubFolder, keyName+".ecdsa.key.json") + case KeyTypeBLS: + filePath = filepath.Join(homePath, OperatorKeystoreSubFolder, keyName+".bls.key.json") + default: + return "", ErrInvalidKeyType + } + + } else { + filePath = filepath.Clean(keyPath) + } + + return filePath, nil +} diff --git a/pkg/operator/keys/export_test.go b/pkg/operator/keys/export_test.go new file mode 100644 index 0000000..b3de12d --- /dev/null +++ b/pkg/operator/keys/export_test.go @@ -0,0 +1,55 @@ +package keys + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetKeyPath(t *testing.T) { + homePath, err := os.UserHomeDir() + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + keyType string + keyPath string + keyName string + err error + expectedPath string + }{ + { + name: "correct key path using keyname", + keyType: KeyTypeECDSA, + keyName: "test", + err: nil, + expectedPath: filepath.Join(homePath, OperatorKeystoreSubFolder, "test.ecdsa.key.json"), + }, + { + name: "correct key path using keypath", + keyType: KeyTypeECDSA, + keyPath: filepath.Join(homePath, "x.json"), + err: nil, + expectedPath: filepath.Join(homePath, "x.json"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path, err := getKeyPath(tt.keyPath, tt.keyName, tt.keyType) + if err != nil { + t.Fatal(err) + } + + if tt.err != nil { + assert.EqualError(t, err, tt.err.Error()) + } else { + assert.Equal(t, tt.expectedPath, path) + } + }) + } +} diff --git a/pkg/operator/keys/flags.go b/pkg/operator/keys/flags.go index 06b5577..523b889 100644 --- a/pkg/operator/keys/flags.go +++ b/pkg/operator/keys/flags.go @@ -17,4 +17,11 @@ var ( Usage: "Use this flag to skip password validation", EnvVars: []string{"INSECURE"}, } + + KeyPathFlag = cli.StringFlag{ + Name: "key-path", + Aliases: []string{"p"}, + Usage: "Use this flag to specify the path of the key", + EnvVars: []string{"KEY_PATH"}, + } )