cmd/clef: encrypt master seed of clef

Signed-off-by: YaoZengzeng <yaozengzeng@zju.edu.cn>
This commit is contained in:
YaoZengzeng 2018-09-17 11:08:50 +08:00
parent 7c71e936a7
commit 5d59347132
7 changed files with 260 additions and 12 deletions

View file

@ -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{
@ -210,13 +223,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
@ -225,7 +249,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
}
@ -250,7 +274,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())
}
@ -274,7 +298,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())
}
@ -336,7 +360,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 {
@ -506,7 +530,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)
@ -519,15 +543,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
@ -535,7 +576,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
@ -613,6 +654,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

View file

@ -76,6 +76,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)
@ -182,6 +184,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"`
}

View file

@ -44,6 +44,10 @@ type HeadlessUI struct {
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 called")
}

View file

@ -239,6 +239,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")

View file

@ -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 {

View file

@ -246,3 +246,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
}

View file

@ -77,6 +77,10 @@ type alwaysDenyUI struct{}
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
}
@ -241,6 +245,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) {
}
@ -512,6 +521,10 @@ type dontCallMe struct {
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