Skip to content

Commit a62612b

Browse files
holimanshekhirin
authored andcommitted
cmd/clef: add importraw feature to clef (ethereum#26058)
This adds a subcommand that imports a raw secp256k1 key into the keystore managed by clef.
1 parent edbb70c commit a62612b

File tree

3 files changed

+303
-7
lines changed

3 files changed

+303
-7
lines changed

cmd/clef/consolecmd_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2022 The go-ethereum Authors
2+
// This file is part of go-ethereum.
3+
//
4+
// go-ethereum is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// go-ethereum is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
"testing"
25+
)
26+
27+
// TestImportRaw tests clef --importraw
28+
func TestImportRaw(t *testing.T) {
29+
keyPath := filepath.Join(os.TempDir(), fmt.Sprintf("%v-tempkey.test", t.Name()))
30+
os.WriteFile(keyPath, []byte("0102030405060708090a0102030405060708090a0102030405060708090a0102"), 0777)
31+
t.Cleanup(func() { os.Remove(keyPath) })
32+
33+
t.Parallel()
34+
t.Run("happy-path", func(t *testing.T) {
35+
// Run clef importraw
36+
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
37+
clef.input("myverylongpassword").input("myverylongpassword")
38+
if out := string(clef.Output()); !strings.Contains(out,
39+
"Key imported:\n Address 0x9160DC9105f7De5dC5E7f3d97ef11DA47269BdA6") {
40+
t.Logf("Output\n%v", out)
41+
t.Error("Failure")
42+
}
43+
})
44+
// tests clef --importraw with mismatched passwords.
45+
t.Run("pw-mismatch", func(t *testing.T) {
46+
// Run clef importraw
47+
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
48+
clef.input("myverylongpassword1").input("myverylongpassword2").WaitExit()
49+
if have, want := clef.StderrText(), "Passwords do not match\n"; have != want {
50+
t.Errorf("have %q, want %q", have, want)
51+
}
52+
})
53+
// tests clef --importraw with a too short password.
54+
t.Run("short-pw", func(t *testing.T) {
55+
// Run clef importraw
56+
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
57+
clef.input("shorty").input("shorty").WaitExit()
58+
if have, want := clef.StderrText(),
59+
"password requirements not met: password too short (<10 characters)\n"; have != want {
60+
t.Errorf("have %q, want %q", have, want)
61+
}
62+
})
63+
}
64+
65+
// TestListAccounts tests clef --list-accounts
66+
func TestListAccounts(t *testing.T) {
67+
keyPath := filepath.Join(os.TempDir(), fmt.Sprintf("%v-tempkey.test", t.Name()))
68+
os.WriteFile(keyPath, []byte("0102030405060708090a0102030405060708090a0102030405060708090a0102"), 0777)
69+
t.Cleanup(func() { os.Remove(keyPath) })
70+
71+
t.Parallel()
72+
t.Run("no-accounts", func(t *testing.T) {
73+
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "list-accounts")
74+
if out := string(clef.Output()); !strings.Contains(out, "The keystore is empty.") {
75+
t.Logf("Output\n%v", out)
76+
t.Error("Failure")
77+
}
78+
})
79+
t.Run("one-account", func(t *testing.T) {
80+
// First, we need to import
81+
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
82+
clef.input("myverylongpassword").input("myverylongpassword").WaitExit()
83+
// Secondly, do a listing, using the same datadir
84+
clef = runWithKeystore(t, clef.Datadir, "--suppress-bootwarn", "--lightkdf", "list-accounts")
85+
if out := string(clef.Output()); !strings.Contains(out, "0x9160DC9105f7De5dC5E7f3d97ef11DA47269BdA6 (keystore:") {
86+
t.Logf("Output\n%v", out)
87+
t.Error("Failure")
88+
}
89+
})
90+
}
91+
92+
// TestListWallets tests clef --list-wallets
93+
func TestListWallets(t *testing.T) {
94+
keyPath := filepath.Join(os.TempDir(), fmt.Sprintf("%v-tempkey.test", t.Name()))
95+
os.WriteFile(keyPath, []byte("0102030405060708090a0102030405060708090a0102030405060708090a0102"), 0777)
96+
t.Cleanup(func() { os.Remove(keyPath) })
97+
98+
t.Parallel()
99+
t.Run("no-accounts", func(t *testing.T) {
100+
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "list-wallets")
101+
if out := string(clef.Output()); !strings.Contains(out, "There are no wallets.") {
102+
t.Logf("Output\n%v", out)
103+
t.Error("Failure")
104+
}
105+
})
106+
t.Run("one-account", func(t *testing.T) {
107+
// First, we need to import
108+
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
109+
clef.input("myverylongpassword").input("myverylongpassword").WaitExit()
110+
// Secondly, do a listing, using the same datadir
111+
clef = runWithKeystore(t, clef.Datadir, "--suppress-bootwarn", "--lightkdf", "list-wallets")
112+
if out := string(clef.Output()); !strings.Contains(out, "Account 0: 0x9160DC9105f7De5dC5E7f3d97ef11DA47269BdA6") {
113+
t.Logf("Output\n%v", out)
114+
t.Error("Failure")
115+
}
116+
})
117+
}

cmd/clef/main.go

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"crypto/sha256"
2424
"encoding/hex"
2525
"encoding/json"
26+
"errors"
2627
"fmt"
2728
"io"
2829
"math/big"
@@ -74,7 +75,7 @@ PURPOSE. See the GNU General Public License for more details.
7475
var (
7576
logLevelFlag = &cli.IntFlag{
7677
Name: "loglevel",
77-
Value: 4,
78+
Value: 3,
7879
Usage: "log level to emit to the screen",
7980
}
8081
advancedMode = &cli.BoolFlag{
@@ -238,6 +239,23 @@ The gendoc generates example structures of the json-rpc communication types.
238239
Description: `
239240
Lists the wallets known to Clef.
240241
`}
242+
importRawCommand = &cli.Command{
243+
Action: accountImport,
244+
Name: "importraw",
245+
Usage: "Import a hex-encoded private key.",
246+
ArgsUsage: "<keyfile>",
247+
Flags: []cli.Flag{
248+
logLevelFlag,
249+
keystoreFlag,
250+
utils.LightKDFFlag,
251+
acceptFlag,
252+
},
253+
Description: `
254+
Imports an unencrypted private key from <keyfile> and creates a new account.
255+
Prints the address.
256+
The keyfile is assumed to contain an unencrypted private key in hexadecimal format.
257+
The account is saved in encrypted format, you are prompted for a password.
258+
`}
241259
)
242260

243261
var app = flags.NewApp("Manage Ethereum account operations")
@@ -273,6 +291,7 @@ func init() {
273291
setCredentialCommand,
274292
delCredentialCommand,
275293
newAccountCommand,
294+
importRawCommand,
276295
gendocCommand,
277296
listAccountsCommand,
278297
listWalletsCommand,
@@ -378,9 +397,9 @@ func attestFile(ctx *cli.Context) error {
378397
return nil
379398
}
380399

381-
func initInternalApi(c *cli.Context) (*core.UIServerAPI, error) {
400+
func initInternalApi(c *cli.Context) (*core.UIServerAPI, core.UIClientAPI, error) {
382401
if err := initialize(c); err != nil {
383-
return nil, err
402+
return nil, nil, err
384403
}
385404
var (
386405
ui = core.NewCommandlineUI()
@@ -391,7 +410,7 @@ func initInternalApi(c *cli.Context) (*core.UIServerAPI, error) {
391410
am := core.StartClefAccountManager(ksLoc, true, lightKdf, "")
392411
api := core.NewSignerAPI(am, 0, true, ui, nil, false, pwStorage)
393412
internalApi := core.NewUIServerAPI(api)
394-
return internalApi, nil
413+
return internalApi, ui, nil
395414
}
396415

397416
func setCredential(ctx *cli.Context) error {
@@ -478,7 +497,7 @@ func initialize(c *cli.Context) error {
478497
}
479498

480499
func newAccount(c *cli.Context) error {
481-
internalApi, err := initInternalApi(c)
500+
internalApi, _, err := initInternalApi(c)
482501
if err != nil {
483502
return err
484503
}
@@ -490,7 +509,7 @@ func newAccount(c *cli.Context) error {
490509
}
491510

492511
func listAccounts(c *cli.Context) error {
493-
internalApi, err := initInternalApi(c)
512+
internalApi, _, err := initInternalApi(c)
494513
if err != nil {
495514
return err
496515
}
@@ -509,7 +528,7 @@ func listAccounts(c *cli.Context) error {
509528
}
510529

511530
func listWallets(c *cli.Context) error {
512-
internalApi, err := initInternalApi(c)
531+
internalApi, _, err := initInternalApi(c)
513532
if err != nil {
514533
return err
515534
}
@@ -528,6 +547,57 @@ func listWallets(c *cli.Context) error {
528547
return nil
529548
}
530549

550+
// accountImport imports a raw hexadecimal private key via CLI.
551+
func accountImport(c *cli.Context) error {
552+
if c.Args().Len() != 1 {
553+
return errors.New("<keyfile> must be given as first argument.")
554+
}
555+
internalApi, ui, err := initInternalApi(c)
556+
if err != nil {
557+
return err
558+
}
559+
pKey, err := crypto.LoadECDSA(c.Args().First())
560+
if err != nil {
561+
return err
562+
}
563+
readPw := func(prompt string) (string, error) {
564+
resp, err := ui.OnInputRequired(core.UserInputRequest{
565+
Title: "Password",
566+
Prompt: prompt,
567+
IsPassword: true,
568+
})
569+
if err != nil {
570+
return "", err
571+
}
572+
return resp.Text, nil
573+
}
574+
first, err := readPw("Please enter a password for the imported account")
575+
if err != nil {
576+
return err
577+
}
578+
second, err := readPw("Please repeat the password you just entered")
579+
if err != nil {
580+
return err
581+
}
582+
if first != second {
583+
return errors.New("Passwords do not match")
584+
}
585+
acc, err := internalApi.ImportRawKey(hex.EncodeToString(crypto.FromECDSA(pKey)), first)
586+
if err != nil {
587+
return err
588+
}
589+
ui.ShowInfo(fmt.Sprintf(`Key imported:
590+
Address %v
591+
Keystore file: %v
592+
593+
The key is now encrypted; losing the password will result in permanently losing
594+
access to the key and all associated funds!
595+
596+
Make sure to backup keystore and passwords in a safe location.`,
597+
acc.Address, acc.URL.Path))
598+
return nil
599+
}
600+
531601
// ipcEndpoint resolves an IPC endpoint based on a configured value, taking into
532602
// account the set data folders as well as the designated platform we're currently
533603
// running on.

cmd/clef/run_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright 2022 The go-ethereum Authors
2+
// This file is part of go-ethereum.
3+
//
4+
// go-ethereum is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// go-ethereum is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"testing"
23+
24+
"github.com/docker/docker/pkg/reexec"
25+
"github.com/ethereum/go-ethereum/internal/cmdtest"
26+
)
27+
28+
const registeredName = "clef-test"
29+
30+
type testproc struct {
31+
*cmdtest.TestCmd
32+
33+
// template variables for expect
34+
Datadir string
35+
Etherbase string
36+
}
37+
38+
func init() {
39+
reexec.Register(registeredName, func() {
40+
if err := app.Run(os.Args); err != nil {
41+
fmt.Fprintln(os.Stderr, err)
42+
os.Exit(1)
43+
}
44+
os.Exit(0)
45+
})
46+
}
47+
48+
func TestMain(m *testing.M) {
49+
// check if we have been reexec'd
50+
if reexec.Init() {
51+
return
52+
}
53+
os.Exit(m.Run())
54+
}
55+
56+
// runClef spawns clef with the given command line args and adds keystore arg.
57+
// This method creates a temporary keystore folder which will be removed after
58+
// the test exits.
59+
func runClef(t *testing.T, args ...string) *testproc {
60+
ddir, err := os.MkdirTemp("", "cleftest-*")
61+
if err != nil {
62+
return nil
63+
}
64+
t.Cleanup(func() {
65+
os.RemoveAll(ddir)
66+
})
67+
return runWithKeystore(t, ddir, args...)
68+
}
69+
70+
// runWithKeystore spawns clef with the given command line args and adds keystore arg.
71+
// This method does _not_ create the keystore folder, but it _does_ add the arg
72+
// to the args.
73+
func runWithKeystore(t *testing.T, keystore string, args ...string) *testproc {
74+
args = append([]string{"--keystore", keystore}, args...)
75+
tt := &testproc{Datadir: keystore}
76+
tt.TestCmd = cmdtest.NewTestCmd(t, tt)
77+
// Boot "clef". This actually runs the test binary but the TestMain
78+
// function will prevent any tests from running.
79+
tt.Run(registeredName, args...)
80+
return tt
81+
}
82+
83+
func (proc *testproc) input(text string) *testproc {
84+
proc.TestCmd.InputLine(text)
85+
return proc
86+
}
87+
88+
/*
89+
// waitForEndpoint waits for the rpc endpoint to appear, or
90+
// aborts after 3 seconds.
91+
func (proc *testproc) waitForEndpoint(t *testing.T) *testproc {
92+
t.Helper()
93+
timeout := 3 * time.Second
94+
ipc := filepath.Join(proc.Datadir, "clef.ipc")
95+
96+
start := time.Now()
97+
for time.Since(start) < timeout {
98+
if _, err := os.Stat(ipc); !errors.Is(err, os.ErrNotExist) {
99+
t.Logf("endpoint %v opened", ipc)
100+
return proc
101+
}
102+
time.Sleep(200 * time.Millisecond)
103+
}
104+
t.Logf("stderr: \n%v", proc.StderrText())
105+
t.Logf("stdout: \n%v", proc.Output())
106+
t.Fatal("endpoint", ipc, "did not open within", timeout)
107+
return proc
108+
}
109+
*/

0 commit comments

Comments
 (0)