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