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