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