aboutsummaryrefslogtreecommitdiffstats
path: root/cmd/clef
diff options
context:
space:
mode:
authorMartin Holst Swende <martin@swende.se>2018-10-09 17:05:41 +0800
committerGitHub <noreply@github.com>2018-10-09 17:05:41 +0800
commitd5c7a6056afdc8c3364b1774b5d2bc4a74b028a6 (patch)
tree3d29cc462f535517d76ff454087d139ea577393d /cmd/clef
parentff5538ad4c20677148ca43e1786fe67898b59425 (diff)
downloaddexon-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.md5
-rw-r--r--cmd/clef/main.go183
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