From e0987f67e04a840b2f5ee30eb089ae2b7b06df5e Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Tue, 19 May 2020 10:44:46 +0200 Subject: [PATCH] cmd/clef, signer/core: password input fixes (#20960) * cmd/clef, signer/core: use better terminal input for passwords, make it possible to avoid boot-up warning * all: move commonly used prompter to isolated (small) package * cmd/clef: Add new --acceptWarn to clef README * cmd/clef: rename flag 'acceptWarn' to 'suppress-bootwarn' Co-authored-by: ligi --- cmd/clef/README.md | 1 + cmd/clef/main.go | 22 +++++++++++++++------- cmd/devp2p/dnscmd.go | 4 ++-- cmd/ethkey/utils.go | 6 +++--- cmd/geth/accountcmd.go | 6 +++--- cmd/geth/chaincmd.go | 4 ++-- cmd/geth/main.go | 4 ++-- cmd/wnode/main.go | 8 ++++---- console/bridge.go | 9 +++++---- console/console.go | 31 ++++++++++++++++--------------- console/console_test.go | 9 +++++---- console/{ => prompt}/prompter.go | 2 +- signer/core/cliui.go | 13 ++++++------- 13 files changed, 65 insertions(+), 54 deletions(-) rename console/{ => prompt}/prompter.go (99%) diff --git a/cmd/clef/README.md b/cmd/clef/README.md index 90afe8c8c..9b10d0387 100644 --- a/cmd/clef/README.md +++ b/cmd/clef/README.md @@ -46,6 +46,7 @@ GLOBAL OPTIONS: --stdio-ui Use STDIN/STDOUT as a channel for an external UI. This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user interface, and can be used when Clef is started by an external process. --stdio-ui-test Mechanism to test interface between Clef and UI. Requires 'stdio-ui'. --advanced If enabled, issues warnings instead of rejections for suspicious requests. Default off + --suppress-bootwarn If set, does not show the warning during boot --help, -h show help --version, -v print the version ``` diff --git a/cmd/clef/main.go b/cmd/clef/main.go index d1ecd53b4..44a72057b 100644 --- a/cmd/clef/main.go +++ b/cmd/clef/main.go @@ -40,7 +40,7 @@ import ( "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/console" + "github.com/ethereum/go-ethereum/console/prompt" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/internal/ethapi" @@ -82,6 +82,10 @@ var ( Name: "advanced", Usage: "If enabled, issues warnings instead of rejections for suspicious requests. Default off", } + acceptFlag = cli.BoolFlag{ + Name: "suppress-bootwarn", + Usage: "If set, does not show the warning during boot", + } keystoreFlag = cli.StringFlag{ Name: "keystore", Value: filepath.Join(node.DefaultDataDir(), "keystore"), @@ -196,6 +200,7 @@ The delpw command removes a password for a given address (keyfile). logLevelFlag, keystoreFlag, utils.LightKDFFlag, + acceptFlag, }, Description: ` The newaccount command creates a new keystore-backed account. It is a convenience-method @@ -235,6 +240,7 @@ func init() { stdiouiFlag, testFlag, advancedMode, + acceptFlag, } app.Action = signer app.Commands = []cli.Command{initCommand, @@ -433,8 +439,10 @@ func initialize(c *cli.Context) error { if c.GlobalBool(stdiouiFlag.Name) { logOutput = os.Stderr // If using the stdioui, we can't do the 'confirm'-flow - fmt.Fprint(logOutput, legalWarning) - } else { + if !c.GlobalBool(acceptFlag.Name) { + fmt.Fprint(logOutput, legalWarning) + } + } else if !c.GlobalBool(acceptFlag.Name) { if !confirm(legalWarning) { return fmt.Errorf("aborted by user") } @@ -910,14 +918,14 @@ func testExternalUI(api *core.SignerAPI) { // getPassPhrase retrieves the password associated with clef, either fetched // from a list of preloaded passphrases, or requested interactively from the user. // TODO: there are many `getPassPhrase` functions, it will be better to abstract them into one. -func getPassPhrase(prompt string, confirmation bool) string { - fmt.Println(prompt) - password, err := console.Stdin.PromptPassword("Password: ") +func getPassPhrase(query string, confirmation bool) string { + fmt.Println(query) + password, err := prompt.Stdin.PromptPassword("Password: ") if err != nil { utils.Fatalf("Failed to read password: %v", err) } if confirmation { - confirm, err := console.Stdin.PromptPassword("Repeat password: ") + confirm, err := prompt.Stdin.PromptPassword("Repeat password: ") if err != nil { utils.Fatalf("Failed to read password confirmation: %v", err) } diff --git a/cmd/devp2p/dnscmd.go b/cmd/devp2p/dnscmd.go index 8ca31504d..13110f21c 100644 --- a/cmd/devp2p/dnscmd.go +++ b/cmd/devp2p/dnscmd.go @@ -27,7 +27,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/console" + "github.com/ethereum/go-ethereum/console/prompt" "github.com/ethereum/go-ethereum/p2p/dnsdisc" "github.com/ethereum/go-ethereum/p2p/enode" cli "gopkg.in/urfave/cli.v1" @@ -226,7 +226,7 @@ func loadSigningKey(keyfile string) *ecdsa.PrivateKey { if err != nil { exit(fmt.Errorf("failed to read the keyfile at '%s': %v", keyfile, err)) } - password, _ := console.Stdin.PromptPassword("Please enter the password for '" + keyfile + "': ") + password, _ := prompt.Stdin.PromptPassword("Please enter the password for '" + keyfile + "': ") key, err := keystore.DecryptKey(keyjson, password) if err != nil { exit(fmt.Errorf("error decrypting key: %v", err)) diff --git a/cmd/ethkey/utils.go b/cmd/ethkey/utils.go index c6cf5c25a..7b5a144c5 100644 --- a/cmd/ethkey/utils.go +++ b/cmd/ethkey/utils.go @@ -23,7 +23,7 @@ import ( "strings" "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/console" + "github.com/ethereum/go-ethereum/console/prompt" "github.com/ethereum/go-ethereum/crypto" "gopkg.in/urfave/cli.v1" ) @@ -31,13 +31,13 @@ import ( // promptPassphrase prompts the user for a passphrase. Set confirmation to true // to require the user to confirm the passphrase. func promptPassphrase(confirmation bool) string { - passphrase, err := console.Stdin.PromptPassword("Password: ") + passphrase, err := prompt.Stdin.PromptPassword("Password: ") if err != nil { utils.Fatalf("Failed to read password: %v", err) } if confirmation { - confirm, err := console.Stdin.PromptPassword("Repeat password: ") + confirm, err := prompt.Stdin.PromptPassword("Repeat password: ") if err != nil { utils.Fatalf("Failed to read password confirmation: %v", err) } diff --git a/cmd/geth/accountcmd.go b/cmd/geth/accountcmd.go index bf0e58318..1723ef99f 100644 --- a/cmd/geth/accountcmd.go +++ b/cmd/geth/accountcmd.go @@ -23,7 +23,7 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/console" + prompt2 "github.com/ethereum/go-ethereum/console/prompt" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "gopkg.in/urfave/cli.v1" @@ -247,12 +247,12 @@ func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) if prompt != "" { fmt.Println(prompt) } - password, err := console.Stdin.PromptPassword("Password: ") + password, err := prompt2.Stdin.PromptPassword("Password: ") if err != nil { utils.Fatalf("Failed to read password: %v", err) } if confirmation { - confirm, err := console.Stdin.PromptPassword("Repeat password: ") + confirm, err := prompt2.Stdin.PromptPassword("Repeat password: ") if err != nil { utils.Fatalf("Failed to read password confirmation: %v", err) } diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index b853c37cc..1f7897919 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -28,7 +28,7 @@ import ( "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/console" + "github.com/ethereum/go-ethereum/console/prompt" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" @@ -521,7 +521,7 @@ func removeDB(ctx *cli.Context) error { // confirmAndRemoveDB prompts the user for a last confirmation and removes the // folder if accepted. func confirmAndRemoveDB(database string, kind string) { - confirm, err := console.Stdin.PromptConfirm(fmt.Sprintf("Remove %s (%s)?", kind, database)) + confirm, err := prompt.Stdin.PromptConfirm(fmt.Sprintf("Remove %s (%s)?", kind, database)) switch { case err != nil: utils.Fatalf("%v", err) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index c5b3a957c..b789356dc 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -33,7 +33,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/console" + "github.com/ethereum/go-ethereum/console/prompt" "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/ethclient" @@ -258,7 +258,7 @@ func init() { } app.After = func(ctx *cli.Context) error { debug.Exit() - console.Stdin.Close() // Resets terminal mode. + prompt.Stdin.Close() // Resets terminal mode. return nil } } diff --git a/cmd/wnode/main.go b/cmd/wnode/main.go index a94ed947d..677cad7ef 100644 --- a/cmd/wnode/main.go +++ b/cmd/wnode/main.go @@ -38,7 +38,7 @@ import ( "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/console" + "github.com/ethereum/go-ethereum/console/prompt" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p" @@ -210,7 +210,7 @@ func initialize() { if *mailServerMode { if len(msPassword) == 0 { - msPassword, err = console.Stdin.PromptPassword("Please enter the Mail Server password: ") + msPassword, err = prompt.Stdin.PromptPassword("Please enter the Mail Server password: ") if err != nil { utils.Fatalf("Failed to read Mail Server password: %s", err) } @@ -346,7 +346,7 @@ func configureNode() { if *requestMail { p2pAccept = true if len(msPassword) == 0 { - msPassword, err = console.Stdin.PromptPassword("Please enter the Mail Server password: ") + msPassword, err = prompt.Stdin.PromptPassword("Please enter the Mail Server password: ") if err != nil { utils.Fatalf("Failed to read Mail Server password: %s", err) } @@ -355,7 +355,7 @@ func configureNode() { if !*asymmetricMode && !*forwarderMode { if len(symPass) == 0 { - symPass, err = console.Stdin.PromptPassword("Please enter the password for symmetric encryption: ") + symPass, err = prompt.Stdin.PromptPassword("Please enter the password for symmetric encryption: ") if err != nil { utils.Fatalf("Failed to read password: %v", err) } diff --git a/console/bridge.go b/console/bridge.go index 1a297ee12..b35f56f3b 100644 --- a/console/bridge.go +++ b/console/bridge.go @@ -28,6 +28,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/scwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/console/prompt" "github.com/ethereum/go-ethereum/internal/jsre" "github.com/ethereum/go-ethereum/rpc" ) @@ -35,13 +36,13 @@ import ( // bridge is a collection of JavaScript utility methods to bride the .js runtime // environment and the Go RPC connection backing the remote method calls. type bridge struct { - client *rpc.Client // RPC client to execute Ethereum requests through - prompter UserPrompter // Input prompter to allow interactive user feedback - printer io.Writer // Output writer to serialize any display strings to + client *rpc.Client // RPC client to execute Ethereum requests through + prompter prompt.UserPrompter // Input prompter to allow interactive user feedback + printer io.Writer // Output writer to serialize any display strings to } // newBridge creates a new JavaScript wrapper around an RPC client. -func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer) *bridge { +func newBridge(client *rpc.Client, prompter prompt.UserPrompter, printer io.Writer) *bridge { return &bridge{ client: client, prompter: prompter, diff --git a/console/console.go b/console/console.go index e2b4835e4..176db1f96 100644 --- a/console/console.go +++ b/console/console.go @@ -29,6 +29,7 @@ import ( "syscall" "github.com/dop251/goja" + "github.com/ethereum/go-ethereum/console/prompt" "github.com/ethereum/go-ethereum/internal/jsre" "github.com/ethereum/go-ethereum/internal/jsre/deps" "github.com/ethereum/go-ethereum/internal/web3ext" @@ -52,26 +53,26 @@ const DefaultPrompt = "> " // Config is the collection of configurations to fine tune the behavior of the // JavaScript console. type Config struct { - DataDir string // Data directory to store the console history at - DocRoot string // Filesystem path from where to load JavaScript files from - Client *rpc.Client // RPC client to execute Ethereum requests through - Prompt string // Input prompt prefix string (defaults to DefaultPrompt) - Prompter UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter) - Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout) - Preload []string // Absolute paths to JavaScript files to preload + DataDir string // Data directory to store the console history at + DocRoot string // Filesystem path from where to load JavaScript files from + Client *rpc.Client // RPC client to execute Ethereum requests through + Prompt string // Input prompt prefix string (defaults to DefaultPrompt) + Prompter prompt.UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter) + Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout) + Preload []string // Absolute paths to JavaScript files to preload } // Console is a JavaScript interpreted runtime environment. It is a fully fledged // JavaScript console attached to a running node via an external or in-process RPC // client. type Console struct { - client *rpc.Client // RPC client to execute Ethereum requests through - jsre *jsre.JSRE // JavaScript runtime environment running the interpreter - prompt string // Input prompt prefix string - prompter UserPrompter // Input prompter to allow interactive user feedback - histPath string // Absolute path to the console scrollback history - history []string // Scroll history maintained by the console - printer io.Writer // Output writer to serialize any display strings to + client *rpc.Client // RPC client to execute Ethereum requests through + jsre *jsre.JSRE // JavaScript runtime environment running the interpreter + prompt string // Input prompt prefix string + prompter prompt.UserPrompter // Input prompter to allow interactive user feedback + histPath string // Absolute path to the console scrollback history + history []string // Scroll history maintained by the console + printer io.Writer // Output writer to serialize any display strings to } // New initializes a JavaScript interpreted runtime environment and sets defaults @@ -79,7 +80,7 @@ type Console struct { func New(config Config) (*Console, error) { // Handle unset config values gracefully if config.Prompter == nil { - config.Prompter = Stdin + config.Prompter = prompt.Stdin } if config.Prompt == "" { config.Prompt = DefaultPrompt diff --git a/console/console_test.go b/console/console_test.go index 9a2b47444..a474f3330 100644 --- a/console/console_test.go +++ b/console/console_test.go @@ -28,6 +28,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/console/prompt" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/internal/jsre" @@ -67,10 +68,10 @@ func (p *hookedPrompter) PromptPassword(prompt string) (string, error) { func (p *hookedPrompter) PromptConfirm(prompt string) (bool, error) { return false, errors.New("not implemented") } -func (p *hookedPrompter) SetHistory(history []string) {} -func (p *hookedPrompter) AppendHistory(command string) {} -func (p *hookedPrompter) ClearHistory() {} -func (p *hookedPrompter) SetWordCompleter(completer WordCompleter) {} +func (p *hookedPrompter) SetHistory(history []string) {} +func (p *hookedPrompter) AppendHistory(command string) {} +func (p *hookedPrompter) ClearHistory() {} +func (p *hookedPrompter) SetWordCompleter(completer prompt.WordCompleter) {} // tester is a console test environment for the console tests to operate on. type tester struct { diff --git a/console/prompter.go b/console/prompt/prompter.go similarity index 99% rename from console/prompter.go rename to console/prompt/prompter.go index 65675061a..810b6c3e1 100644 --- a/console/prompter.go +++ b/console/prompt/prompter.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package console +package prompt import ( "fmt" diff --git a/signer/core/cliui.go b/signer/core/cliui.go index 65114ac4b..27a2f71aa 100644 --- a/signer/core/cliui.go +++ b/signer/core/cliui.go @@ -25,9 +25,9 @@ import ( "sync" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/console/prompt" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/log" - "golang.org/x/crypto/ssh/terminal" ) type CommandlineUI struct { @@ -61,17 +61,16 @@ func (ui *CommandlineUI) readString() string { func (ui *CommandlineUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) { fmt.Printf("## %s\n\n%s\n", info.Title, info.Prompt) + defer fmt.Println("-----------------------") if info.IsPassword { - fmt.Printf("> ") - text, err := terminal.ReadPassword(int(os.Stdin.Fd())) + text, err := prompt.Stdin.PromptPassword("> ") if err != nil { - log.Error("Failed to read password", "err", err) + log.Error("Failed to read password", "error", err) + return UserInputResponse{}, err } - fmt.Println("-----------------------") - return UserInputResponse{string(text)}, err + return UserInputResponse{text}, nil } text := ui.readString() - fmt.Println("-----------------------") return UserInputResponse{text}, nil }