github.com/hhwill/poc-eth@v0.0.0-20240218063348-3bb107c90dbf/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/ethereum/go-ethereum/accounts" 39 "github.com/ethereum/go-ethereum/accounts/keystore" 40 "github.com/ethereum/go-ethereum/cmd/utils" 41 "github.com/ethereum/go-ethereum/common" 42 "github.com/ethereum/go-ethereum/common/hexutil" 43 "github.com/ethereum/go-ethereum/console" 44 "github.com/ethereum/go-ethereum/core/types" 45 "github.com/ethereum/go-ethereum/crypto" 46 "github.com/ethereum/go-ethereum/internal/ethapi" 47 "github.com/ethereum/go-ethereum/log" 48 "github.com/ethereum/go-ethereum/node" 49 "github.com/ethereum/go-ethereum/params" 50 "github.com/ethereum/go-ethereum/rlp" 51 "github.com/ethereum/go-ethereum/rpc" 52 "github.com/ethereum/go-ethereum/signer/core" 53 "github.com/ethereum/go-ethereum/signer/fourbyte" 54 "github.com/ethereum/go-ethereum/signer/rules" 55 "github.com/ethereum/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 func signer(c *cli.Context) error { 408 // If we have some unrecognized command, bail out 409 if args := c.Args(); len(args) > 0 { 410 return fmt.Errorf("invalid command: %q", args[0]) 411 } 412 if err := initialize(c); err != nil { 413 return err 414 } 415 var ( 416 ui core.UIClientAPI 417 ) 418 if c.GlobalBool(stdiouiFlag.Name) { 419 log.Info("Using stdin/stdout as UI-channel") 420 ui = core.NewStdIOUI() 421 } else { 422 log.Info("Using CLI as UI-channel") 423 ui = core.NewCommandlineUI() 424 } 425 // 4bytedb data 426 fourByteLocal := c.GlobalString(customDBFlag.Name) 427 db, err := fourbyte.NewWithFile(fourByteLocal) 428 if err != nil { 429 utils.Fatalf(err.Error()) 430 } 431 embeds, locals := db.Size() 432 log.Info("Loaded 4byte database", "embeds", embeds, "locals", locals, "local", fourByteLocal) 433 434 var ( 435 api core.ExternalAPI 436 pwStorage storage.Storage = &storage.NoStorage{} 437 ) 438 439 configDir := c.GlobalString(configdirFlag.Name) 440 if stretchedKey, err := readMasterKey(c, ui); err != nil { 441 log.Warn("Failed to open master, rules disabled", "err", err) 442 } else { 443 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 444 445 // Generate domain specific keys 446 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 447 jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey) 448 confkey := crypto.Keccak256([]byte("config"), stretchedKey) 449 450 // Initialize the encrypted storages 451 pwStorage = storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 452 jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey) 453 configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey) 454 455 // Do we have a rule-file? 456 if ruleFile := c.GlobalString(ruleFlag.Name); ruleFile != "" { 457 ruleJS, err := ioutil.ReadFile(ruleFile) 458 if err != nil { 459 log.Warn("Could not load rules, disabling", "file", ruleFile, "err", err) 460 } else { 461 shasum := sha256.Sum256(ruleJS) 462 foundShaSum := hex.EncodeToString(shasum[:]) 463 storedShasum, _ := configStorage.Get("ruleset_sha256") 464 if storedShasum != foundShaSum { 465 log.Warn("Rule hash not attested, disabling", "hash", foundShaSum, "attested", storedShasum) 466 } else { 467 // Initialize rules 468 ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage) 469 if err != nil { 470 utils.Fatalf(err.Error()) 471 } 472 ruleEngine.Init(string(ruleJS)) 473 ui = ruleEngine 474 log.Info("Rule engine configured", "file", c.String(ruleFlag.Name)) 475 } 476 } 477 } 478 } 479 var ( 480 chainId = c.GlobalInt64(chainIdFlag.Name) 481 ksLoc = c.GlobalString(keystoreFlag.Name) 482 lightKdf = c.GlobalBool(utils.LightKDFFlag.Name) 483 advanced = c.GlobalBool(advancedMode.Name) 484 nousb = c.GlobalBool(utils.NoUSBFlag.Name) 485 scpath = c.GlobalString(utils.SmartCardDaemonPathFlag.Name) 486 ) 487 log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc, 488 "light-kdf", lightKdf, "advanced", advanced) 489 am := core.StartClefAccountManager(ksLoc, nousb, lightKdf, scpath) 490 apiImpl := core.NewSignerAPI(am, chainId, nousb, ui, db, advanced, pwStorage) 491 492 // Establish the bidirectional communication, by creating a new UI backend and registering 493 // it with the UI. 494 ui.RegisterUIServer(core.NewUIServerAPI(apiImpl)) 495 api = apiImpl 496 // Audit logging 497 if logfile := c.GlobalString(auditLogFlag.Name); logfile != "" { 498 api, err = core.NewAuditLogger(logfile, api) 499 if err != nil { 500 utils.Fatalf(err.Error()) 501 } 502 log.Info("Audit logs configured", "file", logfile) 503 } 504 // register signer API with server 505 var ( 506 extapiURL = "n/a" 507 ipcapiURL = "n/a" 508 ) 509 rpcAPI := []rpc.API{ 510 { 511 Namespace: "account", 512 Public: true, 513 Service: api, 514 Version: "1.0"}, 515 } 516 if c.GlobalBool(utils.RPCEnabledFlag.Name) { 517 vhosts := splitAndTrim(c.GlobalString(utils.RPCVirtualHostsFlag.Name)) 518 cors := splitAndTrim(c.GlobalString(utils.RPCCORSDomainFlag.Name)) 519 520 // start http server 521 httpEndpoint := fmt.Sprintf("%s:%d", c.GlobalString(utils.RPCListenAddrFlag.Name), c.Int(rpcPortFlag.Name)) 522 listener, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcAPI, []string{"account"}, cors, vhosts, rpc.DefaultHTTPTimeouts) 523 if err != nil { 524 utils.Fatalf("Could not start RPC api: %v", err) 525 } 526 extapiURL = fmt.Sprintf("http://%s", httpEndpoint) 527 log.Info("HTTP endpoint opened", "url", extapiURL) 528 529 defer func() { 530 listener.Close() 531 log.Info("HTTP endpoint closed", "url", httpEndpoint) 532 }() 533 } 534 if !c.GlobalBool(utils.IPCDisabledFlag.Name) { 535 if c.IsSet(utils.IPCPathFlag.Name) { 536 ipcapiURL = c.GlobalString(utils.IPCPathFlag.Name) 537 } else { 538 ipcapiURL = filepath.Join(configDir, "clef.ipc") 539 } 540 541 listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI) 542 if err != nil { 543 utils.Fatalf("Could not start IPC api: %v", err) 544 } 545 log.Info("IPC endpoint opened", "url", ipcapiURL) 546 defer func() { 547 listener.Close() 548 log.Info("IPC endpoint closed", "url", ipcapiURL) 549 }() 550 551 } 552 553 if c.GlobalBool(testFlag.Name) { 554 log.Info("Performing UI test") 555 go testExternalUI(apiImpl) 556 } 557 ui.OnSignerStartup(core.StartupInfo{ 558 Info: map[string]interface{}{ 559 "intapi_version": core.InternalAPIVersion, 560 "extapi_version": core.ExternalAPIVersion, 561 "extapi_http": extapiURL, 562 "extapi_ipc": ipcapiURL, 563 }, 564 }) 565 566 abortChan := make(chan os.Signal) 567 signal.Notify(abortChan, os.Interrupt) 568 569 sig := <-abortChan 570 log.Info("Exiting...", "signal", sig) 571 572 return nil 573 } 574 575 // splitAndTrim splits input separated by a comma 576 // and trims excessive white space from the substrings. 577 func splitAndTrim(input string) []string { 578 result := strings.Split(input, ",") 579 for i, r := range result { 580 result[i] = strings.TrimSpace(r) 581 } 582 return result 583 } 584 585 // DefaultConfigDir is the default config directory to use for the vaults and other 586 // persistence requirements. 587 func DefaultConfigDir() string { 588 // Try to place the data folder in the user's home dir 589 home := homeDir() 590 if home != "" { 591 if runtime.GOOS == "darwin" { 592 return filepath.Join(home, "Library", "Signer") 593 } else if runtime.GOOS == "windows" { 594 appdata := os.Getenv("APPDATA") 595 if appdata != "" { 596 return filepath.Join(appdata, "Signer") 597 } else { 598 return filepath.Join(home, "AppData", "Roaming", "Signer") 599 } 600 } else { 601 return filepath.Join(home, ".clef") 602 } 603 } 604 // As we cannot guess a stable location, return empty and handle later 605 return "" 606 } 607 608 func homeDir() string { 609 if home := os.Getenv("HOME"); home != "" { 610 return home 611 } 612 if usr, err := user.Current(); err == nil { 613 return usr.HomeDir 614 } 615 return "" 616 } 617 func readMasterKey(ctx *cli.Context, ui core.UIClientAPI) ([]byte, error) { 618 var ( 619 file string 620 configDir = ctx.GlobalString(configdirFlag.Name) 621 ) 622 if ctx.GlobalIsSet(signerSecretFlag.Name) { 623 file = ctx.GlobalString(signerSecretFlag.Name) 624 } else { 625 file = filepath.Join(configDir, "masterseed.json") 626 } 627 if err := checkFile(file); err != nil { 628 return nil, err 629 } 630 cipherKey, err := ioutil.ReadFile(file) 631 if err != nil { 632 return nil, err 633 } 634 var password string 635 // If ui is not nil, get the password from ui. 636 if ui != nil { 637 resp, err := ui.OnInputRequired(core.UserInputRequest{ 638 Title: "Master Password", 639 Prompt: "Please enter the password to decrypt the master seed", 640 IsPassword: true}) 641 if err != nil { 642 return nil, err 643 } 644 password = resp.Text 645 } else { 646 password = getPassPhrase("Decrypt master seed of clef", false) 647 } 648 masterSeed, err := decryptSeed(cipherKey, password) 649 if err != nil { 650 return nil, fmt.Errorf("failed to decrypt the master seed of clef") 651 } 652 if len(masterSeed) < 256 { 653 return nil, fmt.Errorf("master seed of insufficient length, expected >255 bytes, got %d", len(masterSeed)) 654 } 655 // Create vault location 656 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterSeed)[:10])) 657 err = os.Mkdir(vaultLocation, 0700) 658 if err != nil && !os.IsExist(err) { 659 return nil, err 660 } 661 return masterSeed, nil 662 } 663 664 // checkFile is a convenience function to check if a file 665 // * exists 666 // * is mode 0400 667 func checkFile(filename string) error { 668 info, err := os.Stat(filename) 669 if err != nil { 670 return fmt.Errorf("failed stat on %s: %v", filename, err) 671 } 672 // Check the unix permission bits 673 if info.Mode().Perm()&0377 != 0 { 674 return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String()) 675 } 676 return nil 677 } 678 679 // confirm displays a text and asks for user confirmation 680 func confirm(text string) bool { 681 fmt.Printf(text) 682 fmt.Printf("\nEnter 'ok' to proceed:\n> ") 683 684 text, err := bufio.NewReader(os.Stdin).ReadString('\n') 685 if err != nil { 686 log.Crit("Failed to read user input", "err", err) 687 } 688 if text := strings.TrimSpace(text); text == "ok" { 689 return true 690 } 691 return false 692 } 693 694 func testExternalUI(api *core.SignerAPI) { 695 696 ctx := context.WithValue(context.Background(), "remote", "clef binary") 697 ctx = context.WithValue(ctx, "scheme", "in-proc") 698 ctx = context.WithValue(ctx, "local", "main") 699 errs := make([]string, 0) 700 701 a := common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") 702 addErr := func(errStr string) { 703 log.Info("Test error", "err", errStr) 704 errs = append(errs, errStr) 705 } 706 707 queryUser := func(q string) string { 708 resp, err := api.UI.OnInputRequired(core.UserInputRequest{ 709 Title: "Testing", 710 Prompt: q, 711 }) 712 if err != nil { 713 addErr(err.Error()) 714 } 715 return resp.Text 716 } 717 expectResponse := func(testcase, question, expect string) { 718 if got := queryUser(question); got != expect { 719 addErr(fmt.Sprintf("%s: got %v, expected %v", testcase, got, expect)) 720 } 721 } 722 expectApprove := func(testcase string, err error) { 723 if err == nil || err == accounts.ErrUnknownAccount { 724 return 725 } 726 addErr(fmt.Sprintf("%v: expected no error, got %v", testcase, err.Error())) 727 } 728 expectDeny := func(testcase string, err error) { 729 if err == nil || err != core.ErrRequestDenied { 730 addErr(fmt.Sprintf("%v: expected ErrRequestDenied, got %v", testcase, err)) 731 } 732 } 733 var delay = 1 * time.Second 734 // Test display of info and error 735 { 736 api.UI.ShowInfo("If you see this message, enter 'yes' to next question") 737 time.Sleep(delay) 738 expectResponse("showinfo", "Did you see the message? [yes/no]", "yes") 739 api.UI.ShowError("If you see this message, enter 'yes' to the next question") 740 time.Sleep(delay) 741 expectResponse("showerror", "Did you see the message? [yes/no]", "yes") 742 } 743 { // Sign data test - clique header 744 api.UI.ShowInfo("Please approve the next request for signing a clique header") 745 time.Sleep(delay) 746 cliqueHeader := types.Header{ 747 common.HexToHash("0000H45H"), 748 common.HexToHash("0000H45H"), 749 common.HexToAddress("0000H45H"), 750 common.HexToHash("0000H00H"), 751 common.HexToHash("0000H45H"), 752 common.HexToHash("0000H45H"), 753 types.Bloom{}, 754 big.NewInt(1337), 755 big.NewInt(1337), 756 1338, 757 1338, 758 1338, 759 []byte("Extra data Extra data Extra data Extra data Extra data Extra data Extra data Extra data"), 760 common.HexToHash("0x0000H45H"), 761 types.BlockNonce{}, 762 } 763 cliqueRlp, err := rlp.EncodeToBytes(cliqueHeader) 764 if err != nil { 765 utils.Fatalf("Should not error: %v", err) 766 } 767 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 768 _, err = api.SignData(ctx, accounts.MimetypeClique, *addr, hexutil.Encode(cliqueRlp)) 769 expectApprove("signdata - clique header", err) 770 } 771 { // Sign data test - typed data 772 api.UI.ShowInfo("Please approve the next request for signing EIP-712 typed data") 773 time.Sleep(delay) 774 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 775 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!"}}` 776 //_, err := api.SignData(ctx, accounts.MimetypeTypedData, *addr, hexutil.Encode([]byte(data))) 777 var typedData core.TypedData 778 json.Unmarshal([]byte(data), &typedData) 779 _, err := api.SignTypedData(ctx, *addr, typedData) 780 expectApprove("sign 712 typed data", err) 781 } 782 { // Sign data test - plain text 783 api.UI.ShowInfo("Please approve the next request for signing text") 784 time.Sleep(delay) 785 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 786 _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world"))) 787 expectApprove("signdata - text", err) 788 } 789 { // Sign data test - plain text reject 790 api.UI.ShowInfo("Please deny the next request for signing text") 791 time.Sleep(delay) 792 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 793 _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world"))) 794 expectDeny("signdata - text", err) 795 } 796 { // Sign transaction 797 798 api.UI.ShowInfo("Please reject next transaction") 799 time.Sleep(delay) 800 data := hexutil.Bytes([]byte{}) 801 to := common.NewMixedcaseAddress(a) 802 tx := core.SendTxArgs{ 803 Data: &data, 804 Nonce: 0x1, 805 Value: hexutil.Big(*big.NewInt(6)), 806 From: common.NewMixedcaseAddress(a), 807 To: &to, 808 GasPrice: hexutil.Big(*big.NewInt(5)), 809 Gas: 1000, 810 Input: nil, 811 } 812 _, err := api.SignTransaction(ctx, tx, nil) 813 expectDeny("signtransaction [1]", err) 814 expectResponse("signtransaction [2]", "Did you see any warnings for the last transaction? (yes/no)", "no") 815 } 816 { // Listing 817 api.UI.ShowInfo("Please reject listing-request") 818 time.Sleep(delay) 819 _, err := api.List(ctx) 820 expectDeny("list", err) 821 } 822 { // Import 823 api.UI.ShowInfo("Please reject new account-request") 824 time.Sleep(delay) 825 _, err := api.New(ctx) 826 expectDeny("newaccount", err) 827 } 828 { // Metadata 829 api.UI.ShowInfo("Please check if you see the Origin in next listing (approve or deny)") 830 time.Sleep(delay) 831 api.List(context.WithValue(ctx, "Origin", "origin.com")) 832 expectResponse("metadata - origin", "Did you see origin (origin.com)? [yes/no] ", "yes") 833 } 834 835 for _, e := range errs { 836 log.Error(e) 837 } 838 result := fmt.Sprintf("Tests completed. %d errors:\n%s\n", len(errs), strings.Join(errs, "\n")) 839 api.UI.ShowInfo(result) 840 841 } 842 843 // getPassPhrase retrieves the password associated with clef, either fetched 844 // from a list of preloaded passphrases, or requested interactively from the user. 845 // TODO: there are many `getPassPhrase` functions, it will be better to abstract them into one. 846 func getPassPhrase(prompt string, confirmation bool) string { 847 fmt.Println(prompt) 848 password, err := console.Stdin.PromptPassword("Password: ") 849 if err != nil { 850 utils.Fatalf("Failed to read password: %v", err) 851 } 852 if confirmation { 853 confirm, err := console.Stdin.PromptPassword("Repeat password: ") 854 if err != nil { 855 utils.Fatalf("Failed to read password confirmation: %v", err) 856 } 857 if password != confirm { 858 utils.Fatalf("Passwords do not match") 859 } 860 } 861 return password 862 } 863 864 type encryptedSeedStorage struct { 865 Description string `json:"description"` 866 Version int `json:"version"` 867 Params keystore.CryptoJSON `json:"params"` 868 } 869 870 // encryptSeed uses a similar scheme as the keystore uses, but with a different wrapping, 871 // to encrypt the master seed 872 func encryptSeed(seed []byte, auth []byte, scryptN, scryptP int) ([]byte, error) { 873 cryptoStruct, err := keystore.EncryptDataV3(seed, auth, scryptN, scryptP) 874 if err != nil { 875 return nil, err 876 } 877 return json.Marshal(&encryptedSeedStorage{"Clef seed", 1, cryptoStruct}) 878 } 879 880 // decryptSeed decrypts the master seed 881 func decryptSeed(keyjson []byte, auth string) ([]byte, error) { 882 var encSeed encryptedSeedStorage 883 if err := json.Unmarshal(keyjson, &encSeed); err != nil { 884 return nil, err 885 } 886 if encSeed.Version != 1 { 887 log.Warn(fmt.Sprintf("unsupported encryption format of seed: %d, operation will likely fail", encSeed.Version)) 888 } 889 seed, err := keystore.DecryptDataV3(encSeed.Params, auth) 890 if err != nil { 891 return nil, err 892 } 893 return seed, err 894 } 895 896 // GenDoc outputs examples of all structures used in json-rpc communication 897 func GenDoc(ctx *cli.Context) { 898 899 var ( 900 a = common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") 901 b = common.HexToAddress("0x1111111122222222222233333333334444444444") 902 meta = core.Metadata{ 903 Scheme: "http", 904 Local: "localhost:8545", 905 Origin: "www.malicious.ru", 906 Remote: "localhost:9999", 907 UserAgent: "Firefox 3.2", 908 } 909 output []string 910 add = func(name, desc string, v interface{}) { 911 if data, err := json.MarshalIndent(v, "", " "); err == nil { 912 output = append(output, fmt.Sprintf("### %s\n\n%s\n\nExample:\n```json\n%s\n```", name, desc, data)) 913 } else { 914 log.Error("Error generating output", err) 915 } 916 } 917 ) 918 919 { // Sign plain text request 920 desc := "SignDataRequest contains information about a pending request to sign some data. " + 921 "The data to be signed can be of various types, defined by content-type. Clef has done most " + 922 "of the work in canonicalizing and making sense of the data, and it's up to the UI to present" + 923 "the user with the contents of the `message`" 924 sighash, msg := accounts.TextAndHash([]byte("hello world")) 925 messages := []*core.NameValueType{{"message", msg, accounts.MimetypeTextPlain}} 926 927 add("SignDataRequest", desc, &core.SignDataRequest{ 928 Address: common.NewMixedcaseAddress(a), 929 Meta: meta, 930 ContentType: accounts.MimetypeTextPlain, 931 Rawdata: []byte(msg), 932 Messages: messages, 933 Hash: sighash}) 934 } 935 { // Sign plain text response 936 add("SignDataResponse - approve", "Response to SignDataRequest", 937 &core.SignDataResponse{Approved: true}) 938 add("SignDataResponse - deny", "Response to SignDataRequest", 939 &core.SignDataResponse{}) 940 } 941 { // Sign transaction request 942 desc := "SignTxRequest contains information about a pending request to sign a transaction. " + 943 "Aside from the transaction itself, there is also a `call_info`-struct. That struct contains " + 944 "messages of various types, that the user should be informed of." + 945 "\n\n" + 946 "As in any request, it's important to consider that the `meta` info also contains untrusted data." + 947 "\n\n" + 948 "The `transaction` (on input into clef) can have either `data` or `input` -- if both are set, " + 949 "they must be identical, otherwise an error is generated. " + 950 "However, Clef will always use `data` when passing this struct on (if Clef does otherwise, please file a ticket)" 951 952 data := hexutil.Bytes([]byte{0x01, 0x02, 0x03, 0x04}) 953 add("SignTxRequest", desc, &core.SignTxRequest{ 954 Meta: meta, 955 Callinfo: []core.ValidationInfo{ 956 {"Warning", "Something looks odd, show this message as a warning"}, 957 {"Info", "User should see this aswell"}, 958 }, 959 Transaction: core.SendTxArgs{ 960 Data: &data, 961 Nonce: 0x1, 962 Value: hexutil.Big(*big.NewInt(6)), 963 From: common.NewMixedcaseAddress(a), 964 To: nil, 965 GasPrice: hexutil.Big(*big.NewInt(5)), 966 Gas: 1000, 967 Input: nil, 968 }}) 969 } 970 { // Sign tx response 971 data := hexutil.Bytes([]byte{0x04, 0x03, 0x02, 0x01}) 972 add("SignTxResponse - approve", "Response to request to sign a transaction. This response needs to contain the `transaction`"+ 973 ", because the UI is free to make modifications to the transaction.", 974 &core.SignTxResponse{Approved: true, 975 Transaction: core.SendTxArgs{ 976 Data: &data, 977 Nonce: 0x4, 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 add("SignTxResponse - deny", "Response to SignTxRequest. When denying a request, there's no need to "+ 986 "provide the transaction in return", 987 &core.SignTxResponse{}) 988 } 989 { // WHen a signed tx is ready to go out 990 desc := "SignTransactionResult is used in the call `clef` -> `OnApprovedTx(result)`" + 991 "\n\n" + 992 "This occurs _after_ successful completion of the entire signing procedure, but right before the signed " + 993 "transaction is passed to the external caller. This method (and data) can be used by the UI to signal " + 994 "to the user that the transaction was signed, but it is primarily useful for ruleset implementations." + 995 "\n\n" + 996 "A ruleset that implements a rate limitation needs to know what transactions are sent out to the external " + 997 "interface. By hooking into this methods, the ruleset can maintain track of that count." + 998 "\n\n" + 999 "**OBS:** Note that if an attacker can restore your `clef` data to a previous point in time" + 1000 " (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content. " + 1001 "\n\n" + 1002 "The `OnApproved` method cannot be responded to, it's purely informative" 1003 1004 rlpdata := common.FromHex("0xf85d640101948a8eafb1cf62bfbeb1741769dae1a9dd47996192018026a0716bd90515acb1e68e5ac5867aa11a1e65399c3349d479f5fb698554ebc6f293a04e8a4ebfff434e971e0ef12c5bf3a881b06fd04fc3f8b8a7291fb67a26a1d4ed") 1005 var tx types.Transaction 1006 rlp.DecodeBytes(rlpdata, &tx) 1007 add("OnApproved - SignTransactionResult", desc, ðapi.SignTransactionResult{Raw: rlpdata, Tx: &tx}) 1008 1009 } 1010 { // User input 1011 add("UserInputRequest", "Sent when clef needs the user to provide data. If 'password' is true, the input field should be treated accordingly (echo-free)", 1012 &core.UserInputRequest{IsPassword: true, Title: "The title here", Prompt: "The question to ask the user"}) 1013 add("UserInputResponse", "Response to UserInputRequest", 1014 &core.UserInputResponse{Text: "The textual response from user"}) 1015 } 1016 { // List request 1017 add("ListRequest", "Sent when a request has been made to list addresses. The UI is provided with the "+ 1018 "full `account`s, including local directory names. Note: this information is not passed back to the external caller, "+ 1019 "who only sees the `address`es. ", 1020 &core.ListRequest{ 1021 Meta: meta, 1022 Accounts: []accounts.Account{ 1023 {a, accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/a"}}, 1024 {b, accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/b"}}}, 1025 }) 1026 1027 add("ListResponse", "Response to list request. The response contains a list of all addresses to show to the caller. "+ 1028 "Note: the UI is free to respond with any address the caller, regardless of whether it exists or not", 1029 &core.ListResponse{ 1030 Accounts: []accounts.Account{ 1031 {common.HexToAddress("0xcowbeef000000cowbeef00000000000000000c0w"), accounts.URL{Path: ".. ignored .."}}, 1032 {common.HexToAddress("0xffffffffffffffffffffffffffffffffffffffff"), accounts.URL{}}, 1033 }}) 1034 } 1035 1036 fmt.Println(`## UI Client interface 1037 1038 These data types are defined in the channel between clef and the UI`) 1039 for _, elem := range output { 1040 fmt.Println(elem) 1041 } 1042 }