/
main.go
266 lines (257 loc) · 8.04 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// 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 Ethereum validator wallets or to easily produce keystores from a
// specified secret to move them around in a standard format between Ethereum consensus clients.
package main
import (
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"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/fileutil"
"github.com/prysmaticlabs/prysm/shared/promptutil"
"github.com/prysmaticlabs/prysm/validator/keymanager"
"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 := fileutil.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
})
if err != nil {
return err
}
}
isDir, err := fileutil.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
})
if err != nil {
return err
}
}
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 := fileutil.ExpandPath(outputPath)
if err != nil {
return errors.Wrapf(err, "could not expand path: %s", outputPath)
}
if fileutil.FileExists(fullPath) {
response, err := promptutil.ValidatePrompt(
os.Stdin,
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 := &keymanager.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 := fileutil.WriteFile(fullPath, encodedFile); 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, 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 := &keymanager.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
}
var pubKeyBytes []byte
// Attempt to use the pubkey present in the keystore itself as a field. If unavailable,
// then utilize the public key directly from the private key.
if keystoreFile.Pubkey != "" {
pubKeyBytes, err = hex.DecodeString(keystoreFile.Pubkey)
if err != nil {
return errors.Wrap(err, "could not decode pubkey from keystore")
}
} else {
privKey, err := bls.SecretKeyFromBytes(privKeyBytes)
if err != nil {
return errors.Wrap(err, "could not initialize private key from bytes")
}
pubKeyBytes = privKey.PublicKey().Marshal()
}
fmt.Printf("\nDecrypted keystore %s\n", au.BrightMagenta(fullPath))
fmt.Printf("Privkey: %#x\n", au.BrightGreen(privKeyBytes))
fmt.Printf("Pubkey: %#x\n", au.BrightGreen(pubKeyBytes))
return nil
}