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