github.com/daeglee/go-ethereum@v0.0.0-20190504220456-cad3e8d18e9b/cmd/clef/main.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // This file is part of go-ethereum. 3 // 4 // go-ethereum is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // go-ethereum is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. 16 17 // signer is a utility that can be used so sign transactions and 18 // arbitrary data. 19 package main 20 21 import ( 22 "bufio" 23 "context" 24 "crypto/rand" 25 "crypto/sha256" 26 "encoding/hex" 27 "encoding/json" 28 "fmt" 29 "io" 30 "io/ioutil" 31 "math/big" 32 "os" 33 "os/signal" 34 "os/user" 35 "path/filepath" 36 "runtime" 37 "strings" 38 39 "github.com/ethereum/go-ethereum/accounts" 40 "github.com/ethereum/go-ethereum/accounts/keystore" 41 "github.com/ethereum/go-ethereum/cmd/utils" 42 "github.com/ethereum/go-ethereum/common" 43 "github.com/ethereum/go-ethereum/common/hexutil" 44 "github.com/ethereum/go-ethereum/console" 45 "github.com/ethereum/go-ethereum/core/types" 46 "github.com/ethereum/go-ethereum/crypto" 47 "github.com/ethereum/go-ethereum/internal/ethapi" 48 "github.com/ethereum/go-ethereum/log" 49 "github.com/ethereum/go-ethereum/node" 50 "github.com/ethereum/go-ethereum/params" 51 "github.com/ethereum/go-ethereum/rlp" 52 "github.com/ethereum/go-ethereum/rpc" 53 "github.com/ethereum/go-ethereum/signer/core" 54 "github.com/ethereum/go-ethereum/signer/rules" 55 "github.com/ethereum/go-ethereum/signer/storage" 56 "gopkg.in/urfave/cli.v1" 57 ) 58 59 const legalWarning = ` 60 WARNING! 61 62 Clef is alpha software, and not yet publically released. This software has _not_ been audited, and there 63 are no guarantees about the workings of this software. It may contain severe flaws. You should not use this software 64 unless you agree to take full responsibility for doing so, and know what you are doing. 65 66 TLDR; THIS IS NOT PRODUCTION-READY SOFTWARE! 67 68 ` 69 70 var ( 71 logLevelFlag = cli.IntFlag{ 72 Name: "loglevel", 73 Value: 4, 74 Usage: "log level to emit to the screen", 75 } 76 advancedMode = cli.BoolFlag{ 77 Name: "advanced", 78 Usage: "If enabled, issues warnings instead of rejections for suspicious requests. Default off", 79 } 80 keystoreFlag = cli.StringFlag{ 81 Name: "keystore", 82 Value: filepath.Join(node.DefaultDataDir(), "keystore"), 83 Usage: "Directory for the keystore", 84 } 85 configdirFlag = cli.StringFlag{ 86 Name: "configdir", 87 Value: DefaultConfigDir(), 88 Usage: "Directory for Clef configuration", 89 } 90 chainIdFlag = cli.Int64Flag{ 91 Name: "chainid", 92 Value: params.MainnetChainConfig.ChainID.Int64(), 93 Usage: "Chain id to use for signing (1=mainnet, 3=ropsten, 4=rinkeby, 5=Goerli)", 94 } 95 rpcPortFlag = cli.IntFlag{ 96 Name: "rpcport", 97 Usage: "HTTP-RPC server listening port", 98 Value: node.DefaultHTTPPort + 5, 99 } 100 signerSecretFlag = cli.StringFlag{ 101 Name: "signersecret", 102 Usage: "A file containing the (encrypted) master seed to encrypt Clef data, e.g. keystore credentials and ruleset hash", 103 } 104 dBFlag = cli.StringFlag{ 105 Name: "4bytedb", 106 Usage: "File containing 4byte-identifiers", 107 Value: "./4byte.json", 108 } 109 customDBFlag = cli.StringFlag{ 110 Name: "4bytedb-custom", 111 Usage: "File used for writing new 4byte-identifiers submitted via API", 112 Value: "./4byte-custom.json", 113 } 114 auditLogFlag = cli.StringFlag{ 115 Name: "auditlog", 116 Usage: "File used to emit audit logs. Set to \"\" to disable", 117 Value: "audit.log", 118 } 119 ruleFlag = cli.StringFlag{ 120 Name: "rules", 121 Usage: "Enable rule-engine", 122 Value: "", 123 } 124 stdiouiFlag = cli.BoolFlag{ 125 Name: "stdio-ui", 126 Usage: "Use STDIN/STDOUT as a channel for an external UI. " + 127 "This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " + 128 "interface, and can be used when Clef is started by an external process.", 129 } 130 testFlag = cli.BoolFlag{ 131 Name: "stdio-ui-test", 132 Usage: "Mechanism to test interface between Clef and UI. Requires 'stdio-ui'.", 133 } 134 app = cli.NewApp() 135 initCommand = cli.Command{ 136 Action: utils.MigrateFlags(initializeSecrets), 137 Name: "init", 138 Usage: "Initialize the signer, generate secret storage", 139 ArgsUsage: "", 140 Flags: []cli.Flag{ 141 logLevelFlag, 142 configdirFlag, 143 }, 144 Description: ` 145 The init command generates a master seed which Clef can use to store credentials and data needed for 146 the rule-engine to work.`, 147 } 148 attestCommand = cli.Command{ 149 Action: utils.MigrateFlags(attestFile), 150 Name: "attest", 151 Usage: "Attest that a js-file is to be used", 152 ArgsUsage: "<sha256sum>", 153 Flags: []cli.Flag{ 154 logLevelFlag, 155 configdirFlag, 156 signerSecretFlag, 157 }, 158 Description: ` 159 The attest command stores the sha256 of the rule.js-file that you want to use for automatic processing of 160 incoming requests. 161 162 Whenever you make an edit to the rule file, you need to use attestation to tell 163 Clef that the file is 'safe' to execute.`, 164 } 165 166 setCredentialCommand = cli.Command{ 167 Action: utils.MigrateFlags(setCredential), 168 Name: "setpw", 169 Usage: "Store a credential for a keystore file", 170 ArgsUsage: "<address>", 171 Flags: []cli.Flag{ 172 logLevelFlag, 173 configdirFlag, 174 signerSecretFlag, 175 }, 176 Description: ` 177 The setpw command stores a password for a given address (keyfile). If you enter a blank passphrase, it will 178 remove any stored credential for that address (keyfile) 179 `} 180 gendocCommand = cli.Command{ 181 Action: GenDoc, 182 Name: "gendoc", 183 Usage: "Generate documentation about json-rpc format", 184 Description: ` 185 The gendoc generates example structures of the json-rpc communication types. 186 `} 187 ) 188 189 func init() { 190 app.Name = "Clef" 191 app.Usage = "Manage Ethereum account operations" 192 app.Flags = []cli.Flag{ 193 logLevelFlag, 194 keystoreFlag, 195 configdirFlag, 196 chainIdFlag, 197 utils.LightKDFFlag, 198 utils.NoUSBFlag, 199 utils.RPCListenAddrFlag, 200 utils.RPCVirtualHostsFlag, 201 utils.IPCDisabledFlag, 202 utils.IPCPathFlag, 203 utils.RPCEnabledFlag, 204 rpcPortFlag, 205 signerSecretFlag, 206 dBFlag, 207 customDBFlag, 208 auditLogFlag, 209 ruleFlag, 210 stdiouiFlag, 211 testFlag, 212 advancedMode, 213 } 214 app.Action = signer 215 app.Commands = []cli.Command{initCommand, attestCommand, setCredentialCommand, gendocCommand} 216 217 } 218 func main() { 219 if err := app.Run(os.Args); err != nil { 220 fmt.Fprintln(os.Stderr, err) 221 os.Exit(1) 222 } 223 } 224 225 func initializeSecrets(c *cli.Context) error { 226 if err := initialize(c); err != nil { 227 return err 228 } 229 configDir := c.GlobalString(configdirFlag.Name) 230 231 masterSeed := make([]byte, 256) 232 num, err := io.ReadFull(rand.Reader, masterSeed) 233 if err != nil { 234 return err 235 } 236 if num != len(masterSeed) { 237 return fmt.Errorf("failed to read enough random") 238 } 239 240 n, p := keystore.StandardScryptN, keystore.StandardScryptP 241 if c.GlobalBool(utils.LightKDFFlag.Name) { 242 n, p = keystore.LightScryptN, keystore.LightScryptP 243 } 244 text := "The master seed of clef is locked with a password. Please give a password. Do not forget this password." 245 var password string 246 for { 247 password = getPassPhrase(text, true) 248 if err := core.ValidatePasswordFormat(password); err != nil { 249 fmt.Printf("invalid password: %v\n", err) 250 } else { 251 break 252 } 253 } 254 cipherSeed, err := encryptSeed(masterSeed, []byte(password), n, p) 255 if err != nil { 256 return fmt.Errorf("failed to encrypt master seed: %v", err) 257 } 258 259 err = os.Mkdir(configDir, 0700) 260 if err != nil && !os.IsExist(err) { 261 return err 262 } 263 location := filepath.Join(configDir, "masterseed.json") 264 if _, err := os.Stat(location); err == nil { 265 return fmt.Errorf("file %v already exists, will not overwrite", location) 266 } 267 err = ioutil.WriteFile(location, cipherSeed, 0400) 268 if err != nil { 269 return err 270 } 271 fmt.Printf("A master seed has been generated into %s\n", location) 272 fmt.Printf(` 273 This is required to be able to store credentials, such as : 274 * Passwords for keystores (used by rule engine) 275 * Storage for javascript rules 276 * Hash of rule-file 277 278 You should treat that file with utmost secrecy, and make a backup of it. 279 NOTE: This file does not contain your accounts. Those need to be backed up separately! 280 281 `) 282 return nil 283 } 284 func attestFile(ctx *cli.Context) error { 285 if len(ctx.Args()) < 1 { 286 utils.Fatalf("This command requires an argument.") 287 } 288 if err := initialize(ctx); err != nil { 289 return err 290 } 291 292 stretchedKey, err := readMasterKey(ctx, nil) 293 if err != nil { 294 utils.Fatalf(err.Error()) 295 } 296 configDir := ctx.GlobalString(configdirFlag.Name) 297 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 298 confKey := crypto.Keccak256([]byte("config"), stretchedKey) 299 300 // Initialize the encrypted storages 301 configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confKey) 302 val := ctx.Args().First() 303 configStorage.Put("ruleset_sha256", val) 304 log.Info("Ruleset attestation updated", "sha256", val) 305 return nil 306 } 307 308 func setCredential(ctx *cli.Context) error { 309 if len(ctx.Args()) < 1 { 310 utils.Fatalf("This command requires an address to be passed as an argument.") 311 } 312 if err := initialize(ctx); err != nil { 313 return err 314 } 315 316 address := ctx.Args().First() 317 password := getPassPhrase("Enter a passphrase to store with this address.", true) 318 319 stretchedKey, err := readMasterKey(ctx, nil) 320 if err != nil { 321 utils.Fatalf(err.Error()) 322 } 323 configDir := ctx.GlobalString(configdirFlag.Name) 324 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 325 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 326 327 // Initialize the encrypted storages 328 pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 329 pwStorage.Put(address, password) 330 log.Info("Credential store updated", "key", address) 331 return nil 332 } 333 334 func initialize(c *cli.Context) error { 335 // Set up the logger to print everything 336 logOutput := os.Stdout 337 if c.GlobalBool(stdiouiFlag.Name) { 338 logOutput = os.Stderr 339 // If using the stdioui, we can't do the 'confirm'-flow 340 fmt.Fprintf(logOutput, legalWarning) 341 } else { 342 if !confirm(legalWarning) { 343 return fmt.Errorf("aborted by user") 344 } 345 } 346 347 log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int(logLevelFlag.Name)), log.StreamHandler(logOutput, log.TerminalFormat(true)))) 348 return nil 349 } 350 351 func signer(c *cli.Context) error { 352 if err := initialize(c); err != nil { 353 return err 354 } 355 var ( 356 ui core.UIClientAPI 357 ) 358 if c.GlobalBool(stdiouiFlag.Name) { 359 log.Info("Using stdin/stdout as UI-channel") 360 ui = core.NewStdIOUI() 361 } else { 362 log.Info("Using CLI as UI-channel") 363 ui = core.NewCommandlineUI() 364 } 365 fourByteDb := c.GlobalString(dBFlag.Name) 366 fourByteLocal := c.GlobalString(customDBFlag.Name) 367 db, err := core.NewAbiDBFromFiles(fourByteDb, fourByteLocal) 368 if err != nil { 369 utils.Fatalf(err.Error()) 370 } 371 log.Info("Loaded 4byte db", "signatures", db.Size(), "file", fourByteDb, "local", fourByteLocal) 372 373 var ( 374 api core.ExternalAPI 375 pwStorage storage.Storage = &storage.NoStorage{} 376 ) 377 378 configDir := c.GlobalString(configdirFlag.Name) 379 if stretchedKey, err := readMasterKey(c, ui); err != nil { 380 log.Info("No master seed provided, rules disabled", "error", err) 381 } else { 382 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 383 384 // Generate domain specific keys 385 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 386 jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey) 387 confkey := crypto.Keccak256([]byte("config"), stretchedKey) 388 389 // Initialize the encrypted storages 390 pwStorage = storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 391 jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey) 392 configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey) 393 394 //Do we have a rule-file? 395 if ruleFile := c.GlobalString(ruleFlag.Name); ruleFile != "" { 396 ruleJS, err := ioutil.ReadFile(c.GlobalString(ruleFile)) 397 if err != nil { 398 log.Info("Could not load rulefile, rules not enabled", "file", "rulefile") 399 } else { 400 shasum := sha256.Sum256(ruleJS) 401 foundShaSum := hex.EncodeToString(shasum[:]) 402 storedShasum := configStorage.Get("ruleset_sha256") 403 if storedShasum != foundShaSum { 404 log.Info("Could not validate ruleset hash, rules not enabled", "got", foundShaSum, "expected", storedShasum) 405 } else { 406 // Initialize rules 407 ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage) 408 if err != nil { 409 utils.Fatalf(err.Error()) 410 } 411 ruleEngine.Init(string(ruleJS)) 412 ui = ruleEngine 413 log.Info("Rule engine configured", "file", c.String(ruleFlag.Name)) 414 } 415 } 416 } 417 } 418 var ( 419 chainId = c.GlobalInt64(chainIdFlag.Name) 420 ksLoc = c.GlobalString(keystoreFlag.Name) 421 lightKdf = c.GlobalBool(utils.LightKDFFlag.Name) 422 advanced = c.GlobalBool(advancedMode.Name) 423 nousb = c.GlobalBool(utils.NoUSBFlag.Name) 424 ) 425 log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc, 426 "light-kdf", lightKdf, "advanced", advanced) 427 am := core.StartClefAccountManager(ksLoc, nousb, lightKdf) 428 apiImpl := core.NewSignerAPI(am, chainId, nousb, ui, db, advanced, pwStorage) 429 430 // Establish the bidirectional communication, by creating a new UI backend and registering 431 // it with the UI. 432 ui.RegisterUIServer(core.NewUIServerAPI(apiImpl)) 433 api = apiImpl 434 // Audit logging 435 if logfile := c.GlobalString(auditLogFlag.Name); logfile != "" { 436 api, err = core.NewAuditLogger(logfile, api) 437 if err != nil { 438 utils.Fatalf(err.Error()) 439 } 440 log.Info("Audit logs configured", "file", logfile) 441 } 442 // register signer API with server 443 var ( 444 extapiURL = "n/a" 445 ipcapiURL = "n/a" 446 ) 447 rpcAPI := []rpc.API{ 448 { 449 Namespace: "account", 450 Public: true, 451 Service: api, 452 Version: "1.0"}, 453 } 454 if c.GlobalBool(utils.RPCEnabledFlag.Name) { 455 456 vhosts := splitAndTrim(c.GlobalString(utils.RPCVirtualHostsFlag.Name)) 457 cors := splitAndTrim(c.GlobalString(utils.RPCCORSDomainFlag.Name)) 458 459 // start http server 460 httpEndpoint := fmt.Sprintf("%s:%d", c.GlobalString(utils.RPCListenAddrFlag.Name), c.Int(rpcPortFlag.Name)) 461 listener, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcAPI, []string{"account"}, cors, vhosts, rpc.DefaultHTTPTimeouts) 462 if err != nil { 463 utils.Fatalf("Could not start RPC api: %v", err) 464 } 465 extapiURL = fmt.Sprintf("http://%s", httpEndpoint) 466 log.Info("HTTP endpoint opened", "url", extapiURL) 467 468 defer func() { 469 listener.Close() 470 log.Info("HTTP endpoint closed", "url", httpEndpoint) 471 }() 472 473 } 474 if !c.GlobalBool(utils.IPCDisabledFlag.Name) { 475 if c.IsSet(utils.IPCPathFlag.Name) { 476 ipcapiURL = c.GlobalString(utils.IPCPathFlag.Name) 477 } else { 478 ipcapiURL = filepath.Join(configDir, "clef.ipc") 479 } 480 481 listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI) 482 if err != nil { 483 utils.Fatalf("Could not start IPC api: %v", err) 484 } 485 log.Info("IPC endpoint opened", "url", ipcapiURL) 486 defer func() { 487 listener.Close() 488 log.Info("IPC endpoint closed", "url", ipcapiURL) 489 }() 490 491 } 492 493 if c.GlobalBool(testFlag.Name) { 494 log.Info("Performing UI test") 495 go testExternalUI(apiImpl) 496 } 497 ui.OnSignerStartup(core.StartupInfo{ 498 Info: map[string]interface{}{ 499 "extapi_version": core.ExternalAPIVersion, 500 "intapi_version": core.InternalAPIVersion, 501 "extapi_http": extapiURL, 502 "extapi_ipc": ipcapiURL, 503 }, 504 }) 505 506 abortChan := make(chan os.Signal) 507 signal.Notify(abortChan, os.Interrupt) 508 509 sig := <-abortChan 510 log.Info("Exiting...", "signal", sig) 511 512 return nil 513 } 514 515 // splitAndTrim splits input separated by a comma 516 // and trims excessive white space from the substrings. 517 func splitAndTrim(input string) []string { 518 result := strings.Split(input, ",") 519 for i, r := range result { 520 result[i] = strings.TrimSpace(r) 521 } 522 return result 523 } 524 525 // DefaultConfigDir is the default config directory to use for the vaults and other 526 // persistence requirements. 527 func DefaultConfigDir() string { 528 // Try to place the data folder in the user's home dir 529 home := homeDir() 530 if home != "" { 531 if runtime.GOOS == "darwin" { 532 return filepath.Join(home, "Library", "Signer") 533 } else if runtime.GOOS == "windows" { 534 appdata := os.Getenv("APPDATA") 535 if appdata != "" { 536 return filepath.Join(appdata, "Signer") 537 } else { 538 return filepath.Join(home, "AppData", "Roaming", "Signer") 539 } 540 } else { 541 return filepath.Join(home, ".clef") 542 } 543 } 544 // As we cannot guess a stable location, return empty and handle later 545 return "" 546 } 547 548 func homeDir() string { 549 if home := os.Getenv("HOME"); home != "" { 550 return home 551 } 552 if usr, err := user.Current(); err == nil { 553 return usr.HomeDir 554 } 555 return "" 556 } 557 func readMasterKey(ctx *cli.Context, ui core.UIClientAPI) ([]byte, error) { 558 var ( 559 file string 560 configDir = ctx.GlobalString(configdirFlag.Name) 561 ) 562 if ctx.GlobalIsSet(signerSecretFlag.Name) { 563 file = ctx.GlobalString(signerSecretFlag.Name) 564 } else { 565 file = filepath.Join(configDir, "masterseed.json") 566 } 567 if err := checkFile(file); err != nil { 568 return nil, err 569 } 570 cipherKey, err := ioutil.ReadFile(file) 571 if err != nil { 572 return nil, err 573 } 574 var password string 575 // If ui is not nil, get the password from ui. 576 if ui != nil { 577 resp, err := ui.OnInputRequired(core.UserInputRequest{ 578 Title: "Master Password", 579 Prompt: "Please enter the password to decrypt the master seed", 580 IsPassword: true}) 581 if err != nil { 582 return nil, err 583 } 584 password = resp.Text 585 } else { 586 password = getPassPhrase("Decrypt master seed of clef", false) 587 } 588 masterSeed, err := decryptSeed(cipherKey, password) 589 if err != nil { 590 return nil, fmt.Errorf("failed to decrypt the master seed of clef") 591 } 592 if len(masterSeed) < 256 { 593 return nil, fmt.Errorf("master seed of insufficient length, expected >255 bytes, got %d", len(masterSeed)) 594 } 595 596 // Create vault location 597 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterSeed)[:10])) 598 err = os.Mkdir(vaultLocation, 0700) 599 if err != nil && !os.IsExist(err) { 600 return nil, err 601 } 602 return masterSeed, nil 603 } 604 605 // checkFile is a convenience function to check if a file 606 // * exists 607 // * is mode 0400 608 func checkFile(filename string) error { 609 info, err := os.Stat(filename) 610 if err != nil { 611 return fmt.Errorf("failed stat on %s: %v", filename, err) 612 } 613 // Check the unix permission bits 614 if info.Mode().Perm()&0377 != 0 { 615 return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String()) 616 } 617 return nil 618 } 619 620 // confirm displays a text and asks for user confirmation 621 func confirm(text string) bool { 622 fmt.Printf(text) 623 fmt.Printf("\nEnter 'ok' to proceed:\n>") 624 625 text, err := bufio.NewReader(os.Stdin).ReadString('\n') 626 if err != nil { 627 log.Crit("Failed to read user input", "err", err) 628 } 629 630 if text := strings.TrimSpace(text); text == "ok" { 631 return true 632 } 633 return false 634 } 635 636 func testExternalUI(api *core.SignerAPI) { 637 638 ctx := context.WithValue(context.Background(), "remote", "clef binary") 639 ctx = context.WithValue(ctx, "scheme", "in-proc") 640 ctx = context.WithValue(ctx, "local", "main") 641 errs := make([]string, 0) 642 643 a := common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") 644 645 queryUser := func(q string) string { 646 resp, err := api.UI.OnInputRequired(core.UserInputRequest{ 647 Title: "Testing", 648 Prompt: q, 649 }) 650 if err != nil { 651 errs = append(errs, err.Error()) 652 } 653 return resp.Text 654 } 655 expectResponse := func(testcase, question, expect string) { 656 if got := queryUser(question); got != expect { 657 errs = append(errs, fmt.Sprintf("%s: got %v, expected %v", testcase, got, expect)) 658 } 659 } 660 expectApprove := func(testcase string, err error) { 661 if err == nil || err == accounts.ErrUnknownAccount { 662 return 663 } 664 errs = append(errs, fmt.Sprintf("%v: expected no error, got %v", testcase, err.Error())) 665 } 666 expectDeny := func(testcase string, err error) { 667 if err == nil || err != core.ErrRequestDenied { 668 errs = append(errs, fmt.Sprintf("%v: expected ErrRequestDenied, got %v", testcase, err)) 669 } 670 } 671 672 // Test display of info and error 673 { 674 api.UI.ShowInfo("If you see this message, enter 'yes' to next question") 675 expectResponse("showinfo", "Did you see the message? [yes/no]", "yes") 676 api.UI.ShowError("If you see this message, enter 'yes' to the next question") 677 expectResponse("showerror", "Did you see the message? [yes/no]", "yes") 678 } 679 { // Sign data test - clique header 680 api.UI.ShowInfo("Please approve the next request for signing a clique header") 681 cliqueHeader := types.Header{ 682 common.HexToHash("0000H45H"), 683 common.HexToHash("0000H45H"), 684 common.HexToAddress("0000H45H"), 685 common.HexToHash("0000H00H"), 686 common.HexToHash("0000H45H"), 687 common.HexToHash("0000H45H"), 688 types.Bloom{}, 689 big.NewInt(1337), 690 big.NewInt(1337), 691 1338, 692 1338, 693 big.NewInt(1338), 694 []byte("Extra data Extra data Extra data Extra data Extra data Extra data Extra data Extra data"), 695 common.HexToHash("0x0000H45H"), 696 types.BlockNonce{}, 697 } 698 cliqueRlp, err := rlp.EncodeToBytes(cliqueHeader) 699 if err != nil { 700 utils.Fatalf("Should not error: %v", err) 701 } 702 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 703 _, err = api.SignData(ctx, accounts.MimetypeClique, *addr, hexutil.Encode(cliqueRlp)) 704 expectApprove("signdata - clique header", err) 705 } 706 { // Sign data test - plain text 707 api.UI.ShowInfo("Please approve the next request for signing text") 708 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 709 _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world"))) 710 expectApprove("signdata - text", err) 711 } 712 { // Sign data test - plain text reject 713 api.UI.ShowInfo("Please deny the next request for signing text") 714 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 715 _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world"))) 716 expectDeny("signdata - text", err) 717 } 718 { // Sign transaction 719 720 api.UI.ShowInfo("Please reject next transaction") 721 data := hexutil.Bytes([]byte{}) 722 to := common.NewMixedcaseAddress(a) 723 tx := core.SendTxArgs{ 724 Data: &data, 725 Nonce: 0x1, 726 Value: hexutil.Big(*big.NewInt(6)), 727 From: common.NewMixedcaseAddress(a), 728 To: &to, 729 GasPrice: hexutil.Big(*big.NewInt(5)), 730 Gas: 1000, 731 Input: nil, 732 } 733 _, err := api.SignTransaction(ctx, tx, nil) 734 expectDeny("signtransaction [1]", err) 735 expectResponse("signtransaction [2]", "Did you see any warnings for the last transaction? (yes/no)", "no") 736 } 737 { // Listing 738 api.UI.ShowInfo("Please reject listing-request") 739 _, err := api.List(ctx) 740 expectDeny("list", err) 741 } 742 { // Import 743 api.UI.ShowInfo("Please reject new account-request") 744 _, err := api.New(ctx) 745 expectDeny("newaccount", err) 746 } 747 { // Metadata 748 api.UI.ShowInfo("Please check if you see the Origin in next listing (approve or deny)") 749 api.List(context.WithValue(ctx, "Origin", "origin.com")) 750 expectResponse("metadata - origin", "Did you see origin (origin.com)? [yes/no] ", "yes") 751 } 752 753 for _, e := range errs { 754 log.Error(e) 755 } 756 result := fmt.Sprintf("Tests completed. %d errors:\n%s\n", len(errs), strings.Join(errs, "\n")) 757 api.UI.ShowInfo(result) 758 759 } 760 761 // getPassPhrase retrieves the password associated with clef, either fetched 762 // from a list of preloaded passphrases, or requested interactively from the user. 763 // TODO: there are many `getPassPhrase` functions, it will be better to abstract them into one. 764 func getPassPhrase(prompt string, confirmation bool) string { 765 fmt.Println(prompt) 766 password, err := console.Stdin.PromptPassword("Passphrase: ") 767 if err != nil { 768 utils.Fatalf("Failed to read passphrase: %v", err) 769 } 770 if confirmation { 771 confirm, err := console.Stdin.PromptPassword("Repeat passphrase: ") 772 if err != nil { 773 utils.Fatalf("Failed to read passphrase confirmation: %v", err) 774 } 775 if password != confirm { 776 utils.Fatalf("Passphrases do not match") 777 } 778 } 779 return password 780 } 781 782 type encryptedSeedStorage struct { 783 Description string `json:"description"` 784 Version int `json:"version"` 785 Params keystore.CryptoJSON `json:"params"` 786 } 787 788 // encryptSeed uses a similar scheme as the keystore uses, but with a different wrapping, 789 // to encrypt the master seed 790 func encryptSeed(seed []byte, auth []byte, scryptN, scryptP int) ([]byte, error) { 791 cryptoStruct, err := keystore.EncryptDataV3(seed, auth, scryptN, scryptP) 792 if err != nil { 793 return nil, err 794 } 795 return json.Marshal(&encryptedSeedStorage{"Clef seed", 1, cryptoStruct}) 796 } 797 798 // decryptSeed decrypts the master seed 799 func decryptSeed(keyjson []byte, auth string) ([]byte, error) { 800 var encSeed encryptedSeedStorage 801 if err := json.Unmarshal(keyjson, &encSeed); err != nil { 802 return nil, err 803 } 804 if encSeed.Version != 1 { 805 log.Warn(fmt.Sprintf("unsupported encryption format of seed: %d, operation will likely fail", encSeed.Version)) 806 } 807 seed, err := keystore.DecryptDataV3(encSeed.Params, auth) 808 if err != nil { 809 return nil, err 810 } 811 return seed, err 812 } 813 814 // GenDoc outputs examples of all structures used in json-rpc communication 815 func GenDoc(ctx *cli.Context) { 816 817 var ( 818 a = common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") 819 b = common.HexToAddress("0x1111111122222222222233333333334444444444") 820 meta = core.Metadata{ 821 Scheme: "http", 822 Local: "localhost:8545", 823 Origin: "www.malicious.ru", 824 Remote: "localhost:9999", 825 UserAgent: "Firefox 3.2", 826 } 827 output []string 828 add = func(name, desc string, v interface{}) { 829 if data, err := json.MarshalIndent(v, "", " "); err == nil { 830 output = append(output, fmt.Sprintf("### %s\n\n%s\n\nExample:\n```json\n%s\n```", name, desc, data)) 831 } else { 832 log.Error("Error generating output", err) 833 } 834 } 835 ) 836 837 { // Sign plain text request 838 desc := "SignDataRequest contains information about a pending request to sign some data. " + 839 "The data to be signed can be of various types, defined by content-type. Clef has done most " + 840 "of the work in canonicalizing and making sense of the data, and it's up to the UI to present" + 841 "the user with the contents of the `message`" 842 sighash, msg := accounts.TextAndHash([]byte("hello world")) 843 message := []*core.NameValueType{{"message", msg, accounts.MimetypeTextPlain}} 844 845 add("SignDataRequest", desc, &core.SignDataRequest{ 846 Address: common.NewMixedcaseAddress(a), 847 Meta: meta, 848 ContentType: accounts.MimetypeTextPlain, 849 Rawdata: []byte(msg), 850 Message: message, 851 Hash: sighash}) 852 } 853 { // Sign plain text response 854 add("SignDataResponse - approve", "Response to SignDataRequest", 855 &core.SignDataResponse{Approved: true}) 856 add("SignDataResponse - deny", "Response to SignDataRequest", 857 &core.SignDataResponse{}) 858 } 859 { // Sign transaction request 860 desc := "SignTxRequest contains information about a pending request to sign a transaction. " + 861 "Aside from the transaction itself, there is also a `call_info`-struct. That struct contains " + 862 "messages of various types, that the user should be informed of." + 863 "\n\n" + 864 "As in any request, it's important to consider that the `meta` info also contains untrusted data." + 865 "\n\n" + 866 "The `transaction` (on input into clef) can have either `data` or `input` -- if both are set, " + 867 "they must be identical, otherwise an error is generated. " + 868 "However, Clef will always use `data` when passing this struct on (if Clef does otherwise, please file a ticket)" 869 870 data := hexutil.Bytes([]byte{0x01, 0x02, 0x03, 0x04}) 871 add("SignTxRequest", desc, &core.SignTxRequest{ 872 Meta: meta, 873 Callinfo: []core.ValidationInfo{ 874 {"Warning", "Something looks odd, show this message as a warning"}, 875 {"Info", "User should see this aswell"}, 876 }, 877 Transaction: core.SendTxArgs{ 878 Data: &data, 879 Nonce: 0x1, 880 Value: hexutil.Big(*big.NewInt(6)), 881 From: common.NewMixedcaseAddress(a), 882 To: nil, 883 GasPrice: hexutil.Big(*big.NewInt(5)), 884 Gas: 1000, 885 Input: nil, 886 }}) 887 } 888 { // Sign tx response 889 data := hexutil.Bytes([]byte{0x04, 0x03, 0x02, 0x01}) 890 add("SignTxResponse - approve", "Response to request to sign a transaction. This response needs to contain the `transaction`"+ 891 ", because the UI is free to make modifications to the transaction.", 892 &core.SignTxResponse{Approved: true, 893 Transaction: core.SendTxArgs{ 894 Data: &data, 895 Nonce: 0x4, 896 Value: hexutil.Big(*big.NewInt(6)), 897 From: common.NewMixedcaseAddress(a), 898 To: nil, 899 GasPrice: hexutil.Big(*big.NewInt(5)), 900 Gas: 1000, 901 Input: nil, 902 }}) 903 add("SignTxResponse - deny", "Response to SignTxRequest. When denying a request, there's no need to "+ 904 "provide the transaction in return", 905 &core.SignTxResponse{}) 906 } 907 { // WHen a signed tx is ready to go out 908 desc := "SignTransactionResult is used in the call `clef` -> `OnApprovedTx(result)`" + 909 "\n\n" + 910 "This occurs _after_ successful completion of the entire signing procedure, but right before the signed " + 911 "transaction is passed to the external caller. This method (and data) can be used by the UI to signal " + 912 "to the user that the transaction was signed, but it is primarily useful for ruleset implementations." + 913 "\n\n" + 914 "A ruleset that implements a rate limitation needs to know what transactions are sent out to the external " + 915 "interface. By hooking into this methods, the ruleset can maintain track of that count." + 916 "\n\n" + 917 "**OBS:** Note that if an attacker can restore your `clef` data to a previous point in time" + 918 " (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content. " + 919 "\n\n" + 920 "The `OnApproved` method cannot be responded to, it's purely informative" 921 922 rlpdata := common.FromHex("0xf85d640101948a8eafb1cf62bfbeb1741769dae1a9dd47996192018026a0716bd90515acb1e68e5ac5867aa11a1e65399c3349d479f5fb698554ebc6f293a04e8a4ebfff434e971e0ef12c5bf3a881b06fd04fc3f8b8a7291fb67a26a1d4ed") 923 var tx types.Transaction 924 rlp.DecodeBytes(rlpdata, &tx) 925 add("OnApproved - SignTransactionResult", desc, ðapi.SignTransactionResult{Raw: rlpdata, Tx: &tx}) 926 927 } 928 { // User input 929 add("UserInputRequest", "Sent when clef needs the user to provide data. If 'password' is true, the input field should be treated accordingly (echo-free)", 930 &core.UserInputRequest{IsPassword: true, Title: "The title here", Prompt: "The question to ask the user"}) 931 add("UserInputResponse", "Response to UserInputRequest", 932 &core.UserInputResponse{Text: "The textual response from user"}) 933 } 934 { // List request 935 add("ListRequest", "Sent when a request has been made to list addresses. The UI is provided with the "+ 936 "full `account`s, including local directory names. Note: this information is not passed back to the external caller, "+ 937 "who only sees the `address`es. ", 938 &core.ListRequest{ 939 Meta: meta, 940 Accounts: []accounts.Account{ 941 {a, accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/a"}}, 942 {b, accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/b"}}}, 943 }) 944 945 add("ListResponse", "Response to list request. The response contains a list of all addresses to show to the caller. "+ 946 "Note: the UI is free to respond with any address the caller, regardless of whether it exists or not", 947 &core.ListResponse{ 948 Accounts: []accounts.Account{ 949 {common.HexToAddress("0xcowbeef000000cowbeef00000000000000000c0w"), accounts.URL{Path: ".. ignored .."}}, 950 {common.HexToAddress("0xffffffffffffffffffffffffffffffffffffffff"), accounts.URL{}}, 951 }}) 952 } 953 954 fmt.Println(`## UI Client interface 955 956 These data types are defined in the channel between clef and the UI`) 957 for _, elem := range output { 958 fmt.Println(elem) 959 } 960 } 961 962 /** 963 //Create Account 964 965 curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_new","params":["test"],"id":67}' localhost:8550 966 967 // List accounts 968 969 curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_list","params":[""],"id":67}' http://localhost:8550/ 970 971 // Make Transaction 972 // safeSend(0x12) 973 // 4401a6e40000000000000000000000000000000000000000000000000000000000000012 974 975 // supplied abi 976 curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x82A2A876D39022B3019932D30Cd9c97ad5616813","gas":"0x333","gasPrice":"0x123","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x10", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"},"test"],"id":67}' http://localhost:8550/ 977 978 // Not supplied 979 curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x82A2A876D39022B3019932D30Cd9c97ad5616813","gas":"0x333","gasPrice":"0x123","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x10", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"}],"id":67}' http://localhost:8550/ 980 981 // Sign data 982 983 curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_sign","params":["0x694267f14675d7e1b9494fd8d72fefe1755710fa","bazonk gaz baz"],"id":67}' http://localhost:8550/ 984 985 986 **/