From 287e5cdf79fc06d5cf5c9d3bd3a99a1df1e3cd10 Mon Sep 17 00:00:00 2001 From: Pedro Pombeiro Date: Thu, 19 Dec 2019 19:27:27 +0100 Subject: [PATCH] Abstract `accounts.Key` and geth `keystore` --- account/accounts.go | 69 +--- account/accounts_geth.go | 42 +++ account/accounts_nimbus.go | 28 ++ account/accounts_test.go | 4 +- account/generator/generator.go | 3 +- account/generator/utils.go | 4 +- account/generator/utils_test.go | 8 +- account/keystore.go | 26 -- account/keystore_geth.go | 135 +++++++ account/utils.go | 33 +- account/utils_test.go | 29 +- api/geth_backend.go | 8 +- eth-node/keystore/keystore.go | 15 + eth-node/keystore/passphrase.go | 329 ++++++++++++++++++ eth-node/types/account.go | 8 + eth-node/types/key.go | 24 ++ eth-node/types/keystore.go | 26 ++ lib/library_test_utils.go | 15 +- services/status/account_mock.go | 9 +- services/status/api_test.go | 18 +- services/status/service.go | 5 +- t/e2e/transactions/transactions_test.go | 24 +- transactions/addrlock.go | 12 +- transactions/rpc_wrapper.go | 9 +- transactions/transactor.go | 48 ++- transactions/transactor_test.go | 56 +-- transactions/types.go | 13 +- transactions/types_test.go | 10 +- .../status-go/eth-node/keystore/keystore.go | 15 + .../status-go/eth-node/keystore/passphrase.go | 329 ++++++++++++++++++ .../status-go/eth-node/types/account.go | 8 + .../status-im/status-go/eth-node/types/key.go | 24 ++ .../status-go/eth-node/types/keystore.go | 26 ++ vendor/modules.txt | 1 + waku/rate_limiter_test.go | 2 +- 35 files changed, 1174 insertions(+), 241 deletions(-) create mode 100644 account/accounts_geth.go create mode 100644 account/accounts_nimbus.go delete mode 100644 account/keystore.go create mode 100644 account/keystore_geth.go create mode 100644 eth-node/keystore/keystore.go create mode 100644 eth-node/keystore/passphrase.go create mode 100644 eth-node/types/account.go create mode 100644 eth-node/types/key.go create mode 100644 eth-node/types/keystore.go create mode 100644 vendor/github.com/status-im/status-go/eth-node/keystore/keystore.go create mode 100644 vendor/github.com/status-im/status-go/eth-node/keystore/passphrase.go create mode 100644 vendor/github.com/status-im/status-go/eth-node/types/account.go create mode 100644 vendor/github.com/status-im/status-go/eth-node/types/key.go create mode 100644 vendor/github.com/status-im/status-go/eth-node/types/keystore.go diff --git a/account/accounts.go b/account/accounts.go index ddefbe28d..8e11315f4 100644 --- a/account/accounts.go +++ b/account/accounts.go @@ -10,13 +10,11 @@ import ( "path/filepath" "sync" - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" "github.com/pborman/uuid" "github.com/status-im/status-go/account/generator" "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/keystore" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/extkeys" ) @@ -37,8 +35,7 @@ var zeroAddress = types.Address{} // Manager represents account manager interface. type Manager struct { mu sync.RWMutex - keystore *keystore.KeyStore - manager *accounts.Manager + keystore types.KeyStore accountsGenerator *generator.Generator onboarding *Onboarding @@ -48,46 +45,13 @@ type Manager struct { watchAddresses []types.Address } -// NewManager returns new node account manager. -func NewManager() *Manager { - m := &Manager{} - m.accountsGenerator = generator.New(m) - return m -} - -// InitKeystore sets key manager and key store. -func (m *Manager) InitKeystore(keydir string) error { - m.mu.Lock() - defer m.mu.Unlock() - manager, err := makeAccountManager(keydir) - if err != nil { - return err - } - m.manager = manager - backends := manager.Backends(keystore.KeyStoreType) - if len(backends) == 0 { - return ErrAccountKeyStoreMissing - } - keyStore, ok := backends[0].(*keystore.KeyStore) - if !ok { - return ErrAccountKeyStoreMissing - } - m.keystore = keyStore - return nil -} - -func (m *Manager) GetKeystore() *keystore.KeyStore { +// GetKeystore is only used in tests +func (m *Manager) GetKeystore() types.KeyStore { m.mu.RLock() defer m.mu.RUnlock() return m.keystore } -func (m *Manager) GetManager() *accounts.Manager { - m.mu.RLock() - defer m.mu.RUnlock() - return m.manager -} - // AccountsGenerator returns accountsGenerator. func (m *Manager) AccountsGenerator() *generator.Generator { return m.accountsGenerator @@ -153,7 +117,7 @@ func (m *Manager) RecoverAccount(password, mnemonic string) (Info, error) { // VerifyAccountPassword tries to decrypt a given account key file, with a provided password. // If no error is returned, then account is considered verified. -func (m *Manager) VerifyAccountPassword(keyStoreDir, address, password string) (*keystore.Key, error) { +func (m *Manager) VerifyAccountPassword(keyStoreDir, address, password string) (*types.Key, error) { var err error var foundKeyFile []byte @@ -202,7 +166,7 @@ func (m *Manager) VerifyAccountPassword(keyStoreDir, address, password string) ( } // avoid swap attack - if types.Address(key.Address) != addressObj { + if key.Address != addressObj { return nil, fmt.Errorf("account mismatch: have %s, want %s", key.Address.Hex(), addressObj.Hex()) } @@ -240,9 +204,9 @@ func (m *Manager) SetChatAccount(privKey *ecdsa.PrivateKey) { address := crypto.PubkeyToAddress(privKey.PublicKey) id := uuid.NewRandom() - key := &keystore.Key{ + key := &types.Key{ Id: id, - Address: common.Address(address), + Address: address, PrivateKey: privKey, } @@ -302,9 +266,13 @@ func (m *Manager) ImportAccount(privateKey *ecdsa.PrivateKey, password string) ( account, err := m.keystore.ImportECDSA(privateKey, password) - return types.Address(account.Address), err + return account.Address, err } +// ImportSingleExtendedKey imports an extended key setting it in both the PrivateKey and ExtendedKey fields +// of the Key struct. +// ImportExtendedKey is used in older version of Status where PrivateKey is set to be the BIP44 key at index 0, +// and ExtendedKey is the extended key of the BIP44 key at index 1. func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) { if m.keystore == nil { return "", "", ErrAccountKeyStoreMissing @@ -418,18 +386,17 @@ func (m *Manager) ImportOnboardingAccount(id string, password string) (Info, str // AddressToDecryptedAccount tries to load decrypted key for a given account. // The running node, has a keystore directory which is loaded on start. Key file // for a given address is expected to be in that directory prior to node start. -func (m *Manager) AddressToDecryptedAccount(address, password string) (accounts.Account, *keystore.Key, error) { +func (m *Manager) AddressToDecryptedAccount(address, password string) (types.Account, *types.Key, error) { if m.keystore == nil { - return accounts.Account{}, nil, ErrAccountKeyStoreMissing + return types.Account{}, nil, ErrAccountKeyStoreMissing } account, err := ParseAccountString(address) if err != nil { - return accounts.Account{}, nil, ErrAddressToAccountMappingFailure + return types.Account{}, nil, ErrAddressToAccountMappingFailure } - var key *keystore.Key - account, key, err = m.keystore.AccountDecryptedKey(account, password) + account, key, err := m.keystore.AccountDecryptedKey(account, password) if err != nil { err = fmt.Errorf("%s: %s", ErrAccountToKeyMappingFailure, err) } @@ -444,7 +411,7 @@ func (m *Manager) unlockExtendedKey(address, password string) (*SelectedExtKey, } selectedExtendedKey := &SelectedExtKey{ - Address: types.Address(account.Address), + Address: account.Address, AccountKey: accountKey, } diff --git a/account/accounts_geth.go b/account/accounts_geth.go new file mode 100644 index 000000000..d3eda4c53 --- /dev/null +++ b/account/accounts_geth.go @@ -0,0 +1,42 @@ +// +build !nimbus + +package account + +import ( + "github.com/ethereum/go-ethereum/accounts" + "github.com/status-im/status-go/account/generator" +) + +// GethManager represents account manager interface. +type GethManager struct { + Manager + + manager *accounts.Manager +} + +// NewManager returns new node account manager. +func NewManager() *GethManager { + m := &GethManager{} + m.accountsGenerator = generator.New(m) + return m +} + +// InitKeystore sets key manager and key store. +func (m *GethManager) InitKeystore(keydir string) error { + m.mu.Lock() + defer m.mu.Unlock() + + var err error + m.manager, err = makeAccountManager(keydir) + if err != nil { + return err + } + m.keystore, err = makeKeyStore(m.manager) + return err +} + +func (m *GethManager) GetManager() *accounts.Manager { + m.mu.RLock() + defer m.mu.RUnlock() + return m.manager +} diff --git a/account/accounts_nimbus.go b/account/accounts_nimbus.go new file mode 100644 index 000000000..082237213 --- /dev/null +++ b/account/accounts_nimbus.go @@ -0,0 +1,28 @@ +// +build nimbus + +package account + +import ( + "github.com/status-im/status-go/account/generator" +) + +// NewManager returns new node account manager. +func NewManager() *Manager { + m := &Manager{} + m.accountsGenerator = generator.New(m) + return m +} + +// InitKeystore sets key manager and key store. +func (m *Manager) InitKeystore(keydir string) error { + m.mu.Lock() + defer m.mu.Unlock() + + // TODO: Wire with the Nimbus keystore + manager, err := makeAccountManager(keydir) + if err != nil { + return err + } + m.keystore, err = makeKeyStore(manager) + return err +} diff --git a/account/accounts_test.go b/account/accounts_test.go index b65c6698b..ec6c36f99 100644 --- a/account/accounts_test.go +++ b/account/accounts_test.go @@ -87,7 +87,7 @@ func TestVerifyAccountPassword(t *testing.T) { require.Fail(t, "no error reported, but account key is missing") } accountAddress := types.BytesToAddress(types.FromHex(testCase.address)) - if types.Address(accountKey.Address) != accountAddress { + if accountKey.Address != accountAddress { require.Fail(t, "account mismatch: have %s, want %s", accountKey.Address.Hex(), accountAddress.Hex()) } } @@ -120,7 +120,7 @@ func TestManagerTestSuite(t *testing.T) { type ManagerTestSuite struct { suite.Suite testAccount - accManager *Manager + accManager *GethManager keydir string } diff --git a/account/generator/generator.go b/account/generator/generator.go index 1ac234ea1..8d324f651 100644 --- a/account/generator/generator.go +++ b/account/generator/generator.go @@ -7,7 +7,6 @@ import ( "strings" "sync" - "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/pborman/uuid" "github.com/status-im/status-go/eth-node/crypto" @@ -25,7 +24,7 @@ var ( ) type AccountManager interface { - AddressToDecryptedAccount(address, password string) (accounts.Account, *keystore.Key, error) + AddressToDecryptedAccount(address, password string) (types.Account, *types.Key, error) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) ImportAccount(privateKey *ecdsa.PrivateKey, password string) (types.Address, error) } diff --git a/account/generator/utils.go b/account/generator/utils.go index 818c0103a..a9ed7975b 100644 --- a/account/generator/utils.go +++ b/account/generator/utils.go @@ -4,8 +4,8 @@ import ( "bytes" "errors" - "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/extkeys" ) @@ -21,7 +21,7 @@ var ( // ValidateKeystoreExtendedKey validates the keystore keys, checking that // ExtendedKey is the extended key of PrivateKey. -func ValidateKeystoreExtendedKey(key *keystore.Key) error { +func ValidateKeystoreExtendedKey(key *types.Key) error { if key.ExtendedKey.IsZeroed() { return nil } diff --git a/account/generator/utils_test.go b/account/generator/utils_test.go index 8ecbebdde..cf006c736 100644 --- a/account/generator/utils_test.go +++ b/account/generator/utils_test.go @@ -3,7 +3,7 @@ package generator import ( "testing" - "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/extkeys" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,7 +25,7 @@ func TestValidateKeystoreExtendedKey(t *testing.T) { extendedKey2 := generateTestKey(t) // new keystore file format - key := &keystore.Key{ + key := &types.Key{ PrivateKey: extendedKey1.ToECDSA(), ExtendedKey: extendedKey1, } @@ -33,14 +33,14 @@ func TestValidateKeystoreExtendedKey(t *testing.T) { // old keystore file format where the extended key was // from another derivation path and not the same of the private key - oldKey := &keystore.Key{ + oldKey := &types.Key{ PrivateKey: extendedKey1.ToECDSA(), ExtendedKey: extendedKey2, } assert.Error(t, ValidateKeystoreExtendedKey(oldKey)) // normal key where we don't have an extended key - normalKey := &keystore.Key{ + normalKey := &types.Key{ PrivateKey: extendedKey1.ToECDSA(), ExtendedKey: nil, } diff --git a/account/keystore.go b/account/keystore.go deleted file mode 100644 index aab648471..000000000 --- a/account/keystore.go +++ /dev/null @@ -1,26 +0,0 @@ -package account - -import ( - "io/ioutil" - "os" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" -) - -// makeAccountManager creates ethereum accounts.Manager with single disk backend and lightweight kdf. -// If keydir is empty new temporary directory with go-ethereum-keystore will be intialized. -func makeAccountManager(keydir string) (manager *accounts.Manager, err error) { - if keydir == "" { - // There is no datadir. - keydir, err = ioutil.TempDir("", "go-ethereum-keystore") - } - if err != nil { - return nil, err - } - if err := os.MkdirAll(keydir, 0700); err != nil { - return nil, err - } - config := accounts.Config{InsecureUnlockAllowed: false} - return accounts.NewManager(&config, keystore.NewKeyStore(keydir, keystore.LightScryptN, keystore.LightScryptP)), nil -} diff --git a/account/keystore_geth.go b/account/keystore_geth.go new file mode 100644 index 000000000..760f7fba3 --- /dev/null +++ b/account/keystore_geth.go @@ -0,0 +1,135 @@ +// TODO: Make independent version for Nimbus + +package account + +import ( + "crypto/ecdsa" + "errors" + "io/ioutil" + "os" + "strings" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/common" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/extkeys" +) + +// makeAccountManager creates ethereum accounts.Manager with single disk backend and lightweight kdf. +// If keydir is empty new temporary directory with go-ethereum-keystore will be intialized. +func makeAccountManager(keydir string) (manager *accounts.Manager, err error) { + if keydir == "" { + // There is no datadir. + keydir, err = ioutil.TempDir("", "go-ethereum-keystore") + } + if err != nil { + return nil, err + } + if err := os.MkdirAll(keydir, 0700); err != nil { + return nil, err + } + config := accounts.Config{InsecureUnlockAllowed: false} + return accounts.NewManager(&config, keystore.NewKeyStore(keydir, keystore.LightScryptN, keystore.LightScryptP)), nil +} + +func makeKeyStore(manager *accounts.Manager) (types.KeyStore, error) { + backends := manager.Backends(keystore.KeyStoreType) + if len(backends) == 0 { + return nil, ErrAccountKeyStoreMissing + } + keyStore, ok := backends[0].(*keystore.KeyStore) + if !ok { + return nil, ErrAccountKeyStoreMissing + } + + return adaptGethKeyStore(keyStore), nil +} + +type gethKeyStoreAdapter struct { + keystore *keystore.KeyStore +} + +func adaptGethKeyStore(keystore *keystore.KeyStore) types.KeyStore { + return &gethKeyStoreAdapter{keystore: keystore} +} + +func (k *gethKeyStoreAdapter) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (types.Account, error) { + gethAccount, err := k.keystore.ImportECDSA(priv, passphrase) + return accountFrom(gethAccount), err +} + +func (k *gethKeyStoreAdapter) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (types.Account, error) { + gethAccount, err := k.keystore.ImportSingleExtendedKey(extKey, passphrase) + return accountFrom(gethAccount), err +} + +func (k *gethKeyStoreAdapter) ImportExtendedKeyForPurpose(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, passphrase string) (types.Account, error) { + gethAccount, err := k.keystore.ImportExtendedKeyForPurpose(keyPurpose, extKey, passphrase) + return accountFrom(gethAccount), err +} + +func (k *gethKeyStoreAdapter) AccountDecryptedKey(a types.Account, auth string) (types.Account, *types.Key, error) { + gethAccount, err := gethAccountFrom(a) + if err != nil { + return types.Account{}, nil, err + } + + var gethKey *keystore.Key + gethAccount, gethKey, err = k.keystore.AccountDecryptedKey(gethAccount, auth) + return accountFrom(gethAccount), keyFrom(gethKey), err +} + +func (k *gethKeyStoreAdapter) Delete(a types.Account, auth string) error { + gethAccount, err := gethAccountFrom(a) + if err != nil { + return err + } + + return k.keystore.Delete(gethAccount, auth) +} + +// parseGethURL converts a user supplied URL into the accounts specific structure. +func parseGethURL(url string) (accounts.URL, error) { + parts := strings.Split(url, "://") + if len(parts) != 2 || parts[0] == "" { + return accounts.URL{}, errors.New("protocol scheme missing") + } + return accounts.URL{ + Scheme: parts[0], + Path: parts[1], + }, nil +} + +func gethAccountFrom(account types.Account) (accounts.Account, error) { + var ( + gethAccount accounts.Account + err error + ) + gethAccount.Address = common.Address(account.Address) + if account.URL != "" { + gethAccount.URL, err = parseGethURL(account.URL) + } + return gethAccount, err +} + +func accountFrom(gethAccount accounts.Account) types.Account { + return types.Account{ + Address: types.Address(gethAccount.Address), + URL: gethAccount.URL.String(), + } +} + +func keyFrom(k *keystore.Key) *types.Key { + if k == nil { + return nil + } + + return &types.Key{ + Id: k.Id, + Address: types.Address(k.Address), + PrivateKey: k.PrivateKey, + ExtendedKey: k.ExtendedKey, + SubAccountIndex: k.SubAccountIndex, + } +} diff --git a/account/utils.go b/account/utils.go index bdd6e5e84..a8310dbe4 100644 --- a/account/utils.go +++ b/account/utils.go @@ -5,10 +5,6 @@ import ( "errors" "fmt" - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" - "github.com/status-im/status-go/eth-node/types" ) @@ -75,8 +71,8 @@ type Info struct { // SelectedExtKey is a container for the selected (logged in) external account. type SelectedExtKey struct { Address types.Address - AccountKey *keystore.Key - SubAccounts []accounts.Account + AccountKey *types.Key + SubAccounts []types.Account } // Hex dumps address of a given extended key as hex string. @@ -88,25 +84,14 @@ func (k *SelectedExtKey) Hex() string { return k.Address.Hex() } -// ParseAccountString parses hex encoded string and returns is as accounts.Account. -func ParseAccountString(account string) (accounts.Account, error) { +// ParseAccountString parses hex encoded string and returns is as types.Account. +func ParseAccountString(account string) (types.Account, error) { // valid address, convert to account if types.IsHexAddress(account) { - return accounts.Account{Address: common.HexToAddress(account)}, nil + return types.Account{Address: types.HexToAddress(account)}, nil } - return accounts.Account{}, ErrInvalidAccountAddressOrKey -} - -// GethFromAddress converts account address from string to common.Address. -// The function is useful to format "From" field of send transaction struct. -func GethFromAddress(accountAddress string) common.Address { - from, err := ParseAccountString(accountAddress) - if err != nil { - return common.Address{} - } - - return from.Address + return types.Account{}, ErrInvalidAccountAddressOrKey } // FromAddress converts account address from string to types.Address. @@ -117,12 +102,12 @@ func FromAddress(accountAddress string) types.Address { return types.Address{} } - return types.Address(from.Address) + return from.Address } -// GethToAddress converts account address from string to *common.Address. +// ToAddress converts account address from string to *common.Address. // The function is useful to format "To" field of send transaction struct. -func GethToAddress(accountAddress string) *common.Address { +func ToAddress(accountAddress string) *types.Address { to, err := ParseAccountString(accountAddress) if err != nil { return nil diff --git a/account/utils_test.go b/account/utils_test.go index b33032928..c4cf5525d 100644 --- a/account/utils_test.go +++ b/account/utils_test.go @@ -4,9 +4,9 @@ package account import ( "testing" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/crypto" "github.com/status-im/status-go/account/generator" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/extkeys" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -21,13 +21,13 @@ func (suite *AccountUtilsTestSuite) SetupTest() { suite.validKey = "0xF35E0325dad87e2661c4eF951d58727e6d583d5c" } -func (suite *AccountUtilsTestSuite) TestGethToAddress() { - addr := GethToAddress(suite.validKey) +func (suite *AccountUtilsTestSuite) TestToAddress() { + addr := ToAddress(suite.validKey) suite.Equal(suite.validKey, addr.String()) } -func (suite *AccountUtilsTestSuite) TestGethToAddressInvalidAddress() { - addr := GethToAddress("foobar") +func (suite *AccountUtilsTestSuite) TestToAddressInvalidAddress() { + addr := ToAddress("foobar") suite.Nil(addr) } @@ -46,21 +46,6 @@ func (suite *AccountUtilsTestSuite) TestFromAddress() { } } -func (suite *AccountUtilsTestSuite) TestGethFromAddress() { - var flagtests = []struct { - in string - out string - }{ - {suite.validKey, suite.validKey}, - {"foobar", "0x0000000000000000000000000000000000000000"}, - } - - for _, tt := range flagtests { - addr := GethFromAddress(tt.in) - suite.Equal(tt.out, addr.String()) - } -} - func (suite *AccountUtilsTestSuite) TestHex() { var addr *SelectedExtKey cr, _ := crypto.GenerateKey() @@ -70,7 +55,7 @@ func (suite *AccountUtilsTestSuite) TestHex() { }{ {&SelectedExtKey{ Address: FromAddress(suite.validKey), - AccountKey: &keystore.Key{PrivateKey: cr}, + AccountKey: &types.Key{PrivateKey: cr}, }, suite.validKey}, {addr, "0x0"}, } diff --git a/api/geth_backend.go b/api/geth_backend.go index 82a7410ca..57becbbb5 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -70,7 +70,7 @@ type GethStatusBackend struct { personalAPI *personal.PublicAPI rpcFilters *rpcfilters.Service multiaccountsDB *multiaccounts.Database - accountManager *account.Manager + accountManager *account.GethManager transactor *transactions.Transactor connectionState connectionState appState appState @@ -104,7 +104,7 @@ func (b *GethStatusBackend) StatusNode() *node.StatusNode { } // AccountManager returns reference to account manager -func (b *GethStatusBackend) AccountManager() *account.Manager { +func (b *GethStatusBackend) AccountManager() *account.GethManager { return b.accountManager } @@ -386,7 +386,7 @@ func (b *GethStatusBackend) subscriptionService() gethnode.ServiceConstructor { func (b *GethStatusBackend) accountsService(accountsFeed *event.Feed) gethnode.ServiceConstructor { return func(*gethnode.ServiceContext) (gethnode.Service, error) { - return accountssvc.NewService(accounts.NewDB(b.appDB), b.multiaccountsDB, b.accountManager, accountsFeed), nil + return accountssvc.NewService(accounts.NewDB(b.appDB), b.multiaccountsDB, &b.accountManager.Manager, accountsFeed), nil } } @@ -683,7 +683,7 @@ func (b *GethStatusBackend) getVerifiedWalletAccount(address, password string) ( } return &account.SelectedExtKey{ - Address: types.Address(key.Address), + Address: key.Address, AccountKey: key, }, nil } diff --git a/eth-node/keystore/keystore.go b/eth-node/keystore/keystore.go new file mode 100644 index 000000000..02a3a9e40 --- /dev/null +++ b/eth-node/keystore/keystore.go @@ -0,0 +1,15 @@ +// Imported from github.com/ethereum/go-ethereum/accounts/keystore/keystore.go + +package keystore + +import ( + "errors" +) + +const ( + version = 3 +) + +var ( + ErrDecrypt = errors.New("could not decrypt key with given password") +) diff --git a/eth-node/keystore/passphrase.go b/eth-node/keystore/passphrase.go new file mode 100644 index 000000000..36d8a5755 --- /dev/null +++ b/eth-node/keystore/passphrase.go @@ -0,0 +1,329 @@ +// Imported from github.com/ethereum/go-ethereum/accounts/keystore/passphrase.go +// and github.com/ethereum/go-ethereum/accounts/keystore/presale.go + +package keystore + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/pborman/uuid" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/extkeys" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/scrypt" +) + +const ( + keyHeaderKDF = "scrypt" +) + +type encryptedKeyJSONV3 struct { + Address string `json:"address"` + Crypto CryptoJSON `json:"crypto"` + Id string `json:"id"` + Version int `json:"version"` + ExtendedKey CryptoJSON `json:"extendedkey"` + SubAccountIndex uint32 `json:"subaccountindex"` +} + +type encryptedKeyJSONV1 struct { + Address string `json:"address"` + Crypto CryptoJSON `json:"crypto"` + Id string `json:"id"` + Version string `json:"version"` +} + +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"` +} + +// DecryptKey decrypts a key from a json blob, returning the private key itself. +func DecryptKey(keyjson []byte, auth string) (*types.Key, error) { + // Parse the json into a simple map to fetch the key version + m := make(map[string]interface{}) + if err := json.Unmarshal(keyjson, &m); err != nil { + return nil, err + } + // Depending on the version try to parse one way or another + var ( + keyBytes, keyId []byte + err error + extKeyBytes []byte + extKey *extkeys.ExtendedKey + ) + + subAccountIndex, ok := m["subaccountindex"].(float64) + if !ok { + subAccountIndex = 0 + } + + if version, ok := m["version"].(string); ok && version == "1" { + k := new(encryptedKeyJSONV1) + if err := json.Unmarshal(keyjson, k); err != nil { + return nil, err + } + keyBytes, keyId, err = decryptKeyV1(k, auth) + if err != nil { + return nil, err + } + + extKey, err = extkeys.NewKeyFromString(extkeys.EmptyExtendedKeyString) + } else { + k := new(encryptedKeyJSONV3) + if err := json.Unmarshal(keyjson, k); err != nil { + return nil, err + } + keyBytes, keyId, err = decryptKeyV3(k, auth) + if err != nil { + return nil, err + } + + extKeyBytes, err = decryptExtendedKey(k, auth) + if err != nil { + return nil, err + } + extKey, err = extkeys.NewKeyFromString(string(extKeyBytes)) + } + // Handle any decryption errors and return the key + if err != nil { + return nil, err + } + key := crypto.ToECDSAUnsafe(keyBytes) + + return &types.Key{ + Id: uuid.UUID(keyId), + Address: crypto.PubkeyToAddress(key.PublicKey), + PrivateKey: key, + ExtendedKey: extKey, + SubAccountIndex: uint32(subAccountIndex), + }, 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) + } + keyId = uuid.Parse(keyProtected.Id) + plainText, err := DecryptDataV3(keyProtected.Crypto, auth) + if err != nil { + return nil, nil, err + } + return plainText, keyId, err +} + +func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyId []byte, err error) { + 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 := aesCBCDecrypt(crypto.Keccak256(derivedKey[:16])[:16], cipherText, iv) + if err != nil { + return nil, nil, err + } + return plainText, keyId, err +} + +func decryptExtendedKey(keyProtected *encryptedKeyJSONV3, auth string) (plainText []byte, err error) { + if len(keyProtected.ExtendedKey.CipherText) == 0 { + return []byte(extkeys.EmptyExtendedKeyString), nil + } + + if keyProtected.Version != version { + return nil, fmt.Errorf("Version not supported: %v", keyProtected.Version) + } + + if keyProtected.ExtendedKey.Cipher != "aes-128-ctr" { + return nil, fmt.Errorf("Cipher not supported: %v", keyProtected.ExtendedKey.Cipher) + } + + mac, err := hex.DecodeString(keyProtected.ExtendedKey.MAC) + if err != nil { + return nil, err + } + + iv, err := hex.DecodeString(keyProtected.ExtendedKey.CipherParams.IV) + if err != nil { + return nil, err + } + + cipherText, err := hex.DecodeString(keyProtected.ExtendedKey.CipherText) + if err != nil { + return nil, err + } + + derivedKey, err := getKDFKey(keyProtected.ExtendedKey, 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 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) +} + +// 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 +} + +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 aesCBCDecrypt(key, cipherText, iv []byte) ([]byte, error) { + aesBlock, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + decrypter := cipher.NewCBCDecrypter(aesBlock, iv) + paddedPlaintext := make([]byte, len(cipherText)) + decrypter.CryptBlocks(paddedPlaintext, cipherText) + plaintext := pkcs7Unpad(paddedPlaintext) + if plaintext == nil { + return nil, ErrDecrypt + } + return plaintext, err +} + +// From https://leanpub.com/gocrypto/read#leanpub-auto-block-cipher-modes +func pkcs7Unpad(in []byte) []byte { + if len(in) == 0 { + return nil + } + + padding := in[len(in)-1] + if int(padding) > len(in) || padding > aes.BlockSize { + return nil + } else if padding == 0 { + return nil + } + + for i := len(in) - 1; i > len(in)-int(padding)-1; i-- { + if in[i] != padding { + return nil + } + } + return in[:len(in)-int(padding)] +} diff --git a/eth-node/types/account.go b/eth-node/types/account.go new file mode 100644 index 000000000..61795fe02 --- /dev/null +++ b/eth-node/types/account.go @@ -0,0 +1,8 @@ +package types + +// Account represents an Ethereum account located at a specific location defined +// by the optional URL field. +type Account struct { + Address Address `json:"address"` // Ethereum account address derived from the key + URL string `json:"url"` // Optional resource locator within a backend +} diff --git a/eth-node/types/key.go b/eth-node/types/key.go new file mode 100644 index 000000000..be2e83c0d --- /dev/null +++ b/eth-node/types/key.go @@ -0,0 +1,24 @@ +package types + +import ( + "crypto/ecdsa" + + "github.com/pborman/uuid" + "github.com/status-im/status-go/extkeys" +) + +type Key struct { + Id uuid.UUID // Version 4 "random" for unique id not derived from key data + // to simplify lookups we also store the address + Address Address + // we only store privkey as pubkey/address can be derived from it + // privkey in this struct is always in plaintext + PrivateKey *ecdsa.PrivateKey + // ExtendedKey is the extended key of the PrivateKey itself, and it's used + // to derive child keys. + ExtendedKey *extkeys.ExtendedKey + // SubAccountIndex is DEPRECATED + // It was use in Status to keep track of the number of sub-account created + // before having multi-account support. + SubAccountIndex uint32 +} diff --git a/eth-node/types/keystore.go b/eth-node/types/keystore.go new file mode 100644 index 000000000..1c491912c --- /dev/null +++ b/eth-node/types/keystore.go @@ -0,0 +1,26 @@ +package types + +import ( + "crypto/ecdsa" + + "github.com/status-im/status-go/extkeys" +) + +type KeyStore interface { + // ImportAccount imports the account specified with privateKey. + ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (Account, error) + // ImportSingleExtendedKey imports an extended key setting it in both the PrivateKey and ExtendedKey fields + // of the Key struct. + // ImportExtendedKey is used in older version of Status where PrivateKey is set to be the BIP44 key at index 0, + // and ExtendedKey is the extended key of the BIP44 key at index 1. + ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (Account, error) + // ImportExtendedKeyForPurpose stores ECDSA key (obtained from extended key) along with CKD#2 (root for sub-accounts) + // If key file is not found, it is created. Key is encrypted with the given passphrase. + // Deprecated: status-go is now using ImportSingleExtendedKey + ImportExtendedKeyForPurpose(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, passphrase string) (Account, error) + // AccountDecryptedKey returns decrypted key for account (provided that password is correct). + AccountDecryptedKey(a Account, auth string) (Account, *Key, error) + // Delete deletes the key matched by account if the passphrase is correct. + // If the account contains no filename, the address must match a unique key. + Delete(a Account, auth string) error +} diff --git a/lib/library_test_utils.go b/lib/library_test_utils.go index f94a2bad7..234bfff1b 100644 --- a/lib/library_test_utils.go +++ b/lib/library_test_utils.go @@ -23,12 +23,11 @@ import ( "testing" "time" - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/event" "github.com/status-im/status-go/account" "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/keystore" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/multiaccounts/accounts" "github.com/status-im/status-go/signal" @@ -592,8 +591,8 @@ func testSendTransactionWithLogin(t *testing.T, feed *event.Feed) bool { EnsureNodeSync(statusBackend.StatusNode().EnsureSync) args, err := json.Marshal(transactions.SendTxArgs{ - From: account.GethFromAddress(TestConfig.Account1.WalletAddress), - To: account.GethToAddress(TestConfig.Account2.WalletAddress), + From: account.FromAddress(TestConfig.Account1.WalletAddress), + To: account.ToAddress(TestConfig.Account2.WalletAddress), Value: (*hexutil.Big)(big.NewInt(1000000000000)), }) if err != nil { @@ -625,8 +624,8 @@ func testSendTransactionInvalidPassword(t *testing.T, feed *event.Feed) bool { EnsureNodeSync(statusBackend.StatusNode().EnsureSync) args, err := json.Marshal(transactions.SendTxArgs{ - From: common.HexToAddress(acc.WalletAddress), - To: account.GethToAddress(TestConfig.Account2.WalletAddress), + From: types.HexToAddress(acc.WalletAddress), + To: account.ToAddress(TestConfig.Account2.WalletAddress), Value: (*hexutil.Big)(big.NewInt(1000000000000)), }) if err != nil { @@ -653,8 +652,8 @@ func testFailedTransaction(t *testing.T, feed *event.Feed) bool { EnsureNodeSync(statusBackend.StatusNode().EnsureSync) args, err := json.Marshal(transactions.SendTxArgs{ - From: *account.GethToAddress(TestConfig.Account1.WalletAddress), - To: account.GethToAddress(TestConfig.Account2.WalletAddress), + From: *account.ToAddress(TestConfig.Account1.WalletAddress), + To: account.ToAddress(TestConfig.Account2.WalletAddress), Value: (*hexutil.Big)(big.NewInt(1000000000000)), }) if err != nil { diff --git a/services/status/account_mock.go b/services/status/account_mock.go index 3db532ecb..483cf7f61 100644 --- a/services/status/account_mock.go +++ b/services/status/account_mock.go @@ -6,10 +6,9 @@ package status import ( ecdsa "crypto/ecdsa" - accounts "github.com/ethereum/go-ethereum/accounts" - keystore "github.com/ethereum/go-ethereum/accounts/keystore" gomock "github.com/golang/mock/gomock" account "github.com/status-im/status-go/account" + types "github.com/status-im/status-go/eth-node/types" reflect "reflect" ) @@ -75,11 +74,11 @@ func (m *MockAccountManager) EXPECT() *MockAccountManagerMockRecorder { } // AddressToDecryptedAccount mocks base method -func (m *MockAccountManager) AddressToDecryptedAccount(arg0, arg1 string) (accounts.Account, *keystore.Key, error) { +func (m *MockAccountManager) AddressToDecryptedAccount(arg0, arg1 string) (types.Account, *types.Key, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AddressToDecryptedAccount", arg0, arg1) - ret0, _ := ret[0].(accounts.Account) - ret1, _ := ret[1].(*keystore.Key) + ret0, _ := ret[0].(types.Account) + ret1, _ := ret[1].(*types.Key) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } diff --git a/services/status/api_test.go b/services/status/api_test.go index 40ca22f19..ea9ea2432 100644 --- a/services/status/api_test.go +++ b/services/status/api_test.go @@ -6,8 +6,6 @@ import ( "errors" "testing" - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/golang/mock/gomock" "github.com/status-im/status-go/account" "github.com/status-im/status-go/eth-node/types" @@ -46,10 +44,10 @@ var logintests = []struct { expectedAddressKey: "addressKey", expectedError: nil, prepareExpectations: func(s *StatusSuite) { - key := keystore.Key{ + key := types.Key{ PrivateKey: &ecdsa.PrivateKey{}, } - s.am.EXPECT().AddressToDecryptedAccount("0x01", "password").Return(accounts.Account{}, &key, nil) + s.am.EXPECT().AddressToDecryptedAccount("0x01", "password").Return(types.Account{}, &key, nil) s.w.EXPECT().AddKeyPair(key.PrivateKey).Return("addressKey", nil) loginParams := account.LoginParams{ @@ -65,10 +63,10 @@ var logintests = []struct { expectedAddressKey: "", expectedError: errors.New("foo"), prepareExpectations: func(s *StatusSuite) { - key := keystore.Key{ + key := types.Key{ PrivateKey: &ecdsa.PrivateKey{}, } - s.am.EXPECT().AddressToDecryptedAccount("0x01", "password").Return(accounts.Account{}, &key, errors.New("foo")) + s.am.EXPECT().AddressToDecryptedAccount("0x01", "password").Return(types.Account{}, &key, errors.New("foo")) }, }, { @@ -76,10 +74,10 @@ var logintests = []struct { expectedAddressKey: "", expectedError: errors.New("foo"), prepareExpectations: func(s *StatusSuite) { - key := keystore.Key{ + key := types.Key{ PrivateKey: &ecdsa.PrivateKey{}, } - s.am.EXPECT().AddressToDecryptedAccount("0x01", "password").Return(accounts.Account{}, &key, nil) + s.am.EXPECT().AddressToDecryptedAccount("0x01", "password").Return(types.Account{}, &key, nil) s.w.EXPECT().AddKeyPair(key.PrivateKey).Return("", errors.New("foo")) }, }, @@ -88,10 +86,10 @@ var logintests = []struct { expectedAddressKey: "", expectedError: errors.New("foo"), prepareExpectations: func(s *StatusSuite) { - key := keystore.Key{ + key := types.Key{ PrivateKey: &ecdsa.PrivateKey{}, } - s.am.EXPECT().AddressToDecryptedAccount("0x01", "password").Return(accounts.Account{}, &key, nil) + s.am.EXPECT().AddressToDecryptedAccount("0x01", "password").Return(types.Account{}, &key, nil) s.w.EXPECT().AddKeyPair(key.PrivateKey).Return("", nil) loginParams := account.LoginParams{ diff --git a/services/status/service.go b/services/status/service.go index 211624c2f..0284d6375 100644 --- a/services/status/service.go +++ b/services/status/service.go @@ -3,12 +3,11 @@ package status import ( "crypto/ecdsa" - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/rpc" "github.com/status-im/status-go/account" + "github.com/status-im/status-go/eth-node/types" ) // Make sure that Service implements node.Service interface. @@ -21,7 +20,7 @@ type WhisperService interface { // AccountManager interface to manage account actions type AccountManager interface { - AddressToDecryptedAccount(string, string) (accounts.Account, *keystore.Key, error) + AddressToDecryptedAccount(string, string) (types.Account, *types.Key, error) SelectAccount(account.LoginParams) error CreateAccount(password string) (accountInfo account.Info, mnemonic string, err error) } diff --git a/t/e2e/transactions/transactions_test.go b/t/e2e/transactions/transactions_test.go index ce5cfb8b5..f467b287b 100644 --- a/t/e2e/transactions/transactions_test.go +++ b/t/e2e/transactions/transactions_test.go @@ -121,7 +121,7 @@ func (s *TransactionsTestSuite) TestEmptyToFieldPreserved() { utils.EnsureNodeSync(s.Backend.StatusNode().EnsureSync) args := transactions.SendTxArgs{ - From: account.GethFromAddress(utils.TestConfig.Account1.WalletAddress), + From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), } hash, err := s.Backend.SendTransaction(args, utils.TestConfig.Account1.Password) @@ -135,7 +135,7 @@ func (s *TransactionsTestSuite) TestSendContractTxCompat() { utils.CheckTestSkipForNetworks(s.T(), params.MainNetworkID) initFunc := func(byteCode []byte, args *transactions.SendTxArgs) { - args.Data = (hexutil.Bytes)(byteCode) + args.Data = (types.HexBytes)(byteCode) } s.testSendContractTx(initFunc, nil, "") } @@ -148,8 +148,8 @@ func (s *TransactionsTestSuite) TestSendContractTxCollision() { // Scenario 1: Both fields are filled and have the same value, expect success initFunc := func(byteCode []byte, args *transactions.SendTxArgs) { - args.Input = (hexutil.Bytes)(byteCode) - args.Data = (hexutil.Bytes)(byteCode) + args.Input = (types.HexBytes)(byteCode) + args.Data = (types.HexBytes)(byteCode) } s.testSendContractTx(initFunc, nil, "") @@ -164,8 +164,8 @@ func (s *TransactionsTestSuite) TestSendContractTxCollision() { } initFunc2 := func(byteCode []byte, args *transactions.SendTxArgs) { - args.Input = (hexutil.Bytes)(byteCode) - args.Data = (hexutil.Bytes)(inverted(byteCode)) + args.Input = (types.HexBytes)(byteCode) + args.Data = (types.HexBytes)(inverted(byteCode)) } s.testSendContractTx(initFunc2, transactions.ErrInvalidSendTxArgs, "expected error when invalid tx args are sent") } @@ -174,7 +174,7 @@ func (s *TransactionsTestSuite) TestSendContractTx() { utils.CheckTestSkipForNetworks(s.T(), params.MainNetworkID) initFunc := func(byteCode []byte, args *transactions.SendTxArgs) { - args.Input = (hexutil.Bytes)(byteCode) + args.Input = (types.HexBytes)(byteCode) } s.testSendContractTx(initFunc, nil, "") } @@ -199,7 +199,7 @@ func (s *TransactionsTestSuite) testSendContractTx(setInputAndDataValue initFunc gas := uint64(params.DefaultGas) args := transactions.SendTxArgs{ - From: account.GethFromAddress(utils.TestConfig.Account1.WalletAddress), + From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), To: nil, // marker, contract creation is expected //Value: (*hexutil.Big)(new(big.Int).Mul(big.NewInt(1), common.Ether)), Gas: (*hexutil.Uint64)(&gas), @@ -231,8 +231,8 @@ func (s *TransactionsTestSuite) TestSendEther() { utils.EnsureNodeSync(s.Backend.StatusNode().EnsureSync) hash, err := s.Backend.SendTransaction(transactions.SendTxArgs{ - From: account.GethFromAddress(utils.TestConfig.Account1.WalletAddress), - To: account.GethToAddress(utils.TestConfig.Account2.WalletAddress), + From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), + To: account.ToAddress(utils.TestConfig.Account2.WalletAddress), Value: (*hexutil.Big)(big.NewInt(1000000000000)), }, utils.TestConfig.Account1.Password) s.NoError(err) @@ -257,8 +257,8 @@ func (s *TransactionsTestSuite) TestSendEtherTxUpstream() { defer s.LogoutAndStop() hash, err := s.Backend.SendTransaction(transactions.SendTxArgs{ - From: account.GethFromAddress(utils.TestConfig.Account1.WalletAddress), - To: account.GethToAddress(utils.TestConfig.Account2.WalletAddress), + From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), + To: account.ToAddress(utils.TestConfig.Account2.WalletAddress), GasPrice: (*hexutil.Big)(big.NewInt(28000000000)), Value: (*hexutil.Big)(big.NewInt(1000000000000)), }, utils.TestConfig.Account1.Password) diff --git a/transactions/addrlock.go b/transactions/addrlock.go index 69a7a9229..24940110b 100644 --- a/transactions/addrlock.go +++ b/transactions/addrlock.go @@ -5,21 +5,21 @@ package transactions import ( "sync" - "github.com/ethereum/go-ethereum/common" + "github.com/status-im/status-go/eth-node/types" ) // AddrLocker provides locks for addresses type AddrLocker struct { mu sync.Mutex - locks map[common.Address]*sync.Mutex + locks map[types.Address]*sync.Mutex } // lock returns the lock of the given address. -func (l *AddrLocker) lock(address common.Address) *sync.Mutex { +func (l *AddrLocker) lock(address types.Address) *sync.Mutex { l.mu.Lock() defer l.mu.Unlock() if l.locks == nil { - l.locks = make(map[common.Address]*sync.Mutex) + l.locks = make(map[types.Address]*sync.Mutex) } if _, ok := l.locks[address]; !ok { l.locks[address] = new(sync.Mutex) @@ -30,11 +30,11 @@ func (l *AddrLocker) lock(address common.Address) *sync.Mutex { // LockAddr locks an account's mutex. This is used to prevent another tx getting the // same nonce until the lock is released. The mutex prevents the (an identical nonce) from // being read again during the time that the first transaction is being signed. -func (l *AddrLocker) LockAddr(address common.Address) { +func (l *AddrLocker) LockAddr(address types.Address) { l.lock(address).Lock() } // UnlockAddr unlocks the mutex of the given account. -func (l *AddrLocker) UnlockAddr(address common.Address) { +func (l *AddrLocker) UnlockAddr(address types.Address) { l.lock(address).Unlock() } diff --git a/transactions/rpc_wrapper.go b/transactions/rpc_wrapper.go index 05873a5e7..fe37a7cd5 100644 --- a/transactions/rpc_wrapper.go +++ b/transactions/rpc_wrapper.go @@ -7,8 +7,9 @@ import ( ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" + gethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" + "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/rpc" ) @@ -57,12 +58,12 @@ func (w *rpcWrapper) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uin // // If the transaction was a contract creation use the TransactionReceipt method to get the // contract address after the transaction has been mined. -func (w *rpcWrapper) SendTransaction(ctx context.Context, tx *types.Transaction) error { +func (w *rpcWrapper) SendTransaction(ctx context.Context, tx *gethtypes.Transaction) error { data, err := rlp.EncodeToBytes(tx) if err != nil { return err } - return w.rpcClient.CallContext(ctx, nil, "eth_sendRawTransaction", hexutil.Encode(data)) + return w.rpcClient.CallContext(ctx, nil, "eth_sendRawTransaction", types.EncodeHex(data)) } func toCallArg(msg ethereum.CallMsg) interface{} { @@ -71,7 +72,7 @@ func toCallArg(msg ethereum.CallMsg) interface{} { "to": msg.To, } if len(msg.Data) > 0 { - arg["data"] = hexutil.Bytes(msg.Data) + arg["data"] = types.HexBytes(msg.Data) } if msg.Value != nil { arg["value"] = (*hexutil.Big)(msg.Value) diff --git a/transactions/transactor.go b/transactions/transactor.go index d66d6d657..7a15d0987 100644 --- a/transactions/transactor.go +++ b/transactions/transactor.go @@ -10,12 +10,13 @@ import ( "time" ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" gethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/account" + "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/rpc" ) @@ -170,9 +171,18 @@ func (t *Transactor) HashTransaction(args SendTxArgs) (validatedArgs SendTxArgs, if args.Gas == nil { ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout) defer cancel() + + var ( + gethTo common.Address + gethToPtr *common.Address + ) + if args.To != nil { + gethTo = common.Address(*args.To) + gethToPtr = &gethTo + } gas, err = t.gasCalculator.EstimateGas(ctx, ethereum.CallMsg{ - From: args.From, - To: args.To, + From: common.Address(args.From), + To: gethToPtr, GasPrice: gasPrice, Value: value, Data: args.GetInput(), @@ -238,7 +248,7 @@ func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKe }() ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout) defer cancel() - nonce, err = t.pendingNonceProvider.PendingNonceAt(ctx, args.From) + nonce, err = t.pendingNonceProvider.PendingNonceAt(ctx, common.Address(args.From)) if err != nil { return hash, err } @@ -264,9 +274,18 @@ func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKe if args.Gas == nil { ctx, cancel = context.WithTimeout(context.Background(), t.rpcCallTimeout) defer cancel() + + var ( + gethTo common.Address + gethToPtr *common.Address + ) + if args.To != nil { + gethTo = common.Address(*args.To) + gethToPtr = &gethTo + } gas, err = t.gasCalculator.EstimateGas(ctx, ethereum.CallMsg{ - From: args.From, - To: args.To, + From: common.Address(args.From), + To: gethToPtr, GasPrice: gasPrice, Value: value, Data: args.GetInput(), @@ -282,14 +301,7 @@ func (t *Transactor) validateAndPropagate(selectedAccount *account.SelectedExtKe gas = uint64(*args.Gas) } - var tx *gethtypes.Transaction - if args.To != nil { - tx = gethtypes.NewTransaction(nonce, *args.To, value, gas, gasPrice, args.GetInput()) - t.logNewTx(args, gas, gasPrice, value) - } else { - tx = gethtypes.NewContractCreation(nonce, value, gas, gasPrice, args.GetInput()) - t.logNewContract(args, gas, gasPrice, value, nonce) - } + tx := t.buildTransactionWithOverrides(nonce, value, gas, gasPrice, args) signedTx, err := gethtypes.SignTx(tx, gethtypes.NewEIP155Signer(chainID), selectedAccount.AccountKey.PrivateKey) if err != nil { @@ -310,10 +322,14 @@ func (t *Transactor) buildTransaction(args SendTxArgs) *gethtypes.Transaction { gas := uint64(*args.Gas) gasPrice := (*big.Int)(args.GasPrice) + return t.buildTransactionWithOverrides(nonce, value, gas, gasPrice, args) +} + +func (t *Transactor) buildTransactionWithOverrides(nonce uint64, value *big.Int, gas uint64, gasPrice *big.Int, args SendTxArgs) *gethtypes.Transaction { var tx *gethtypes.Transaction if args.To != nil { - tx = gethtypes.NewTransaction(nonce, *args.To, value, gas, gasPrice, args.GetInput()) + tx = gethtypes.NewTransaction(nonce, common.Address(*args.To), value, gas, gasPrice, args.GetInput()) t.logNewTx(args, gas, gasPrice, value) } else { tx = gethtypes.NewContractCreation(nonce, value, gas, gasPrice, args.GetInput()) @@ -337,7 +353,7 @@ func (t *Transactor) getTransactionNonce(args SendTxArgs) (newNonce uint64, err // get the remote nonce ctx, cancel := context.WithTimeout(context.Background(), t.rpcCallTimeout) defer cancel() - remoteNonce, err = t.pendingNonceProvider.PendingNonceAt(ctx, args.From) + remoteNonce, err = t.pendingNonceProvider.PendingNonceAt(ctx, common.Address(args.From)) if err != nil { return newNonce, err } diff --git a/transactions/transactor_test.go b/transactions/transactor_test.go index 789f13bc9..e404250dd 100644 --- a/transactions/transactor_test.go +++ b/transactions/transactor_test.go @@ -11,7 +11,6 @@ import ( "time" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" - "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" @@ -23,6 +22,7 @@ import ( "github.com/golang/mock/gomock" "github.com/status-im/status-go/account" "github.com/status-im/status-go/contracts/ens/contract" + "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/params" "github.com/status-im/status-go/rpc" @@ -104,7 +104,7 @@ func (s *TransactorSuite) setupTransactionPoolAPI(args SendTxArgs, returnNonce, func (s *TransactorSuite) rlpEncodeTx(args SendTxArgs, config *params.NodeConfig, account *account.SelectedExtKey, nonce *hexutil.Uint64, gas hexutil.Uint64, gasPrice *big.Int) hexutil.Bytes { newTx := gethtypes.NewTransaction( uint64(*nonce), - *args.To, + common.Address(*args.To), args.Value.ToInt(), uint64(gas), gasPrice, @@ -122,7 +122,7 @@ func (s *TransactorSuite) TestGasValues() { key, _ := gethcrypto.GenerateKey() selectedAccount := &account.SelectedExtKey{ Address: account.FromAddress(utils.TestConfig.Account1.WalletAddress), - AccountKey: &keystore.Key{PrivateKey: key}, + AccountKey: &types.Key{PrivateKey: key}, } testCases := []struct { name string @@ -155,8 +155,8 @@ func (s *TransactorSuite) TestGasValues() { s.T().Run(testCase.name, func(t *testing.T) { s.SetupTest() args := SendTxArgs{ - From: account.GethFromAddress(utils.TestConfig.Account1.WalletAddress), - To: account.GethToAddress(utils.TestConfig.Account2.WalletAddress), + From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), + To: account.ToAddress(utils.TestConfig.Account2.WalletAddress), Gas: testCase.gas, GasPrice: testCase.gasPrice, } @@ -171,10 +171,10 @@ func (s *TransactorSuite) TestGasValues() { func (s *TransactorSuite) TestArgsValidation() { args := SendTxArgs{ - From: account.GethFromAddress(utils.TestConfig.Account1.WalletAddress), - To: account.GethToAddress(utils.TestConfig.Account2.WalletAddress), - Data: hexutil.Bytes([]byte{0x01, 0x02}), - Input: hexutil.Bytes([]byte{0x02, 0x01}), + From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), + To: account.ToAddress(utils.TestConfig.Account2.WalletAddress), + Data: types.HexBytes([]byte{0x01, 0x02}), + Input: types.HexBytes([]byte{0x02, 0x01}), } s.False(args.Valid()) selectedAccount := &account.SelectedExtKey{ @@ -186,8 +186,8 @@ func (s *TransactorSuite) TestArgsValidation() { func (s *TransactorSuite) TestAccountMismatch() { args := SendTxArgs{ - From: account.GethFromAddress(utils.TestConfig.Account1.WalletAddress), - To: account.GethToAddress(utils.TestConfig.Account2.WalletAddress), + From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), + To: account.ToAddress(utils.TestConfig.Account2.WalletAddress), } var err error @@ -216,14 +216,14 @@ func (s *TransactorSuite) TestLocalNonce() { key, _ := gethcrypto.GenerateKey() selectedAccount := &account.SelectedExtKey{ Address: account.FromAddress(utils.TestConfig.Account1.WalletAddress), - AccountKey: &keystore.Key{PrivateKey: key}, + AccountKey: &types.Key{PrivateKey: key}, } nonce := hexutil.Uint64(0) for i := 0; i < txCount; i++ { args := SendTxArgs{ - From: account.GethFromAddress(utils.TestConfig.Account1.WalletAddress), - To: account.GethToAddress(utils.TestConfig.Account2.WalletAddress), + From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), + To: account.ToAddress(utils.TestConfig.Account2.WalletAddress), } s.setupTransactionPoolAPI(args, nonce, hexutil.Uint64(i), selectedAccount, nil) @@ -235,8 +235,8 @@ func (s *TransactorSuite) TestLocalNonce() { nonce = hexutil.Uint64(5) args := SendTxArgs{ - From: account.GethFromAddress(utils.TestConfig.Account1.WalletAddress), - To: account.GethToAddress(utils.TestConfig.Account2.WalletAddress), + From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), + To: account.ToAddress(utils.TestConfig.Account2.WalletAddress), } s.setupTransactionPoolAPI(args, nonce, nonce, selectedAccount, nil) @@ -250,8 +250,8 @@ func (s *TransactorSuite) TestLocalNonce() { testErr := errors.New("test") s.txServiceMock.EXPECT().GetTransactionCount(gomock.Any(), gomock.Eq(common.Address(selectedAccount.Address)), gethrpc.PendingBlockNumber).Return(nil, testErr) args = SendTxArgs{ - From: account.GethFromAddress(utils.TestConfig.Account1.WalletAddress), - To: account.GethToAddress(utils.TestConfig.Account2.WalletAddress), + From: account.FromAddress(utils.TestConfig.Account1.WalletAddress), + To: account.ToAddress(utils.TestConfig.Account2.WalletAddress), } _, err = s.manager.SendTransaction(args, selectedAccount) @@ -269,14 +269,14 @@ func (s *TransactorSuite) TestContractCreation() { backend := backends.NewSimulatedBackend(genesis, math.MaxInt64) selectedAccount := &account.SelectedExtKey{ Address: types.Address(testaddr), - AccountKey: &keystore.Key{PrivateKey: key}, + AccountKey: &types.Key{PrivateKey: key}, } s.manager.sender = backend s.manager.gasCalculator = backend s.manager.pendingNonceProvider = backend tx := SendTxArgs{ - From: testaddr, - Input: hexutil.Bytes(common.FromHex(contract.ENSBin)), + From: types.Address(testaddr), + Input: types.FromHex(contract.ENSBin), } hash, err := s.manager.SendTransaction(tx, selectedAccount) @@ -288,9 +288,9 @@ func (s *TransactorSuite) TestContractCreation() { } func (s *TransactorSuite) TestSendTransactionWithSignature() { - privKey, err := gethcrypto.GenerateKey() + privKey, err := crypto.GenerateKey() s.Require().NoError(err) - address := gethcrypto.PubkeyToAddress(privKey.PublicKey) + address := crypto.PubkeyToAddress(privKey.PublicKey) scenarios := []struct { localNonce hexutil.Uint64 @@ -340,7 +340,7 @@ func (s *TransactorSuite) TestSendTransactionWithSignature() { // simulate transaction signed externally signer := gethtypes.NewEIP155Signer(chainID) - tx := gethtypes.NewTransaction(uint64(nonce), to, (*big.Int)(value), uint64(gas), (*big.Int)(gasPrice), data) + tx := gethtypes.NewTransaction(uint64(nonce), common.Address(to), (*big.Int)(value), uint64(gas), (*big.Int)(gasPrice), data) hash := signer.Hash(tx) sig, err := gethcrypto.Sign(hash[:], privKey) s.Require().NoError(err) @@ -350,7 +350,7 @@ func (s *TransactorSuite) TestSendTransactionWithSignature() { s.Require().NoError(err) s.txServiceMock.EXPECT(). - GetTransactionCount(gomock.Any(), address, gethrpc.PendingBlockNumber). + GetTransactionCount(gomock.Any(), common.Address(address), gethrpc.PendingBlockNumber). Return(&scenario.localNonce, nil) if !scenario.expectError { @@ -382,9 +382,9 @@ func (s *TransactorSuite) TestSendTransactionWithSignature_InvalidSignature() { } func (s *TransactorSuite) TestHashTransaction() { - privKey, err := gethcrypto.GenerateKey() + privKey, err := crypto.GenerateKey() s.Require().NoError(err) - address := gethcrypto.PubkeyToAddress(privKey.PublicKey) + address := crypto.PubkeyToAddress(privKey.PublicKey) remoteNonce := hexutil.Uint64(1) txNonce := hexutil.Uint64(0) @@ -405,7 +405,7 @@ func (s *TransactorSuite) TestHashTransaction() { } s.txServiceMock.EXPECT(). - GetTransactionCount(gomock.Any(), address, gethrpc.PendingBlockNumber). + GetTransactionCount(gomock.Any(), common.Address(address), gethrpc.PendingBlockNumber). Return(&remoteNonce, nil) newArgs, hash, err := s.manager.HashTransaction(args) diff --git a/transactions/types.go b/transactions/types.go index 0acf59791..4d4ccd557 100644 --- a/transactions/types.go +++ b/transactions/types.go @@ -8,6 +8,7 @@ import ( ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/status-im/status-go/eth-node/types" ) var ( @@ -36,8 +37,8 @@ type GasCalculator interface { // This struct is based on go-ethereum's type in internal/ethapi/api.go, but we have freedom // over the exact layout of this struct. type SendTxArgs struct { - From common.Address `json:"from"` - To *common.Address `json:"to"` + From types.Address `json:"from"` + To *types.Address `json:"to"` Gas *hexutil.Uint64 `json:"gas"` GasPrice *hexutil.Big `json:"gasPrice"` Value *hexutil.Big `json:"value"` @@ -45,8 +46,8 @@ type SendTxArgs struct { // We keep both "input" and "data" for backward compatibility. // "input" is a preferred field. // see `vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go:1107` - Input hexutil.Bytes `json:"input"` - Data hexutil.Bytes `json:"data"` + Input types.HexBytes `json:"input"` + Data types.HexBytes `json:"data"` } // Valid checks whether this structure is filled in correctly. @@ -61,7 +62,7 @@ func (args SendTxArgs) Valid() bool { } // GetInput returns either Input or Data field's value dependent on what is filled. -func (args SendTxArgs) GetInput() hexutil.Bytes { +func (args SendTxArgs) GetInput() types.HexBytes { if !isNilOrEmpty(args.Input) { return args.Input } @@ -69,6 +70,6 @@ func (args SendTxArgs) GetInput() hexutil.Bytes { return args.Data } -func isNilOrEmpty(bytes hexutil.Bytes) bool { +func isNilOrEmpty(bytes types.HexBytes) bool { return bytes == nil || len(bytes) == 0 } diff --git a/transactions/types_test.go b/transactions/types_test.go index 3476eede6..a63889442 100644 --- a/transactions/types_test.go +++ b/transactions/types_test.go @@ -3,17 +3,17 @@ package transactions import ( "testing" - "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/status-im/status-go/eth-node/types" "github.com/stretchr/testify/assert" ) func TestSendTxArgsValidity(t *testing.T) { // 1. If only data fields is set, valid and return data - bytes1 := hexutil.Bytes([]byte{0xAA, 0xBB, 0xCC, 0xDD}) - bytes2 := hexutil.Bytes([]byte{0x00, 0x01, 0x02}) + bytes1 := types.HexBytes([]byte{0xAA, 0xBB, 0xCC, 0xDD}) + bytes2 := types.HexBytes([]byte{0x00, 0x01, 0x02}) - bytesEmpty := hexutil.Bytes([]byte{}) + bytesEmpty := types.HexBytes([]byte{}) doSendTxValidityTest(t, SendTxArgs{}, true, nil) doSendTxValidityTest(t, SendTxArgs{Input: bytes1}, true, bytes1) @@ -25,7 +25,7 @@ func TestSendTxArgsValidity(t *testing.T) { doSendTxValidityTest(t, SendTxArgs{Input: bytesEmpty, Data: bytesEmpty}, true, bytesEmpty) } -func doSendTxValidityTest(t *testing.T, args SendTxArgs, expectValid bool, expectValue hexutil.Bytes) { +func doSendTxValidityTest(t *testing.T, args SendTxArgs, expectValid bool, expectValue types.HexBytes) { assert.Equal(t, expectValid, args.Valid(), "Valid() returned unexpected value") if expectValid { assert.Equal(t, expectValue, args.GetInput(), "GetInput() returned unexpected value") diff --git a/vendor/github.com/status-im/status-go/eth-node/keystore/keystore.go b/vendor/github.com/status-im/status-go/eth-node/keystore/keystore.go new file mode 100644 index 000000000..02a3a9e40 --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/keystore/keystore.go @@ -0,0 +1,15 @@ +// Imported from github.com/ethereum/go-ethereum/accounts/keystore/keystore.go + +package keystore + +import ( + "errors" +) + +const ( + version = 3 +) + +var ( + ErrDecrypt = errors.New("could not decrypt key with given password") +) diff --git a/vendor/github.com/status-im/status-go/eth-node/keystore/passphrase.go b/vendor/github.com/status-im/status-go/eth-node/keystore/passphrase.go new file mode 100644 index 000000000..36d8a5755 --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/keystore/passphrase.go @@ -0,0 +1,329 @@ +// Imported from github.com/ethereum/go-ethereum/accounts/keystore/passphrase.go +// and github.com/ethereum/go-ethereum/accounts/keystore/presale.go + +package keystore + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/pborman/uuid" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/extkeys" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/scrypt" +) + +const ( + keyHeaderKDF = "scrypt" +) + +type encryptedKeyJSONV3 struct { + Address string `json:"address"` + Crypto CryptoJSON `json:"crypto"` + Id string `json:"id"` + Version int `json:"version"` + ExtendedKey CryptoJSON `json:"extendedkey"` + SubAccountIndex uint32 `json:"subaccountindex"` +} + +type encryptedKeyJSONV1 struct { + Address string `json:"address"` + Crypto CryptoJSON `json:"crypto"` + Id string `json:"id"` + Version string `json:"version"` +} + +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"` +} + +// DecryptKey decrypts a key from a json blob, returning the private key itself. +func DecryptKey(keyjson []byte, auth string) (*types.Key, error) { + // Parse the json into a simple map to fetch the key version + m := make(map[string]interface{}) + if err := json.Unmarshal(keyjson, &m); err != nil { + return nil, err + } + // Depending on the version try to parse one way or another + var ( + keyBytes, keyId []byte + err error + extKeyBytes []byte + extKey *extkeys.ExtendedKey + ) + + subAccountIndex, ok := m["subaccountindex"].(float64) + if !ok { + subAccountIndex = 0 + } + + if version, ok := m["version"].(string); ok && version == "1" { + k := new(encryptedKeyJSONV1) + if err := json.Unmarshal(keyjson, k); err != nil { + return nil, err + } + keyBytes, keyId, err = decryptKeyV1(k, auth) + if err != nil { + return nil, err + } + + extKey, err = extkeys.NewKeyFromString(extkeys.EmptyExtendedKeyString) + } else { + k := new(encryptedKeyJSONV3) + if err := json.Unmarshal(keyjson, k); err != nil { + return nil, err + } + keyBytes, keyId, err = decryptKeyV3(k, auth) + if err != nil { + return nil, err + } + + extKeyBytes, err = decryptExtendedKey(k, auth) + if err != nil { + return nil, err + } + extKey, err = extkeys.NewKeyFromString(string(extKeyBytes)) + } + // Handle any decryption errors and return the key + if err != nil { + return nil, err + } + key := crypto.ToECDSAUnsafe(keyBytes) + + return &types.Key{ + Id: uuid.UUID(keyId), + Address: crypto.PubkeyToAddress(key.PublicKey), + PrivateKey: key, + ExtendedKey: extKey, + SubAccountIndex: uint32(subAccountIndex), + }, 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) + } + keyId = uuid.Parse(keyProtected.Id) + plainText, err := DecryptDataV3(keyProtected.Crypto, auth) + if err != nil { + return nil, nil, err + } + return plainText, keyId, err +} + +func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyId []byte, err error) { + 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 := aesCBCDecrypt(crypto.Keccak256(derivedKey[:16])[:16], cipherText, iv) + if err != nil { + return nil, nil, err + } + return plainText, keyId, err +} + +func decryptExtendedKey(keyProtected *encryptedKeyJSONV3, auth string) (plainText []byte, err error) { + if len(keyProtected.ExtendedKey.CipherText) == 0 { + return []byte(extkeys.EmptyExtendedKeyString), nil + } + + if keyProtected.Version != version { + return nil, fmt.Errorf("Version not supported: %v", keyProtected.Version) + } + + if keyProtected.ExtendedKey.Cipher != "aes-128-ctr" { + return nil, fmt.Errorf("Cipher not supported: %v", keyProtected.ExtendedKey.Cipher) + } + + mac, err := hex.DecodeString(keyProtected.ExtendedKey.MAC) + if err != nil { + return nil, err + } + + iv, err := hex.DecodeString(keyProtected.ExtendedKey.CipherParams.IV) + if err != nil { + return nil, err + } + + cipherText, err := hex.DecodeString(keyProtected.ExtendedKey.CipherText) + if err != nil { + return nil, err + } + + derivedKey, err := getKDFKey(keyProtected.ExtendedKey, 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 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) +} + +// 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 +} + +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 aesCBCDecrypt(key, cipherText, iv []byte) ([]byte, error) { + aesBlock, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + decrypter := cipher.NewCBCDecrypter(aesBlock, iv) + paddedPlaintext := make([]byte, len(cipherText)) + decrypter.CryptBlocks(paddedPlaintext, cipherText) + plaintext := pkcs7Unpad(paddedPlaintext) + if plaintext == nil { + return nil, ErrDecrypt + } + return plaintext, err +} + +// From https://leanpub.com/gocrypto/read#leanpub-auto-block-cipher-modes +func pkcs7Unpad(in []byte) []byte { + if len(in) == 0 { + return nil + } + + padding := in[len(in)-1] + if int(padding) > len(in) || padding > aes.BlockSize { + return nil + } else if padding == 0 { + return nil + } + + for i := len(in) - 1; i > len(in)-int(padding)-1; i-- { + if in[i] != padding { + return nil + } + } + return in[:len(in)-int(padding)] +} diff --git a/vendor/github.com/status-im/status-go/eth-node/types/account.go b/vendor/github.com/status-im/status-go/eth-node/types/account.go new file mode 100644 index 000000000..61795fe02 --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/types/account.go @@ -0,0 +1,8 @@ +package types + +// Account represents an Ethereum account located at a specific location defined +// by the optional URL field. +type Account struct { + Address Address `json:"address"` // Ethereum account address derived from the key + URL string `json:"url"` // Optional resource locator within a backend +} diff --git a/vendor/github.com/status-im/status-go/eth-node/types/key.go b/vendor/github.com/status-im/status-go/eth-node/types/key.go new file mode 100644 index 000000000..be2e83c0d --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/types/key.go @@ -0,0 +1,24 @@ +package types + +import ( + "crypto/ecdsa" + + "github.com/pborman/uuid" + "github.com/status-im/status-go/extkeys" +) + +type Key struct { + Id uuid.UUID // Version 4 "random" for unique id not derived from key data + // to simplify lookups we also store the address + Address Address + // we only store privkey as pubkey/address can be derived from it + // privkey in this struct is always in plaintext + PrivateKey *ecdsa.PrivateKey + // ExtendedKey is the extended key of the PrivateKey itself, and it's used + // to derive child keys. + ExtendedKey *extkeys.ExtendedKey + // SubAccountIndex is DEPRECATED + // It was use in Status to keep track of the number of sub-account created + // before having multi-account support. + SubAccountIndex uint32 +} diff --git a/vendor/github.com/status-im/status-go/eth-node/types/keystore.go b/vendor/github.com/status-im/status-go/eth-node/types/keystore.go new file mode 100644 index 000000000..1c491912c --- /dev/null +++ b/vendor/github.com/status-im/status-go/eth-node/types/keystore.go @@ -0,0 +1,26 @@ +package types + +import ( + "crypto/ecdsa" + + "github.com/status-im/status-go/extkeys" +) + +type KeyStore interface { + // ImportAccount imports the account specified with privateKey. + ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (Account, error) + // ImportSingleExtendedKey imports an extended key setting it in both the PrivateKey and ExtendedKey fields + // of the Key struct. + // ImportExtendedKey is used in older version of Status where PrivateKey is set to be the BIP44 key at index 0, + // and ExtendedKey is the extended key of the BIP44 key at index 1. + ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (Account, error) + // ImportExtendedKeyForPurpose stores ECDSA key (obtained from extended key) along with CKD#2 (root for sub-accounts) + // If key file is not found, it is created. Key is encrypted with the given passphrase. + // Deprecated: status-go is now using ImportSingleExtendedKey + ImportExtendedKeyForPurpose(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, passphrase string) (Account, error) + // AccountDecryptedKey returns decrypted key for account (provided that password is correct). + AccountDecryptedKey(a Account, auth string) (Account, *Key, error) + // Delete deletes the key matched by account if the passphrase is correct. + // If the account contains no filename, the address must match a unique key. + Delete(a Account, auth string) error +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 9733fc84d..6f4a00773 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -374,6 +374,7 @@ github.com/status-im/status-go/eth-node/bridge/geth github.com/status-im/status-go/eth-node/bridge/geth/ens github.com/status-im/status-go/eth-node/crypto github.com/status-im/status-go/eth-node/crypto/ecies +github.com/status-im/status-go/eth-node/keystore github.com/status-im/status-go/eth-node/types github.com/status-im/status-go/eth-node/types/ens # github.com/status-im/status-go/extkeys v1.0.0 => ./extkeys diff --git a/waku/rate_limiter_test.go b/waku/rate_limiter_test.go index 11dbc495b..4d0616486 100644 --- a/waku/rate_limiter_test.go +++ b/waku/rate_limiter_test.go @@ -116,7 +116,7 @@ func TestPeerLimiterHandlerWithWhitelisting(t *testing.T) { LimitPerSecIP: 1, LimitPerSecPeerID: 1, WhitelistedIPs: []string{""}, // no IP is represented as string - WhitelistedPeerIDs: []enode.ID{enode.ID{0xaa, 0xbb, 0xcc}}, + WhitelistedPeerIDs: []enode.ID{{0xaa, 0xbb, 0xcc}}, }, h) p := &Peer{ peer: p2p.NewPeer(enode.ID{0xaa, 0xbb, 0xcc}, "test-peer", nil),