Skip to content

Commit ec21316

Browse files
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>
1 parent 89bd5f3 commit ec21316

File tree

4 files changed

+503
-0
lines changed

4 files changed

+503
-0
lines changed

tools/keystores/BUILD.bazel

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_test")
2+
load("@prysm//tools/go:def.bzl", "go_library")
3+
4+
go_library(
5+
name = "go_default_library",
6+
srcs = ["main.go"],
7+
importpath = "github.com/prysmaticlabs/prysm/tools/keystores",
8+
visibility = ["//visibility:private"],
9+
deps = [
10+
"//shared/bls:go_default_library",
11+
"//shared/params:go_default_library",
12+
"//shared/promptutil:go_default_library",
13+
"//validator/keymanager/v2:go_default_library",
14+
"@com_github_google_uuid//:go_default_library",
15+
"@com_github_logrusorgru_aurora//:go_default_library",
16+
"@com_github_pkg_errors//:go_default_library",
17+
"@com_github_urfave_cli_v2//:go_default_library",
18+
"@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library",
19+
],
20+
)
21+
22+
go_binary(
23+
name = "keystores",
24+
embed = [":go_default_library"],
25+
visibility = ["//visibility:public"],
26+
)
27+
28+
go_test(
29+
name = "go_default_test",
30+
srcs = ["main_test.go"],
31+
embed = [":go_default_library"],
32+
deps = [
33+
"//shared/bls:go_default_library",
34+
"//shared/params:go_default_library",
35+
"//shared/testutil:go_default_library",
36+
"//shared/testutil/assert:go_default_library",
37+
"//shared/testutil/require:go_default_library",
38+
"//validator/keymanager/v2:go_default_library",
39+
"@com_github_google_uuid//:go_default_library",
40+
"@com_github_urfave_cli_v2//:go_default_library",
41+
"@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library",
42+
],
43+
)

tools/keystores/main.go

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
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 eth2 wallets or to easily produce keystores from a
4+
// specified secret to move them around in a standard format between eth2 clients.
5+
package main
6+
7+
import (
8+
"encoding/hex"
9+
"encoding/json"
10+
"fmt"
11+
"io/ioutil"
12+
"log"
13+
"os"
14+
"os/user"
15+
"path"
16+
"path/filepath"
17+
"strings"
18+
19+
"github.com/google/uuid"
20+
"github.com/logrusorgru/aurora"
21+
"github.com/pkg/errors"
22+
"github.com/prysmaticlabs/prysm/shared/bls"
23+
"github.com/prysmaticlabs/prysm/shared/params"
24+
"github.com/prysmaticlabs/prysm/shared/promptutil"
25+
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
26+
"github.com/urfave/cli/v2"
27+
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
28+
)
29+
30+
var (
31+
keystoresFlag = &cli.StringFlag{
32+
Name: "keystores",
33+
Value: "",
34+
Usage: "Path to a file or directory containing keystore files",
35+
Required: true,
36+
}
37+
passwordFlag = &cli.StringFlag{
38+
Name: "password",
39+
Value: "",
40+
Usage: "Password for the keystore(s)",
41+
}
42+
privateKeyFlag = &cli.StringFlag{
43+
Name: "private-key",
44+
Value: "",
45+
Usage: "Hex string for the BLS12-381 private key you wish encrypt into a keystore file",
46+
Required: true,
47+
}
48+
outputPathFlag = &cli.StringFlag{
49+
Name: "output-path",
50+
Value: "",
51+
Usage: "Output path to write the newly encrypted keystore file",
52+
Required: true,
53+
}
54+
au = aurora.NewAurora(true /* enable colors */)
55+
)
56+
57+
func main() {
58+
app := &cli.App{
59+
Name: "Keystore utility",
60+
Description: "Utility to encrypt and decrypt EIP-2335 compliant keystore.json files for BLS12-381 private keys",
61+
Usage: "",
62+
Commands: []*cli.Command{
63+
{
64+
Name: "decrypt",
65+
Usage: "decrypt a specified keystore file or directory containing keystore files",
66+
Flags: []cli.Flag{
67+
keystoresFlag,
68+
passwordFlag,
69+
},
70+
Action: decrypt,
71+
},
72+
{
73+
Name: "encrypt",
74+
Usage: "encrypt a specified hex value of a BLS12-381 private key into a keystore file",
75+
Flags: []cli.Flag{
76+
passwordFlag,
77+
privateKeyFlag,
78+
outputPathFlag,
79+
},
80+
Action: encrypt,
81+
},
82+
},
83+
}
84+
err := app.Run(os.Args)
85+
if err != nil {
86+
log.Fatal(err)
87+
}
88+
}
89+
90+
func decrypt(cliCtx *cli.Context) error {
91+
keystorePath := cliCtx.String(keystoresFlag.Name)
92+
if keystorePath == "" {
93+
return errors.New("--keystore must be set")
94+
}
95+
fullPath, err := expandPath(keystorePath)
96+
if err != nil {
97+
return errors.Wrapf(err, "could not expand path: %s", keystorePath)
98+
}
99+
password := cliCtx.String(passwordFlag.Name)
100+
isPasswordSet := cliCtx.IsSet(passwordFlag.Name)
101+
if !isPasswordSet {
102+
password, err = promptutil.PasswordPrompt("Input the keystore(s) password", func(s string) error {
103+
// Any password is valid.
104+
return nil
105+
})
106+
}
107+
isDir, err := hasDir(fullPath)
108+
if err != nil {
109+
return errors.Wrapf(err, "could not check if path exists: %s", fullPath)
110+
}
111+
if isDir {
112+
files, err := ioutil.ReadDir(fullPath)
113+
if err != nil {
114+
return errors.Wrapf(err, "could not read directory: %s", fullPath)
115+
}
116+
for _, f := range files {
117+
if f.IsDir() {
118+
continue
119+
}
120+
keystorePath := filepath.Join(fullPath, f.Name())
121+
if err := readAndDecryptKeystore(keystorePath, password); err != nil {
122+
fmt.Printf("could not read nor decrypt keystore at path %s: %v\n", keystorePath, err)
123+
}
124+
}
125+
return nil
126+
}
127+
return readAndDecryptKeystore(fullPath, password)
128+
}
129+
130+
// Attempts to encrypt a passed-in BLS12-3381 private key into the EIP-2335
131+
// keystore.json format. If a file at the specified output path exists, asks the user
132+
// to confirm overwriting its contents. If the value passed in is not a valid BLS12-381
133+
// private key, the function will fail.
134+
func encrypt(cliCtx *cli.Context) error {
135+
var err error
136+
password := cliCtx.String(passwordFlag.Name)
137+
isPasswordSet := cliCtx.IsSet(passwordFlag.Name)
138+
if !isPasswordSet {
139+
password, err = promptutil.PasswordPrompt("Input the keystore(s) password", func(s string) error {
140+
// Any password is valid.
141+
return nil
142+
})
143+
}
144+
privateKeyString := cliCtx.String(privateKeyFlag.Name)
145+
if privateKeyString == "" {
146+
return errors.New("--private-key must not be empty")
147+
}
148+
outputPath := cliCtx.String(outputPathFlag.Name)
149+
if outputPath == "" {
150+
return errors.New("--output-path must be set")
151+
}
152+
fullPath, err := expandPath(outputPath)
153+
if err != nil {
154+
return errors.Wrapf(err, "could not expand path: %s", outputPath)
155+
}
156+
exists, err := fileExists(fullPath)
157+
if err != nil {
158+
return errors.Wrapf(err, "could not check if file exists: %s", fullPath)
159+
}
160+
if exists {
161+
response, err := promptutil.ValidatePrompt(
162+
fmt.Sprintf("file at path %s already exists, are you sure you want to overwrite it? [y/n]", fullPath),
163+
func(s string) error {
164+
input := strings.ToLower(s)
165+
if input != "y" && input != "n" {
166+
return errors.New("please confirm the above text")
167+
}
168+
return nil
169+
},
170+
)
171+
if err != nil {
172+
return errors.Wrap(err, "could not validate prompt confirmation")
173+
}
174+
if response == "n" {
175+
return nil
176+
}
177+
}
178+
if len(privateKeyString) > 2 && strings.Contains(privateKeyString, "0x") {
179+
privateKeyString = privateKeyString[2:] // Strip the 0x prefix, if any.
180+
}
181+
bytesValue, err := hex.DecodeString(privateKeyString)
182+
if err != nil {
183+
return errors.Wrapf(err, "could not decode as hex string: %s", privateKeyString)
184+
}
185+
privKey, err := bls.SecretKeyFromBytes(bytesValue)
186+
if err != nil {
187+
return errors.Wrap(err, "not a valid BLS12-381 private key")
188+
}
189+
pubKey := fmt.Sprintf("%x", privKey.PublicKey().Marshal())
190+
encryptor := keystorev4.New()
191+
id, err := uuid.NewRandom()
192+
if err != nil {
193+
return errors.Wrap(err, "could not generate new random uuid")
194+
}
195+
cryptoFields, err := encryptor.Encrypt(bytesValue, password)
196+
if err != nil {
197+
return errors.Wrap(err, "could not encrypt into new keystore")
198+
}
199+
item := &v2keymanager.Keystore{
200+
Crypto: cryptoFields,
201+
ID: id.String(),
202+
Version: encryptor.Version(),
203+
Pubkey: pubKey,
204+
Name: encryptor.Name(),
205+
}
206+
encodedFile, err := json.MarshalIndent(item, "", "\t")
207+
if err != nil {
208+
return errors.Wrap(err, "could not json marshal keystore")
209+
}
210+
if err := ioutil.WriteFile(fullPath, encodedFile, params.BeaconIoConfig().ReadWritePermissions); err != nil {
211+
return errors.Wrapf(err, "could not write file at path: %s", fullPath)
212+
}
213+
fmt.Printf(
214+
"\nWrote encrypted keystore file at path %s\n",
215+
au.BrightMagenta(fullPath),
216+
)
217+
fmt.Printf("Pubkey: %s\n", au.BrightGreen(
218+
fmt.Sprintf("%#x", privKey.PublicKey().Marshal()),
219+
))
220+
return nil
221+
}
222+
223+
// Reads the keystore file at the provided path and attempts
224+
// to decrypt it with the specified passwords.
225+
func readAndDecryptKeystore(fullPath string, password string) error {
226+
file, err := ioutil.ReadFile(fullPath)
227+
if err != nil {
228+
return errors.Wrapf(err, "could not read file at path: %s", fullPath)
229+
}
230+
decryptor := keystorev4.New()
231+
keystoreFile := &v2keymanager.Keystore{}
232+
233+
if err := json.Unmarshal(file, keystoreFile); err != nil {
234+
return errors.Wrap(err, "could not JSON unmarshal keystore file")
235+
}
236+
// We extract the validator signing private key from the keystore
237+
// by utilizing the password.
238+
privKeyBytes, err := decryptor.Decrypt(keystoreFile.Crypto, password)
239+
if err != nil {
240+
if strings.Contains(err.Error(), "invalid checksum") {
241+
return fmt.Errorf("incorrect password for keystore at path: %s", fullPath)
242+
}
243+
return err
244+
}
245+
publicKeyBytes, err := hex.DecodeString(keystoreFile.Pubkey)
246+
if err != nil {
247+
return errors.Wrapf(err, "could not parse public key for keystore at path: %s", fullPath)
248+
}
249+
fmt.Printf("\nDecrypted keystore %s\n", au.BrightMagenta(fullPath))
250+
fmt.Printf("Privkey: %#x\n", au.BrightGreen(privKeyBytes))
251+
fmt.Printf("Pubkey: %#x\n", au.BrightGreen(publicKeyBytes))
252+
return nil
253+
}
254+
255+
// Checks if the item at the specified path exists and is a directory.
256+
func hasDir(dirPath string) (bool, error) {
257+
fullPath, err := expandPath(dirPath)
258+
if err != nil {
259+
return false, err
260+
}
261+
info, err := os.Stat(fullPath)
262+
if err != nil {
263+
if os.IsNotExist(err) {
264+
return false, nil
265+
}
266+
return false, err
267+
}
268+
return info.IsDir(), nil
269+
}
270+
271+
// Check if a file at the specified path exists.
272+
func fileExists(filePath string) (bool, error) {
273+
fullPath, err := expandPath(filePath)
274+
if err != nil {
275+
return false, err
276+
}
277+
if _, err := os.Stat(fullPath); err != nil {
278+
if os.IsNotExist(err) {
279+
return false, nil
280+
}
281+
return false, err
282+
}
283+
return true, nil
284+
}
285+
286+
// Expands a file path
287+
// 1. replace tilde with users home dir
288+
// 2. expands embedded environment variables
289+
// 3. cleans the path, e.g. /a/b/../c -> /a/c
290+
// Note, it has limitations, e.g. ~someuser/tmp will not be expanded
291+
func expandPath(p string) (string, error) {
292+
if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") {
293+
if home := homeDir(); home != "" {
294+
p = home + p[1:]
295+
}
296+
}
297+
return filepath.Abs(path.Clean(os.ExpandEnv(p)))
298+
}
299+
300+
func homeDir() string {
301+
if home := os.Getenv("HOME"); home != "" {
302+
return home
303+
}
304+
if usr, err := user.Current(); err == nil {
305+
return usr.HomeDir
306+
}
307+
return ""
308+
}

0 commit comments

Comments
 (0)