Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions cmd/clef/consolecmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2022 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.

package main

import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)

// TestImportRaw tests clef --importraw
func TestImportRaw(t *testing.T) {
keyPath := filepath.Join(os.TempDir(), fmt.Sprintf("%v-tempkey.test", t.Name()))
os.WriteFile(keyPath, []byte("0102030405060708090a0102030405060708090a0102030405060708090a0102"), 0777)
t.Cleanup(func() { os.Remove(keyPath) })

t.Parallel()
t.Run("happy-path", func(t *testing.T) {
// Run clef importraw
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
clef.input("myverylongpassword").input("myverylongpassword")
if out := string(clef.Output()); !strings.Contains(out,
"Key imported:\n Address 0x9160DC9105f7De5dC5E7f3d97ef11DA47269BdA6") {
t.Logf("Output\n%v", out)
t.Error("Failure")
}
})
// tests clef --importraw with mismatched passwords.
t.Run("pw-mismatch", func(t *testing.T) {
// Run clef importraw
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
clef.input("myverylongpassword1").input("myverylongpassword2").WaitExit()
if have, want := clef.StderrText(), "Passwords do not match\n"; have != want {
t.Errorf("have %q, want %q", have, want)
}
})
// tests clef --importraw with a too short password.
t.Run("short-pw", func(t *testing.T) {
// Run clef importraw
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
clef.input("shorty").input("shorty").WaitExit()
if have, want := clef.StderrText(),
"password requirements not met: password too short (<10 characters)\n"; have != want {
t.Errorf("have %q, want %q", have, want)
}
})
}

// TestListAccounts tests clef --list-accounts
func TestListAccounts(t *testing.T) {
keyPath := filepath.Join(os.TempDir(), fmt.Sprintf("%v-tempkey.test", t.Name()))
os.WriteFile(keyPath, []byte("0102030405060708090a0102030405060708090a0102030405060708090a0102"), 0777)
t.Cleanup(func() { os.Remove(keyPath) })

t.Parallel()
t.Run("no-accounts", func(t *testing.T) {
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "list-accounts")
if out := string(clef.Output()); !strings.Contains(out, "The keystore is empty.") {
t.Logf("Output\n%v", out)
t.Error("Failure")
}
})
t.Run("one-account", func(t *testing.T) {
// First, we need to import
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
clef.input("myverylongpassword").input("myverylongpassword").WaitExit()
// Secondly, do a listing, using the same datadir
clef = runWithKeystore(t, clef.Datadir, "--suppress-bootwarn", "--lightkdf", "list-accounts")
if out := string(clef.Output()); !strings.Contains(out, "0x9160DC9105f7De5dC5E7f3d97ef11DA47269BdA6 (keystore:") {
t.Logf("Output\n%v", out)
t.Error("Failure")
}
})
}

// TestListWallets tests clef --list-wallets
func TestListWallets(t *testing.T) {
keyPath := filepath.Join(os.TempDir(), fmt.Sprintf("%v-tempkey.test", t.Name()))
os.WriteFile(keyPath, []byte("0102030405060708090a0102030405060708090a0102030405060708090a0102"), 0777)
t.Cleanup(func() { os.Remove(keyPath) })

t.Parallel()
t.Run("no-accounts", func(t *testing.T) {
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "list-wallets")
if out := string(clef.Output()); !strings.Contains(out, "There are no wallets.") {
t.Logf("Output\n%v", out)
t.Error("Failure")
}
})
t.Run("one-account", func(t *testing.T) {
// First, we need to import
clef := runClef(t, "--suppress-bootwarn", "--lightkdf", "importraw", keyPath)
clef.input("myverylongpassword").input("myverylongpassword").WaitExit()
// Secondly, do a listing, using the same datadir
clef = runWithKeystore(t, clef.Datadir, "--suppress-bootwarn", "--lightkdf", "list-wallets")
if out := string(clef.Output()); !strings.Contains(out, "Account 0: 0x9160DC9105f7De5dC5E7f3d97ef11DA47269BdA6") {
t.Logf("Output\n%v", out)
t.Error("Failure")
}
})
}
84 changes: 77 additions & 7 deletions cmd/clef/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
Expand Down Expand Up @@ -74,7 +75,7 @@ PURPOSE. See the GNU General Public License for more details.
var (
logLevelFlag = &cli.IntFlag{
Name: "loglevel",
Value: 4,
Value: 3,
Usage: "log level to emit to the screen",
}
advancedMode = &cli.BoolFlag{
Expand Down Expand Up @@ -238,6 +239,23 @@ The gendoc generates example structures of the json-rpc communication types.
Description: `
Lists the wallets known to Clef.
`}
importRawCommand = &cli.Command{
Action: accountImport,
Name: "importraw",
Usage: "Import a hex-encoded private key.",
ArgsUsage: "<keyfile>",
Flags: []cli.Flag{
logLevelFlag,
keystoreFlag,
utils.LightKDFFlag,
acceptFlag,
},
Description: `
Imports an unencrypted private key from <keyfile> and creates a new account.
Prints the address.
The keyfile is assumed to contain an unencrypted private key in hexadecimal format.
The account is saved in encrypted format, you are prompted for a password.
`}
)

var app = flags.NewApp("Manage Ethereum account operations")
Expand Down Expand Up @@ -273,6 +291,7 @@ func init() {
setCredentialCommand,
delCredentialCommand,
newAccountCommand,
importRawCommand,
gendocCommand,
listAccountsCommand,
listWalletsCommand,
Expand Down Expand Up @@ -378,9 +397,9 @@ func attestFile(ctx *cli.Context) error {
return nil
}

func initInternalApi(c *cli.Context) (*core.UIServerAPI, error) {
func initInternalApi(c *cli.Context) (*core.UIServerAPI, core.UIClientAPI, error) {
if err := initialize(c); err != nil {
return nil, err
return nil, nil, err
}
var (
ui = core.NewCommandlineUI()
Expand All @@ -391,7 +410,7 @@ func initInternalApi(c *cli.Context) (*core.UIServerAPI, error) {
am := core.StartClefAccountManager(ksLoc, true, lightKdf, "")
api := core.NewSignerAPI(am, 0, true, ui, nil, false, pwStorage)
internalApi := core.NewUIServerAPI(api)
return internalApi, nil
return internalApi, ui, nil
}

func setCredential(ctx *cli.Context) error {
Expand Down Expand Up @@ -478,7 +497,7 @@ func initialize(c *cli.Context) error {
}

func newAccount(c *cli.Context) error {
internalApi, err := initInternalApi(c)
internalApi, _, err := initInternalApi(c)
if err != nil {
return err
}
Expand All @@ -490,7 +509,7 @@ func newAccount(c *cli.Context) error {
}

func listAccounts(c *cli.Context) error {
internalApi, err := initInternalApi(c)
internalApi, _, err := initInternalApi(c)
if err != nil {
return err
}
Expand All @@ -509,7 +528,7 @@ func listAccounts(c *cli.Context) error {
}

func listWallets(c *cli.Context) error {
internalApi, err := initInternalApi(c)
internalApi, _, err := initInternalApi(c)
if err != nil {
return err
}
Expand All @@ -528,6 +547,57 @@ func listWallets(c *cli.Context) error {
return nil
}

// accountImport imports a raw hexadecimal private key via CLI.
func accountImport(c *cli.Context) error {
if c.Args().Len() != 1 {
return errors.New("<keyfile> must be given as first argument.")
}
internalApi, ui, err := initInternalApi(c)
if err != nil {
return err
}
pKey, err := crypto.LoadECDSA(c.Args().First())
if err != nil {
return err
}
readPw := func(prompt string) (string, error) {
resp, err := ui.OnInputRequired(core.UserInputRequest{
Title: "Password",
Prompt: prompt,
IsPassword: true,
})
if err != nil {
return "", err
}
return resp.Text, nil
}
first, err := readPw("Please enter a password for the imported account")
if err != nil {
return err
}
second, err := readPw("Please repeat the password you just entered")
if err != nil {
return err
}
if first != second {
return errors.New("Passwords do not match")
}
acc, err := internalApi.ImportRawKey(hex.EncodeToString(crypto.FromECDSA(pKey)), first)
if err != nil {
return err
}
ui.ShowInfo(fmt.Sprintf(`Key imported:
Address %v
Keystore file: %v

The key is now encrypted; losing the password will result in permanently losing
access to the key and all associated funds!

Make sure to backup keystore and passwords in a safe location.`,
acc.Address, acc.URL.Path))
return nil
}

// ipcEndpoint resolves an IPC endpoint based on a configured value, taking into
// account the set data folders as well as the designated platform we're currently
// running on.
Expand Down
109 changes: 109 additions & 0 deletions cmd/clef/run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2022 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// go-ethereum is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.

package main

import (
"fmt"
"os"
"testing"

"github.com/docker/docker/pkg/reexec"
"github.com/ethereum/go-ethereum/internal/cmdtest"
)

const registeredName = "clef-test"

type testproc struct {
*cmdtest.TestCmd

// template variables for expect
Datadir string
Etherbase string
}

func init() {
reexec.Register(registeredName, func() {
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
os.Exit(0)
})
}

func TestMain(m *testing.M) {
// check if we have been reexec'd
if reexec.Init() {
return
}
os.Exit(m.Run())
}

// runClef spawns clef with the given command line args and adds keystore arg.
// This method creates a temporary keystore folder which will be removed after
// the test exits.
func runClef(t *testing.T, args ...string) *testproc {
ddir, err := os.MkdirTemp("", "cleftest-*")
if err != nil {
return nil
}
t.Cleanup(func() {
os.RemoveAll(ddir)
})
return runWithKeystore(t, ddir, args...)
}

// runWithKeystore spawns clef with the given command line args and adds keystore arg.
// This method does _not_ create the keystore folder, but it _does_ add the arg
// to the args.
func runWithKeystore(t *testing.T, keystore string, args ...string) *testproc {
args = append([]string{"--keystore", keystore}, args...)
tt := &testproc{Datadir: keystore}
tt.TestCmd = cmdtest.NewTestCmd(t, tt)
// Boot "clef". This actually runs the test binary but the TestMain
// function will prevent any tests from running.
tt.Run(registeredName, args...)
return tt
}

func (proc *testproc) input(text string) *testproc {
proc.TestCmd.InputLine(text)
return proc
}

/*
// waitForEndpoint waits for the rpc endpoint to appear, or
// aborts after 3 seconds.
func (proc *testproc) waitForEndpoint(t *testing.T) *testproc {
t.Helper()
timeout := 3 * time.Second
ipc := filepath.Join(proc.Datadir, "clef.ipc")

start := time.Now()
for time.Since(start) < timeout {
if _, err := os.Stat(ipc); !errors.Is(err, os.ErrNotExist) {
t.Logf("endpoint %v opened", ipc)
return proc
}
time.Sleep(200 * time.Millisecond)
}
t.Logf("stderr: \n%v", proc.StderrText())
t.Logf("stdout: \n%v", proc.Output())
t.Fatal("endpoint", ipc, "did not open within", timeout)
return proc
}
*/