diff options
author | Martin Holst Swende <martin@swende.se> | 2018-10-09 17:05:41 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-10-09 17:05:41 +0800 |
commit | d5c7a6056afdc8c3364b1774b5d2bc4a74b028a6 (patch) | |
tree | 3d29cc462f535517d76ff454087d139ea577393d /cmd/clef | |
parent | ff5538ad4c20677148ca43e1786fe67898b59425 (diff) | |
download | dexon-d5c7a6056afdc8c3364b1774b5d2bc4a74b028a6.tar.gz dexon-d5c7a6056afdc8c3364b1774b5d2bc4a74b028a6.tar.zst dexon-d5c7a6056afdc8c3364b1774b5d2bc4a74b028a6.zip |
cmd/clef: encrypt the master seed on disk (#17704)
* cmd/clef: encrypt master seed of clef
Signed-off-by: YaoZengzeng <yaozengzeng@zju.edu.cn>
* keystore: refactor for external use of encryption
* clef: utilize keystore encryption, check flags correctly
* clef: validate master password
* clef: add json wrapping around encrypted master seed
Diffstat (limited to 'cmd/clef')
-rw-r--r-- | cmd/clef/intapi_changelog.md | 5 | ||||
-rw-r--r-- | cmd/clef/main.go | 183 |
2 files changed, 141 insertions, 47 deletions
diff --git a/cmd/clef/intapi_changelog.md b/cmd/clef/intapi_changelog.md index 9e13f67d0..92a39a268 100644 --- a/cmd/clef/intapi_changelog.md +++ b/cmd/clef/intapi_changelog.md @@ -1,5 +1,9 @@ ### Changelog for internal API (ui-api) +### 3.0.0 + +* Make use of `OnInputRequired(info UserInputRequest)` for obtaining master password during startup + ### 2.1.0 * Add `OnInputRequired(info UserInputRequest)` to internal API. This method is used when Clef needs user input, e.g. passwords. @@ -14,7 +18,6 @@ The following structures are used: UserInputResponse struct { Text string `json:"text"` } -``` ### 2.0.0 diff --git a/cmd/clef/main.go b/cmd/clef/main.go index c060285be..6098b1ac2 100644 --- a/cmd/clef/main.go +++ b/cmd/clef/main.go @@ -35,8 +35,10 @@ import ( "runtime" "strings" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/console" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/node" @@ -48,10 +50,10 @@ import ( ) // ExternalAPIVersion -- see extapi_changelog.md -const ExternalAPIVersion = "3.0.0" +const ExternalAPIVersion = "4.0.0" // InternalAPIVersion -- see intapi_changelog.md -const InternalAPIVersion = "2.0.0" +const InternalAPIVersion = "3.0.0" const legalWarning = ` WARNING! @@ -91,7 +93,7 @@ var ( } signerSecretFlag = cli.StringFlag{ Name: "signersecret", - Usage: "A file containing the password used to encrypt Clef credentials, e.g. keystore credentials and ruleset hash", + Usage: "A file containing the (encrypted) master seed to encrypt Clef data, e.g. keystore credentials and ruleset hash", } dBFlag = cli.StringFlag{ Name: "4bytedb", @@ -212,25 +214,45 @@ func initializeSecrets(c *cli.Context) error { if err := initialize(c); err != nil { return err } - configDir := c.String(configdirFlag.Name) + configDir := c.GlobalString(configdirFlag.Name) masterSeed := make([]byte, 256) - n, err := io.ReadFull(rand.Reader, masterSeed) + num, err := io.ReadFull(rand.Reader, masterSeed) if err != nil { return err } - if n != len(masterSeed) { + if num != len(masterSeed) { return fmt.Errorf("failed to read enough random") } + + n, p := keystore.StandardScryptN, keystore.StandardScryptP + if c.GlobalBool(utils.LightKDFFlag.Name) { + n, p = keystore.LightScryptN, keystore.LightScryptP + } + text := "The master seed of clef is locked with a password. Please give a password. Do not forget this password." + var password string + for { + password = getPassPhrase(text, true) + if err := core.ValidatePasswordFormat(password); err != nil { + fmt.Printf("invalid password: %v\n", err) + } else { + break + } + } + cipherSeed, err := encryptSeed(masterSeed, []byte(password), n, p) + if err != nil { + return fmt.Errorf("failed to encrypt master seed: %v", err) + } + err = os.Mkdir(configDir, 0700) if err != nil && !os.IsExist(err) { return err } - location := filepath.Join(configDir, "secrets.dat") + location := filepath.Join(configDir, "masterseed.json") if _, err := os.Stat(location); err == nil { return fmt.Errorf("file %v already exists, will not overwrite", location) } - err = ioutil.WriteFile(location, masterSeed, 0400) + err = ioutil.WriteFile(location, cipherSeed, 0400) if err != nil { return err } @@ -255,11 +277,11 @@ func attestFile(ctx *cli.Context) error { return err } - stretchedKey, err := readMasterKey(ctx) + stretchedKey, err := readMasterKey(ctx, nil) if err != nil { utils.Fatalf(err.Error()) } - configDir := ctx.String(configdirFlag.Name) + configDir := ctx.GlobalString(configdirFlag.Name) vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) confKey := crypto.Keccak256([]byte("config"), stretchedKey) @@ -279,11 +301,11 @@ func addCredential(ctx *cli.Context) error { return err } - stretchedKey, err := readMasterKey(ctx) + stretchedKey, err := readMasterKey(ctx, nil) if err != nil { utils.Fatalf(err.Error()) } - configDir := ctx.String(configdirFlag.Name) + configDir := ctx.GlobalString(configdirFlag.Name) vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) @@ -302,7 +324,7 @@ func addCredential(ctx *cli.Context) error { func initialize(c *cli.Context) error { // Set up the logger to print everything logOutput := os.Stdout - if c.Bool(stdiouiFlag.Name) { + if c.GlobalBool(stdiouiFlag.Name) { logOutput = os.Stderr // If using the stdioui, we can't do the 'confirm'-flow fmt.Fprintf(logOutput, legalWarning) @@ -323,26 +345,28 @@ func signer(c *cli.Context) error { var ( ui core.SignerUI ) - if c.Bool(stdiouiFlag.Name) { + if c.GlobalBool(stdiouiFlag.Name) { log.Info("Using stdin/stdout as UI-channel") ui = core.NewStdIOUI() } else { log.Info("Using CLI as UI-channel") ui = core.NewCommandlineUI() } - db, err := core.NewAbiDBFromFiles(c.String(dBFlag.Name), c.String(customDBFlag.Name)) + fourByteDb := c.GlobalString(dBFlag.Name) + fourByteLocal := c.GlobalString(customDBFlag.Name) + db, err := core.NewAbiDBFromFiles(fourByteDb, fourByteLocal) if err != nil { utils.Fatalf(err.Error()) } - log.Info("Loaded 4byte db", "signatures", db.Size(), "file", c.String("4bytedb")) + log.Info("Loaded 4byte db", "signatures", db.Size(), "file", fourByteDb, "local", fourByteLocal) var ( api core.ExternalAPI ) - configDir := c.String(configdirFlag.Name) - if stretchedKey, err := readMasterKey(c); err != nil { - log.Info("No master seed provided, rules disabled") + configDir := c.GlobalString(configdirFlag.Name) + if stretchedKey, err := readMasterKey(c, ui); err != nil { + log.Info("No master seed provided, rules disabled", "error", err) } else { if err != nil { @@ -361,7 +385,7 @@ func signer(c *cli.Context) error { configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey) //Do we have a rule-file? - ruleJS, err := ioutil.ReadFile(c.String(ruleFlag.Name)) + ruleJS, err := ioutil.ReadFile(c.GlobalString(ruleFlag.Name)) if err != nil { log.Info("Could not load rulefile, rules not enabled", "file", "rulefile") } else { @@ -385,17 +409,15 @@ func signer(c *cli.Context) error { } apiImpl := core.NewSignerAPI( - c.Int64(utils.NetworkIdFlag.Name), - c.String(keystoreFlag.Name), - c.Bool(utils.NoUSBFlag.Name), + c.GlobalInt64(utils.NetworkIdFlag.Name), + c.GlobalString(keystoreFlag.Name), + c.GlobalBool(utils.NoUSBFlag.Name), ui, db, - c.Bool(utils.LightKDFFlag.Name), - c.Bool(advancedMode.Name)) - + c.GlobalBool(utils.LightKDFFlag.Name), + c.GlobalBool(advancedMode.Name)) api = apiImpl - // Audit logging - if logfile := c.String(auditLogFlag.Name); logfile != "" { + if logfile := c.GlobalString(auditLogFlag.Name); logfile != "" { api, err = core.NewAuditLogger(logfile, api) if err != nil { utils.Fatalf(err.Error()) @@ -414,13 +436,13 @@ func signer(c *cli.Context) error { Service: api, Version: "1.0"}, } - if c.Bool(utils.RPCEnabledFlag.Name) { + if c.GlobalBool(utils.RPCEnabledFlag.Name) { vhosts := splitAndTrim(c.GlobalString(utils.RPCVirtualHostsFlag.Name)) cors := splitAndTrim(c.GlobalString(utils.RPCCORSDomainFlag.Name)) // start http server - httpEndpoint := fmt.Sprintf("%s:%d", c.String(utils.RPCListenAddrFlag.Name), c.Int(rpcPortFlag.Name)) + httpEndpoint := fmt.Sprintf("%s:%d", c.GlobalString(utils.RPCListenAddrFlag.Name), c.Int(rpcPortFlag.Name)) listener, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcAPI, []string{"account"}, cors, vhosts, rpc.DefaultHTTPTimeouts) if err != nil { utils.Fatalf("Could not start RPC api: %v", err) @@ -434,9 +456,9 @@ func signer(c *cli.Context) error { }() } - if !c.Bool(utils.IPCDisabledFlag.Name) { + if !c.GlobalBool(utils.IPCDisabledFlag.Name) { if c.IsSet(utils.IPCPathFlag.Name) { - ipcapiURL = c.String(utils.IPCPathFlag.Name) + ipcapiURL = c.GlobalString(utils.IPCPathFlag.Name) } else { ipcapiURL = filepath.Join(configDir, "clef.ipc") } @@ -453,7 +475,7 @@ func signer(c *cli.Context) error { } - if c.Bool(testFlag.Name) { + if c.GlobalBool(testFlag.Name) { log.Info("Performing UI test") go testExternalUI(apiImpl) } @@ -512,36 +534,52 @@ func homeDir() string { } return "" } -func readMasterKey(ctx *cli.Context) ([]byte, error) { +func readMasterKey(ctx *cli.Context, ui core.SignerUI) ([]byte, error) { var ( file string - configDir = ctx.String(configdirFlag.Name) + configDir = ctx.GlobalString(configdirFlag.Name) ) - if ctx.IsSet(signerSecretFlag.Name) { - file = ctx.String(signerSecretFlag.Name) + if ctx.GlobalIsSet(signerSecretFlag.Name) { + file = ctx.GlobalString(signerSecretFlag.Name) } else { - file = filepath.Join(configDir, "secrets.dat") + file = filepath.Join(configDir, "masterseed.json") } if err := checkFile(file); err != nil { return nil, err } - masterKey, err := ioutil.ReadFile(file) + cipherKey, err := ioutil.ReadFile(file) if err != nil { return nil, err } - if len(masterKey) < 256 { - return nil, fmt.Errorf("master key of insufficient length, expected >255 bytes, got %d", len(masterKey)) + var password string + // If ui is not nil, get the password from ui. + if ui != nil { + resp, err := ui.OnInputRequired(core.UserInputRequest{ + Title: "Master Password", + Prompt: "Please enter the password to decrypt the master seed", + IsPassword: true}) + if err != nil { + return nil, err + } + password = resp.Text + } else { + password = getPassPhrase("Decrypt master seed of clef", false) + } + masterSeed, err := decryptSeed(cipherKey, password) + if err != nil { + return nil, fmt.Errorf("failed to decrypt the master seed of clef") + } + if len(masterSeed) < 256 { + return nil, fmt.Errorf("master seed of insufficient length, expected >255 bytes, got %d", len(masterSeed)) } + // Create vault location - vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterKey)[:10])) + vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterSeed)[:10])) err = os.Mkdir(vaultLocation, 0700) if err != nil && !os.IsExist(err) { return nil, err } - //!TODO, use KDF to stretch the master key - // stretched_key := stretch_key(master_key) - - return masterKey, nil + return masterSeed, nil } // checkFile is a convenience function to check if a file @@ -619,6 +657,59 @@ func testExternalUI(api *core.SignerAPI) { } +// getPassPhrase retrieves the password associated with clef, either fetched +// from a list of preloaded passphrases, or requested interactively from the user. +// TODO: there are many `getPassPhrase` functions, it will be better to abstract them into one. +func getPassPhrase(prompt string, confirmation bool) string { + fmt.Println(prompt) + password, err := console.Stdin.PromptPassword("Passphrase: ") + if err != nil { + utils.Fatalf("Failed to read passphrase: %v", err) + } + if confirmation { + confirm, err := console.Stdin.PromptPassword("Repeat passphrase: ") + if err != nil { + utils.Fatalf("Failed to read passphrase confirmation: %v", err) + } + if password != confirm { + utils.Fatalf("Passphrases do not match") + } + } + return password +} + +type encryptedSeedStorage struct { + Description string `json:"description"` + Version int `json:"version"` + Params keystore.CryptoJSON `json:"params"` +} + +// encryptSeed uses a similar scheme as the keystore uses, but with a different wrapping, +// to encrypt the master seed +func encryptSeed(seed []byte, auth []byte, scryptN, scryptP int) ([]byte, error) { + cryptoStruct, err := keystore.EncryptDataV3(seed, auth, scryptN, scryptP) + if err != nil { + return nil, err + } + return json.Marshal(&encryptedSeedStorage{"Clef seed", 1, cryptoStruct}) +} + +// decryptSeed decrypts the master seed +func decryptSeed(keyjson []byte, auth string) ([]byte, error) { + var encSeed encryptedSeedStorage + if err := json.Unmarshal(keyjson, &encSeed); err != nil { + return nil, err + } + if encSeed.Version != 1 { + log.Warn(fmt.Sprintf("unsupported encryption format of seed: %d, operation will likely fail", encSeed.Version)) + } + seed, err := keystore.DecryptDataV3(encSeed.Params, auth) + if err != nil { + return nil, err + } + return seed, err +} + /** //Create Account |