github.com/prysmaticlabs/prysm@v1.4.4/tools/keystores/main.go (about)

     1  // This tool allows for simple encrypting and decrypting of EIP-2335 compliant, BLS12-381
     2  // keystore.json files which as password protected. This is helpful in development to inspect
     3  // the contents of keystores created by Ethereum validator wallets or to easily produce keystores from a
     4  // specified secret to move them around in a standard format between Ethereum consensus clients.
     5  package main
     6  
     7  import (
     8  	"encoding/hex"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"log"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  
    17  	"github.com/google/uuid"
    18  	"github.com/logrusorgru/aurora"
    19  	"github.com/pkg/errors"
    20  	"github.com/prysmaticlabs/prysm/shared/bls"
    21  	"github.com/prysmaticlabs/prysm/shared/fileutil"
    22  	"github.com/prysmaticlabs/prysm/shared/promptutil"
    23  	"github.com/prysmaticlabs/prysm/validator/keymanager"
    24  	"github.com/urfave/cli/v2"
    25  	keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
    26  )
    27  
    28  var (
    29  	keystoresFlag = &cli.StringFlag{
    30  		Name:     "keystores",
    31  		Value:    "",
    32  		Usage:    "Path to a file or directory containing keystore files",
    33  		Required: true,
    34  	}
    35  	passwordFlag = &cli.StringFlag{
    36  		Name:  "password",
    37  		Value: "",
    38  		Usage: "Password for the keystore(s)",
    39  	}
    40  	privateKeyFlag = &cli.StringFlag{
    41  		Name:     "private-key",
    42  		Value:    "",
    43  		Usage:    "Hex string for the BLS12-381 private key you wish encrypt into a keystore file",
    44  		Required: true,
    45  	}
    46  	outputPathFlag = &cli.StringFlag{
    47  		Name:     "output-path",
    48  		Value:    "",
    49  		Usage:    "Output path to write the newly encrypted keystore file",
    50  		Required: true,
    51  	}
    52  	au = aurora.NewAurora(true /* enable colors */)
    53  )
    54  
    55  func main() {
    56  	app := &cli.App{
    57  		Name:        "Keystore utility",
    58  		Description: "Utility to encrypt and decrypt EIP-2335 compliant keystore.json files for BLS12-381 private keys",
    59  		Usage:       "",
    60  		Commands: []*cli.Command{
    61  			{
    62  				Name:  "decrypt",
    63  				Usage: "decrypt a specified keystore file or directory containing keystore files",
    64  				Flags: []cli.Flag{
    65  					keystoresFlag,
    66  					passwordFlag,
    67  				},
    68  				Action: decrypt,
    69  			},
    70  			{
    71  				Name:  "encrypt",
    72  				Usage: "encrypt a specified hex value of a BLS12-381 private key into a keystore file",
    73  				Flags: []cli.Flag{
    74  					passwordFlag,
    75  					privateKeyFlag,
    76  					outputPathFlag,
    77  				},
    78  				Action: encrypt,
    79  			},
    80  		},
    81  	}
    82  	err := app.Run(os.Args)
    83  	if err != nil {
    84  		log.Fatal(err)
    85  	}
    86  }
    87  
    88  func decrypt(cliCtx *cli.Context) error {
    89  	keystorePath := cliCtx.String(keystoresFlag.Name)
    90  	if keystorePath == "" {
    91  		return errors.New("--keystore must be set")
    92  	}
    93  	fullPath, err := fileutil.ExpandPath(keystorePath)
    94  	if err != nil {
    95  		return errors.Wrapf(err, "could not expand path: %s", keystorePath)
    96  	}
    97  	password := cliCtx.String(passwordFlag.Name)
    98  	isPasswordSet := cliCtx.IsSet(passwordFlag.Name)
    99  	if !isPasswordSet {
   100  		password, err = promptutil.PasswordPrompt("Input the keystore(s) password", func(s string) error {
   101  			// Any password is valid.
   102  			return nil
   103  		})
   104  		if err != nil {
   105  			return err
   106  		}
   107  	}
   108  	isDir, err := fileutil.HasDir(fullPath)
   109  	if err != nil {
   110  		return errors.Wrapf(err, "could not check if path exists: %s", fullPath)
   111  	}
   112  	if isDir {
   113  		files, err := ioutil.ReadDir(fullPath)
   114  		if err != nil {
   115  			return errors.Wrapf(err, "could not read directory: %s", fullPath)
   116  		}
   117  		for _, f := range files {
   118  			if f.IsDir() {
   119  				continue
   120  			}
   121  			keystorePath := filepath.Join(fullPath, f.Name())
   122  			if err := readAndDecryptKeystore(keystorePath, password); err != nil {
   123  				fmt.Printf("could not read nor decrypt keystore at path %s: %v\n", keystorePath, err)
   124  			}
   125  		}
   126  		return nil
   127  	}
   128  	return readAndDecryptKeystore(fullPath, password)
   129  }
   130  
   131  // Attempts to encrypt a passed-in BLS12-3381 private key into the EIP-2335
   132  // keystore.json format. If a file at the specified output path exists, asks the user
   133  // to confirm overwriting its contents. If the value passed in is not a valid BLS12-381
   134  // private key, the function will fail.
   135  func encrypt(cliCtx *cli.Context) error {
   136  	var err error
   137  	password := cliCtx.String(passwordFlag.Name)
   138  	isPasswordSet := cliCtx.IsSet(passwordFlag.Name)
   139  	if !isPasswordSet {
   140  		password, err = promptutil.PasswordPrompt("Input the keystore(s) password", func(s string) error {
   141  			// Any password is valid.
   142  			return nil
   143  		})
   144  		if err != nil {
   145  			return err
   146  		}
   147  	}
   148  	privateKeyString := cliCtx.String(privateKeyFlag.Name)
   149  	if privateKeyString == "" {
   150  		return errors.New("--private-key must not be empty")
   151  	}
   152  	outputPath := cliCtx.String(outputPathFlag.Name)
   153  	if outputPath == "" {
   154  		return errors.New("--output-path must be set")
   155  	}
   156  	fullPath, err := fileutil.ExpandPath(outputPath)
   157  	if err != nil {
   158  		return errors.Wrapf(err, "could not expand path: %s", outputPath)
   159  	}
   160  	if fileutil.FileExists(fullPath) {
   161  		response, err := promptutil.ValidatePrompt(
   162  			os.Stdin,
   163  			fmt.Sprintf("file at path %s already exists, are you sure you want to overwrite it? [y/n]", fullPath),
   164  			func(s string) error {
   165  				input := strings.ToLower(s)
   166  				if input != "y" && input != "n" {
   167  					return errors.New("please confirm the above text")
   168  				}
   169  				return nil
   170  			},
   171  		)
   172  		if err != nil {
   173  			return errors.Wrap(err, "could not validate prompt confirmation")
   174  		}
   175  		if response == "n" {
   176  			return nil
   177  		}
   178  	}
   179  	if len(privateKeyString) > 2 && strings.Contains(privateKeyString, "0x") {
   180  		privateKeyString = privateKeyString[2:] // Strip the 0x prefix, if any.
   181  	}
   182  	bytesValue, err := hex.DecodeString(privateKeyString)
   183  	if err != nil {
   184  		return errors.Wrapf(err, "could not decode as hex string: %s", privateKeyString)
   185  	}
   186  	privKey, err := bls.SecretKeyFromBytes(bytesValue)
   187  	if err != nil {
   188  		return errors.Wrap(err, "not a valid BLS12-381 private key")
   189  	}
   190  	pubKey := fmt.Sprintf("%x", privKey.PublicKey().Marshal())
   191  	encryptor := keystorev4.New()
   192  	id, err := uuid.NewRandom()
   193  	if err != nil {
   194  		return errors.Wrap(err, "could not generate new random uuid")
   195  	}
   196  	cryptoFields, err := encryptor.Encrypt(bytesValue, password)
   197  	if err != nil {
   198  		return errors.Wrap(err, "could not encrypt into new keystore")
   199  	}
   200  	item := &keymanager.Keystore{
   201  		Crypto:  cryptoFields,
   202  		ID:      id.String(),
   203  		Version: encryptor.Version(),
   204  		Pubkey:  pubKey,
   205  		Name:    encryptor.Name(),
   206  	}
   207  	encodedFile, err := json.MarshalIndent(item, "", "\t")
   208  	if err != nil {
   209  		return errors.Wrap(err, "could not json marshal keystore")
   210  	}
   211  	if err := fileutil.WriteFile(fullPath, encodedFile); err != nil {
   212  		return errors.Wrapf(err, "could not write file at path: %s", fullPath)
   213  	}
   214  	fmt.Printf(
   215  		"\nWrote encrypted keystore file at path %s\n",
   216  		au.BrightMagenta(fullPath),
   217  	)
   218  	fmt.Printf("Pubkey: %s\n", au.BrightGreen(
   219  		fmt.Sprintf("%#x", privKey.PublicKey().Marshal()),
   220  	))
   221  	return nil
   222  }
   223  
   224  // Reads the keystore file at the provided path and attempts
   225  // to decrypt it with the specified passwords.
   226  func readAndDecryptKeystore(fullPath, password string) error {
   227  	file, err := ioutil.ReadFile(fullPath)
   228  	if err != nil {
   229  		return errors.Wrapf(err, "could not read file at path: %s", fullPath)
   230  	}
   231  	decryptor := keystorev4.New()
   232  	keystoreFile := &keymanager.Keystore{}
   233  
   234  	if err := json.Unmarshal(file, keystoreFile); err != nil {
   235  		return errors.Wrap(err, "could not JSON unmarshal keystore file")
   236  	}
   237  	// We extract the validator signing private key from the keystore
   238  	// by utilizing the password.
   239  	privKeyBytes, err := decryptor.Decrypt(keystoreFile.Crypto, password)
   240  	if err != nil {
   241  		if strings.Contains(err.Error(), "invalid checksum") {
   242  			return fmt.Errorf("incorrect password for keystore at path: %s", fullPath)
   243  		}
   244  		return err
   245  	}
   246  
   247  	var pubKeyBytes []byte
   248  	// Attempt to use the pubkey present in the keystore itself as a field. If unavailable,
   249  	// then utilize the public key directly from the private key.
   250  	if keystoreFile.Pubkey != "" {
   251  		pubKeyBytes, err = hex.DecodeString(keystoreFile.Pubkey)
   252  		if err != nil {
   253  			return errors.Wrap(err, "could not decode pubkey from keystore")
   254  		}
   255  	} else {
   256  		privKey, err := bls.SecretKeyFromBytes(privKeyBytes)
   257  		if err != nil {
   258  			return errors.Wrap(err, "could not initialize private key from bytes")
   259  		}
   260  		pubKeyBytes = privKey.PublicKey().Marshal()
   261  	}
   262  	fmt.Printf("\nDecrypted keystore %s\n", au.BrightMagenta(fullPath))
   263  	fmt.Printf("Privkey: %#x\n", au.BrightGreen(privKeyBytes))
   264  	fmt.Printf("Pubkey: %#x\n", au.BrightGreen(pubKeyBytes))
   265  	return nil
   266  }