diff --git a/.codecov.yml b/.codecov.yml index 1c90602afd..602a444602 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -11,7 +11,7 @@ coverage: project: default: target: auto - threshold: 1% + threshold: 1.5% patch: no changes: no diff --git a/WORKSPACE b/WORKSPACE index 8cce23dcc2..98cc728ef2 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -650,6 +650,12 @@ go_repository( importpath = "github.com/boltdb/bolt", ) +go_repository( + name = "com_github_pborman_uuid", + commit = "8b1b92947f46224e3b97bb1a3a5b0382be00d31e", + importpath = "github.com/pborman/uuid", +) + go_repository( name = "com_github_libp2p_go_buffer_pool", commit = "058210c5a0d042677367d923eb8a6dc072a15f7f", diff --git a/beacon-chain/blockchain/service_test.go b/beacon-chain/blockchain/service_test.go index 2b2fc9f104..8ddb3c9a45 100644 --- a/beacon-chain/blockchain/service_test.go +++ b/beacon-chain/blockchain/service_test.go @@ -7,11 +7,10 @@ import ( "math/big" "testing" - "github.com/prysmaticlabs/prysm/beacon-chain/casper" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" gethTypes "github.com/ethereum/go-ethereum/core/types" + "github.com/prysmaticlabs/prysm/beacon-chain/casper" "github.com/prysmaticlabs/prysm/beacon-chain/db" "github.com/prysmaticlabs/prysm/beacon-chain/internal" "github.com/prysmaticlabs/prysm/beacon-chain/powchain" diff --git a/beacon-chain/casper/incentives_test.go b/beacon-chain/casper/incentives_test.go index 1bf9bacae0..768f9cf574 100644 --- a/beacon-chain/casper/incentives_test.go +++ b/beacon-chain/casper/incentives_test.go @@ -4,10 +4,9 @@ import ( "math" "testing" - "github.com/prysmaticlabs/prysm/shared/mathutil" - "github.com/prysmaticlabs/prysm/beacon-chain/params" pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" + "github.com/prysmaticlabs/prysm/shared/mathutil" ) func NewValidators() []*pb.ValidatorRecord { diff --git a/beacon-chain/main.go b/beacon-chain/main.go index c2dac6fd5f..bf2d496203 100644 --- a/beacon-chain/main.go +++ b/beacon-chain/main.go @@ -75,6 +75,8 @@ VERSION: cmd.EnableTracingFlag, cmd.TracingEndpointFlag, cmd.TraceSampleFractionFlag, + cmd.KeystorePasswordFlag, + cmd.KeystoreDirectoryFlag, debug.PProfFlag, debug.PProfAddrFlag, debug.PProfPortFlag, diff --git a/beacon-chain/node/p2p_config.go b/beacon-chain/node/p2p_config.go index 9eab1268ab..344ac010d6 100644 --- a/beacon-chain/node/p2p_config.go +++ b/beacon-chain/node/p2p_config.go @@ -2,12 +2,11 @@ package node import ( "github.com/golang/protobuf/proto" + pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" "github.com/prysmaticlabs/prysm/shared/cmd" "github.com/prysmaticlabs/prysm/shared/p2p" "github.com/prysmaticlabs/prysm/shared/p2p/adapter/tracer" "github.com/urfave/cli" - - pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" ) var topicMappings = map[pb.Topic]proto.Message{ diff --git a/beacon-chain/simulator/service.go b/beacon-chain/simulator/service.go index c36d1ca45c..dd4ae84346 100644 --- a/beacon-chain/simulator/service.go +++ b/beacon-chain/simulator/service.go @@ -6,10 +6,9 @@ import ( "fmt" "time" + "github.com/ethereum/go-ethereum/common" "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes" - - "github.com/ethereum/go-ethereum/common" "github.com/prysmaticlabs/prysm/beacon-chain/params" "github.com/prysmaticlabs/prysm/beacon-chain/types" pb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" diff --git a/shared/bls/bls.go b/shared/bls/bls.go index 4cab3c02a5..3dc16388f4 100644 --- a/shared/bls/bls.go +++ b/shared/bls/bls.go @@ -3,17 +3,58 @@ // aggregating BLS signatures used by Ethereum Serenity. package bls -import "fmt" +import ( + "fmt" + "math/big" +) // Signature used in the BLS signature scheme. type Signature struct{} // SecretKey used in the BLS scheme. -type SecretKey struct{} +type SecretKey struct { + K *big.Int +} // PublicKey corresponding to secret key used in the BLS scheme. type PublicKey struct{} +// PublicKey returns the corresponding public key for the +// Secret Key +func (s *SecretKey) PublicKey() (*PublicKey, error) { + return &PublicKey{}, nil +} + +// BufferedSecretKey returns the secret key in a byte format. +func (s *SecretKey) BufferedSecretKey() []byte { + return s.K.Bytes() +} + +// BufferedPublicKey returns the public key in a byte format. +func (p *PublicKey) BufferedPublicKey() []byte { + return []byte{} +} + +// UnBufferSecretKey takes the byte representation of a secret key +// and sets it to a big int of the underlying secret key object. +func (s *SecretKey) UnBufferSecretKey(bufferedKey []byte) { + s.K = big.NewInt(0).SetBytes(bufferedKey) + +} + +// UnBufferPublicKey takes the byte representation of a public key +// and sets it to a big int of the underlying public key object. +func (p *PublicKey) UnBufferPublicKey(bufferedKey []byte) { + +} + +// GenerateKey generates a new secret key using a seed. +func GenerateKey(seed []byte) *SecretKey { + return &SecretKey{ + K: big.NewInt(0).SetBytes(seed), + } +} + // Sign a message using a secret key - in a beacon/validator client, // this key will come from and be unlocked from the account keystore. func Sign(sec *SecretKey, msg []byte) (*Signature, error) { diff --git a/shared/bls/bls_test.go b/shared/bls/bls_test.go index 6f35c925d8..d6f7413749 100644 --- a/shared/bls/bls_test.go +++ b/shared/bls/bls_test.go @@ -12,6 +12,13 @@ func TestSign(t *testing.T) { } } +func TestPublicKey(t *testing.T) { + sk := &SecretKey{} + if _, err := sk.PublicKey(); err != nil { + t.Errorf("Expected nil error, received %v", err) + } +} + func TestVerifySig(t *testing.T) { pk := &PublicKey{} msg := []byte{} diff --git a/shared/cmd/flags.go b/shared/cmd/flags.go index 764c1fcf5a..0745efcc88 100644 --- a/shared/cmd/flags.go +++ b/shared/cmd/flags.go @@ -47,7 +47,7 @@ var ( Name: "enable-tracing", Usage: "Enable request tracing.", } - // TracingEndpointFlag flag defines the http enpoint for serving traces to Jaeger. + // TracingEndpointFlag flag defines the http endpoint for serving traces to Jaeger. TracingEndpointFlag = cli.StringFlag{ Name: "tracing-endpoint", Usage: "Tracing endpoint defines where beacon chain traces are exposed to Jaeger.", @@ -60,4 +60,15 @@ var ( Usage: "Indicate what fraction of p2p messages are sampled for tracing.", Value: 0.20, } + // KeystoreDirectoryFlag defines a flag to indicate where the keystore of the user + // is located. + KeystoreDirectoryFlag = DirectoryFlag{ + Name: "keystore-dir", + Usage: "Keystore directory indicates which directory the keystore is located.", + } + // KeystorePasswordFlag defines the password that will unlock the keystore file. + KeystorePasswordFlag = cli.StringFlag{ + Name: "keystore-password", + Usage: "Keystore password is used to unlock the keystore so that the users decrypted keys can be used.", + } ) diff --git a/shared/keystore/BUILD.bazel b/shared/keystore/BUILD.bazel new file mode 100644 index 0000000000..e933947f82 --- /dev/null +++ b/shared/keystore/BUILD.bazel @@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "key.go", + "keystore.go", + "utils.go", + ], + importpath = "github.com/prysmaticlabs/prysm/shared/keystore", + visibility = ["//visibility:public"], + deps = [ + "//shared/bls:go_default_library", + "@com_github_ethereum_go_ethereum//common/math:go_default_library", + "@com_github_ethereum_go_ethereum//crypto:go_default_library", + "@com_github_pborman_uuid//:go_default_library", + "@org_golang_x_crypto//pbkdf2:go_default_library", + "@org_golang_x_crypto//scrypt:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "key_test.go", + "keystore_test.go", + ], + embed = [":go_default_library"], + deps = [ + "//shared/bls:go_default_library", + "@com_github_pborman_uuid//:go_default_library", + ], +) diff --git a/shared/keystore/key.go b/shared/keystore/key.go new file mode 100644 index 0000000000..800b989998 --- /dev/null +++ b/shared/keystore/key.go @@ -0,0 +1,204 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// Modified by Prysmatic Labs 2018 +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/pborman/uuid" + "github.com/prysmaticlabs/prysm/shared/bls" +) + +const ( + keyHeaderKDF = "scrypt" + + // StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB + // memory and taking approximately 1s CPU time on a modern processor. + StandardScryptN = 1 << 18 + + // StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB + // memory and taking approximately 1s CPU time on a modern processor. + StandardScryptP = 1 + + // LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB + // memory and taking approximately 100ms CPU time on a modern processor. + LightScryptN = 1 << 12 + + // LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB + // memory and taking approximately 100ms CPU time on a modern processor. + LightScryptP = 6 + + scryptR = 8 + scryptDKLen = 32 +) + +// Key is the object that stores all the user data related to their public/secret keys. +type Key struct { + ID uuid.UUID // Version 4 "random" for unique id not derived from key data + + PublicKey *bls.PublicKey // Represents the public key of the user. + + SecretKey *bls.SecretKey // Represents the private key of the user. +} + +type keyStore interface { + // Loads and decrypts the key from disk. + GetKey(filename string, password string) (*Key, error) + // Writes and encrypts the key. + StoreKey(filename string, k *Key, auth string) error + // Joins filename with the key directory unless it is already absolute. + JoinPath(filename string) string +} + +type plainKeyJSON struct { + PublicKey string `json:"address"` + SecretKey string `json:"privatekey"` + ID string `json:"id"` +} + +type encryptedKeyJSON struct { + PublicKey string `json:"publickey"` + Crypto cryptoJSON `json:"crypto"` + ID string `json:"id"` +} + +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"` +} + +// MarshalJSON marshalls a key struct into a JSON blob. +func (k *Key) MarshalJSON() (j []byte, err error) { + jStruct := plainKeyJSON{ + hex.EncodeToString(k.PublicKey.BufferedPublicKey()), + hex.EncodeToString(k.SecretKey.BufferedSecretKey()), + k.ID.String(), + } + j, err = json.Marshal(jStruct) + return j, err +} + +// UnmarshalJSON unmarshals a blob into a key struct. +func (k *Key) UnmarshalJSON(j []byte) (err error) { + keyJSON := new(plainKeyJSON) + err = json.Unmarshal(j, &keyJSON) + if err != nil { + return err + } + + u := new(uuid.UUID) + *u = uuid.Parse(keyJSON.ID) + k.ID = *u + pubkey, err := hex.DecodeString(keyJSON.PublicKey) + if err != nil { + return err + } + seckey, err := hex.DecodeString(keyJSON.SecretKey) + if err != nil { + return err + } + + k.PublicKey.UnBufferPublicKey(pubkey) + k.SecretKey.UnBufferSecretKey(seckey) + + return nil +} + +func newKeyFromBLS(blsKey *bls.SecretKey) (*Key, error) { + id := uuid.NewRandom() + pubkey, err := blsKey.PublicKey() + if err != nil { + return nil, err + } + key := &Key{ + ID: id, + PublicKey: pubkey, + SecretKey: blsKey, + } + return key, nil +} + +// NewKey generates a new random key. +func NewKey(rand io.Reader) (*Key, error) { + randBytes := make([]byte, 64) + _, err := rand.Read(randBytes) + if err != nil { + return nil, fmt.Errorf("key generation: could not read from random source: %v", err) + } + secretKey := bls.GenerateKey(randBytes) + + return newKeyFromBLS(secretKey) +} + +func storeNewRandomKey(ks keyStore, rand io.Reader, password string) error { + key, err := NewKey(rand) + if err != nil { + return err + } + + if err := ks.StoreKey(ks.JoinPath(keyFileName(key.PublicKey)), key, password); err != nil { + zeroKey(key.SecretKey) + return err + } + return nil +} + +func writeKeyFile(file string, content []byte) error { + // Create the keystore directory with appropriate permissions + // in case it is not present yet. + const dirPerm = 0700 + if err := os.MkdirAll(filepath.Dir(file), dirPerm); err != nil { + return err + } + // Atomic write: create a temporary hidden file first + // then move it into place. TempFile assigns mode 0600. + f, err := ioutil.TempFile(filepath.Dir(file), "."+filepath.Base(file)+".tmp") + if err != nil { + return err + } + if _, err := f.Write(content); err != nil { + newErr := f.Close() + if newErr != nil { + err = newErr + } + newErr = os.Remove(f.Name()) + if newErr != nil { + err = newErr + } + return err + } + if err := f.Close(); err != nil { + return err + } + return os.Rename(f.Name(), file) +} diff --git a/shared/keystore/key_test.go b/shared/keystore/key_test.go new file mode 100644 index 0000000000..a55c303cf1 --- /dev/null +++ b/shared/keystore/key_test.go @@ -0,0 +1,113 @@ +package keystore + +import ( + "bytes" + "crypto/rand" + "io/ioutil" + "math/big" + "os" + "testing" + + "github.com/pborman/uuid" + "github.com/prysmaticlabs/prysm/shared/bls" +) + +func TestMarshalAndUnmarshal(t *testing.T) { + testID := uuid.NewRandom() + blsKey := &bls.SecretKey{ + K: big.NewInt(10), + } + key := &Key{ + ID: testID, + SecretKey: blsKey, + } + marshalledObject, err := key.MarshalJSON() + if err != nil { + t.Fatalf("unable to marshall key %v", err) + } + newKey := &Key{ + ID: []byte{}, + SecretKey: &bls.SecretKey{ + K: big.NewInt(0), + }, + } + + err = newKey.UnmarshalJSON(marshalledObject) + if err != nil { + t.Fatalf("unable to unmarshall object %v", err) + } + + if !bytes.Equal([]byte(newKey.ID), []byte(testID)) { + t.Fatalf("retrieved id not the same as pre serialized id: %v ", newKey.ID) + } +} + +func TestStoreRandomKey(t *testing.T) { + tmpdir := os.TempDir() + filedir := tmpdir + "/keystore" + ks := &keyStorePassphrase{ + keysDirPath: filedir, + scryptN: LightScryptN, + scryptP: LightScryptP, + } + + reader := rand.Reader + + if err := storeNewRandomKey(ks, reader, "password"); err != nil { + t.Fatalf("storage of random key unsuccessful %v", err) + } + + if err := os.RemoveAll(filedir); err != nil { + t.Errorf("unable to remove temporary files %v", err) + } + +} +func TestNewKeyFromBLS(t *testing.T) { + blskey := &bls.SecretKey{ + K: big.NewInt(20), + } + + key, err := newKeyFromBLS(blskey) + if err != nil { + t.Fatalf("could not get new key from bls %v", err) + } + + expectedNum := big.NewInt(20) + + if expectedNum.Cmp(key.SecretKey.K) != 0 { + t.Fatalf("secret key is not of the expected value %d", key.SecretKey.K) + } + + reader := rand.Reader + + _, err = NewKey(reader) + if err != nil { + t.Fatalf("random key unable to be generated: %v", err) + } + +} + +func TestWriteFile(t *testing.T) { + tmpdir := os.TempDir() + filedir := tmpdir + "/keystore" + + testKeystore := []byte{'t', 'e', 's', 't'} + + err := writeKeyFile(filedir, testKeystore) + if err != nil { + t.Fatalf("unable to write file %v", err) + } + + keystore, err := ioutil.ReadFile(filedir) + if err != nil { + t.Fatalf("unable to retrieve file %v", err) + } + + if !bytes.Equal(keystore, testKeystore) { + t.Fatalf("retrieved keystore is not the same %v", keystore) + } + + if err := os.RemoveAll(filedir); err != nil { + t.Errorf("unable to remove temporary files %v", err) + } +} diff --git a/shared/keystore/keystore.go b/shared/keystore/keystore.go new file mode 100644 index 0000000000..c882305c7b --- /dev/null +++ b/shared/keystore/keystore.go @@ -0,0 +1,245 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// Modified by Prysmatic Labs 2018 +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "bytes" + "crypto/aes" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "path/filepath" + + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" + "github.com/pborman/uuid" + "github.com/prysmaticlabs/prysm/shared/bls" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/scrypt" +) + +var ( + // ErrDecrypt is the standard error message when decryption is a failure. + ErrDecrypt = errors.New("could not decrypt key with given passphrase") +) + +type keyStorePassphrase struct { + keysDirPath string + scryptN int + scryptP int +} + +// RetrievePubKey retrieves the public key from the keystore. +func RetrievePubKey(directory string, password string) (*bls.PublicKey, error) { + + ks := keyStorePassphrase{ + keysDirPath: directory, + scryptN: StandardScryptN, + scryptP: StandardScryptP, + } + key, err := ks.GetKey(ks.keysDirPath, password) + return key.PublicKey, err +} + +func (ks keyStorePassphrase) GetKey(filename, password string) (*Key, error) { + // Load the key from the keystore and decrypt its contents + // #nosec G304 + keyjson, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return DecryptKey(keyjson, password) +} + +func (ks keyStorePassphrase) StoreKey(filename string, key *Key, auth string) error { + keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) + if err != nil { + return err + } + return writeKeyFile(filename, keyjson) +} + +func (ks keyStorePassphrase) JoinPath(filename string) string { + if filepath.IsAbs(filename) { + return filename + } + return filepath.Join(ks.keysDirPath, filename) +} + +// StoreRandomKey generates a key, encrypts with 'auth' and stores in the given directory +func StoreRandomKey(dir, password string, scryptN, scryptP int) error { + err := storeNewRandomKey(keyStorePassphrase{dir, scryptN, scryptP}, rand.Reader, password) + return err +} + +// EncryptKey encrypts a key using the specified scrypt parameters into a json +// blob that can be decrypted later on. +func EncryptKey(key *Key, password string, scryptN, scryptP int) ([]byte, error) { + authArray := []byte(password) + 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) + if err != nil { + return nil, err + } + + encryptKey := derivedKey[:16] + keyBytes := math.PaddedBigBytes(key.SecretKey.K, 32) + + iv := make([]byte, aes.BlockSize) // 16 + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, errors.New("reading from crypto/rand failed: " + err.Error()) + } + + cipherText, err := aesCTRXOR(encryptKey, keyBytes, 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), + } + encryptedJSON := encryptedKeyJSON{ + hex.EncodeToString(key.PublicKey.BufferedPublicKey()), + cryptoStruct, + key.ID.String(), + } + return json.Marshal(encryptedJSON) +} + +// DecryptKey decrypts a key from a json blob, returning the private key itself. +func DecryptKey(keyjson []byte, password string) (*Key, error) { + + var keyBytes, keyID []byte + var err error + + k := new(encryptedKeyJSON) + if err := json.Unmarshal(keyjson, k); err != nil { + return nil, err + } + + keyBytes, keyID, err = decryptKeyJSON(k, password) + // Handle any decryption errors and return the key + if err != nil { + return nil, err + } + + key := &bls.SecretKey{} + key.UnBufferSecretKey(keyBytes) + pubkey, err := key.PublicKey() + if err != nil { + return nil, err + } + + return &Key{ + ID: uuid.UUID(keyID), + PublicKey: pubkey, + SecretKey: key, + }, nil +} + +func decryptKeyJSON(keyProtected *encryptedKeyJSON, auth string) (keyBytes []byte, keyID []byte, err error) { + keyID = uuid.Parse(keyProtected.ID) + if keyProtected.Crypto.Cipher != "aes-128-ctr" { + return nil, nil, fmt.Errorf("Cipher not supported: %v", keyProtected.Crypto.Cipher) + } + + 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) + if err != nil { + return nil, nil, err + } + return plainText, keyID, nil +} + +func getKDFKey(cryptoJSON cryptoJSON, auth string) ([]byte, error) { + authArray := []byte(auth) + 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(authArray, 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(authArray, salt, c, dkLen, sha256.New) + return key, nil + } + + return nil, fmt.Errorf("Unsupported KDF: %s", cryptoJSON.KDF) +} diff --git a/shared/keystore/keystore_test.go b/shared/keystore/keystore_test.go new file mode 100644 index 0000000000..85a8be35bc --- /dev/null +++ b/shared/keystore/keystore_test.go @@ -0,0 +1,75 @@ +package keystore + +import ( + "bytes" + "crypto/rand" + "math/big" + "os" + "testing" + + "github.com/pborman/uuid" + "github.com/prysmaticlabs/prysm/shared/bls" +) + +func TestStoreandGetKey(t *testing.T) { + tmpdir := os.TempDir() + filedir := tmpdir + "/keystore" + ks := &keyStorePassphrase{ + keysDirPath: filedir, + scryptN: LightScryptN, + scryptP: LightScryptP, + } + + key, err := NewKey(rand.Reader) + if err != nil { + t.Fatalf("key generation failed %v", err) + } + + if err := ks.StoreKey(filedir, key, "password"); err != nil { + t.Fatalf("unable to store key %v", err) + } + + newkey, err := ks.GetKey(filedir, "password") + if err != nil { + t.Fatalf("unable to get key %v", err) + } + + if newkey.SecretKey.K.Cmp(key.SecretKey.K) != 0 { + t.Fatalf("retrieved secret keys are not equal %v , %v", newkey.SecretKey.K, key.SecretKey.K) + } + + if err := os.RemoveAll(filedir); err != nil { + t.Errorf("unable to remove temporary files %v", err) + } +} +func TestEncryptDecryptKey(t *testing.T) { + newID := uuid.NewRandom() + keyValue := big.NewInt(1e16) + password := "test" + + key := &Key{ + ID: newID, + SecretKey: &bls.SecretKey{ + K: keyValue, + }, + } + + keyjson, err := EncryptKey(key, password, LightScryptN, LightScryptP) + if err != nil { + t.Fatalf("unable to encrypt key %v", err) + } + + newkey, err := DecryptKey(keyjson, password) + if err != nil { + t.Fatalf("unable to decrypt keystore %v", err) + } + + if !bytes.Equal(newkey.ID, newID) { + t.Fatalf("decrypted key's uuid doesn't match %v", newkey.ID) + } + + if newkey.SecretKey.K.Cmp(keyValue) != 0 { + t.Fatalf("decrypted key's value is not equal %v", newkey.SecretKey.K) + } + +} diff --git a/shared/keystore/utils.go b/shared/keystore/utils.go new file mode 100644 index 0000000000..d978ddfb4c --- /dev/null +++ b/shared/keystore/utils.go @@ -0,0 +1,75 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// Modified by Prysmatic Labs 2018 +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/hex" + "fmt" + "time" + + "github.com/prysmaticlabs/prysm/shared/bls" +) + +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 ensureInt(x interface{}) int { + res, ok := x.(int) + if !ok { + res = int(x.(float64)) + } + return res +} + +// keyFileName implements the naming convention for keyfiles: +// UTC---
+func keyFileName(pubkey *bls.PublicKey) string { + ts := time.Now().UTC() + return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(pubkey.BufferedPublicKey())) +} + +func toISO8601(t time.Time) string { + var tz string + name, offset := t.Zone() + if name == "UTC" { + tz = "Z" + } else { + tz = fmt.Sprintf("%03d00", offset/3600) + } + return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d.%09d%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) +} + +// zeroKey zeroes a private key in memory. +func zeroKey(k *bls.SecretKey) { + b := k.K.Bits() + for i := range b { + b[i] = 0 + } +} diff --git a/shared/p2p/service.go b/shared/p2p/service.go index c53759c428..b2148a51cc 100644 --- a/shared/p2p/service.go +++ b/shared/p2p/service.go @@ -7,9 +7,6 @@ import ( "sync" "github.com/golang/protobuf/proto" - "github.com/prysmaticlabs/prysm/shared/event" - "github.com/sirupsen/logrus" - ds "github.com/ipfs/go-datastore" dsync "github.com/ipfs/go-datastore/sync" libp2p "github.com/libp2p/go-libp2p" @@ -17,6 +14,8 @@ import ( kaddht "github.com/libp2p/go-libp2p-kad-dht" pubsub "github.com/libp2p/go-libp2p-pubsub" rhost "github.com/libp2p/go-libp2p/p2p/host/routed" + "github.com/prysmaticlabs/prysm/shared/event" + "github.com/sirupsen/logrus" ) // Sender represents a struct that is able to relay information via p2p. diff --git a/validator/main.go b/validator/main.go index 48227e192f..b44c10ffa1 100644 --- a/validator/main.go +++ b/validator/main.go @@ -69,6 +69,8 @@ VERSION: cmd.EnableTracingFlag, cmd.TracingEndpointFlag, cmd.TraceSampleFractionFlag, + cmd.KeystorePasswordFlag, + cmd.KeystoreDirectoryFlag, debug.PProfFlag, debug.PProfAddrFlag, debug.PProfPortFlag, diff --git a/validator/node/BUILD.bazel b/validator/node/BUILD.bazel index 0581e7b915..3e95acb288 100644 --- a/validator/node/BUILD.bazel +++ b/validator/node/BUILD.bazel @@ -4,11 +4,7 @@ go_test( name = "go_default_test", srcs = ["node_test.go"], embed = [":go_default_library"], - deps = [ - "//shared/testutil:go_default_library", - "@com_github_sirupsen_logrus//hooks/test:go_default_library", - "@com_github_urfave_cli//:go_default_library", - ], + deps = ["@com_github_urfave_cli//:go_default_library"], ) go_library( @@ -25,6 +21,7 @@ go_library( "//shared/cmd:go_default_library", "//shared/database:go_default_library", "//shared/debug:go_default_library", + "//shared/keystore:go_default_library", "//shared/p2p:go_default_library", "//shared/p2p/adapter/tracer:go_default_library", "//validator/attester:go_default_library", diff --git a/validator/node/node.go b/validator/node/node.go index d3162a1142..4d3236d54a 100644 --- a/validator/node/node.go +++ b/validator/node/node.go @@ -15,6 +15,7 @@ import ( "github.com/prysmaticlabs/prysm/shared/cmd" "github.com/prysmaticlabs/prysm/shared/database" "github.com/prysmaticlabs/prysm/shared/debug" + "github.com/prysmaticlabs/prysm/shared/keystore" "github.com/prysmaticlabs/prysm/shared/p2p" "github.com/prysmaticlabs/prysm/validator/attester" "github.com/prysmaticlabs/prysm/validator/beacon" @@ -60,17 +61,15 @@ func NewValidatorClient(ctx *cli.Context) (*ValidatorClient, error) { var pubKey []byte - inputKey := ctx.GlobalString(types.PubKeyFlag.Name) - if inputKey == "" { - var err error - pubKey, err = GeneratePubKey() + keystorePath := ctx.GlobalString(cmd.KeystoreDirectoryFlag.Name) + password := ctx.GlobalString(cmd.KeystorePasswordFlag.Name) + if keystorePath != "" && password != "" { + blspubkey, err := keystore.RetrievePubKey(keystorePath, password) if err != nil { return nil, err } - log.Warnf("PublicKey not detected, generating a new one: %v", pubKey) - } else { - pubKey = []byte(inputKey) + pubKey = blspubkey.BufferedPublicKey() } if err := ValidatorClient.startDB(ctx); err != nil { diff --git a/validator/node/node_test.go b/validator/node/node_test.go index 96b902b05f..fb6ac66a20 100644 --- a/validator/node/node_test.go +++ b/validator/node/node_test.go @@ -4,24 +4,18 @@ import ( "flag" "testing" - "github.com/prysmaticlabs/prysm/shared/testutil" - logTest "github.com/sirupsen/logrus/hooks/test" "github.com/urfave/cli" ) // Test that the sharding node can build with default flag values. func TestNode_Builds(t *testing.T) { - hook := logTest.NewGlobal() app := cli.NewApp() set := flag.NewFlagSet("test", 0) set.String("datadir", "/tmp/datadir", "the node data directory") - set.String("pukey", "f24f252", "set public key of validator") context := cli.NewContext(app, set, nil) _, err := NewValidatorClient(context) if err != nil { t.Fatalf("Failed to create ValidatorClient: %v", err) } - - testutil.AssertLogsContain(t, hook, "PublicKey not detected, generating a new one") } diff --git a/validator/node/p2p_config.go b/validator/node/p2p_config.go index 7fdb05f76c..ce20720645 100644 --- a/validator/node/p2p_config.go +++ b/validator/node/p2p_config.go @@ -2,12 +2,11 @@ package node import ( "github.com/golang/protobuf/proto" + pb "github.com/prysmaticlabs/prysm/proto/sharding/p2p/v1" "github.com/prysmaticlabs/prysm/shared/cmd" "github.com/prysmaticlabs/prysm/shared/p2p" "github.com/prysmaticlabs/prysm/shared/p2p/adapter/tracer" "github.com/urfave/cli" - - pb "github.com/prysmaticlabs/prysm/proto/sharding/p2p/v1" ) var topicMappings = map[pb.Topic]proto.Message{ diff --git a/validator/types/collation.go b/validator/types/collation.go index 74f825262a..1514726298 100644 --- a/validator/types/collation.go +++ b/validator/types/collation.go @@ -2,7 +2,6 @@ package types import ( "fmt" - "math/big" "github.com/ethereum/go-ethereum/common"