main.go 39.0 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// Copyright 2018 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 (
	"bufio"
	"context"
	"crypto/rand"
	"crypto/sha256"
F
Felix Lange 已提交
24
	"encoding/hex"
25 26 27 28
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
29
	"math/big"
30
	"os"
F
Felix Lange 已提交
31
	"os/signal"
32 33
	"path/filepath"
	"runtime"
34
	"sort"
35
	"strings"
36
	"time"
37

38
	"github.com/ethereum/go-ethereum/accounts"
39
	"github.com/ethereum/go-ethereum/accounts/keystore"
40 41
	"github.com/ethereum/go-ethereum/cmd/utils"
	"github.com/ethereum/go-ethereum/common"
42
	"github.com/ethereum/go-ethereum/common/hexutil"
43
	"github.com/ethereum/go-ethereum/core/types"
44
	"github.com/ethereum/go-ethereum/crypto"
45
	"github.com/ethereum/go-ethereum/internal/ethapi"
46
	"github.com/ethereum/go-ethereum/internal/flags"
47 48
	"github.com/ethereum/go-ethereum/log"
	"github.com/ethereum/go-ethereum/node"
49
	"github.com/ethereum/go-ethereum/params"
50
	"github.com/ethereum/go-ethereum/rlp"
51 52
	"github.com/ethereum/go-ethereum/rpc"
	"github.com/ethereum/go-ethereum/signer/core"
53
	"github.com/ethereum/go-ethereum/signer/core/apitypes"
54
	"github.com/ethereum/go-ethereum/signer/fourbyte"
55 56
	"github.com/ethereum/go-ethereum/signer/rules"
	"github.com/ethereum/go-ethereum/signer/storage"
57
	"github.com/mattn/go-colorable"
58
	"github.com/mattn/go-isatty"
59 60 61 62
	"gopkg.in/urfave/cli.v1"
)

const legalWarning = `
63
WARNING!
64

65
Clef is an account management tool. It may, like any software, contain bugs.
66

67
Please take care to
68
- backup your keystore files,
69
- verify that the keystore(s) can be opened with your password.
70

71 72
Clef 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
73
PURPOSE. See the GNU General Public License for more details.
74 75 76 77 78 79 80 81
`

var (
	logLevelFlag = cli.IntFlag{
		Name:  "loglevel",
		Value: 4,
		Usage: "log level to emit to the screen",
	}
82 83 84 85
	advancedMode = cli.BoolFlag{
		Name:  "advanced",
		Usage: "If enabled, issues warnings instead of rejections for suspicious requests. Default off",
	}
86 87 88 89
	acceptFlag = cli.BoolFlag{
		Name:  "suppress-bootwarn",
		Usage: "If set, does not show the warning during boot",
	}
90 91 92 93 94 95 96 97 98 99
	keystoreFlag = cli.StringFlag{
		Name:  "keystore",
		Value: filepath.Join(node.DefaultDataDir(), "keystore"),
		Usage: "Directory for the keystore",
	}
	configdirFlag = cli.StringFlag{
		Name:  "configdir",
		Value: DefaultConfigDir(),
		Usage: "Directory for Clef configuration",
	}
100 101 102
	chainIdFlag = cli.Int64Flag{
		Name:  "chainid",
		Value: params.MainnetChainConfig.ChainID.Int64(),
103
		Usage: "Chain id to use for signing (1=mainnet, 3=Ropsten, 4=Rinkeby, 5=Goerli)",
104
	}
105
	rpcPortFlag = cli.IntFlag{
106
		Name:  "http.port",
107 108 109 110 111
		Usage: "HTTP-RPC server listening port",
		Value: node.DefaultHTTPPort + 5,
	}
	signerSecretFlag = cli.StringFlag{
		Name:  "signersecret",
112
		Usage: "A file containing the (encrypted) master seed to encrypt Clef data, e.g. keystore credentials and ruleset hash",
113 114 115 116 117 118 119 120 121 122 123 124 125
	}
	customDBFlag = cli.StringFlag{
		Name:  "4bytedb-custom",
		Usage: "File used for writing new 4byte-identifiers submitted via API",
		Value: "./4byte-custom.json",
	}
	auditLogFlag = cli.StringFlag{
		Name:  "auditlog",
		Usage: "File used to emit audit logs. Set to \"\" to disable",
		Value: "audit.log",
	}
	ruleFlag = cli.StringFlag{
		Name:  "rules",
126
		Usage: "Path to the rule file to auto-authorize requests with",
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
	}
	stdiouiFlag = cli.BoolFlag{
		Name: "stdio-ui",
		Usage: "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.",
	}
	testFlag = cli.BoolFlag{
		Name:  "stdio-ui-test",
		Usage: "Mechanism to test interface between Clef and UI. Requires 'stdio-ui'.",
	}
	app         = cli.NewApp()
	initCommand = cli.Command{
		Action:    utils.MigrateFlags(initializeSecrets),
		Name:      "init",
		Usage:     "Initialize the signer, generate secret storage",
		ArgsUsage: "",
		Flags: []cli.Flag{
			logLevelFlag,
			configdirFlag,
		},
		Description: `
149
The init command generates a master seed which Clef can use to store credentials and data needed for
150 151 152 153 154 155 156 157 158 159 160 161 162
the rule-engine to work.`,
	}
	attestCommand = cli.Command{
		Action:    utils.MigrateFlags(attestFile),
		Name:      "attest",
		Usage:     "Attest that a js-file is to be used",
		ArgsUsage: "<sha256sum>",
		Flags: []cli.Flag{
			logLevelFlag,
			configdirFlag,
			signerSecretFlag,
		},
		Description: `
163 164
The attest command stores the sha256 of the rule.js-file that you want to use for automatic processing of
incoming requests.
165

166
Whenever you make an edit to the rule file, you need to use attestation to tell
167 168
Clef that the file is 'safe' to execute.`,
	}
169 170 171
	setCredentialCommand = cli.Command{
		Action:    utils.MigrateFlags(setCredential),
		Name:      "setpw",
172
		Usage:     "Store a credential for a keystore file",
173
		ArgsUsage: "<address>",
174 175 176 177 178 179
		Flags: []cli.Flag{
			logLevelFlag,
			configdirFlag,
			signerSecretFlag,
		},
		Description: `
180 181 182 183 184 185 186 187 188 189 190 191 192 193
The setpw command stores a password for a given address (keyfile).
`}
	delCredentialCommand = cli.Command{
		Action:    utils.MigrateFlags(removeCredential),
		Name:      "delpw",
		Usage:     "Remove a credential for a keystore file",
		ArgsUsage: "<address>",
		Flags: []cli.Flag{
			logLevelFlag,
			configdirFlag,
			signerSecretFlag,
		},
		Description: `
The delpw command removes a password for a given address (keyfile).
194
`}
195 196 197 198 199 200 201 202 203
	newAccountCommand = cli.Command{
		Action:    utils.MigrateFlags(newAccount),
		Name:      "newaccount",
		Usage:     "Create a new account",
		ArgsUsage: "",
		Flags: []cli.Flag{
			logLevelFlag,
			keystoreFlag,
			utils.LightKDFFlag,
204
			acceptFlag,
205 206 207 208 209 210
		},
		Description: `
The newaccount command creates a new keystore-backed account. It is a convenience-method
which can be used in lieu of an external UI.`,
	}

211 212 213 214 215 216 217
	gendocCommand = cli.Command{
		Action: GenDoc,
		Name:   "gendoc",
		Usage:  "Generate documentation about json-rpc format",
		Description: `
The gendoc generates example structures of the json-rpc communication types.
`}
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
// AppHelpFlagGroups is the application flags, grouped by functionality.
var AppHelpFlagGroups = []flags.FlagGroup{
	{
		Name: "FLAGS",
		Flags: []cli.Flag{
			logLevelFlag,
			keystoreFlag,
			configdirFlag,
			chainIdFlag,
			utils.LightKDFFlag,
			utils.NoUSBFlag,
			utils.SmartCardDaemonPathFlag,
			utils.HTTPListenAddrFlag,
			utils.HTTPVirtualHostsFlag,
			utils.IPCDisabledFlag,
			utils.IPCPathFlag,
			utils.HTTPEnabledFlag,
			rpcPortFlag,
			signerSecretFlag,
			customDBFlag,
			auditLogFlag,
			ruleFlag,
			stdiouiFlag,
			testFlag,
			advancedMode,
			acceptFlag,
		},
	},
}

250 251 252 253 254 255 256
func init() {
	app.Name = "Clef"
	app.Usage = "Manage Ethereum account operations"
	app.Flags = []cli.Flag{
		logLevelFlag,
		keystoreFlag,
		configdirFlag,
257
		chainIdFlag,
258 259
		utils.LightKDFFlag,
		utils.NoUSBFlag,
260
		utils.SmartCardDaemonPathFlag,
261 262
		utils.HTTPListenAddrFlag,
		utils.HTTPVirtualHostsFlag,
263 264
		utils.IPCDisabledFlag,
		utils.IPCPathFlag,
265
		utils.HTTPEnabledFlag,
266 267 268 269 270 271 272
		rpcPortFlag,
		signerSecretFlag,
		customDBFlag,
		auditLogFlag,
		ruleFlag,
		stdiouiFlag,
		testFlag,
273
		advancedMode,
274
		acceptFlag,
275 276
	}
	app.Action = signer
277 278 279 280 281 282
	app.Commands = []cli.Command{initCommand,
		attestCommand,
		setCredentialCommand,
		delCredentialCommand,
		newAccountCommand,
		gendocCommand}
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
	cli.CommandHelpTemplate = flags.CommandHelpTemplate
	// Override the default app help template
	cli.AppHelpTemplate = flags.ClefAppHelpTemplate

	// Override the default app help printer, but only for the global app help
	originalHelpPrinter := cli.HelpPrinter
	cli.HelpPrinter = func(w io.Writer, tmpl string, data interface{}) {
		if tmpl == flags.ClefAppHelpTemplate {
			// Render out custom usage screen
			originalHelpPrinter(w, tmpl, flags.HelpData{App: data, FlagGroups: AppHelpFlagGroups})
		} else if tmpl == flags.CommandHelpTemplate {
			// Iterate over all command specific flags and categorize them
			categorized := make(map[string][]cli.Flag)
			for _, flag := range data.(cli.Command).Flags {
				if _, ok := categorized[flag.String()]; !ok {
					categorized[flags.FlagCategory(flag, AppHelpFlagGroups)] = append(categorized[flags.FlagCategory(flag, AppHelpFlagGroups)], flag)
				}
			}

			// sort to get a stable ordering
			sorted := make([]flags.FlagGroup, 0, len(categorized))
			for cat, flgs := range categorized {
				sorted = append(sorted, flags.FlagGroup{Name: cat, Flags: flgs})
			}
			sort.Sort(flags.ByCategory(sorted))

			// add sorted array to data and render with default printer
			originalHelpPrinter(w, tmpl, map[string]interface{}{
				"cmd":              data,
				"categorizedFlags": sorted,
			})
		} else {
			originalHelpPrinter(w, tmpl, data)
		}
	}
318
}
319

320 321 322 323 324 325 326 327
func main() {
	if err := app.Run(os.Args); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func initializeSecrets(c *cli.Context) error {
328
	// Get past the legal message
329 330 331
	if err := initialize(c); err != nil {
		return err
	}
332
	// Ensure the master key does not yet exist, we're not willing to overwrite
333
	configDir := c.GlobalString(configdirFlag.Name)
334 335 336 337 338 339 340 341
	if err := os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) {
		return err
	}
	location := filepath.Join(configDir, "masterseed.json")
	if _, err := os.Stat(location); err == nil {
		return fmt.Errorf("master key %v already exists, will not overwrite", location)
	}
	// Key file does not exist yet, generate a new one and encrypt it
342
	masterSeed := make([]byte, 256)
343
	num, err := io.ReadFull(rand.Reader, masterSeed)
344 345 346
	if err != nil {
		return err
	}
347
	if num != len(masterSeed) {
348 349
		return fmt.Errorf("failed to read enough random")
	}
350 351 352 353
	n, p := keystore.StandardScryptN, keystore.StandardScryptP
	if c.GlobalBool(utils.LightKDFFlag.Name) {
		n, p = keystore.LightScryptN, keystore.LightScryptP
	}
354
	text := "The master seed of clef will be locked with a password.\nPlease specify a password. Do not forget this password!"
355 356
	var password string
	for {
357
		password = utils.GetPassPhrase(text, true)
358 359 360
		if err := core.ValidatePasswordFormat(password); err != nil {
			fmt.Printf("invalid password: %v\n", err)
		} else {
361
			fmt.Println()
362 363 364 365 366 367 368
			break
		}
	}
	cipherSeed, err := encryptSeed(masterSeed, []byte(password), n, p)
	if err != nil {
		return fmt.Errorf("failed to encrypt master seed: %v", err)
	}
369 370
	// Double check the master key path to ensure nothing wrote there in between
	if err = os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) {
371 372 373
		return err
	}
	if _, err := os.Stat(location); err == nil {
374
		return fmt.Errorf("master key %v already exists, will not overwrite", location)
375
	}
376 377
	// Write the file and print the usual warning message
	if err = ioutil.WriteFile(location, cipherSeed, 0400); err != nil {
378 379 380 381
		return err
	}
	fmt.Printf("A master seed has been generated into %s\n", location)
	fmt.Printf(`
382
This is required to be able to store credentials, such as:
383
* Passwords for keystores (used by rule engine)
384 385
* Storage for JavaScript auto-signing rules
* Hash of JavaScript rule-file
386

387 388 389
You should treat 'masterseed.json' with utmost secrecy and make a backup of it!
* The password is necessary but not enough, you need to back up the master seed too!
* The master seed does not contain your accounts, those need to be backed up separately!
390 391 392 393 394 395 396 397 398 399 400 401

`)
	return nil
}
func attestFile(ctx *cli.Context) error {
	if len(ctx.Args()) < 1 {
		utils.Fatalf("This command requires an argument.")
	}
	if err := initialize(ctx); err != nil {
		return err
	}

402
	stretchedKey, err := readMasterKey(ctx, nil)
403 404 405
	if err != nil {
		utils.Fatalf(err.Error())
	}
406
	configDir := ctx.GlobalString(configdirFlag.Name)
407 408 409 410 411 412 413 414 415 416 417
	vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
	confKey := crypto.Keccak256([]byte("config"), stretchedKey)

	// Initialize the encrypted storages
	configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confKey)
	val := ctx.Args().First()
	configStorage.Put("ruleset_sha256", val)
	log.Info("Ruleset attestation updated", "sha256", val)
	return nil
}

418
func setCredential(ctx *cli.Context) error {
419
	if len(ctx.Args()) < 1 {
420
		utils.Fatalf("This command requires an address to be passed as an argument")
421 422 423 424
	}
	if err := initialize(ctx); err != nil {
		return err
	}
425 426 427 428 429
	addr := ctx.Args().First()
	if !common.IsHexAddress(addr) {
		utils.Fatalf("Invalid address specified: %s", addr)
	}
	address := common.HexToAddress(addr)
430
	password := utils.GetPassPhrase("Please enter a password to store for this address:", true)
431 432 433 434 435 436 437 438 439
	fmt.Println()

	stretchedKey, err := readMasterKey(ctx, nil)
	if err != nil {
		utils.Fatalf(err.Error())
	}
	configDir := ctx.GlobalString(configdirFlag.Name)
	vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
	pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
440

441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
	pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
	pwStorage.Put(address.Hex(), password)

	log.Info("Credential store updated", "set", address)
	return nil
}

func removeCredential(ctx *cli.Context) error {
	if len(ctx.Args()) < 1 {
		utils.Fatalf("This command requires an address to be passed as an argument")
	}
	if err := initialize(ctx); err != nil {
		return err
	}
	addr := ctx.Args().First()
	if !common.IsHexAddress(addr) {
		utils.Fatalf("Invalid address specified: %s", addr)
	}
	address := common.HexToAddress(addr)
460

461
	stretchedKey, err := readMasterKey(ctx, nil)
462 463 464
	if err != nil {
		utils.Fatalf(err.Error())
	}
465
	configDir := ctx.GlobalString(configdirFlag.Name)
466 467 468 469
	vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
	pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)

	pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
470 471 472
	pwStorage.Del(address.Hex())

	log.Info("Credential store updated", "unset", address)
473 474 475
	return nil
}

476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500
func newAccount(c *cli.Context) error {
	if err := initialize(c); err != nil {
		return err
	}
	// The newaccount is meant for users using the CLI, since 'real' external
	// UIs can use the UI-api instead. So we'll just use the native CLI UI here.
	var (
		ui                        = core.NewCommandlineUI()
		pwStorage storage.Storage = &storage.NoStorage{}
		ksLoc                     = c.GlobalString(keystoreFlag.Name)
		lightKdf                  = c.GlobalBool(utils.LightKDFFlag.Name)
	)
	log.Info("Starting clef", "keystore", ksLoc, "light-kdf", lightKdf)
	am := core.StartClefAccountManager(ksLoc, true, lightKdf, "")
	// This gives is us access to the external API
	apiImpl := core.NewSignerAPI(am, 0, true, ui, nil, false, pwStorage)
	// This gives us access to the internal API
	internalApi := core.NewUIServerAPI(apiImpl)
	addr, err := internalApi.New(context.Background())
	if err == nil {
		fmt.Printf("Generated account %v\n", addr.String())
	}
	return err
}

501 502 503
func initialize(c *cli.Context) error {
	// Set up the logger to print everything
	logOutput := os.Stdout
504
	if c.GlobalBool(stdiouiFlag.Name) {
505 506
		logOutput = os.Stderr
		// If using the stdioui, we can't do the 'confirm'-flow
507 508 509 510
		if !c.GlobalBool(acceptFlag.Name) {
			fmt.Fprint(logOutput, legalWarning)
		}
	} else if !c.GlobalBool(acceptFlag.Name) {
511 512 513
		if !confirm(legalWarning) {
			return fmt.Errorf("aborted by user")
		}
514
		fmt.Println()
515
	}
516 517 518 519 520 521 522
	usecolor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb"
	output := io.Writer(logOutput)
	if usecolor {
		output = colorable.NewColorable(logOutput)
	}
	log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int(logLevelFlag.Name)), log.StreamHandler(output, log.TerminalFormat(usecolor))))

523 524 525
	return nil
}

526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
// 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.
func ipcEndpoint(ipcPath, datadir string) string {
	// On windows we can only use plain top-level pipes
	if runtime.GOOS == "windows" {
		if strings.HasPrefix(ipcPath, `\\.\pipe\`) {
			return ipcPath
		}
		return `\\.\pipe\` + ipcPath
	}
	// Resolve names into the data directory full paths otherwise
	if filepath.Base(ipcPath) == ipcPath {
		if datadir == "" {
			return filepath.Join(os.TempDir(), ipcPath)
		}
		return filepath.Join(datadir, ipcPath)
	}
	return ipcPath
}

547
func signer(c *cli.Context) error {
548 549 550 551
	// If we have some unrecognized command, bail out
	if args := c.Args(); len(args) > 0 {
		return fmt.Errorf("invalid command: %q", args[0])
	}
552 553 554 555
	if err := initialize(c); err != nil {
		return err
	}
	var (
556
		ui core.UIClientAPI
557
	)
558
	if c.GlobalBool(stdiouiFlag.Name) {
559 560 561 562 563 564
		log.Info("Using stdin/stdout as UI-channel")
		ui = core.NewStdIOUI()
	} else {
		log.Info("Using CLI as UI-channel")
		ui = core.NewCommandlineUI()
	}
565
	// 4bytedb data
566
	fourByteLocal := c.GlobalString(customDBFlag.Name)
567
	db, err := fourbyte.NewWithFile(fourByteLocal)
568 569 570
	if err != nil {
		utils.Fatalf(err.Error())
	}
571 572
	embeds, locals := db.Size()
	log.Info("Loaded 4byte database", "embeds", embeds, "locals", locals, "local", fourByteLocal)
573 574

	var (
575 576
		api       core.ExternalAPI
		pwStorage storage.Storage = &storage.NoStorage{}
577
	)
578 579
	configDir := c.GlobalString(configdirFlag.Name)
	if stretchedKey, err := readMasterKey(c, ui); err != nil {
580
		log.Warn("Failed to open master, rules disabled", "err", err)
581 582 583 584 585 586 587 588 589
	} else {
		vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))

		// Generate domain specific keys
		pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
		jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey)
		confkey := crypto.Keccak256([]byte("config"), stretchedKey)

		// Initialize the encrypted storages
590
		pwStorage = storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
591 592 593
		jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey)
		configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey)

594
		// Do we have a rule-file?
595
		if ruleFile := c.GlobalString(ruleFlag.Name); ruleFile != "" {
596
			ruleJS, err := ioutil.ReadFile(ruleFile)
597
			if err != nil {
598
				log.Warn("Could not load rules, disabling", "file", ruleFile, "err", err)
599
			} else {
600 601
				shasum := sha256.Sum256(ruleJS)
				foundShaSum := hex.EncodeToString(shasum[:])
602
				storedShasum, _ := configStorage.Get("ruleset_sha256")
603
				if storedShasum != foundShaSum {
604
					log.Warn("Rule hash not attested, disabling", "hash", foundShaSum, "attested", storedShasum)
605 606 607 608 609 610 611 612 613
				} else {
					// Initialize rules
					ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage)
					if err != nil {
						utils.Fatalf(err.Error())
					}
					ruleEngine.Init(string(ruleJS))
					ui = ruleEngine
					log.Info("Rule engine configured", "file", c.String(ruleFlag.Name))
614 615 616 617
				}
			}
		}
	}
618 619 620 621 622 623
	var (
		chainId  = c.GlobalInt64(chainIdFlag.Name)
		ksLoc    = c.GlobalString(keystoreFlag.Name)
		lightKdf = c.GlobalBool(utils.LightKDFFlag.Name)
		advanced = c.GlobalBool(advancedMode.Name)
		nousb    = c.GlobalBool(utils.NoUSBFlag.Name)
624
		scpath   = c.GlobalString(utils.SmartCardDaemonPathFlag.Name)
625 626 627
	)
	log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc,
		"light-kdf", lightKdf, "advanced", advanced)
628
	am := core.StartClefAccountManager(ksLoc, nousb, lightKdf, scpath)
629
	apiImpl := core.NewSignerAPI(am, chainId, nousb, ui, db, advanced, pwStorage)
630 631 632 633

	// Establish the bidirectional communication, by creating a new UI backend and registering
	// it with the UI.
	ui.RegisterUIServer(core.NewUIServerAPI(apiImpl))
634 635
	api = apiImpl
	// Audit logging
636
	if logfile := c.GlobalString(auditLogFlag.Name); logfile != "" {
637 638 639 640 641 642 643 644
		api, err = core.NewAuditLogger(logfile, api)
		if err != nil {
			utils.Fatalf(err.Error())
		}
		log.Info("Audit logs configured", "file", logfile)
	}
	// register signer API with server
	var (
K
kiel barry 已提交
645 646
		extapiURL = "n/a"
		ipcapiURL = "n/a"
647
	)
K
kiel barry 已提交
648
	rpcAPI := []rpc.API{
649 650 651 652 653 654
		{
			Namespace: "account",
			Public:    true,
			Service:   api,
			Version:   "1.0"},
	}
655
	if c.GlobalBool(utils.HTTPEnabledFlag.Name) {
656 657
		vhosts := utils.SplitAndTrim(c.GlobalString(utils.HTTPVirtualHostsFlag.Name))
		cors := utils.SplitAndTrim(c.GlobalString(utils.HTTPCORSDomainFlag.Name))
658

659
		srv := rpc.NewServer()
660
		err := node.RegisterApis(rpcAPI, []string{"account"}, srv, false)
661 662 663 664 665
		if err != nil {
			utils.Fatalf("Could not register API: %w", err)
		}
		handler := node.NewHTTPHandlerStack(srv, cors, vhosts)

666 667 668
		// set port
		port := c.Int(rpcPortFlag.Name)

669
		// start http server
670
		httpEndpoint := fmt.Sprintf("%s:%d", c.GlobalString(utils.HTTPListenAddrFlag.Name), port)
671
		httpServer, addr, err := node.StartHTTPEndpoint(httpEndpoint, rpc.DefaultHTTPTimeouts, handler)
672 673 674
		if err != nil {
			utils.Fatalf("Could not start RPC api: %v", err)
		}
675
		extapiURL = fmt.Sprintf("http://%v/", addr)
K
kiel barry 已提交
676
		log.Info("HTTP endpoint opened", "url", extapiURL)
677 678

		defer func() {
679 680
			// Don't bother imposing a timeout here.
			httpServer.Shutdown(context.Background())
681
			log.Info("HTTP endpoint closed", "url", extapiURL)
682 683
		}()
	}
684
	if !c.GlobalBool(utils.IPCDisabledFlag.Name) {
685 686
		givenPath := c.GlobalString(utils.IPCPathFlag.Name)
		ipcapiURL = ipcEndpoint(filepath.Join(givenPath, "clef.ipc"), configDir)
K
kiel barry 已提交
687
		listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI)
688 689 690
		if err != nil {
			utils.Fatalf("Could not start IPC api: %v", err)
		}
K
kiel barry 已提交
691
		log.Info("IPC endpoint opened", "url", ipcapiURL)
692 693
		defer func() {
			listener.Close()
K
kiel barry 已提交
694
			log.Info("IPC endpoint closed", "url", ipcapiURL)
695 696 697
		}()
	}

698
	if c.GlobalBool(testFlag.Name) {
699 700 701 702 703
		log.Info("Performing UI test")
		go testExternalUI(apiImpl)
	}
	ui.OnSignerStartup(core.StartupInfo{
		Info: map[string]interface{}{
704
			"intapi_version": core.InternalAPIVersion,
705
			"extapi_version": core.ExternalAPIVersion,
K
kiel barry 已提交
706 707
			"extapi_http":    extapiURL,
			"extapi_ipc":     ipcapiURL,
708 709 710
		},
	})

711
	abortChan := make(chan os.Signal, 1)
712 713 714 715 716 717 718 719 720 721 722 723
	signal.Notify(abortChan, os.Interrupt)

	sig := <-abortChan
	log.Info("Exiting...", "signal", sig)

	return nil
}

// DefaultConfigDir is the default config directory to use for the vaults and other
// persistence requirements.
func DefaultConfigDir() string {
	// Try to place the data folder in the user's home dir
724
	home := utils.HomeDir()
725 726 727 728
	if home != "" {
		if runtime.GOOS == "darwin" {
			return filepath.Join(home, "Library", "Signer")
		} else if runtime.GOOS == "windows" {
729 730 731 732
			appdata := os.Getenv("APPDATA")
			if appdata != "" {
				return filepath.Join(appdata, "Signer")
			}
733
			return filepath.Join(home, "AppData", "Roaming", "Signer")
734
		}
735
		return filepath.Join(home, ".clef")
736 737 738 739 740
	}
	// As we cannot guess a stable location, return empty and handle later
	return ""
}

741
func readMasterKey(ctx *cli.Context, ui core.UIClientAPI) ([]byte, error) {
742 743
	var (
		file      string
744
		configDir = ctx.GlobalString(configdirFlag.Name)
745
	)
746 747
	if ctx.GlobalIsSet(signerSecretFlag.Name) {
		file = ctx.GlobalString(signerSecretFlag.Name)
748
	} else {
749
		file = filepath.Join(configDir, "masterseed.json")
750 751 752 753
	}
	if err := checkFile(file); err != nil {
		return nil, err
	}
754
	cipherKey, err := ioutil.ReadFile(file)
755 756 757
	if err != nil {
		return nil, err
	}
758 759 760 761 762 763 764 765 766 767 768 769
	var password string
	// If ui is not nil, get the password from ui.
	if ui != nil {
		resp, err := ui.OnInputRequired(core.UserInputRequest{
			Title:      "Master Password",
			Prompt:     "Please enter the password to decrypt the master seed",
			IsPassword: true})
		if err != nil {
			return nil, err
		}
		password = resp.Text
	} else {
770
		password = utils.GetPassPhrase("Decrypt master seed of clef", false)
771 772 773 774 775 776 777
	}
	masterSeed, err := decryptSeed(cipherKey, password)
	if err != nil {
		return nil, fmt.Errorf("failed to decrypt the master seed of clef")
	}
	if len(masterSeed) < 256 {
		return nil, fmt.Errorf("master seed of insufficient length, expected >255 bytes, got %d", len(masterSeed))
778 779
	}
	// Create vault location
780
	vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterSeed)[:10]))
781 782 783 784
	err = os.Mkdir(vaultLocation, 0700)
	if err != nil && !os.IsExist(err) {
		return nil, err
	}
785
	return masterSeed, nil
786 787 788 789
}

// checkFile is a convenience function to check if a file
// * exists
790
// * is mode 0400 (unix only)
791 792 793 794 795 796
func checkFile(filename string) error {
	info, err := os.Stat(filename)
	if err != nil {
		return fmt.Errorf("failed stat on %s: %v", filename, err)
	}
	// Check the unix permission bits
797 798 799
	// However, on windows, we cannot use the unix perm-bits, see
	// https://github.com/ethereum/go-ethereum/issues/20123
	if runtime.GOOS != "windows" && info.Mode().Perm()&0377 != 0 {
800 801 802 803 804 805 806
		return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String())
	}
	return nil
}

// confirm displays a text and asks for user confirmation
func confirm(text string) bool {
807
	fmt.Print(text)
808
	fmt.Printf("\nEnter 'ok' to proceed:\n> ")
809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826

	text, err := bufio.NewReader(os.Stdin).ReadString('\n')
	if err != nil {
		log.Crit("Failed to read user input", "err", err)
	}
	if text := strings.TrimSpace(text); text == "ok" {
		return true
	}
	return false
}

func testExternalUI(api *core.SignerAPI) {

	ctx := context.WithValue(context.Background(), "remote", "clef binary")
	ctx = context.WithValue(ctx, "scheme", "in-proc")
	ctx = context.WithValue(ctx, "local", "main")
	errs := make([]string, 0)

827
	a := common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef")
828
	addErr := func(errStr string) {
829
		log.Info("Test error", "err", errStr)
830 831
		errs = append(errs, errStr)
	}
832

833 834 835 836 837 838
	queryUser := func(q string) string {
		resp, err := api.UI.OnInputRequired(core.UserInputRequest{
			Title:  "Testing",
			Prompt: q,
		})
		if err != nil {
839
			addErr(err.Error())
840
		}
841
		return resp.Text
842
	}
843 844
	expectResponse := func(testcase, question, expect string) {
		if got := queryUser(question); got != expect {
845
			addErr(fmt.Sprintf("%s: got %v, expected %v", testcase, got, expect))
846
		}
847
	}
848 849 850 851
	expectApprove := func(testcase string, err error) {
		if err == nil || err == accounts.ErrUnknownAccount {
			return
		}
852
		addErr(fmt.Sprintf("%v: expected no error, got %v", testcase, err.Error()))
853 854 855
	}
	expectDeny := func(testcase string, err error) {
		if err == nil || err != core.ErrRequestDenied {
856
			addErr(fmt.Sprintf("%v: expected ErrRequestDenied, got %v", testcase, err))
857 858
		}
	}
859
	var delay = 1 * time.Second
860 861 862
	// Test display of info and error
	{
		api.UI.ShowInfo("If you see this message, enter 'yes' to next question")
863
		time.Sleep(delay)
864 865
		expectResponse("showinfo", "Did you see the message? [yes/no]", "yes")
		api.UI.ShowError("If you see this message, enter 'yes' to the next question")
866
		time.Sleep(delay)
867 868 869 870
		expectResponse("showerror", "Did you see the message? [yes/no]", "yes")
	}
	{ // Sign data test - clique header
		api.UI.ShowInfo("Please approve the next request for signing a clique header")
871
		time.Sleep(delay)
872
		cliqueHeader := types.Header{
F
Felix Lange 已提交
873 874 875 876 877 878 879 880 881 882 883 884 885
			ParentHash:  common.HexToHash("0000H45H"),
			UncleHash:   common.HexToHash("0000H45H"),
			Coinbase:    common.HexToAddress("0000H45H"),
			Root:        common.HexToHash("0000H00H"),
			TxHash:      common.HexToHash("0000H45H"),
			ReceiptHash: common.HexToHash("0000H45H"),
			Difficulty:  big.NewInt(1337),
			Number:      big.NewInt(1337),
			GasLimit:    1338,
			GasUsed:     1338,
			Time:        1338,
			Extra:       []byte("Extra data Extra data Extra data  Extra data  Extra data  Extra data  Extra data Extra data"),
			MixDigest:   common.HexToHash("0x0000H45H"),
886 887 888 889 890 891 892 893 894
		}
		cliqueRlp, err := rlp.EncodeToBytes(cliqueHeader)
		if err != nil {
			utils.Fatalf("Should not error: %v", err)
		}
		addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899")
		_, err = api.SignData(ctx, accounts.MimetypeClique, *addr, hexutil.Encode(cliqueRlp))
		expectApprove("signdata - clique header", err)
	}
895 896 897 898 899 900 901
	{ // Sign data test - typed data
		api.UI.ShowInfo("Please approve the next request for signing EIP-712 typed data")
		time.Sleep(delay)
		addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899")
		data := `{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Person":[{"name":"name","type":"string"},{"name":"test","type":"uint8"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","domain":{"name":"Ether Mail","version":"1","chainId":"1","verifyingContract":"0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","test":"3","wallet":"0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","test":"2"},"contents":"Hello, Bob!"}}`
		//_, err := api.SignData(ctx, accounts.MimetypeTypedData, *addr, hexutil.Encode([]byte(data)))
		var typedData core.TypedData
902 903
		json.Unmarshal([]byte(data), &typedData)
		_, err := api.SignTypedData(ctx, *addr, typedData)
904 905
		expectApprove("sign 712 typed data", err)
	}
906 907
	{ // Sign data test - plain text
		api.UI.ShowInfo("Please approve the next request for signing text")
908
		time.Sleep(delay)
909 910 911 912 913 914
		addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899")
		_, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world")))
		expectApprove("signdata - text", err)
	}
	{ // Sign data test - plain text reject
		api.UI.ShowInfo("Please deny the next request for signing text")
915
		time.Sleep(delay)
916 917 918 919 920 921 922
		addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899")
		_, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world")))
		expectDeny("signdata - text", err)
	}
	{ // Sign transaction

		api.UI.ShowInfo("Please reject next transaction")
923
		time.Sleep(delay)
924 925
		data := hexutil.Bytes([]byte{})
		to := common.NewMixedcaseAddress(a)
926
		tx := apitypes.SendTxArgs{
927 928 929 930 931
			Data:     &data,
			Nonce:    0x1,
			Value:    hexutil.Big(*big.NewInt(6)),
			From:     common.NewMixedcaseAddress(a),
			To:       &to,
932
			GasPrice: (*hexutil.Big)(big.NewInt(5)),
933 934 935 936 937 938 939 940 941
			Gas:      1000,
			Input:    nil,
		}
		_, err := api.SignTransaction(ctx, tx, nil)
		expectDeny("signtransaction [1]", err)
		expectResponse("signtransaction [2]", "Did you see any warnings for the last transaction? (yes/no)", "no")
	}
	{ // Listing
		api.UI.ShowInfo("Please reject listing-request")
942
		time.Sleep(delay)
943 944 945 946 947
		_, err := api.List(ctx)
		expectDeny("list", err)
	}
	{ // Import
		api.UI.ShowInfo("Please reject new account-request")
948
		time.Sleep(delay)
949 950 951 952 953
		_, err := api.New(ctx)
		expectDeny("newaccount", err)
	}
	{ // Metadata
		api.UI.ShowInfo("Please check if you see the Origin in next listing (approve or deny)")
954
		time.Sleep(delay)
955 956 957 958 959 960 961 962 963 964
		api.List(context.WithValue(ctx, "Origin", "origin.com"))
		expectResponse("metadata - origin", "Did you see origin (origin.com)? [yes/no] ", "yes")
	}

	for _, e := range errs {
		log.Error(e)
	}
	result := fmt.Sprintf("Tests completed. %d errors:\n%s\n", len(errs), strings.Join(errs, "\n"))
	api.UI.ShowInfo(result)

965 966
}

967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998
type encryptedSeedStorage struct {
	Description string              `json:"description"`
	Version     int                 `json:"version"`
	Params      keystore.CryptoJSON `json:"params"`
}

// encryptSeed uses a similar scheme as the keystore uses, but with a different wrapping,
// to encrypt the master seed
func encryptSeed(seed []byte, auth []byte, scryptN, scryptP int) ([]byte, error) {
	cryptoStruct, err := keystore.EncryptDataV3(seed, auth, scryptN, scryptP)
	if err != nil {
		return nil, err
	}
	return json.Marshal(&encryptedSeedStorage{"Clef seed", 1, cryptoStruct})
}

// decryptSeed decrypts the master seed
func decryptSeed(keyjson []byte, auth string) ([]byte, error) {
	var encSeed encryptedSeedStorage
	if err := json.Unmarshal(keyjson, &encSeed); err != nil {
		return nil, err
	}
	if encSeed.Version != 1 {
		log.Warn(fmt.Sprintf("unsupported encryption format of seed: %d, operation will likely fail", encSeed.Version))
	}
	seed, err := keystore.DecryptDataV3(encSeed.Params, auth)
	if err != nil {
		return nil, err
	}
	return seed, err
}

999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016
// GenDoc outputs examples of all structures used in json-rpc communication
func GenDoc(ctx *cli.Context) {

	var (
		a    = common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef")
		b    = common.HexToAddress("0x1111111122222222222233333333334444444444")
		meta = core.Metadata{
			Scheme:    "http",
			Local:     "localhost:8545",
			Origin:    "www.malicious.ru",
			Remote:    "localhost:9999",
			UserAgent: "Firefox 3.2",
		}
		output []string
		add    = func(name, desc string, v interface{}) {
			if data, err := json.MarshalIndent(v, "", "  "); err == nil {
				output = append(output, fmt.Sprintf("### %s\n\n%s\n\nExample:\n```json\n%s\n```", name, desc, data))
			} else {
S
Satpal 已提交
1017
				log.Error("Error generating output", "err", err)
1018 1019 1020 1021 1022 1023 1024 1025 1026 1027
			}
		}
	)

	{ // Sign plain text request
		desc := "SignDataRequest contains information about a pending request to sign some data. " +
			"The data to be signed can be of various types, defined by content-type. Clef has done most " +
			"of the work in canonicalizing and making sense of the data, and it's up to the UI to present" +
			"the user with the contents of the `message`"
		sighash, msg := accounts.TextAndHash([]byte("hello world"))
F
Felix Lange 已提交
1028
		messages := []*core.NameValueType{{Name: "message", Value: msg, Typ: accounts.MimetypeTextPlain}}
1029 1030 1031 1032 1033 1034

		add("SignDataRequest", desc, &core.SignDataRequest{
			Address:     common.NewMixedcaseAddress(a),
			Meta:        meta,
			ContentType: accounts.MimetypeTextPlain,
			Rawdata:     []byte(msg),
1035
			Messages:    messages,
1036 1037 1038 1039
			Hash:        sighash})
	}
	{ // Sign plain text response
		add("SignDataResponse - approve", "Response to SignDataRequest",
1040
			&core.SignDataResponse{Approved: true})
1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057
		add("SignDataResponse - deny", "Response to SignDataRequest",
			&core.SignDataResponse{})
	}
	{ // Sign transaction request
		desc := "SignTxRequest contains information about a pending request to sign a transaction. " +
			"Aside from the transaction itself, there is also a `call_info`-struct. That struct contains " +
			"messages of various types, that the user should be informed of." +
			"\n\n" +
			"As in any request, it's important to consider that the `meta` info also contains untrusted data." +
			"\n\n" +
			"The `transaction` (on input into clef) can have either `data` or `input` -- if both are set, " +
			"they must be identical, otherwise an error is generated. " +
			"However, Clef will always use `data` when passing this struct on (if Clef does otherwise, please file a ticket)"

		data := hexutil.Bytes([]byte{0x01, 0x02, 0x03, 0x04})
		add("SignTxRequest", desc, &core.SignTxRequest{
			Meta: meta,
1058
			Callinfo: []apitypes.ValidationInfo{
F
Felix Lange 已提交
1059 1060
				{Typ: "Warning", Message: "Something looks odd, show this message as a warning"},
				{Typ: "Info", Message: "User should see this as well"},
1061
			},
1062
			Transaction: apitypes.SendTxArgs{
1063 1064 1065 1066 1067
				Data:     &data,
				Nonce:    0x1,
				Value:    hexutil.Big(*big.NewInt(6)),
				From:     common.NewMixedcaseAddress(a),
				To:       nil,
1068
				GasPrice: (*hexutil.Big)(big.NewInt(5)),
1069 1070 1071 1072 1073 1074
				Gas:      1000,
				Input:    nil,
			}})
	}
	{ // Sign tx response
		data := hexutil.Bytes([]byte{0x04, 0x03, 0x02, 0x01})
1075
		add("SignTxResponse - approve", "Response to request to sign a transaction. This response needs to contain the `transaction`"+
1076
			", because the UI is free to make modifications to the transaction.",
1077
			&core.SignTxResponse{Approved: true,
1078
				Transaction: apitypes.SendTxArgs{
1079 1080 1081 1082 1083
					Data:     &data,
					Nonce:    0x4,
					Value:    hexutil.Big(*big.NewInt(6)),
					From:     common.NewMixedcaseAddress(a),
					To:       nil,
1084
					GasPrice: (*hexutil.Big)(big.NewInt(5)),
1085 1086 1087
					Gas:      1000,
					Input:    nil,
				}})
1088
		add("SignTxResponse - deny", "Response to SignTxRequest. When denying a request, there's no need to "+
1089
			"provide the transaction in return",
1090
			&core.SignTxResponse{})
1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108
	}
	{ // WHen a signed tx is ready to go out
		desc := "SignTransactionResult is used in the call `clef` -> `OnApprovedTx(result)`" +
			"\n\n" +
			"This occurs _after_ successful completion of the entire signing procedure, but right before the signed " +
			"transaction is passed to the external caller. This method (and data) can be used by the UI to signal " +
			"to the user that the transaction was signed, but it is primarily useful for ruleset implementations." +
			"\n\n" +
			"A ruleset that implements a rate limitation needs to know what transactions are sent out to the external " +
			"interface. By hooking into this methods, the ruleset can maintain track of that count." +
			"\n\n" +
			"**OBS:** Note that if an attacker can restore your `clef` data to a previous point in time" +
			" (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content. " +
			"\n\n" +
			"The `OnApproved` method cannot be responded to, it's purely informative"

		rlpdata := common.FromHex("0xf85d640101948a8eafb1cf62bfbeb1741769dae1a9dd47996192018026a0716bd90515acb1e68e5ac5867aa11a1e65399c3349d479f5fb698554ebc6f293a04e8a4ebfff434e971e0ef12c5bf3a881b06fd04fc3f8b8a7291fb67a26a1d4ed")
		var tx types.Transaction
1109
		tx.UnmarshalBinary(rlpdata)
1110 1111 1112 1113 1114 1115
		add("OnApproved - SignTransactionResult", desc, &ethapi.SignTransactionResult{Raw: rlpdata, Tx: &tx})

	}
	{ // User input
		add("UserInputRequest", "Sent when clef needs the user to provide data. If 'password' is true, the input field should be treated accordingly (echo-free)",
			&core.UserInputRequest{IsPassword: true, Title: "The title here", Prompt: "The question to ask the user"})
1116
		add("UserInputResponse", "Response to UserInputRequest",
1117 1118 1119 1120 1121 1122 1123 1124 1125
			&core.UserInputResponse{Text: "The textual response from user"})
	}
	{ // List request
		add("ListRequest", "Sent when a request has been made to list addresses. The UI is provided with the "+
			"full `account`s, including local directory names. Note: this information is not passed back to the external caller, "+
			"who only sees the `address`es. ",
			&core.ListRequest{
				Meta: meta,
				Accounts: []accounts.Account{
F
Felix Lange 已提交
1126 1127
					{Address: a, URL: accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/a"}},
					{Address: b, URL: accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/b"}}},
1128 1129
			})

1130
		add("ListResponse", "Response to list request. The response contains a list of all addresses to show to the caller. "+
1131 1132 1133
			"Note: the UI is free to respond with any address the caller, regardless of whether it exists or not",
			&core.ListResponse{
				Accounts: []accounts.Account{
F
Felix Lange 已提交
1134 1135 1136 1137 1138 1139 1140
					{
						Address: common.HexToAddress("0xcowbeef000000cowbeef00000000000000000c0w"),
						URL:     accounts.URL{Path: ".. ignored .."},
					},
					{
						Address: common.HexToAddress("0xffffffffffffffffffffffffffffffffffffffff"),
					},
1141 1142 1143 1144 1145 1146 1147 1148 1149 1150
				}})
	}

	fmt.Println(`## UI Client interface

These data types are defined in the channel between clef and the UI`)
	for _, elem := range output {
		fmt.Println(elem)
	}
}