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