Tool for Decrypting/Encrypting EIP-2335 Keystores (#6900)

* begin keystore tool

* add bazel

* keystores tool

* fully decrypt

* implement encrypt

* complete encrypt and decrypt implementations

* finalize tool

* gaz

* fix viz

* help

Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com>
This commit is contained in:
Raul Jordan
2020-08-06 00:26:52 -05:00
committed by GitHub
parent 89bd5f3a5a
commit ec21316efd
4 changed files with 503 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_test")
load("@prysm//tools/go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "github.com/prysmaticlabs/prysm/tools/keystores",
visibility = ["//visibility:private"],
deps = [
"//shared/bls:go_default_library",
"//shared/params:go_default_library",
"//shared/promptutil:go_default_library",
"//validator/keymanager/v2:go_default_library",
"@com_github_google_uuid//:go_default_library",
"@com_github_logrusorgru_aurora//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
"@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library",
],
)
go_binary(
name = "keystores",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["main_test.go"],
embed = [":go_default_library"],
deps = [
"//shared/bls:go_default_library",
"//shared/params:go_default_library",
"//shared/testutil:go_default_library",
"//shared/testutil/assert:go_default_library",
"//shared/testutil/require:go_default_library",
"//validator/keymanager/v2:go_default_library",
"@com_github_google_uuid//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
"@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library",
],
)

308
tools/keystores/main.go Normal file
View File

@@ -0,0 +1,308 @@
// This tool allows for simple encrypting and decrypting of EIP-2335 compliant, BLS12-381
// keystore.json files which as password protected. This is helpful in development to inspect
// the contents of keystores created by eth2 wallets or to easily produce keystores from a
// specified secret to move them around in a standard format between eth2 clients.
package main
import (
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"os/user"
"path"
"path/filepath"
"strings"
"github.com/google/uuid"
"github.com/logrusorgru/aurora"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/shared/bls"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/promptutil"
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
"github.com/urfave/cli/v2"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
var (
keystoresFlag = &cli.StringFlag{
Name: "keystores",
Value: "",
Usage: "Path to a file or directory containing keystore files",
Required: true,
}
passwordFlag = &cli.StringFlag{
Name: "password",
Value: "",
Usage: "Password for the keystore(s)",
}
privateKeyFlag = &cli.StringFlag{
Name: "private-key",
Value: "",
Usage: "Hex string for the BLS12-381 private key you wish encrypt into a keystore file",
Required: true,
}
outputPathFlag = &cli.StringFlag{
Name: "output-path",
Value: "",
Usage: "Output path to write the newly encrypted keystore file",
Required: true,
}
au = aurora.NewAurora(true /* enable colors */)
)
func main() {
app := &cli.App{
Name: "Keystore utility",
Description: "Utility to encrypt and decrypt EIP-2335 compliant keystore.json files for BLS12-381 private keys",
Usage: "",
Commands: []*cli.Command{
{
Name: "decrypt",
Usage: "decrypt a specified keystore file or directory containing keystore files",
Flags: []cli.Flag{
keystoresFlag,
passwordFlag,
},
Action: decrypt,
},
{
Name: "encrypt",
Usage: "encrypt a specified hex value of a BLS12-381 private key into a keystore file",
Flags: []cli.Flag{
passwordFlag,
privateKeyFlag,
outputPathFlag,
},
Action: encrypt,
},
},
}
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}
func decrypt(cliCtx *cli.Context) error {
keystorePath := cliCtx.String(keystoresFlag.Name)
if keystorePath == "" {
return errors.New("--keystore must be set")
}
fullPath, err := expandPath(keystorePath)
if err != nil {
return errors.Wrapf(err, "could not expand path: %s", keystorePath)
}
password := cliCtx.String(passwordFlag.Name)
isPasswordSet := cliCtx.IsSet(passwordFlag.Name)
if !isPasswordSet {
password, err = promptutil.PasswordPrompt("Input the keystore(s) password", func(s string) error {
// Any password is valid.
return nil
})
}
isDir, err := hasDir(fullPath)
if err != nil {
return errors.Wrapf(err, "could not check if path exists: %s", fullPath)
}
if isDir {
files, err := ioutil.ReadDir(fullPath)
if err != nil {
return errors.Wrapf(err, "could not read directory: %s", fullPath)
}
for _, f := range files {
if f.IsDir() {
continue
}
keystorePath := filepath.Join(fullPath, f.Name())
if err := readAndDecryptKeystore(keystorePath, password); err != nil {
fmt.Printf("could not read nor decrypt keystore at path %s: %v\n", keystorePath, err)
}
}
return nil
}
return readAndDecryptKeystore(fullPath, password)
}
// Attempts to encrypt a passed-in BLS12-3381 private key into the EIP-2335
// keystore.json format. If a file at the specified output path exists, asks the user
// to confirm overwriting its contents. If the value passed in is not a valid BLS12-381
// private key, the function will fail.
func encrypt(cliCtx *cli.Context) error {
var err error
password := cliCtx.String(passwordFlag.Name)
isPasswordSet := cliCtx.IsSet(passwordFlag.Name)
if !isPasswordSet {
password, err = promptutil.PasswordPrompt("Input the keystore(s) password", func(s string) error {
// Any password is valid.
return nil
})
}
privateKeyString := cliCtx.String(privateKeyFlag.Name)
if privateKeyString == "" {
return errors.New("--private-key must not be empty")
}
outputPath := cliCtx.String(outputPathFlag.Name)
if outputPath == "" {
return errors.New("--output-path must be set")
}
fullPath, err := expandPath(outputPath)
if err != nil {
return errors.Wrapf(err, "could not expand path: %s", outputPath)
}
exists, err := fileExists(fullPath)
if err != nil {
return errors.Wrapf(err, "could not check if file exists: %s", fullPath)
}
if exists {
response, err := promptutil.ValidatePrompt(
fmt.Sprintf("file at path %s already exists, are you sure you want to overwrite it? [y/n]", fullPath),
func(s string) error {
input := strings.ToLower(s)
if input != "y" && input != "n" {
return errors.New("please confirm the above text")
}
return nil
},
)
if err != nil {
return errors.Wrap(err, "could not validate prompt confirmation")
}
if response == "n" {
return nil
}
}
if len(privateKeyString) > 2 && strings.Contains(privateKeyString, "0x") {
privateKeyString = privateKeyString[2:] // Strip the 0x prefix, if any.
}
bytesValue, err := hex.DecodeString(privateKeyString)
if err != nil {
return errors.Wrapf(err, "could not decode as hex string: %s", privateKeyString)
}
privKey, err := bls.SecretKeyFromBytes(bytesValue)
if err != nil {
return errors.Wrap(err, "not a valid BLS12-381 private key")
}
pubKey := fmt.Sprintf("%x", privKey.PublicKey().Marshal())
encryptor := keystorev4.New()
id, err := uuid.NewRandom()
if err != nil {
return errors.Wrap(err, "could not generate new random uuid")
}
cryptoFields, err := encryptor.Encrypt(bytesValue, password)
if err != nil {
return errors.Wrap(err, "could not encrypt into new keystore")
}
item := &v2keymanager.Keystore{
Crypto: cryptoFields,
ID: id.String(),
Version: encryptor.Version(),
Pubkey: pubKey,
Name: encryptor.Name(),
}
encodedFile, err := json.MarshalIndent(item, "", "\t")
if err != nil {
return errors.Wrap(err, "could not json marshal keystore")
}
if err := ioutil.WriteFile(fullPath, encodedFile, params.BeaconIoConfig().ReadWritePermissions); err != nil {
return errors.Wrapf(err, "could not write file at path: %s", fullPath)
}
fmt.Printf(
"\nWrote encrypted keystore file at path %s\n",
au.BrightMagenta(fullPath),
)
fmt.Printf("Pubkey: %s\n", au.BrightGreen(
fmt.Sprintf("%#x", privKey.PublicKey().Marshal()),
))
return nil
}
// Reads the keystore file at the provided path and attempts
// to decrypt it with the specified passwords.
func readAndDecryptKeystore(fullPath string, password string) error {
file, err := ioutil.ReadFile(fullPath)
if err != nil {
return errors.Wrapf(err, "could not read file at path: %s", fullPath)
}
decryptor := keystorev4.New()
keystoreFile := &v2keymanager.Keystore{}
if err := json.Unmarshal(file, keystoreFile); err != nil {
return errors.Wrap(err, "could not JSON unmarshal keystore file")
}
// We extract the validator signing private key from the keystore
// by utilizing the password.
privKeyBytes, err := decryptor.Decrypt(keystoreFile.Crypto, password)
if err != nil {
if strings.Contains(err.Error(), "invalid checksum") {
return fmt.Errorf("incorrect password for keystore at path: %s", fullPath)
}
return err
}
publicKeyBytes, err := hex.DecodeString(keystoreFile.Pubkey)
if err != nil {
return errors.Wrapf(err, "could not parse public key for keystore at path: %s", fullPath)
}
fmt.Printf("\nDecrypted keystore %s\n", au.BrightMagenta(fullPath))
fmt.Printf("Privkey: %#x\n", au.BrightGreen(privKeyBytes))
fmt.Printf("Pubkey: %#x\n", au.BrightGreen(publicKeyBytes))
return nil
}
// Checks if the item at the specified path exists and is a directory.
func hasDir(dirPath string) (bool, error) {
fullPath, err := expandPath(dirPath)
if err != nil {
return false, err
}
info, err := os.Stat(fullPath)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return info.IsDir(), nil
}
// Check if a file at the specified path exists.
func fileExists(filePath string) (bool, error) {
fullPath, err := expandPath(filePath)
if err != nil {
return false, err
}
if _, err := os.Stat(fullPath); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
// Expands a file path
// 1. replace tilde with users home dir
// 2. expands embedded environment variables
// 3. cleans the path, e.g. /a/b/../c -> /a/c
// Note, it has limitations, e.g. ~someuser/tmp will not be expanded
func expandPath(p string) (string, error) {
if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") {
if home := homeDir(); home != "" {
p = home + p[1:]
}
}
return filepath.Abs(path.Clean(os.ExpandEnv(p)))
}
func homeDir() string {
if home := os.Getenv("HOME"); home != "" {
return home
}
if usr, err := user.Current(); err == nil {
return usr.HomeDir
}
return ""
}

View File

@@ -0,0 +1,151 @@
package main
import (
"crypto/rand"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/uuid"
"github.com/prysmaticlabs/prysm/shared/bls"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/testutil"
"github.com/prysmaticlabs/prysm/shared/testutil/assert"
"github.com/prysmaticlabs/prysm/shared/testutil/require"
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
"github.com/urfave/cli/v2"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)
type cliConfig struct {
keystoresPath string
password string
privateKey string
outputPath string
}
func setupCliContext(
tb testing.TB,
conf *cliConfig,
) *cli.Context {
app := cli.App{}
set := flag.NewFlagSet("test", 0)
set.String(keystoresFlag.Name, conf.keystoresPath, "")
set.String(passwordFlag.Name, conf.password, "")
set.String(privateKeyFlag.Name, conf.privateKey, "")
set.String(outputPathFlag.Name, conf.outputPath, "")
assert.NoError(tb, set.Set(keystoresFlag.Name, conf.keystoresPath))
assert.NoError(tb, set.Set(passwordFlag.Name, conf.password))
assert.NoError(tb, set.Set(privateKeyFlag.Name, conf.privateKey))
assert.NoError(tb, set.Set(outputPathFlag.Name, conf.outputPath))
return cli.NewContext(&app, set, nil)
}
func createRandomKeystore(t testing.TB, password string) (*v2keymanager.Keystore, bls.SecretKey) {
encryptor := keystorev4.New()
id, err := uuid.NewRandom()
require.NoError(t, err)
validatingKey := bls.RandKey()
pubKey := validatingKey.PublicKey().Marshal()
cryptoFields, err := encryptor.Encrypt(validatingKey.Marshal(), password)
require.NoError(t, err)
return &v2keymanager.Keystore{
Crypto: cryptoFields,
Pubkey: fmt.Sprintf("%x", pubKey),
ID: id.String(),
Version: encryptor.Version(),
Name: encryptor.Name(),
}, validatingKey
}
func setupRandomDir(t testing.TB) string {
randPath, err := rand.Int(rand.Reader, big.NewInt(1000000))
require.NoError(t, err)
randDir := filepath.Join(testutil.TempDir(), fmt.Sprintf("/%d", randPath))
require.NoError(t, os.MkdirAll(randDir, os.ModePerm))
t.Cleanup(func() {
require.NoError(t, os.RemoveAll(randDir), "Failed to remove directory")
})
return randDir
}
func TestDecrypt(t *testing.T) {
keystoresDir := setupRandomDir(t)
password := "secretPassw0rd$1999"
keystore, privKey := createRandomKeystore(t, password)
// We write a random keystore to a keystores directory.
encodedKeystore, err := json.MarshalIndent(keystore, "", "\t")
require.NoError(t, err)
keystoreFilePath := filepath.Join(keystoresDir, "keystore.json")
require.NoError(t, ioutil.WriteFile(
keystoreFilePath, encodedKeystore, params.BeaconIoConfig().ReadWritePermissions),
)
cliCtx := setupCliContext(t, &cliConfig{
keystoresPath: keystoreFilePath,
password: password,
})
rescueStdout := os.Stdout
r, w, err := os.Pipe()
require.NoError(t, err)
os.Stdout = w
// We attempt to decrypt the keystore file we just wrote to disk.
require.NoError(t, decrypt(cliCtx))
require.NoError(t, w.Close())
out, err := ioutil.ReadAll(r)
require.NoError(t, err)
// We capture output from stdout.
os.Stdout = rescueStdout
stringOutput := string(out)
// We capture the results of stdout to check the public key and private keys
// were both printed to stdout.
assert.Equal(t, strings.Contains(stringOutput, keystore.Pubkey), true)
assert.Equal(t, strings.Contains(stringOutput, fmt.Sprintf("%#x", privKey.Marshal())), true)
}
func TestEncrypt(t *testing.T) {
keystoresDir := setupRandomDir(t)
password := "secretPassw0rd$1999"
keystoreFilePath := filepath.Join(keystoresDir, "keystore.json")
privKey := bls.RandKey()
cliCtx := setupCliContext(t, &cliConfig{
outputPath: keystoreFilePath,
password: password,
privateKey: fmt.Sprintf("%#x", privKey.Marshal()),
})
rescueStdout := os.Stdout
r, w, err := os.Pipe()
require.NoError(t, err)
os.Stdout = w
// We attempt to encrypt the secret key and save it to the output path.
require.NoError(t, encrypt(cliCtx))
require.NoError(t, w.Close())
out, err := ioutil.ReadAll(r)
require.NoError(t, err)
// We capture output from stdout.
os.Stdout = rescueStdout
stringOutput := string(out)
// We capture the results of stdout to check the public key was printed to stdout.
assert.Equal(
t,
strings.Contains(stringOutput, fmt.Sprintf("%x", privKey.PublicKey().Marshal())),
true,
)
}