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