From 3b0f776d1d1e4eae0c89f9c935d9f8d2e4088064 Mon Sep 17 00:00:00 2001 From: Victor Farazdagi Date: Sun, 21 Aug 2016 09:45:59 +0300 Subject: [PATCH] BIP44-compliant CKD + sending transactions w/o unlocking --- src/extkeys/hdkey.go | 48 +++ src/extkeys/hdkey_test.go | 100 +++--- src/extkeys/utils.go | 6 +- src/gethdep.go | 91 ++++-- src/gethdep_test.go | 291 +++++++++++++++--- src/library.go | 24 +- src/main.go | 4 +- .../go-ethereum/accounts/account_manager.go | 13 +- .../go-ethereum/internal/ethapi/api.go | 4 +- .../go-ethereum/les/status_backend.go | 4 +- .../ethereum/go-ethereum/whisper/whisper.go | 29 +- 11 files changed, 471 insertions(+), 143 deletions(-) diff --git a/src/extkeys/hdkey.go b/src/extkeys/hdkey.go index 4e10309bb..9d35611f6 100644 --- a/src/extkeys/hdkey.go +++ b/src/extkeys/hdkey.go @@ -53,6 +53,11 @@ const ( // fingerprint, 4 bytes child number, 32 bytes chain code, and 33 bytes // public/private key data. serializedKeyLen = 4 + 1 + 4 + 4 + 32 + 33 // 78 bytes + + CoinTypeBTC = 0 // 0x80000000 + CoinTypeTestNet = 1 // 0x80000001 + CoinTypeETH = 60 // 0x8000003c + CoinTypeETC = 60 // 0x80000000 ) var ( @@ -63,11 +68,15 @@ var ( ErrDerivingHardenedFromPublic = errors.New("cannot derive a hardened key from public key") ErrBadChecksum = errors.New("bad extended key checksum") ErrInvalidKeyLen = errors.New("serialized extended key length is invalid") + ErrDerivingChild = errors.New("error deriving child key") + ErrInvalidMasterKey = errors.New("invalid master key supplied") PrivateKeyVersion, _ = hex.DecodeString("0488ADE4") PublicKeyVersion, _ = hex.DecodeString("0488B21E") ) +type CoinType int + type ExtendedKey struct { Version []byte // 4 bytes, mainnet: 0x0488B21E public, 0x0488ADE4 private; testnet: 0x043587CF public, 0x04358394 private Depth uint16 // 1 byte, depth: 0x00 for master nodes, 0x01 for level-1 derived keys, .... @@ -184,6 +193,45 @@ func (parent *ExtendedKey) Child(i uint32) (*ExtendedKey, error) { return child, nil } +// Child1 returns Status CKD#1 (used for ETH and SHH). +// BIP44 format is used: m / purpose' / coin_type' / account' / change / address_index +func (master *ExtendedKey) BIP44Child(coinType, i uint32) (*ExtendedKey, error) { + if !master.IsPrivate { + return nil, ErrInvalidMasterKey + } + + if master.Depth != 0 { + return nil, ErrInvalidMasterKey + } + + // m/44'/60'/0'/0/index + extKey, err := master.Derive([]uint32{ + HardenedKeyStart + 44, // purpose + HardenedKeyStart + coinType, // cointype + HardenedKeyStart + 0, // account + 0, // 0 - public, 1 - private + i, // index + }) + if err != nil { + return nil, err + } + + return extKey, nil +} + +func (parent *ExtendedKey) Derive(path []uint32) (*ExtendedKey, error) { + var err error + extKey := parent + for _, i := range path { + extKey, err = extKey.Child(i) + if err != nil { + return nil, ErrDerivingChild + } + } + + return extKey, nil +} + func (k *ExtendedKey) Neuter() (*ExtendedKey, error) { // Already an extended public key. if !k.IsPrivate { diff --git a/src/extkeys/hdkey_test.go b/src/extkeys/hdkey_test.go index b0c3b6b27..495234347 100644 --- a/src/extkeys/hdkey_test.go +++ b/src/extkeys/hdkey_test.go @@ -12,7 +12,6 @@ import ( ) func TestBIP32Vectors(t *testing.T) { - hkStart := uint32(0x80000000) tests := []struct { name string seed string @@ -31,35 +30,35 @@ func TestBIP32Vectors(t *testing.T) { { "test vector 1 chain m/0H", "000102030405060708090a0b0c0d0e0f", - []uint32{hkStart}, + []uint32{extkeys.HardenedKeyStart}, "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw", "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7", }, { "test vector 1 chain m/0H/1", "000102030405060708090a0b0c0d0e0f", - []uint32{hkStart, 1}, + []uint32{extkeys.HardenedKeyStart, 1}, "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ", "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs", }, { "test vector 1 chain m/0H/1/2H", "000102030405060708090a0b0c0d0e0f", - []uint32{hkStart, 1, hkStart + 2}, + []uint32{extkeys.HardenedKeyStart, 1, extkeys.HardenedKeyStart + 2}, "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", }, { "test vector 1 chain m/0H/1/2H/2", "000102030405060708090a0b0c0d0e0f", - []uint32{hkStart, 1, hkStart + 2, 2}, + []uint32{extkeys.HardenedKeyStart, 1, extkeys.HardenedKeyStart + 2, 2}, "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV", "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334", }, { "test vector 1 chain m/0H/1/2H/2/1000000000", "000102030405060708090a0b0c0d0e0f", - []uint32{hkStart, 1, hkStart + 2, 2, 1000000000}, + []uint32{extkeys.HardenedKeyStart, 1, extkeys.HardenedKeyStart + 2, 2, 1000000000}, "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy", "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76", }, @@ -81,28 +80,28 @@ func TestBIP32Vectors(t *testing.T) { { "test vector 2 chain m/0/2147483647H", "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", - []uint32{0, hkStart + 2147483647}, + []uint32{0, extkeys.HardenedKeyStart + 2147483647}, "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a", "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9", }, { "test vector 2 chain m/0/2147483647H/1", "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", - []uint32{0, hkStart + 2147483647, 1}, + []uint32{0, extkeys.HardenedKeyStart + 2147483647, 1}, "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon", "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef", }, { "test vector 2 chain m/0/2147483647H/1/2147483646H", "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", - []uint32{0, hkStart + 2147483647, 1, hkStart + 2147483646}, + []uint32{0, extkeys.HardenedKeyStart + 2147483647, 1, extkeys.HardenedKeyStart + 2147483646}, "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc", }, { "test vector 2 chain m/0/2147483647H/1/2147483646H/2", "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", - []uint32{0, hkStart + 2147483647, 1, hkStart + 2147483646, 2}, + []uint32{0, extkeys.HardenedKeyStart + 2147483647, 1, extkeys.HardenedKeyStart + 2147483646, 2}, "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt", "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j", }, @@ -127,12 +126,10 @@ tests: continue } - for _, ind := range test.path { - extKey, err = extKey.Child(ind) - if err != nil { - t.Errorf("cannot derive child: %v", err) - continue tests - } + extKey, err = extKey.Derive(test.path) + if err != nil { + t.Errorf("cannot derive child: %v", err) + continue tests } privKeyStr := extKey.String() @@ -271,16 +268,10 @@ tests: t.Errorf("NewKeyFromString #%d (%s): unexpected error creating extended key: %v", i, test.name, err) continue } - - for _, childNum := range test.path { - var err error - extKey, err = extKey.Child(childNum) - if err != nil { - t.Errorf("err: %v", err) - continue tests - } - - t.Logf("test %d (%s): %s", i, test.name, extKey.String()) + extKey, err = extKey.Derive(test.path) + if err != nil { + t.Errorf("cannot derive child: %v", err) + continue tests } privStr := extKey.String() @@ -288,6 +279,8 @@ tests: t.Errorf("Child #%d (%s): mismatched serialized private extended key -- got: %s, want: %s", i, test.name, privStr, test.wantPriv) continue + } else { + t.Logf("test %d (%s): %s", i, test.name, extKey.String()) } } @@ -391,21 +384,18 @@ tests: continue } - for _, childNum := range test.path { - var err error - extKey, err = extKey.Child(childNum) - if err != nil { - t.Errorf("err: %v", err) - continue tests - } - - t.Logf("test %d (%s): %s", i, test.name, extKey.String()) + extKey, err = extKey.Derive(test.path) + if err != nil { + t.Errorf("cannot derive child: %v", err) + continue tests } pubStr := extKey.String() if pubStr != test.wantPub { t.Errorf("Child #%d (%s): mismatched serialized public extended key -- got: %s, want: %s", i, test.name, pubStr, test.wantPub) continue + } else { + t.Logf("test %d (%s): %s", i, test.name, extKey.String()) } } } @@ -448,6 +438,17 @@ func TestErrors(t *testing.T) { t.Errorf("Child: mismatched error -- got: %v, want: %v", err, extkeys.ErrDerivingHardenedFromPublic) } + _, err = pubKey.BIP44Child(extkeys.CoinTypeETH, 0) + if err != extkeys.ErrInvalidMasterKey { + t.Errorf("BIP44Child: mistmatched error -- got: %v, want: %v", err, extkeys.ErrInvalidMasterKey) + } + + childKey, _ := extKey.Child(extkeys.HardenedKeyStart + 1) + _, err = childKey.BIP44Child(extkeys.CoinTypeETH, 0) // this should be called from master only + if err != extkeys.ErrInvalidMasterKey { + t.Errorf("BIP44Child: mistmatched error -- got: %v, want: %v", err, extkeys.ErrInvalidMasterKey) + } + // NewKeyFromString failure tests. tests := []struct { name string @@ -497,6 +498,35 @@ func TestErrors(t *testing.T) { } } +func TestBIP44ChildDerivation(t *testing.T) { + keyString := "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" + derivedKey1String := "xprvA38t8tFW4vbuB7WJXEqMFmZqRrcZUKWqqMcGjjKjr2hbfvPhRtLLJGL4ayWG8shF1VkuUikVGodGshLiKRS7WrdsrGSVDQCY33qoPBxG2Kp" + derivedKey2String := "xprvA38t8tFW4vbuDgBNpekPnuMSfpWziDLdF7W9Zd3mPy6eDEkM5F17vk59RtVoFbNdBBq84EJf5CqdZhhEoBkAM4DXHQsDqvUxVnncfnDQEFg" + + extKey, err := extkeys.NewKeyFromString(keyString) + if err != nil { + t.Errorf("NewKeyFromString: cannot create extended key") + } + + accounKey1, err := extKey.BIP44Child(extkeys.CoinTypeETH, 0) + if err != nil { + t.Errorf("Error dering BIP44-compliant key") + } + if accounKey1.String() != derivedKey1String { + t.Errorf("BIP44Child: key mismatch -- got: %v, want: %v", accounKey1.String(), derivedKey1String) + } + t.Logf("Account 1 key: %s", accounKey1.String()) + + accounKey2, err := extKey.BIP44Child(extkeys.CoinTypeETH, 1) + if err != nil { + t.Errorf("Error dering BIP44-compliant key") + } + if accounKey2.String() != derivedKey2String { + t.Errorf("BIP44Child: key mismatch -- got: %v, want: %v", accounKey2.String(), derivedKey2String) + } + t.Logf("Account 1 key: %s", accounKey2.String()) +} + //func TestNewKey(t *testing.T) { // mnemonic := NewMnemonic() // diff --git a/src/extkeys/utils.go b/src/extkeys/utils.go index 1ed655a99..383c66c57 100644 --- a/src/extkeys/utils.go +++ b/src/extkeys/utils.go @@ -14,9 +14,9 @@ var ( ) func splitHMAC(seed, salt []byte) (secretKey, chainCode []byte, err error) { - hmac := hmac.New(sha512.New, salt) - hmac.Write(seed) - I := hmac.Sum(nil) + data := hmac.New(sha512.New, salt) + data.Write(seed) + I := data.Sum(nil) // Split I into two 32-byte sequences, IL and IR. // IL = master secret key diff --git a/src/gethdep.go b/src/gethdep.go index 4d1a82e55..63f7f8707 100644 --- a/src/gethdep.go +++ b/src/gethdep.go @@ -9,13 +9,10 @@ extern bool GethServiceSignalEvent( const char *jsonEvent ); import "C" import ( + "encoding/json" "errors" "fmt" - "io/ioutil" - "time" - "encoding/json" - "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -26,8 +23,13 @@ import ( ) var ( - scryptN = 4096 - scryptP = 6 + ErrInvalidGethNode = errors.New("no running node detected for account unlock") + ErrInvalidWhisperService = errors.New("whisper service is unavailable") + ErrInvalidAccountManager = errors.New("could not retrieve account manager") + ErrAddressToAccountMappingFailure = errors.New("cannot retreive a valid account for a given address") + ErrAccountToKeyMappingFailure = errors.New("cannot retreive a valid key for a given account") + ErrUnlockCalled = errors.New("no need to unlock accounts, use Login() instead") + ErrWhisperIdentityInjectionFailure = errors.New("failed to inject identity into Whisper") ) // createAccount creates an internal geth account @@ -45,25 +47,27 @@ func createAccount(password string) (string, string, string, error) { return "", "", "", errextra.Wrap(err, "Can not create mnemonic seed") } - // generate extended key (see BIP32) + // generate extended master key (see BIP32) extKey, err := extkeys.NewMaster(m.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt)) if err != nil { return "", "", "", errextra.Wrap(err, "Can not create master extended key") } + // derive hardened child (see BIP44) + extChild1, err := extKey.BIP44Child(extkeys.CoinTypeETH, 0) + if err != nil { + return "", "", "", errextra.Wrap(err, "Can not derive hardened child key (#1)") + } + // generate the account - account, err := accountManager.NewAccountUsingExtendedKey(extKey, password, w) + account, err := accountManager.NewAccountUsingExtendedKey(extChild1, password, w) if err != nil { return "", "", "", errextra.Wrap(err, "Account manager could not create the account") } address := fmt.Sprintf("%x", account.Address) // recover the public key to return - keyContents, err := ioutil.ReadFile(account.File) - if err != nil { - return address, "", "", errextra.Wrap(err, "Could not load the key contents") - } - key, err := accounts.DecryptKey(keyContents, password) + account, key, err := accountManager.AccountDecryptedKey(account, password) if err != nil { return address, "", "", errextra.Wrap(err, "Could not recover the key") } @@ -90,7 +94,13 @@ func remindAccountDetails(password, mnemonic string) (string, string, error) { return "", "", errextra.Wrap(err, "Can not create master extended key") } - privateKeyECDSA := extKey.ToECDSA() + // derive hardened child (see BIP44) + extChild1, err := extKey.BIP44Child(extkeys.CoinTypeETH, 0) + if err != nil { + return "", "", errextra.Wrap(err, "Can not derive hardened child key (#1)") + } + + privateKeyECDSA := extChild1.ToECDSA() address := fmt.Sprintf("%x", crypto.PubkeyToAddress(privateKeyECDSA.PublicKey)) pubKey := common.ToHex(crypto.FromECDSAPub(&privateKeyECDSA.PublicKey)) @@ -105,30 +115,43 @@ func remindAccountDetails(password, mnemonic string) (string, string, error) { return "", "", errors.New("No running node detected for account unlock") } +func selectAccount(address, password string) error { + if currentNode == nil { + return ErrInvalidGethNode + } + + if accountManager == nil { + return ErrInvalidAccountManager + } + account, err := utils.MakeAddress(accountManager, address) + if err != nil { + return ErrAddressToAccountMappingFailure + } + + account, accountKey, err := accountManager.AccountDecryptedKey(account, password) + if err != nil { + return fmt.Errorf("%s: %v", ErrAccountToKeyMappingFailure.Error(), err) + } + + if whisperService == nil { + return ErrInvalidWhisperService + } + if err := whisperService.InjectIdentity(accountKey.PrivateKey); err != nil { + return ErrWhisperIdentityInjectionFailure + } + + return nil +} + // unlockAccount unlocks an existing account for a certain duration and // inject the account as a whisper identity if the account was created as // a whisper enabled account func unlockAccount(address, password string, seconds int) error { - - if currentNode != nil { - - if accountManager != nil { - account, err := utils.MakeAddress(accountManager, address) - if err != nil { - return errextra.Wrap(err, "Could not retrieve account from address") - } - - err = accountManager.TimedUnlock(account, password, time.Duration(seconds)*time.Second) - if err != nil { - return errextra.Wrap(err, "Could not decrypt account") - } - return nil - } - return errors.New("Could not retrieve account manager") + if currentNode == nil { + return ErrInvalidGethNode } - return errors.New("No running node detected for account unlock") - + return ErrUnlockCalled } // createAndStartNode creates a node entity and starts the @@ -172,12 +195,12 @@ func onSendTransactionRequest(queuedTx les.QueuedTx) { C.GethServiceSignalEvent(C.CString(string(body))) } -func completeTransaction(hash string) (common.Hash, error) { +func completeTransaction(hash, password string) (common.Hash, error) { if currentNode != nil { if lightEthereum != nil { backend := lightEthereum.StatusBackend - return backend.CompleteQueuedTransaction(les.QueuedTxHash(hash)) + return backend.CompleteQueuedTransaction(les.QueuedTxHash(hash), password) } return common.Hash{}, errors.New("can not retrieve LES service") diff --git a/src/gethdep_test.go b/src/gethdep_test.go index 0fcb2fd6b..62951a089 100644 --- a/src/gethdep_test.go +++ b/src/gethdep_test.go @@ -1,10 +1,10 @@ package main import ( - "fmt" - "os" "errors" + "fmt" "math/big" + "os" "path/filepath" "testing" "time" @@ -24,11 +24,16 @@ const ( testAddress = "0x89b50b2b26947ccad43accaef76c21d175ad85f4" testAddressPassword = "asdf" testNodeSyncSeconds = 180 - testAccountPassword = "badpassword" + newAccountPassword = "badpassword" + + whisperMessage1 = "test message 1 (K1 -> K1)" + whisperMessage2 = "test message 2 (K1 -> '')" + whisperMessage3 = "test message 3 ('' -> '')" + whisperMessage4 = "test message 4 ('' -> K1)" + whisperMessage5 = "test message 5 (K2 -> K1)" ) -// TestAccountBindings makes sure we can create an account and subsequently unlock that account -func TestAccountBindings(t *testing.T) { +func TestRemindAccountDetails(t *testing.T) { err := prepareTestNode() if err != nil { t.Error(err) @@ -36,16 +41,15 @@ func TestAccountBindings(t *testing.T) { } // create an account - address, pubKey, mnemonic, err := createAccount(testAccountPassword) + address, pubKey, mnemonic, err := createAccount(newAccountPassword) if err != nil { - fmt.Println(err.Error()) - t.Error("Test failed: could not create account") + t.Errorf("could not create account: %v", err) return } glog.V(logger.Info).Infof("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic) // try reminding using password + mnemonic - addressCheck, pubKeyCheck, err := remindAccountDetails(testAccountPassword, mnemonic) + addressCheck, pubKeyCheck, err := remindAccountDetails(newAccountPassword, mnemonic) if err != nil { t.Errorf("remind details failed: %v", err) return @@ -53,54 +57,241 @@ func TestAccountBindings(t *testing.T) { if address != addressCheck || pubKey != pubKeyCheck { t.Error("Test failed: remind account details failed to pull the correct details") } +} - // unlock the created account - err = unlockAccount(address, "badpassword", 3) +func TestAccountSelect(t *testing.T) { + + err := prepareTestNode() if err != nil { - fmt.Println(err) - t.Error("Test failed: could not unlock account") + t.Error(err) + return } - time.Sleep(2 * time.Second) // test to see if the account was injected in whisper var whisperInstance *whisper.Whisper if err := currentNode.Service(&whisperInstance); err != nil { t.Errorf("whisper service not running: %v", err) } - identitySucsess := whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey))) - if !identitySucsess || err != nil { - t.Errorf("Test failed: identity not injected into whisper: %v", err) + + // create an accounts + address1, pubKey1, _, err := createAccount(newAccountPassword) + if err != nil { + fmt.Println(err.Error()) + t.Error("Test failed: could not create account") + return + } + glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address1, pubKey1) + + address2, pubKey2, _, err := createAccount(newAccountPassword) + if err != nil { + fmt.Println(err.Error()) + t.Error("Test failed: could not create account") + return + } + glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address2, pubKey2) + + // inject key of newly created account into Whisper, as identity + if whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) { + t.Errorf("identity already present in whisper") } - // test to see if we can post with the injected whisper identity + // try selecting with wrong password + err = selectAccount(address1, "wrongPassword") + if err == nil { + t.Errorf("select account is expected to throw error: wrong password used") + return + } + err = selectAccount(address1, newAccountPassword) + if err != nil { + t.Errorf("Test failed: could not select account: %v", err) + return + } + if !whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) { + t.Errorf("identity not injected into whisper: %v", err) + } + + // select another account, make sure that previous account is wiped out from Whisper cache + if whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey2))) { + t.Errorf("identity already present in whisper") + } + err = selectAccount(address2, newAccountPassword) + if err != nil { + t.Errorf("Test failed: could not select account: %v", err) + return + } + if !whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey2))) { + t.Errorf("identity not injected into whisper: %v", err) + } + if whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) { + t.Errorf("identity should be removed, but it is still present in whisper") + } +} + +func TestWhisperMessaging(t *testing.T) { + err := prepareTestNode() + if err != nil { + t.Error(err) + return + } + + // test to see if the account was injected in whisper + var whisperInstance *whisper.Whisper + if err := currentNode.Service(&whisperInstance); err != nil { + t.Errorf("whisper service not running: %v", err) + } + whisperAPI := whisper.NewPublicWhisperAPI(whisperInstance) + + // prepare message postArgs := whisper.PostArgs{ - From: pubKey, - To: pubKey, - TTL: 100, + From: "", + To: "", + TTL: 10, Topics: [][]byte{[]byte("test topic")}, Payload: "test message", } - whisperAPI := whisper.NewPublicWhisperAPI(whisperInstance) - postSuccess, err := whisperAPI.Post(postArgs) - if !postSuccess || err != nil { - t.Errorf("Test failed: Could not post to whisper: %v", err) - } - // import test account (with test ether on it) - err = copyFile(filepath.Join(testDataDir, "testnet", "keystore", "test-account.pk"), filepath.Join("data", "test-account.pk")) + // create an accounts + address1, pubKey1, _, err := createAccount(newAccountPassword) if err != nil { - t.Errorf("Test failed: cannot copy test account PK: %v", err) + fmt.Println(err.Error()) + t.Error("Test failed: could not create account") return } - time.Sleep(2 * time.Second) + glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address1, pubKey1) - // unlock test account (to send ether from it) - err = unlockAccount(testAddress, testAddressPassword, 300) + address2, pubKey2, _, err := createAccount(newAccountPassword) if err != nil { - fmt.Println(err) - t.Error("Test failed: could not unlock account") + fmt.Println(err.Error()) + t.Error("Test failed: could not create account") + return } - time.Sleep(2 * time.Second) + glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address2, pubKey2) + + // start watchers + var receivedMessages = map[string]bool{ + whisperMessage1: false, + whisperMessage2: false, + whisperMessage3: false, + whisperMessage4: false, + whisperMessage5: false, + } + whisperService.Watch(whisper.Filter{ + //From: crypto.ToECDSAPub(common.FromHex(pubKey1)), + //To: crypto.ToECDSAPub(common.FromHex(pubKey2)), + Fn: func(msg *whisper.Message) { + glog.V(logger.Info).Infof("Whisper message received: %s", msg.Payload) + receivedMessages[string(msg.Payload)] = true + }, + }) + + // inject key of newly created account into Whisper, as identity + if whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) { + t.Errorf("identity already present in whisper") + } + err = selectAccount(address1, newAccountPassword) + if err != nil { + t.Errorf("Test failed: could not select account: %v", err) + return + } + identitySucceess := whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) + if !identitySucceess || err != nil { + t.Errorf("identity not injected into whisper: %v", err) + } + if whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey2))) { // ensure that second id is not injected + t.Errorf("identity already present in whisper") + } + + // double selecting (shouldn't be a problem) + err = selectAccount(address1, newAccountPassword) + if err != nil { + t.Errorf("Test failed: could not select account: %v", err) + return + } + + // TEST 0: From != nil && To != nil: encrypted signed message (but we cannot decrypt it - so watchers will not report this) + postArgs.From = pubKey1 + postArgs.To = pubKey2 // owner of that public key will be able to decrypt it + postSuccess, err := whisperAPI.Post(postArgs) + if !postSuccess || err != nil { + t.Errorf("could not post to whisper: %v", err) + } + + // TEST 1: From != nil && To != nil: encrypted signed message (to self) + postArgs.From = pubKey1 + postArgs.To = pubKey1 + postArgs.Payload = whisperMessage1 + postSuccess, err = whisperAPI.Post(postArgs) + if !postSuccess || err != nil { + t.Errorf("could not post to whisper: %v", err) + } + + // send from account that is not in Whisper identity list + postArgs.From = pubKey2 + postSuccess, err = whisperAPI.Post(postArgs) + if err == nil || err.Error() != fmt.Sprintf("unknown identity to send from: %s", pubKey2) { + t.Errorf("expected error not voiced: we are sending from non-injected whisper identity") + } + + // TEST 2: From != nil && To == nil: signed broadcast (known sender) + postArgs.From = pubKey1 + postArgs.To = "" + postArgs.Payload = whisperMessage2 + postSuccess, err = whisperAPI.Post(postArgs) + if !postSuccess || err != nil { + t.Errorf("could not post to whisper: %v", err) + } + + // TEST 3: From == nil && To == nil: anonymous broadcast + postArgs.From = "" + postArgs.To = "" + postArgs.Payload = whisperMessage3 + postSuccess, err = whisperAPI.Post(postArgs) + if !postSuccess || err != nil { + t.Errorf("could not post to whisper: %v", err) + } + + // TEST 4: From == nil && To != nil: encrypted anonymous message + postArgs.From = "" + postArgs.To = pubKey1 + postArgs.Payload = whisperMessage4 + postSuccess, err = whisperAPI.Post(postArgs) + if !postSuccess || err != nil { + t.Errorf("could not post to whisper: %v", err) + } + + // TEST 5: From != nil && To != nil: encrypted and signed response + postArgs.From = "" + postArgs.To = pubKey1 + postArgs.Payload = whisperMessage5 + postSuccess, err = whisperAPI.Post(postArgs) + if !postSuccess || err != nil { + t.Errorf("could not post to whisper: %v", err) + } + + time.Sleep(2 * time.Second) // allow whisper to poll + for message, status := range receivedMessages { + if !status { + t.Errorf("Expected message not received: %s", message) + } + } + +} + +func TestQueuedTransactions(t *testing.T) { + err := prepareTestNode() + if err != nil { + t.Error(err) + return + } + + // create an account + address, pubKey, mnemonic, err := createAccount(newAccountPassword) + if err != nil { + fmt.Println(err.Error()) + t.Error("Test failed: could not create account") + return + } + glog.V(logger.Info).Infof("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic) // test transaction queueing var lightEthereum *les.LightEthereum @@ -114,8 +305,9 @@ func TestAccountBindings(t *testing.T) { backend.SetTransactionQueueHandler(func(queuedTx les.QueuedTx) { glog.V(logger.Info).Infof("Queued transaction hash: %v\n", queuedTx.Hash.Hex()) var txHash common.Hash - if txHash, err = completeTransaction(queuedTx.Hash.Hex()); err != nil { + if txHash, err = completeTransaction(queuedTx.Hash.Hex(), testAddressPassword); err != nil { t.Errorf("Test failed: cannot complete queued transation[%s]: %v", queuedTx.Hash.Hex(), err) + return } glog.V(logger.Info).Infof("Transaction complete: https://testnet.etherscan.io/tx/%s", txHash.Hex()) @@ -123,7 +315,7 @@ func TestAccountBindings(t *testing.T) { }) // try completing non-existing transaction - if _, err := completeTransaction("0x1234512345123451234512345123456123451234512345123451234512345123"); err == nil { + if _, err := completeTransaction("0x1234512345123451234512345123456123451234512345123451234512345123", testAddressPassword); err == nil { t.Errorf("Test failed: error expected and not recieved") } @@ -152,14 +344,13 @@ func TestAccountBindings(t *testing.T) { t.Error("Test failed: transaction was never queued or completed") } - //// clean up - //err = os.RemoveAll(".ethereumtest") - //if err != nil { - // t.Error("Test failed: could not clean up temporary datadir") - //} } func prepareTestNode() error { + if currentNode != nil { + return nil + } + rpcport = 8546 // in order to avoid conflicts with running react-native app syncRequired := false @@ -173,6 +364,13 @@ func prepareTestNode() error { return err } + // import test account (with test ether on it) + err = copyFile(filepath.Join(testDataDir, "testnet", "keystore", "test-account.pk"), filepath.Join("data", "test-account.pk")) + if err != nil { + glog.V(logger.Warn).Infof("Test failed: cannot copy test account PK: %v", err) + return err + } + // start geth node and wait for it to initialize go createAndStartNode(dataDir) time.Sleep(5 * time.Second) @@ -183,7 +381,16 @@ func prepareTestNode() error { if syncRequired { glog.V(logger.Warn).Infof("Sync is required, it will take %d seconds", testNodeSyncSeconds) time.Sleep(testNodeSyncSeconds * time.Second) // LES syncs headers, so that we are up do date when it is done + } else { + time.Sleep(10 * time.Second) } return nil } + +func cleanup() { + err := os.RemoveAll(testDataDir) + if err != nil { + glog.V(logger.Warn).Infof("Test failed: could not clean up temporary datadir") + } +} diff --git a/src/library.go b/src/library.go index 3f56b26dd..6118029c4 100644 --- a/src/library.go +++ b/src/library.go @@ -58,10 +58,22 @@ func RemindAccountDetails(password, mnemonic *C.char) *C.char { //export Login func Login(address, password *C.char) *C.char { - // Equivalent to unlocking an account briefly, to inject a whisper identity, - // then locking the account again - out := UnlockAccount(address, password, 1) - return out + // loads a key file (for a given address), tries to decrypt it using the password, to verify ownership + // if verified, purges all the previous identities from Whisper, and injects verified key as shh identity + err := selectAccount(C.GoString(address), C.GoString(password)) + + errString := emptyError + if err != nil { + fmt.Fprintln(os.Stderr, err) + errString = err.Error() + } + + out := JSONError{ + Error: errString, + } + outBytes, _ := json.Marshal(&out) + + return C.CString(string(outBytes)) } //export UnlockAccount @@ -87,8 +99,8 @@ func UnlockAccount(address, password *C.char, seconds int) *C.char { } //export CompleteTransaction -func CompleteTransaction(hash *C.char) *C.char { - txHash, err := completeTransaction(C.GoString(hash)) +func CompleteTransaction(hash, password *C.char) *C.char { + txHash, err := completeTransaction(C.GoString(hash), C.GoString(password)) errString := emptyError if err != nil { diff --git a/src/main.go b/src/main.go index f7413879d..204da7889 100644 --- a/src/main.go +++ b/src/main.go @@ -137,7 +137,7 @@ func makeDefaultExtra() []byte { } func preprocessDataDir(dataDir string) (string, error) { - testDataDir := path.Join(dataDir, "testnet") + testDataDir := path.Join(dataDir, "testnet", "keystore") if _, err := os.Stat(testDataDir); os.IsNotExist(err) { if err := os.MkdirAll(testDataDir, 0755); err != nil { return dataDir, ErrDataDirPreprocessingFailed @@ -145,7 +145,7 @@ func preprocessDataDir(dataDir string) (string, error) { } // copy over static peer nodes list (LES auto-discovery is not stable yet) - dst := filepath.Join(testDataDir, "static-nodes.json") + dst := filepath.Join(dataDir, "testnet", "static-nodes.json") if _, err := os.Stat(dst); os.IsNotExist(err) { src := filepath.Join("data", "static-nodes.json") if err := copyFile(dst, src); err != nil { diff --git a/src/vendor/github.com/ethereum/go-ethereum/accounts/account_manager.go b/src/vendor/github.com/ethereum/go-ethereum/accounts/account_manager.go index 13ac16065..57bf5d6d2 100644 --- a/src/vendor/github.com/ethereum/go-ethereum/accounts/account_manager.go +++ b/src/vendor/github.com/ethereum/go-ethereum/accounts/account_manager.go @@ -123,6 +123,10 @@ func (am *Manager) Accounts() []Account { return am.cache.accounts() } +func (am *Manager) AccountDecryptedKey(a Account, auth string) (Account, *Key, error) { + return am.getDecryptedKey(a, auth) +} + // DeleteAccount deletes the key matched by account if the passphrase is correct. // If a contains no filename, the address must match a unique key. func (am *Manager) DeleteAccount(a Account, passphrase string) error { @@ -327,15 +331,6 @@ func (am *Manager) NewAccountUsingExtendedKey(k *extkeys.ExtendedKey, passphrase // than waiting for file system notifications to pick it up. am.cache.add(account) - // sync key to subprotocols (e.g., whisper identity) - if am.sync != nil { - address := fmt.Sprintf("%x", account.Address) - err := am.syncAccounts(address, key) - if err != nil { - return account, fmt.Errorf("failed to sync accounts: %s", err.Error()) - } - } - return account, nil } diff --git a/src/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go b/src/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go index b893e8399..16b97616a 100644 --- a/src/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go +++ b/src/vendor/github.com/ethereum/go-ethereum/internal/ethapi/api.go @@ -1173,7 +1173,7 @@ func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args Sen // CompleteQueuedTransaction creates a transaction by unpacking queued transaction, signs it and submits to the // transaction pool. -func (s *PublicTransactionPoolAPI) CompleteQueuedTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { +func (s *PublicTransactionPoolAPI) CompleteQueuedTransaction(ctx context.Context, args SendTxArgs, passphrase string) (common.Hash, error) { var err error args, err = prepareSendTxArgs(ctx, args, s.b) if err != nil { @@ -1195,7 +1195,7 @@ func (s *PublicTransactionPoolAPI) CompleteQueuedTransaction(ctx context.Context tx = types.NewTransaction(args.Nonce.Uint64(), *args.To, args.Value.BigInt(), args.Gas.BigInt(), args.GasPrice.BigInt(), common.FromHex(args.Data)) } - signature, err := s.b.AccountManager().Sign(args.From, tx.SigHash().Bytes()) + signature, err := s.b.AccountManager().SignWithPassphrase(args.From, passphrase, tx.SigHash().Bytes()) if err != nil { return common.Hash{}, err } diff --git a/src/vendor/github.com/ethereum/go-ethereum/les/status_backend.go b/src/vendor/github.com/ethereum/go-ethereum/les/status_backend.go index 91e140b88..67022cef9 100644 --- a/src/vendor/github.com/ethereum/go-ethereum/les/status_backend.go +++ b/src/vendor/github.com/ethereum/go-ethereum/les/status_backend.go @@ -102,13 +102,13 @@ func (b *StatusBackend) SendTransaction(ctx context.Context, args SendTxArgs) er } // CompleteQueuedTransaction wraps call to PublicTransactionPoolAPI.CompleteQueuedTransaction -func (b *StatusBackend) CompleteQueuedTransaction(hash QueuedTxHash) (common.Hash, error) { +func (b *StatusBackend) CompleteQueuedTransaction(hash QueuedTxHash, passphrase string) (common.Hash, error) { queuedTx, err := b.txEvictingQueue.getQueuedTransaction(hash) if err != nil { return common.Hash{}, err } - return b.txapi.CompleteQueuedTransaction(context.Background(), ethapi.SendTxArgs(queuedTx.Args)) + return b.txapi.CompleteQueuedTransaction(context.Background(), ethapi.SendTxArgs(queuedTx.Args), passphrase) } // GetTransactionQueue wraps call to PublicTransactionPoolAPI.GetTransactionQueue diff --git a/src/vendor/github.com/ethereum/go-ethereum/whisper/whisper.go b/src/vendor/github.com/ethereum/go-ethereum/whisper/whisper.go index 13f4bcd8f..987bc742f 100644 --- a/src/vendor/github.com/ethereum/go-ethereum/whisper/whisper.go +++ b/src/vendor/github.com/ethereum/go-ethereum/whisper/whisper.go @@ -65,7 +65,8 @@ type Whisper struct { protocol p2p.Protocol filters *filter.Filters - keys map[string]*ecdsa.PrivateKey + keys map[string]*ecdsa.PrivateKey + keysMu sync.RWMutex // Mutex to sync identity keys messages map[common.Hash]*Envelope // Pool of messages currently tracked by this node expirations map[uint32]*set.SetNonTS // Message expiration pool (TODO: something lighter) @@ -130,7 +131,9 @@ func (self *Whisper) NewIdentity() *ecdsa.PrivateKey { if err != nil { panic(err) } + self.keysMu.Lock() self.keys[string(crypto.FromECDSAPub(&key.PublicKey))] = key + self.keysMu.Unlock() return key } @@ -138,25 +141,32 @@ func (self *Whisper) NewIdentity() *ecdsa.PrivateKey { // HasIdentity checks if the the whisper node is configured with the private key // of the specified public pair. func (self *Whisper) HasIdentity(key *ecdsa.PublicKey) bool { + self.keysMu.RLock() + defer self.keysMu.RUnlock() + return self.keys[string(crypto.FromECDSAPub(key))] != nil } // GetIdentity retrieves the private key of the specified public identity. func (self *Whisper) GetIdentity(key *ecdsa.PublicKey) *ecdsa.PrivateKey { + self.keysMu.RLock() + defer self.keysMu.RUnlock() + return self.keys[string(crypto.FromECDSAPub(key))] } // InjectIdentity injects a manually added identity/key pair into the whisper keys func (self *Whisper) InjectIdentity(key *ecdsa.PrivateKey) error { - - identity := string(crypto.FromECDSAPub(&key.PublicKey)) - self.keys[identity] = key - if _, ok := self.keys[identity]; !ok { - return fmt.Errorf("key insert into keys map failed") + if self.HasIdentity(&key.PublicKey) { // no need to re-inject + return nil } - identityString := common.ToHex(crypto.FromECDSAPub(&key.PublicKey)) - fmt.Printf("Injected identity into whisper: %s\n", identityString) + self.keysMu.Lock() + self.keys = make(map[string]*ecdsa.PrivateKey) // reset key store + self.keys[string(crypto.FromECDSAPub(&key.PublicKey))] = key + self.keysMu.Unlock() + + fmt.Printf("Injected identity into whisper: %s\n", common.ToHex(crypto.FromECDSAPub(&key.PublicKey))) return nil } @@ -308,6 +318,9 @@ func (self *Whisper) postEvent(envelope *Envelope) { // returning the decrypted message and the key used to achieve it. If not keys // are configured, open will return the payload as if non encrypted. func (self *Whisper) open(envelope *Envelope) *Message { + self.keysMu.RLock() + defer self.keysMu.RUnlock() + // Short circuit if no identity is set, and assume clear-text if len(self.keys) == 0 { if message, err := envelope.Open(nil); err == nil {