From d5c7a6056afdc8c3364b1774b5d2bc4a74b028a6 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Tue, 9 Oct 2018 11:05:41 +0200 Subject: [PATCH] cmd/clef: encrypt the master seed on disk (#17704) * cmd/clef: encrypt master seed of clef Signed-off-by: YaoZengzeng * 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 --- accounts/keystore/key.go | 6 +- accounts/keystore/keystore_passphrase.go | 98 ++++++------ cmd/clef/intapi_changelog.md | 5 +- cmd/clef/main.go | 183 +++++++++++++++++------ signer/core/api.go | 6 + signer/rules/rules_test.go | 13 ++ 6 files changed, 218 insertions(+), 93 deletions(-) diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 9e3e4856c4..0564751c43 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -66,19 +66,19 @@ type plainKeyJSON struct { type encryptedKeyJSONV3 struct { Address string `json:"address"` - Crypto cryptoJSON `json:"crypto"` + Crypto CryptoJSON `json:"crypto"` Id string `json:"id"` Version int `json:"version"` } type encryptedKeyJSONV1 struct { Address string `json:"address"` - Crypto cryptoJSON `json:"crypto"` + Crypto CryptoJSON `json:"crypto"` Id string `json:"id"` Version string `json:"version"` } -type cryptoJSON struct { +type CryptoJSON struct { Cipher string `json:"cipher"` CipherText string `json:"ciphertext"` CipherParams cipherparamsJSON `json:"cipherparams"` diff --git a/accounts/keystore/keystore_passphrase.go b/accounts/keystore/keystore_passphrase.go index 5aa3a6bbd8..9794f32fe1 100644 --- a/accounts/keystore/keystore_passphrase.go +++ b/accounts/keystore/keystore_passphrase.go @@ -135,29 +135,26 @@ func (ks keyStorePassphrase) JoinPath(filename string) string { return filepath.Join(ks.keysDirPath, filename) } -// EncryptKey encrypts a key using the specified scrypt parameters into a json -// blob that can be decrypted later on. -func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { - authArray := []byte(auth) +// Encryptdata encrypts the data given as 'data' with the password 'auth'. +func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) { salt := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, salt); err != nil { panic("reading from crypto/rand failed: " + err.Error()) } - derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptR, scryptP, scryptDKLen) + derivedKey, err := scrypt.Key(auth, salt, scryptN, scryptR, scryptP, scryptDKLen) if err != nil { - return nil, err + return CryptoJSON{}, err } encryptKey := derivedKey[:16] - keyBytes := math.PaddedBigBytes(key.PrivateKey.D, 32) iv := make([]byte, aes.BlockSize) // 16 if _, err := io.ReadFull(rand.Reader, iv); err != nil { panic("reading from crypto/rand failed: " + err.Error()) } - cipherText, err := aesCTRXOR(encryptKey, keyBytes, iv) + cipherText, err := aesCTRXOR(encryptKey, data, iv) if err != nil { - return nil, err + return CryptoJSON{}, err } mac := crypto.Keccak256(derivedKey[16:32], cipherText) @@ -167,12 +164,11 @@ func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { scryptParamsJSON["p"] = scryptP scryptParamsJSON["dklen"] = scryptDKLen scryptParamsJSON["salt"] = hex.EncodeToString(salt) - cipherParamsJSON := cipherparamsJSON{ IV: hex.EncodeToString(iv), } - cryptoStruct := cryptoJSON{ + cryptoStruct := CryptoJSON{ Cipher: "aes-128-ctr", CipherText: hex.EncodeToString(cipherText), CipherParams: cipherParamsJSON, @@ -180,6 +176,17 @@ func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { KDFParams: scryptParamsJSON, MAC: hex.EncodeToString(mac), } + return cryptoStruct, nil +} + +// EncryptKey encrypts a key using the specified scrypt parameters into a json +// blob that can be decrypted later on. +func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { + keyBytes := math.PaddedBigBytes(key.PrivateKey.D, 32) + cryptoStruct, err := EncryptDataV3(keyBytes, []byte(auth), scryptN, scryptP) + if err != nil { + return nil, err + } encryptedKeyJSONV3 := encryptedKeyJSONV3{ hex.EncodeToString(key.Address[:]), cryptoStruct, @@ -226,43 +233,48 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) { PrivateKey: key, }, nil } +func DecryptDataV3(cryptoJson CryptoJSON, auth string) ([]byte, error) { + if cryptoJson.Cipher != "aes-128-ctr" { + return nil, fmt.Errorf("Cipher not supported: %v", cryptoJson.Cipher) + } + mac, err := hex.DecodeString(cryptoJson.MAC) + if err != nil { + return nil, err + } + + iv, err := hex.DecodeString(cryptoJson.CipherParams.IV) + if err != nil { + return nil, err + } + + cipherText, err := hex.DecodeString(cryptoJson.CipherText) + if err != nil { + return nil, err + } + + derivedKey, err := getKDFKey(cryptoJson, auth) + if err != nil { + return nil, err + } + + calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) + if !bytes.Equal(calculatedMAC, mac) { + return nil, ErrDecrypt + } + + plainText, err := aesCTRXOR(derivedKey[:16], cipherText, iv) + if err != nil { + return nil, err + } + return plainText, err +} func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byte, keyId []byte, err error) { if keyProtected.Version != version { return nil, nil, fmt.Errorf("Version not supported: %v", keyProtected.Version) } - - if keyProtected.Crypto.Cipher != "aes-128-ctr" { - return nil, nil, fmt.Errorf("Cipher not supported: %v", keyProtected.Crypto.Cipher) - } - keyId = uuid.Parse(keyProtected.Id) - mac, err := hex.DecodeString(keyProtected.Crypto.MAC) - if err != nil { - return nil, nil, err - } - - iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV) - if err != nil { - return nil, nil, err - } - - cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) - if err != nil { - return nil, nil, err - } - - derivedKey, err := getKDFKey(keyProtected.Crypto, auth) - if err != nil { - return nil, nil, err - } - - calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) - if !bytes.Equal(calculatedMAC, mac) { - return nil, nil, ErrDecrypt - } - - plainText, err := aesCTRXOR(derivedKey[:16], cipherText, iv) + plainText, err := DecryptDataV3(keyProtected.Crypto, auth) if err != nil { return nil, nil, err } @@ -303,7 +315,7 @@ func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byt return plainText, keyId, err } -func getKDFKey(cryptoJSON cryptoJSON, auth string) ([]byte, error) { +func getKDFKey(cryptoJSON CryptoJSON, auth string) ([]byte, error) { authArray := []byte(auth) salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string)) if err != nil { diff --git a/cmd/clef/intapi_changelog.md b/cmd/clef/intapi_changelog.md index 9e13f67d03..92a39a268f 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 c060285be6..6098b1ac21 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 diff --git a/signer/core/api.go b/signer/core/api.go index c380fe9773..2b96cdb5f3 100644 --- a/signer/core/api.go +++ b/signer/core/api.go @@ -197,6 +197,12 @@ type ( Message struct { Text string `json:"text"` } + PasswordRequest struct { + Prompt string `json:"prompt"` + } + PasswordResponse struct { + Password string `json:"password"` + } StartupInfo struct { Info map[string]interface{} `json:"info"` } diff --git a/signer/rules/rules_test.go b/signer/rules/rules_test.go index c2f92d51f2..55614077ca 100644 --- a/signer/rules/rules_test.go +++ b/signer/rules/rules_test.go @@ -81,6 +81,10 @@ func (alwaysDenyUI) OnInputRequired(info core.UserInputRequest) (core.UserInputR func (alwaysDenyUI) OnSignerStartup(info core.StartupInfo) { } +func (alwaysDenyUI) OnMasterPassword(request *core.PasswordRequest) (core.PasswordResponse, error) { + return core.PasswordResponse{}, nil +} + func (alwaysDenyUI) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { return core.SignTxResponse{Transaction: request.Transaction, Approved: false, Password: ""}, nil } @@ -250,6 +254,11 @@ func (d *dummyUI) ShowInfo(message string) { func (d *dummyUI) OnApprovedTx(tx ethapi.SignTransactionResult) { d.calls = append(d.calls, "OnApprovedTx") } + +func (d *dummyUI) OnMasterPassword(request *core.PasswordRequest) (core.PasswordResponse, error) { + return core.PasswordResponse{}, nil +} + func (d *dummyUI) OnSignerStartup(info core.StartupInfo) { } @@ -526,6 +535,10 @@ func (d *dontCallMe) OnInputRequired(info core.UserInputRequest) (core.UserInput func (d *dontCallMe) OnSignerStartup(info core.StartupInfo) { } +func (d *dontCallMe) OnMasterPassword(request *core.PasswordRequest) (core.PasswordResponse, error) { + return core.PasswordResponse{}, nil +} + func (d *dontCallMe) ApproveTx(request *core.SignTxRequest) (core.SignTxResponse, error) { d.t.Fatalf("Did not expect next-handler to be called") return core.SignTxResponse{}, core.ErrRequestDenied