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