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