diff --git a/cmd/clef/main.go b/cmd/clef/main.go index c060285be6..ffc2146759 100644 --- a/cmd/clef/main.go +++ b/cmd/clef/main.go @@ -20,7 +20,10 @@ package main import ( "bufio" + "bytes" "context" + "crypto/aes" + "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/hex" @@ -35,8 +38,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" @@ -44,6 +49,8 @@ import ( "github.com/ethereum/go-ethereum/signer/core" "github.com/ethereum/go-ethereum/signer/rules" "github.com/ethereum/go-ethereum/signer/storage" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/scrypt" "gopkg.in/urfave/cli.v1" ) @@ -63,6 +70,12 @@ unless you agree to take full responsibility for doing so, and know what you are TLDR; THIS IS NOT PRODUCTION-READY SOFTWARE! ` +const ( + keyHeaderKDF = "scrypt" + + scryptR = 8 + scryptDKLen = 32 +) var ( logLevelFlag = cli.IntFlag{ @@ -215,13 +228,24 @@ func initializeSecrets(c *cli.Context) error { configDir := c.String(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.Bool(utils.LightKDFFlag.Name) { + n, p = keystore.LightScryptN, keystore.LightScryptP + } + password := getPassPhrase("The master seed of clef is locked with a password. Please give a password. Do not forget this password.", true) + 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 @@ -230,7 +254,7 @@ func initializeSecrets(c *cli.Context) error { 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,7 +279,7 @@ func attestFile(ctx *cli.Context) error { return err } - stretchedKey, err := readMasterKey(ctx) + stretchedKey, err := readMasterKey(ctx, nil) if err != nil { utils.Fatalf(err.Error()) } @@ -279,7 +303,7 @@ func addCredential(ctx *cli.Context) error { return err } - stretchedKey, err := readMasterKey(ctx) + stretchedKey, err := readMasterKey(ctx, nil) if err != nil { utils.Fatalf(err.Error()) } @@ -341,7 +365,7 @@ func signer(c *cli.Context) error { ) configDir := c.String(configdirFlag.Name) - if stretchedKey, err := readMasterKey(c); err != nil { + if stretchedKey, err := readMasterKey(c, ui); err != nil { log.Info("No master seed provided, rules disabled") } else { @@ -512,7 +536,7 @@ 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) @@ -525,15 +549,32 @@ func readMasterKey(ctx *cli.Context) ([]byte, error) { 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.OnMasterPassword(&core.PasswordRequest{Prompt: "password to decrypt master seed"}) + if err != nil { + return nil, err + } + password = resp.Password + } else { + password = getPassPhrase("Decrypt master seed of clef", false) } + masterSeed, err := decryptSeed(cipherKey, []byte(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 @@ -541,7 +582,7 @@ func readMasterKey(ctx *cli.Context) ([]byte, error) { //!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 +660,174 @@ 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 cryptoJSON struct { + Cipher string `json:"cipher"` + CipherText string `json:"ciphertext"` + CipherParams cipherparamsJSON `json:"cipherparams"` + KDF string `json:"kdf"` + KDFParams map[string]interface{} `json:"kdfparams"` + MAC string `json:"mac"` +} + +type cipherparamsJSON struct { + IV string `json:"iv"` +} + +func encryptSeed(seed []byte, auth []byte, scryptN, scryptP int) ([]byte, 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(auth, salt, scryptN, scryptR, scryptP, scryptDKLen) + if err != nil { + return nil, err + } + encryptKey := derivedKey[:16] + + 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, seed, iv) + if err != nil { + return nil, err + } + mac := crypto.Keccak256(derivedKey[16:32], cipherText) + + scryptParamsJSON := make(map[string]interface{}, 5) + scryptParamsJSON["n"] = scryptN + scryptParamsJSON["r"] = scryptR + scryptParamsJSON["p"] = scryptP + scryptParamsJSON["dklen"] = scryptDKLen + scryptParamsJSON["salt"] = hex.EncodeToString(salt) + + cipherParamsJSON := cipherparamsJSON{ + IV: hex.EncodeToString(iv), + } + + cryptoStruct := cryptoJSON{ + Cipher: "aes-128-ctr", + CipherText: hex.EncodeToString(cipherText), + CipherParams: cipherParamsJSON, + KDF: keyHeaderKDF, + KDFParams: scryptParamsJSON, + MAC: hex.EncodeToString(mac), + } + return json.Marshal(cryptoStruct) +} + +func decryptSeed(keyjson []byte, auth []byte) ([]byte, error) { + var cryptoStruct cryptoJSON + if err := json.Unmarshal(keyjson, &cryptoStruct); err != nil { + return nil, err + } + + if cryptoStruct.Cipher != "aes-128-ctr" { + return nil, fmt.Errorf("Cipher not supported: %v", cryptoStruct.Cipher) + } + + mac, err := hex.DecodeString(cryptoStruct.MAC) + if err != nil { + return nil, err + } + + iv, err := hex.DecodeString(cryptoStruct.CipherParams.IV) + if err != nil { + return nil, err + } + + cipherText, err := hex.DecodeString(cryptoStruct.CipherText) + if err != nil { + return nil, err + } + + derivedKey, err := getKDFKey(cryptoStruct, auth) + if err != nil { + return nil, err + } + + calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) + if !bytes.Equal(calculatedMAC, mac) { + return nil, fmt.Errorf("could not decrypt seed with given passphrase") + } + + seed, err := aesCTRXOR(derivedKey[:16], cipherText, iv) + if err != nil { + return nil, err + } + return seed, err +} + +func aesCTRXOR(key, inText, iv []byte) ([]byte, error) { + // AES-128 is selected due to size of encryptKey. + aesBlock, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + stream := cipher.NewCTR(aesBlock, iv) + outText := make([]byte, len(inText)) + stream.XORKeyStream(outText, inText) + return outText, err +} + +func getKDFKey(cryptoJSON cryptoJSON, auth []byte) ([]byte, error) { + salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string)) + if err != nil { + return nil, err + } + dkLen := ensureInt(cryptoJSON.KDFParams["dklen"]) + + if cryptoJSON.KDF == keyHeaderKDF { + n := ensureInt(cryptoJSON.KDFParams["n"]) + r := ensureInt(cryptoJSON.KDFParams["r"]) + p := ensureInt(cryptoJSON.KDFParams["p"]) + return scrypt.Key(auth, salt, n, r, p, dkLen) + + } else if cryptoJSON.KDF == "pbkdf2" { + c := ensureInt(cryptoJSON.KDFParams["c"]) + prf := cryptoJSON.KDFParams["prf"].(string) + if prf != "hmac-sha256" { + return nil, fmt.Errorf("Unsupported PBKDF2 PRF: %s", prf) + } + key := pbkdf2.Key(auth, salt, c, dkLen, sha256.New) + return key, nil + } + + return nil, fmt.Errorf("Unsupported KDF: %s", cryptoJSON.KDF) +} + +// TODO: can we do without this when unmarshalling dynamic JSON? +// why do integers in KDF params end up as float64 and not int after +// unmarshal? +func ensureInt(x interface{}) int { + res, ok := x.(int) + if !ok { + res = int(x.(float64)) + } + return res +} + /** //Create Account diff --git a/signer/core/api.go b/signer/core/api.go index c380fe9773..967e9eadeb 100644 --- a/signer/core/api.go +++ b/signer/core/api.go @@ -79,6 +79,8 @@ type SignerUI interface { // OnApprovedTx notifies the UI about a transaction having been successfully signed. // This method can be used by a UI to keep track of e.g. how much has been sent to a particular recipient. OnApprovedTx(tx ethapi.SignTransactionResult) + // OnMasterPassword is invoked when the signer boots, and tells the UI to input the password for the master seed. + OnMasterPassword(request *PasswordRequest) (PasswordResponse, error) // OnSignerStartup is invoked when the signer boots, and tells the UI info about external API location and version // information OnSignerStartup(info StartupInfo) @@ -197,6 +199,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/core/api_test.go b/signer/core/api_test.go index a8aa23896e..08f78b120d 100644 --- a/signer/core/api_test.go +++ b/signer/core/api_test.go @@ -49,6 +49,10 @@ func (ui *HeadlessUI) OnInputRequired(info UserInputRequest) (UserInputResponse, func (ui *HeadlessUI) OnSignerStartup(info StartupInfo) { } +func (ui *HeadlessUI) OnMasterPassword(request *PasswordRequest) (PasswordResponse, error) { + return PasswordResponse{}, nil +} + func (ui *HeadlessUI) OnApprovedTx(tx ethapi.SignTransactionResult) { fmt.Printf("OnApproved()\n") } diff --git a/signer/core/cliui.go b/signer/core/cliui.go index 940f1f43aa..6bd37dd855 100644 --- a/signer/core/cliui.go +++ b/signer/core/cliui.go @@ -264,6 +264,10 @@ func (ui *CommandlineUI) OnApprovedTx(tx ethapi.SignTransactionResult) { spew.Dump(tx.Tx) } +func (ui *CommandlineUI) OnMasterPassword(request *PasswordRequest) (PasswordResponse, error) { + return PasswordResponse{ui.readPasswordText(request.Prompt)}, nil +} + func (ui *CommandlineUI) OnSignerStartup(info StartupInfo) { fmt.Printf("------- Signer info -------\n") diff --git a/signer/core/stdioui.go b/signer/core/stdioui.go index 64032386fc..0c4b3726dd 100644 --- a/signer/core/stdioui.go +++ b/signer/core/stdioui.go @@ -105,6 +105,12 @@ func (ui *StdIOUI) OnApprovedTx(tx ethapi.SignTransactionResult) { } } +func (ui *StdIOUI) OnMasterPassword(request *PasswordRequest) (PasswordResponse, error) { + var result PasswordResponse + err := ui.dispatch("OnMasterPassword", request, &result) + return result, err +} + func (ui *StdIOUI) OnSignerStartup(info StartupInfo) { err := ui.dispatch("OnSignerStartup", info, nil) if err != nil { diff --git a/signer/rules/rules.go b/signer/rules/rules.go index 07c34db220..d690fcd763 100644 --- a/signer/rules/rules.go +++ b/signer/rules/rules.go @@ -252,3 +252,7 @@ func (r *rulesetUI) OnApprovedTx(tx ethapi.SignTransactionResult) { log.Info("error occurred during execution", "error", err) } } + +func (r *rulesetUI) OnMasterPassword(request *core.PasswordRequest) (core.PasswordResponse, error) { + return core.PasswordResponse{}, nil +} 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