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 <ligi@ligi.de>
This commit is contained in:
Martin Holst Swende 2020-05-19 10:44:46 +02:00 committed by GitHub
parent 3666da8a4b
commit e0987f67e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 65 additions and 54 deletions

@ -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 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'. --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 --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 --help, -h show help
--version, -v print the version --version, -v print the version
``` ```

@ -40,7 +40,7 @@ import (
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "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/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/internal/ethapi"
@ -82,6 +82,10 @@ var (
Name: "advanced", Name: "advanced",
Usage: "If enabled, issues warnings instead of rejections for suspicious requests. Default off", 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{ keystoreFlag = cli.StringFlag{
Name: "keystore", Name: "keystore",
Value: filepath.Join(node.DefaultDataDir(), "keystore"), Value: filepath.Join(node.DefaultDataDir(), "keystore"),
@ -196,6 +200,7 @@ The delpw command removes a password for a given address (keyfile).
logLevelFlag, logLevelFlag,
keystoreFlag, keystoreFlag,
utils.LightKDFFlag, utils.LightKDFFlag,
acceptFlag,
}, },
Description: ` Description: `
The newaccount command creates a new keystore-backed account. It is a convenience-method The newaccount command creates a new keystore-backed account. It is a convenience-method
@ -235,6 +240,7 @@ func init() {
stdiouiFlag, stdiouiFlag,
testFlag, testFlag,
advancedMode, advancedMode,
acceptFlag,
} }
app.Action = signer app.Action = signer
app.Commands = []cli.Command{initCommand, app.Commands = []cli.Command{initCommand,
@ -433,8 +439,10 @@ func initialize(c *cli.Context) error {
if c.GlobalBool(stdiouiFlag.Name) { if c.GlobalBool(stdiouiFlag.Name) {
logOutput = os.Stderr logOutput = os.Stderr
// If using the stdioui, we can't do the 'confirm'-flow // If using the stdioui, we can't do the 'confirm'-flow
fmt.Fprint(logOutput, legalWarning) if !c.GlobalBool(acceptFlag.Name) {
} else { fmt.Fprint(logOutput, legalWarning)
}
} else if !c.GlobalBool(acceptFlag.Name) {
if !confirm(legalWarning) { if !confirm(legalWarning) {
return fmt.Errorf("aborted by user") return fmt.Errorf("aborted by user")
} }
@ -910,14 +918,14 @@ func testExternalUI(api *core.SignerAPI) {
// getPassPhrase retrieves the password associated with clef, either fetched // getPassPhrase retrieves the password associated with clef, either fetched
// from a list of preloaded passphrases, or requested interactively from the user. // 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. // TODO: there are many `getPassPhrase` functions, it will be better to abstract them into one.
func getPassPhrase(prompt string, confirmation bool) string { func getPassPhrase(query string, confirmation bool) string {
fmt.Println(prompt) fmt.Println(query)
password, err := console.Stdin.PromptPassword("Password: ") password, err := prompt.Stdin.PromptPassword("Password: ")
if err != nil { if err != nil {
utils.Fatalf("Failed to read password: %v", err) utils.Fatalf("Failed to read password: %v", err)
} }
if confirmation { if confirmation {
confirm, err := console.Stdin.PromptPassword("Repeat password: ") confirm, err := prompt.Stdin.PromptPassword("Repeat password: ")
if err != nil { if err != nil {
utils.Fatalf("Failed to read password confirmation: %v", err) utils.Fatalf("Failed to read password confirmation: %v", err)
} }

@ -27,7 +27,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common" "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/dnsdisc"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
cli "gopkg.in/urfave/cli.v1" cli "gopkg.in/urfave/cli.v1"
@ -226,7 +226,7 @@ func loadSigningKey(keyfile string) *ecdsa.PrivateKey {
if err != nil { if err != nil {
exit(fmt.Errorf("failed to read the keyfile at '%s': %v", keyfile, err)) 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) key, err := keystore.DecryptKey(keyjson, password)
if err != nil { if err != nil {
exit(fmt.Errorf("error decrypting key: %v", err)) exit(fmt.Errorf("error decrypting key: %v", err))

@ -23,7 +23,7 @@ import (
"strings" "strings"
"github.com/ethereum/go-ethereum/cmd/utils" "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" "github.com/ethereum/go-ethereum/crypto"
"gopkg.in/urfave/cli.v1" "gopkg.in/urfave/cli.v1"
) )
@ -31,13 +31,13 @@ import (
// promptPassphrase prompts the user for a passphrase. Set confirmation to true // promptPassphrase prompts the user for a passphrase. Set confirmation to true
// to require the user to confirm the passphrase. // to require the user to confirm the passphrase.
func promptPassphrase(confirmation bool) string { func promptPassphrase(confirmation bool) string {
passphrase, err := console.Stdin.PromptPassword("Password: ") passphrase, err := prompt.Stdin.PromptPassword("Password: ")
if err != nil { if err != nil {
utils.Fatalf("Failed to read password: %v", err) utils.Fatalf("Failed to read password: %v", err)
} }
if confirmation { if confirmation {
confirm, err := console.Stdin.PromptPassword("Repeat password: ") confirm, err := prompt.Stdin.PromptPassword("Repeat password: ")
if err != nil { if err != nil {
utils.Fatalf("Failed to read password confirmation: %v", err) utils.Fatalf("Failed to read password confirmation: %v", err)
} }

@ -23,7 +23,7 @@ import (
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/cmd/utils" "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/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"gopkg.in/urfave/cli.v1" "gopkg.in/urfave/cli.v1"
@ -247,12 +247,12 @@ func getPassPhrase(prompt string, confirmation bool, i int, passwords []string)
if prompt != "" { if prompt != "" {
fmt.Println(prompt) fmt.Println(prompt)
} }
password, err := console.Stdin.PromptPassword("Password: ") password, err := prompt2.Stdin.PromptPassword("Password: ")
if err != nil { if err != nil {
utils.Fatalf("Failed to read password: %v", err) utils.Fatalf("Failed to read password: %v", err)
} }
if confirmation { if confirmation {
confirm, err := console.Stdin.PromptPassword("Repeat password: ") confirm, err := prompt2.Stdin.PromptPassword("Repeat password: ")
if err != nil { if err != nil {
utils.Fatalf("Failed to read password confirmation: %v", err) utils.Fatalf("Failed to read password confirmation: %v", err)
} }

@ -28,7 +28,7 @@ import (
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "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"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state" "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 // confirmAndRemoveDB prompts the user for a last confirmation and removes the
// folder if accepted. // folder if accepted.
func confirmAndRemoveDB(database string, kind string) { 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 { switch {
case err != nil: case err != nil:
utils.Fatalf("%v", err) utils.Fatalf("%v", err)

@ -33,7 +33,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "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"
"github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
@ -258,7 +258,7 @@ func init() {
} }
app.After = func(ctx *cli.Context) error { app.After = func(ctx *cli.Context) error {
debug.Exit() debug.Exit()
console.Stdin.Close() // Resets terminal mode. prompt.Stdin.Close() // Resets terminal mode.
return nil return nil
} }
} }

@ -38,7 +38,7 @@ import (
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "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/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
@ -210,7 +210,7 @@ func initialize() {
if *mailServerMode { if *mailServerMode {
if len(msPassword) == 0 { 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 { if err != nil {
utils.Fatalf("Failed to read Mail Server password: %s", err) utils.Fatalf("Failed to read Mail Server password: %s", err)
} }
@ -346,7 +346,7 @@ func configureNode() {
if *requestMail { if *requestMail {
p2pAccept = true p2pAccept = true
if len(msPassword) == 0 { 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 { if err != nil {
utils.Fatalf("Failed to read Mail Server password: %s", err) utils.Fatalf("Failed to read Mail Server password: %s", err)
} }
@ -355,7 +355,7 @@ func configureNode() {
if !*asymmetricMode && !*forwarderMode { if !*asymmetricMode && !*forwarderMode {
if len(symPass) == 0 { 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 { if err != nil {
utils.Fatalf("Failed to read password: %v", err) utils.Fatalf("Failed to read password: %v", err)
} }

@ -28,6 +28,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/scwallet" "github.com/ethereum/go-ethereum/accounts/scwallet"
"github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet"
"github.com/ethereum/go-ethereum/common/hexutil" "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/internal/jsre"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
) )
@ -35,13 +36,13 @@ import (
// bridge is a collection of JavaScript utility methods to bride the .js runtime // bridge is a collection of JavaScript utility methods to bride the .js runtime
// environment and the Go RPC connection backing the remote method calls. // environment and the Go RPC connection backing the remote method calls.
type bridge struct { type bridge struct {
client *rpc.Client // RPC client to execute Ethereum requests through client *rpc.Client // RPC client to execute Ethereum requests through
prompter UserPrompter // Input prompter to allow interactive user feedback prompter prompt.UserPrompter // Input prompter to allow interactive user feedback
printer io.Writer // Output writer to serialize any display strings to printer io.Writer // Output writer to serialize any display strings to
} }
// newBridge creates a new JavaScript wrapper around an RPC client. // 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{ return &bridge{
client: client, client: client,
prompter: prompter, prompter: prompter,

@ -29,6 +29,7 @@ import (
"syscall" "syscall"
"github.com/dop251/goja" "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"
"github.com/ethereum/go-ethereum/internal/jsre/deps" "github.com/ethereum/go-ethereum/internal/jsre/deps"
"github.com/ethereum/go-ethereum/internal/web3ext" "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 // Config is the collection of configurations to fine tune the behavior of the
// JavaScript console. // JavaScript console.
type Config struct { type Config struct {
DataDir string // Data directory to store the console history at DataDir string // Data directory to store the console history at
DocRoot string // Filesystem path from where to load JavaScript files from DocRoot string // Filesystem path from where to load JavaScript files from
Client *rpc.Client // RPC client to execute Ethereum requests through Client *rpc.Client // RPC client to execute Ethereum requests through
Prompt string // Input prompt prefix string (defaults to DefaultPrompt) Prompt string // Input prompt prefix string (defaults to DefaultPrompt)
Prompter UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter) 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) Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout)
Preload []string // Absolute paths to JavaScript files to preload Preload []string // Absolute paths to JavaScript files to preload
} }
// Console is a JavaScript interpreted runtime environment. It is a fully fledged // 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 // JavaScript console attached to a running node via an external or in-process RPC
// client. // client.
type Console struct { type Console struct {
client *rpc.Client // RPC client to execute Ethereum requests through client *rpc.Client // RPC client to execute Ethereum requests through
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
prompt string // Input prompt prefix string prompt string // Input prompt prefix string
prompter UserPrompter // Input prompter to allow interactive user feedback prompter prompt.UserPrompter // Input prompter to allow interactive user feedback
histPath string // Absolute path to the console scrollback history histPath string // Absolute path to the console scrollback history
history []string // Scroll history maintained by the console history []string // Scroll history maintained by the console
printer io.Writer // Output writer to serialize any display strings to printer io.Writer // Output writer to serialize any display strings to
} }
// New initializes a JavaScript interpreted runtime environment and sets defaults // New initializes a JavaScript interpreted runtime environment and sets defaults
@ -79,7 +80,7 @@ type Console struct {
func New(config Config) (*Console, error) { func New(config Config) (*Console, error) {
// Handle unset config values gracefully // Handle unset config values gracefully
if config.Prompter == nil { if config.Prompter == nil {
config.Prompter = Stdin config.Prompter = prompt.Stdin
} }
if config.Prompt == "" { if config.Prompt == "" {
config.Prompt = DefaultPrompt config.Prompt = DefaultPrompt

@ -28,6 +28,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/ethash" "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/core"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/internal/jsre" "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) { func (p *hookedPrompter) PromptConfirm(prompt string) (bool, error) {
return false, errors.New("not implemented") return false, errors.New("not implemented")
} }
func (p *hookedPrompter) SetHistory(history []string) {} func (p *hookedPrompter) SetHistory(history []string) {}
func (p *hookedPrompter) AppendHistory(command string) {} func (p *hookedPrompter) AppendHistory(command string) {}
func (p *hookedPrompter) ClearHistory() {} func (p *hookedPrompter) ClearHistory() {}
func (p *hookedPrompter) SetWordCompleter(completer WordCompleter) {} func (p *hookedPrompter) SetWordCompleter(completer prompt.WordCompleter) {}
// tester is a console test environment for the console tests to operate on. // tester is a console test environment for the console tests to operate on.
type tester struct { type tester struct {

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License // You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package console package prompt
import ( import (
"fmt" "fmt"

@ -25,9 +25,9 @@ import (
"sync" "sync"
"github.com/ethereum/go-ethereum/common/hexutil" "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/internal/ethapi"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"golang.org/x/crypto/ssh/terminal"
) )
type CommandlineUI struct { type CommandlineUI struct {
@ -61,17 +61,16 @@ func (ui *CommandlineUI) readString() string {
func (ui *CommandlineUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) { func (ui *CommandlineUI) OnInputRequired(info UserInputRequest) (UserInputResponse, error) {
fmt.Printf("## %s\n\n%s\n", info.Title, info.Prompt) fmt.Printf("## %s\n\n%s\n", info.Title, info.Prompt)
defer fmt.Println("-----------------------")
if info.IsPassword { if info.IsPassword {
fmt.Printf("> ") text, err := prompt.Stdin.PromptPassword("> ")
text, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil { 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{text}, nil
return UserInputResponse{string(text)}, err
} }
text := ui.readString() text := ui.readString()
fmt.Println("-----------------------")
return UserInputResponse{text}, nil return UserInputResponse{text}, nil
} }