diff --git a/README.md b/README.md index 61e36afec4..5f1423276c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +# Go Ethereum (Status fork) + +This is a forked version of the official `go-ethereum` repository. For detailed information on patches applied, see [https://github.com/status-im/status-go/geth-patches/](https://github.com/status-im/status-go/geth-patches/). + +# Original README ## Go Ethereum Official golang implementation of the Ethereum protocol. diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 211fa863d7..65c83f3b0c 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/pborman/uuid" + "github.com/status-im/status-go/extkeys" ) const ( @@ -46,6 +47,10 @@ type Key struct { // we only store privkey as pubkey/address can be derived from it // privkey in this struct is always in plaintext PrivateKey *ecdsa.PrivateKey + // extended key is the root node for new hardened children i.e. sub-accounts + ExtendedKey *extkeys.ExtendedKey + // next index to be used for sub-account child derivation + SubAccountIndex uint32 } type keyStore interface { @@ -65,10 +70,12 @@ type plainKeyJSON struct { } type encryptedKeyJSONV3 struct { - Address string `json:"address"` - Crypto cryptoJSON `json:"crypto"` - Id string `json:"id"` - Version int `json:"version"` + 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 { @@ -137,6 +144,40 @@ func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { return key } +func newKeyFromExtendedKey(extKey *extkeys.ExtendedKey) (*Key, error) { + var ( + extChild1, extChild2 *extkeys.ExtendedKey + err error + ) + + if extKey.Depth == 0 { // we are dealing with master key + // CKD#1 - main account + extChild1, err = extKey.BIP44Child(extkeys.CoinTypeETH, 0) + if err != nil { + return &Key{}, err + } + + // CKD#2 - sub-accounts root + extChild2, err = extKey.BIP44Child(extkeys.CoinTypeETH, 1) + if err != nil { + return &Key{}, err + } + } else { // we are dealing with non-master key, so it is safe to persist and extend from it + extChild1 = extKey + extChild2 = extKey + } + + privateKeyECDSA := extChild1.ToECDSA() + id := uuid.NewRandom() + key := &Key{ + Id: id, + Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey), + PrivateKey: privateKeyECDSA, + ExtendedKey: extChild2, + } + return key, nil +} + // NewKeyForDirectICAP generates a key whose address fits into < 155 bits so it can fit // into the Direct ICAP spec. for simplicity and easier compatibility with other libs, we // retry until the first byte is 0. diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index 80ccd37419..7506081450 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -38,6 +38,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/event" + "github.com/status-im/status-go/extkeys" ) var ( @@ -228,6 +229,11 @@ func (ks *KeyStore) Accounts() []accounts.Account { return ks.cache.accounts() } +// AccountDecryptedKey returns decrypted key for account (provided that password is correct). +func (ks *KeyStore) AccountDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) { + return ks.getDecryptedKey(a, auth) +} + // 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. func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { @@ -453,6 +459,34 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco return ks.importKey(key, passphrase) } +// ImportExtendedKey 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. +func (ks *KeyStore) ImportExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) { + key, err := newKeyFromExtendedKey(extKey) + if err != nil { + zeroKey(key.PrivateKey) + return accounts.Account{}, err + } + + // if account is already imported, return cached version + if ks.cache.hasAddress(key.Address) { + a := accounts.Account{ + Address: key.Address, + } + ks.cache.maybeReload() + ks.cache.mu.Lock() + a, err := ks.cache.find(a) + ks.cache.mu.Unlock() + if err != nil { + zeroKey(key.PrivateKey) + return a, err + } + return a, nil + } + + return ks.importKey(key, passphrase) +} + func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}} if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil { @@ -463,6 +497,15 @@ func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, er return a, nil } +func (ks *KeyStore) IncSubAccountIndex(a accounts.Account, passphrase string) error { + a, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return err + } + key.SubAccountIndex++ + return ks.storage.StoreKey(a.URL.Path, key, passphrase) +} + // Update changes the passphrase of an existing account. func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error { a, key, err := ks.getDecryptedKey(a, passphrase) @@ -486,6 +529,9 @@ func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (account // zeroKey zeroes a private key in memory. func zeroKey(k *ecdsa.PrivateKey) { + if k == nil { + return + } b := k.D.Bits() for i := range b { b[i] = 0 diff --git a/accounts/keystore/keystore_passphrase.go b/accounts/keystore/keystore_passphrase.go index eaec39f7df..902b213e2a 100644 --- a/accounts/keystore/keystore_passphrase.go +++ b/accounts/keystore/keystore_passphrase.go @@ -41,6 +41,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/randentropy" "github.com/pborman/uuid" + "github.com/status-im/status-go/extkeys" "golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/scrypt" ) @@ -151,15 +152,62 @@ func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { KDFParams: scryptParamsJSON, MAC: hex.EncodeToString(mac), } + encryptedExtendedKey, err := EncryptExtendedKey(key.ExtendedKey, auth, scryptN, scryptP) + if err != nil { + return nil, err + } encryptedKeyJSONV3 := encryptedKeyJSONV3{ hex.EncodeToString(key.Address[:]), cryptoStruct, key.Id.String(), version, + encryptedExtendedKey, + key.SubAccountIndex, } return json.Marshal(encryptedKeyJSONV3) } +func EncryptExtendedKey(extKey *extkeys.ExtendedKey, auth string, scryptN, scryptP int) (cryptoJSON, error) { + if extKey == nil { + return cryptoJSON{}, nil + } + authArray := []byte(auth) + salt := randentropy.GetEntropyCSPRNG(32) + derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptR, scryptP, scryptDKLen) + if err != nil { + return cryptoJSON{}, err + } + encryptKey := derivedKey[:16] + keyBytes := []byte(extKey.String()) + + iv := randentropy.GetEntropyCSPRNG(aes.BlockSize) // 16 + cipherText, err := aesCTRXOR(encryptKey, keyBytes, iv) + if err != nil { + return cryptoJSON{}, err + } + mac := crypto.Keccak256(derivedKey[16:32], cipherText) + + scryptParamsJSON := make(map[string]interface{}, 5) + scryptParamsJSON["n"] = scryptN + scryptParamsJSON["r"] = scryptR + scryptParamsJSON["p"] = scryptP + scryptParamsJSON["dklen"] = scryptDKLen + scryptParamsJSON["salt"] = hex.EncodeToString(salt) + + cipherParamsJSON := cipherparamsJSON{ + IV: hex.EncodeToString(iv), + } + + return cryptoJSON{ + Cipher: "aes-128-ctr", + CipherText: hex.EncodeToString(cipherText), + CipherParams: cipherParamsJSON, + KDF: "scrypt", + KDFParams: scryptParamsJSON, + MAC: hex.EncodeToString(mac), + }, nil +} + // DecryptKey decrypts a key from a json blob, returning the private key itself. func DecryptKey(keyjson []byte, auth string) (*Key, error) { // Parse the json into a simple map to fetch the key version @@ -171,20 +219,43 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) { 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 @@ -192,9 +263,11 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) { key := crypto.ToECDSAUnsafe(keyBytes) return &Key{ - Id: uuid.UUID(keyId), - Address: crypto.PubkeyToAddress(key.PublicKey), - PrivateKey: key, + Id: uuid.UUID(keyId), + Address: crypto.PubkeyToAddress(key.PublicKey), + PrivateKey: key, + ExtendedKey: extKey, + SubAccountIndex: uint32(subAccountIndex), }, nil } @@ -274,6 +347,51 @@ func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byt 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)) diff --git a/cmd/wnode/main.go b/cmd/wnode/main.go index 05e6b29086..235d368ad7 100644 --- a/cmd/wnode/main.go +++ b/cmd/wnode/main.go @@ -98,6 +98,8 @@ var ( argEnode = flag.String("boot", "", "bootstrap node you want to connect to (e.g. enode://e454......08d50@52.176.211.200:16428)") argTopic = flag.String("topic", "", "topic in hexadecimal format (e.g. 70a4beef)") argSaveDir = flag.String("savedir", "", "directory where incoming messages will be saved as files") + argSymPass = flag.String("sympass", "", "SymKey password") + argMsPass = flag.String("mspass", "", "Mailserver password") ) func main() { @@ -146,6 +148,13 @@ func processArgs() { } else if *fileExMode { utils.Fatalf("Parameter 'savedir' is mandatory for file exchange mode") } + if len(*argSymPass) > 0 { + symPass = *argSymPass + } + + if len(*argMsPass) > 0 { + msPassword = *argMsPass + } if *echoMode { echo() @@ -415,10 +424,24 @@ func run() { } else if *fileExMode { sendFilesLoop() } else { - sendLoop() + pingLoop() // instead of sendLoop() } } +func pingLoop() { + ticker := time.NewTicker(time.Second * 120) + + for { + select { + case <-ticker.C: + fmt.Println("I am alive: ", time.Now()) + case <-done: + return + } + } + +} + func sendLoop() { for { s := scanLine("") diff --git a/common/message/message.go b/common/message/message.go new file mode 100644 index 0000000000..c9f8965c43 --- /dev/null +++ b/common/message/message.go @@ -0,0 +1,65 @@ +package message + +// Direction defines a int type to indicate a message as either incoming or outgoing. +type Direction int + +// consts of all message direction values. +const ( + IncomingMessage Direction = iota + 1 + OutgoingMessage +) + +// String returns the representation of giving direction. +func (d Direction) String() string { + switch d { + case IncomingMessage: + return "IncomingMessage" + case OutgoingMessage: + return "OutgoingMessage" + } + + return "MessageDirectionUnknown" +} + +// Status defines a int type to indicate different status value of a +// message state. +type Status int + +// consts of all message delivery status. +const ( + PendingStatus Status = iota + 1 + QueuedStatus + CachedStatus + SentStatus + ExpiredStatus + ProcessingStatus + ResentStatus + RejectedStatus + DeliveredStatus +) + +// String returns the representation of giving state. +func (s Status) String() string { + switch s { + case PendingStatus: + return "Pending" + case QueuedStatus: + return "Queued" + case CachedStatus: + return "Cached" + case SentStatus: + return "Sent" + case ProcessingStatus: + return "Processing" + case ExpiredStatus: + return "ExpiredTTL" + case ResentStatus: + return "Resent" + case RejectedStatus: + return "Rejected" + case DeliveredStatus: + return "Delivered" + } + + return "unknown" +} diff --git a/containers/docker/status-alpine/geth/Dockerfile b/containers/docker/status-alpine/geth/Dockerfile new file mode 100644 index 0000000000..2cc2177d09 --- /dev/null +++ b/containers/docker/status-alpine/geth/Dockerfile @@ -0,0 +1,20 @@ +FROM alpine:3.5 + +RUN \ + apk add --update go git make gcc musl-dev linux-headers ca-certificates && \ + + # clone status-go + mkdir -p /usr/lib/go/src/github.com/status-im && \ + git clone --depth 1 --branch 0.9.7 https://github.com/status-im/status-go.git /usr/lib/go/src/github.com/status-im/status-go && \ + + # clone go-ethereum (and install everything) + git clone --depth 1 --branch status/1.6.1-stable https://github.com/status-im/go-ethereum && \ + (cd go-ethereum && make geth) && \ + cp go-ethereum/build/bin/geth /geth && \ + apk del go git make gcc musl-dev linux-headers && \ + rm -rf /go-ethereum && rm -rf /var/cache/apk/* + +EXPOSE 8545 +EXPOSE 30303 + +ENTRYPOINT ["/geth"] diff --git a/containers/docker/status-alpine/swarm/Dockerfile b/containers/docker/status-alpine/swarm/Dockerfile new file mode 100644 index 0000000000..685bf5ad9c --- /dev/null +++ b/containers/docker/status-alpine/swarm/Dockerfile @@ -0,0 +1,19 @@ +FROM alpine:3.5 + +RUN \ + apk add --update go git make gcc musl-dev linux-headers ca-certificates && \ + + # clone status-go + mkdir -p /usr/lib/go/src/github.com/status-im && \ + git clone --depth 1 --branch develop https://github.com/status-im/status-go.git /usr/lib/go/src/github.com/status-im/status-go && \ + + # clone go-ethereum (and install everything) + git clone --depth 1 --branch status/1.6.1-stable https://github.com/status-im/go-ethereum && \ + (cd go-ethereum && build/env.sh go run build/ci.go install ./cmd/swarm) && \ + cp go-ethereum/build/bin/swarm /swarm && \ + apk del go git make gcc musl-dev linux-headers && \ + rm -rf /go-ethereum && rm -rf /var/cache/apk/* + +EXPOSE 30399 + +ENTRYPOINT ["/swarm"] diff --git a/containers/docker/status-alpine/wnode/Dockerfile b/containers/docker/status-alpine/wnode/Dockerfile new file mode 100644 index 0000000000..e160ff9d7a --- /dev/null +++ b/containers/docker/status-alpine/wnode/Dockerfile @@ -0,0 +1,19 @@ +FROM alpine:3.5 + +RUN \ + apk add --update go git make gcc musl-dev linux-headers ca-certificates && \ + + # clone status-go + mkdir -p /usr/lib/go/src/github.com/status-im && \ + git clone --depth 1 --branch develop https://github.com/status-im/status-go.git /usr/lib/go/src/github.com/status-im/status-go && \ + + # clone go-ethereum (and install everything) + git clone --depth 1 --branch status/1.6.1-stable https://github.com/status-im/go-ethereum && \ + (cd go-ethereum && build/env.sh go run build/ci.go install ./cmd/wnode) && \ + cp go-ethereum/build/bin/wnode /wnode && \ + apk del go git make gcc musl-dev linux-headers && \ + rm -rf /go-ethereum && rm -rf /var/cache/apk/* + +EXPOSE 30379 + +ENTRYPOINT ["/wnode"] diff --git a/eth/api_backend.go b/eth/api_backend.go index 91f392f94f..471275de5a 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -32,14 +32,20 @@ import ( "github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" ) // EthApiBackend implements ethapi.Backend for full nodes type EthApiBackend struct { - eth *Ethereum - gpo *gasprice.Oracle + eth *Ethereum + gpo *gasprice.Oracle + statusBackend *ethapi.StatusBackend +} + +func (b *EthApiBackend) GetStatusBackend() *ethapi.StatusBackend { + return b.statusBackend } func (b *EthApiBackend) ChainConfig() *params.ChainConfig { diff --git a/eth/backend.go b/eth/backend.go index 1cd9e8fffa..2fbdec4e27 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -169,7 +169,7 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) { eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.engine) eth.miner.SetExtra(makeExtraData(config.ExtraData)) - eth.ApiBackend = &EthApiBackend{eth, nil} + eth.ApiBackend = &EthApiBackend{eth, nil, nil} gpoParams := config.GPO if gpoParams.Default == nil { gpoParams.Default = config.GasPrice diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 025f426177..d8f48a8908 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -178,15 +178,24 @@ func (s *PublicTxPoolAPI) Inspect() map[string]map[string]map[string]string { // It offers only methods that can retrieve accounts. type PublicAccountAPI struct { am *accounts.Manager + b Backend } // NewPublicAccountAPI creates a new PublicAccountAPI. -func NewPublicAccountAPI(am *accounts.Manager) *PublicAccountAPI { - return &PublicAccountAPI{am: am} +func NewPublicAccountAPI(b Backend) *PublicAccountAPI { + return &PublicAccountAPI{ + am: b.AccountManager(), + b: b, + } } // Accounts returns the collection of accounts this node manages func (s *PublicAccountAPI) Accounts() []common.Address { + backend := s.b.GetStatusBackend() + if backend != nil { + return backend.am.Accounts() + } + addresses := make([]common.Address, 0) // return [] instead of nil if empty for _, wallet := range s.am.Wallets() { for _, account := range wallet.Accounts() { @@ -216,6 +225,11 @@ func NewPrivateAccountAPI(b Backend, nonceLock *AddrLocker) *PrivateAccountAPI { // ListAccounts will return a list of addresses for accounts this node manages. func (s *PrivateAccountAPI) ListAccounts() []common.Address { + backend := s.b.GetStatusBackend() + if backend != nil { + return backend.am.Accounts() + } + addresses := make([]common.Address, 0) // return [] instead of nil if empty for _, wallet := range s.am.Wallets() { for _, account := range wallet.Accounts() { @@ -1122,10 +1136,46 @@ func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (c return tx.Hash(), nil } -// SendTransaction creates a transaction for the given argument, sign it and submit it to the +// SendTransactionWithPassphrase creates a transaction by unpacking queued transaction, signs it and submits to the +// transaction pool. +// @Status +func (s *PublicTransactionPoolAPI) SendTransactionWithPassphrase(ctx context.Context, args SendTxArgs, passphrase string) (common.Hash, error) { + // Look up the wallet containing the requested signer + account := accounts.Account{Address: args.From} + + wallet, err := s.b.AccountManager().Find(account) + if err != nil { + return common.Hash{}, err + } + + if args.Nonce == nil { + // Hold the addresse's mutex around signing to prevent concurrent assignment of + // the same nonce to multiple accounts. + s.nonceLock.LockAddr(args.From) + defer s.nonceLock.UnlockAddr(args.From) + } + + // Set some sanity defaults and terminate on failure + if err := args.setDefaults(ctx, s.b); err != nil { + return common.Hash{}, err + } + // Assemble the transaction and sign with the wallet + tx := args.toTransaction() + + var chainID *big.Int + if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) { + chainID = config.ChainId + } + signed, err := wallet.SignTxWithPassphrase(account, passphrase, tx, chainID) + if err != nil { + return common.Hash{}, err + } + return submitTransaction(ctx, s.b, signed) +} + +// SendTransaction creates a transaction by unpacking queued transaction, signs it and submits to the // transaction pool. func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { - // Look up the wallet containing the requested signer account := accounts.Account{Address: args.From} diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 368fa48726..cac58dfc0c 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -68,6 +68,8 @@ type Backend interface { ChainConfig() *params.ChainConfig CurrentBlock() *types.Block + + GetStatusBackend() *StatusBackend } func GetAPIs(apiBackend Backend) []rpc.API { @@ -105,7 +107,7 @@ func GetAPIs(apiBackend Backend) []rpc.API { }, { Namespace: "eth", Version: "1.0", - Service: NewPublicAccountAPI(apiBackend.AccountManager()), + Service: NewPublicAccountAPI(apiBackend), Public: true, }, { Namespace: "personal", diff --git a/internal/ethapi/status_backend.go b/internal/ethapi/status_backend.go new file mode 100644 index 0000000000..c4e553cae7 --- /dev/null +++ b/internal/ethapi/status_backend.go @@ -0,0 +1,88 @@ +package ethapi + +import ( + "context" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/les/status" + "github.com/ethereum/go-ethereum/log" +) + +// StatusBackend exposes Ethereum internals to support custom semantics in status-go bindings +type StatusBackend struct { + eapi *PublicEthereumAPI // Wrapper around the Ethereum object to access metadata + bcapi *PublicBlockChainAPI // Wrapper around the blockchain to access chain data + txapi *PublicTransactionPoolAPI // Wrapper around the transaction pool to access transaction data + + am *status.AccountManager +} + +var ( + ErrStatusBackendNotInited = errors.New("StatusIM backend is not properly inited") +) + +// NewStatusBackend creates a new backend using an existing Ethereum object. +func NewStatusBackend(apiBackend Backend) *StatusBackend { + log.Info("StatusIM: backend service inited") + return &StatusBackend{ + eapi: NewPublicEthereumAPI(apiBackend), + bcapi: NewPublicBlockChainAPI(apiBackend), + txapi: NewPublicTransactionPoolAPI(apiBackend, new(AddrLocker)), + am: status.NewAccountManager(apiBackend.AccountManager()), + } +} + +// SetAccountsFilterHandler sets a callback that is triggered when account list is requested +func (b *StatusBackend) SetAccountsFilterHandler(fn status.AccountsFilterHandler) { + b.am.SetAccountsFilterHandler(fn) +} + +// AccountManager returns reference to account manager +func (b *StatusBackend) AccountManager() *status.AccountManager { + return b.am +} + +// SendTransaction wraps call to PublicTransactionPoolAPI.SendTransactionWithPassphrase +func (b *StatusBackend) SendTransaction(ctx context.Context, args status.SendTxArgs, passphrase string) (common.Hash, error) { + if ctx == nil { + ctx = context.Background() + } + + if estimatedGas, err := b.EstimateGas(ctx, args); err == nil { + if estimatedGas.ToInt().Cmp(big.NewInt(defaultGas)) == 1 { // gas > defaultGas + args.Gas = estimatedGas + } + } + + return b.txapi.SendTransactionWithPassphrase(ctx, SendTxArgs(args), passphrase) +} + +// EstimateGas uses underlying blockchain API to obtain gas for a given tx arguments +func (b *StatusBackend) EstimateGas(ctx context.Context, args status.SendTxArgs) (*hexutil.Big, error) { + if args.Gas != nil { + return args.Gas, nil + } + + var gasPrice hexutil.Big + if args.GasPrice != nil { + gasPrice = *args.GasPrice + } + + var value hexutil.Big + if args.Value != nil { + value = *args.Value + } + + callArgs := CallArgs{ + From: args.From, + To: args.To, + GasPrice: gasPrice, + Value: value, + Data: args.Data, + } + + return b.bcapi.EstimateGas(ctx, callArgs) +} diff --git a/les/api_backend.go b/les/api_backend.go index 56f617a7db..f839f24e61 100644 --- a/les/api_backend.go +++ b/les/api_backend.go @@ -32,14 +32,20 @@ import ( "github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/light" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" ) type LesApiBackend struct { - eth *LightEthereum - gpo *gasprice.Oracle + eth *LightEthereum + gpo *gasprice.Oracle + statusBackend *ethapi.StatusBackend +} + +func (b *LesApiBackend) GetStatusBackend() *ethapi.StatusBackend { + return b.statusBackend } func (b *LesApiBackend) ChainConfig() *params.ChainConfig { diff --git a/les/backend.go b/les/backend.go index 333df920e0..71fbc181b5 100644 --- a/les/backend.go +++ b/les/backend.go @@ -75,6 +75,8 @@ type LightEthereum struct { netRPCService *ethapi.PublicNetAPI wg sync.WaitGroup + + StatusBackend *ethapi.StatusBackend } func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) { @@ -126,12 +128,17 @@ func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) { if leth.protocolManager, err = NewProtocolManager(leth.chainConfig, true, ClientProtocolVersions, config.NetworkId, leth.eventMux, leth.engine, leth.peers, leth.blockchain, nil, chainDb, leth.odr, leth.relay, quitSync, &leth.wg); err != nil { return nil, err } - leth.ApiBackend = &LesApiBackend{leth, nil} + leth.ApiBackend = &LesApiBackend{leth, nil, nil} gpoParams := config.GPO if gpoParams.Default == nil { gpoParams.Default = config.GasPrice } leth.ApiBackend.gpo = gasprice.NewOracle(leth.ApiBackend, gpoParams) + + // inject status-im backend + leth.ApiBackend.statusBackend = ethapi.NewStatusBackend(leth.ApiBackend) + leth.StatusBackend = leth.ApiBackend.statusBackend // alias + return leth, nil } diff --git a/les/status/accounts.go b/les/status/accounts.go new file mode 100644 index 0000000000..78bd2ad924 --- /dev/null +++ b/les/status/accounts.go @@ -0,0 +1,45 @@ +package status + +import ( + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" +) + +// AccountManager abstracts both internal account manager and extra filter status backend requires +type AccountManager struct { + am *accounts.Manager + accountsFilterHandler AccountsFilterHandler +} + +// NewAccountManager creates a new AccountManager +func NewAccountManager(am *accounts.Manager) *AccountManager { + return &AccountManager{ + am: am, + } +} + +// AccountsFilterHandler function to filter out accounts list +type AccountsFilterHandler func([]common.Address) []common.Address + +// Accounts returns accounts' addresses of currently logged in user. +// Since status supports HD keys, the following list is returned: +// [addressCDK#1, addressCKD#2->Child1, addressCKD#2->Child2, .. addressCKD#2->ChildN] +func (d *AccountManager) Accounts() []common.Address { + var addresses []common.Address + for _, wallet := range d.am.Wallets() { + for _, account := range wallet.Accounts() { + addresses = append(addresses, account.Address) + } + } + + if d.accountsFilterHandler != nil { + return d.accountsFilterHandler(addresses) + } + + return addresses +} + +// SetAccountsFilterHandler sets filtering function for accounts list +func (d *AccountManager) SetAccountsFilterHandler(fn AccountsFilterHandler) { + d.accountsFilterHandler = fn +} diff --git a/les/status/types.go b/les/status/types.go new file mode 100644 index 0000000000..04437bdb6a --- /dev/null +++ b/les/status/types.go @@ -0,0 +1,17 @@ +package status + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// SendTxArgs represents the arguments to submit a new transaction into the transaction pool. +type SendTxArgs struct { + From common.Address `json:"from"` + To *common.Address `json:"to"` + Gas *hexutil.Big `json:"gas"` + GasPrice *hexutil.Big `json:"gasPrice"` + Value *hexutil.Big `json:"value"` + Data hexutil.Bytes `json:"data"` + Nonce *hexutil.Uint64 `json:"nonce"` +} diff --git a/light/postprocess.go b/light/postprocess.go index e7e513880f..dc6562be89 100644 --- a/light/postprocess.go +++ b/light/postprocess.go @@ -66,12 +66,20 @@ var ( chtRoot: common.HexToHash("6f56dc61936752cc1f8c84b4addabdbe6a1c19693de3f21cb818362df2117f03"), bloomTrieRoot: common.HexToHash("aca7d7c504d22737242effc3fdc604a762a0af9ced898036b5986c3a15220208"), } + + statusRopstenCheckpoint = trustedCheckpoint{ + name: "Ropsten testnet", + sectionIdx: 67, + sectionHead: common.HexToHash("9832cf2ce760d4e3a7922fbfedeaa5dce67f1772e0f729f67c806bfafdedc370"), + chtRoot: common.HexToHash("60d43984a1d55e93f4296f4b48bf5af350476fe48679a73263bd57d8a324c9d4"), + bloomTrieRoot: common.HexToHash("fd81543dc619f6d1148e766b942c90296343c2cd0fd464946678f27f35feb59b"), + } ) // trustedCheckpoints associates each known checkpoint with the genesis hash of the chain it belongs to var trustedCheckpoints = map[common.Hash]trustedCheckpoint{ params.MainnetGenesisHash: mainnetCheckpoint, - params.TestnetGenesisHash: ropstenCheckpoint, + params.TestnetGenesisHash: statusRopstenCheckpoint, } var ( diff --git a/whisper/notifications/discovery.go b/whisper/notifications/discovery.go new file mode 100644 index 0000000000..de8ec85bef --- /dev/null +++ b/whisper/notifications/discovery.go @@ -0,0 +1,154 @@ +package notifications + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + whisper "github.com/ethereum/go-ethereum/whisper/whisperv5" +) + +const ( + topicDiscoverServer = "DISCOVER_NOTIFICATION_SERVER" + topicProposeServer = "PROPOSE_NOTIFICATION_SERVER" + topicServerAccepted = "ACCEPT_NOTIFICATION_SERVER" + topicAckClientSubscription = "ACK_NOTIFICATION_SERVER_SUBSCRIPTION" +) + +// discoveryService abstract notification server discovery protocol +type discoveryService struct { + server *NotificationServer + + discoverFilterID string + serverAcceptedFilterID string +} + +// messageProcessingFn is a callback used to process incoming client requests +type messageProcessingFn func(*whisper.ReceivedMessage) error + +func NewDiscoveryService(notificationServer *NotificationServer) *discoveryService { + return &discoveryService{ + server: notificationServer, + } +} + +// Start installs necessary filters to watch for incoming discovery requests, +// then in separate routine starts watcher loop +func (s *discoveryService) Start() error { + var err error + + // notification server discovery requests + s.discoverFilterID, err = s.server.installKeyFilter(topicDiscoverServer, s.server.protocolKey) + if err != nil { + return fmt.Errorf("failed installing filter: %v", err) + } + go s.server.requestProcessorLoop(s.discoverFilterID, topicDiscoverServer, s.processDiscoveryRequest) + + // notification server accept/select requests + s.serverAcceptedFilterID, err = s.server.installKeyFilter(topicServerAccepted, s.server.protocolKey) + if err != nil { + return fmt.Errorf("failed installing filter: %v", err) + } + go s.server.requestProcessorLoop(s.serverAcceptedFilterID, topicServerAccepted, s.processServerAcceptedRequest) + + log.Info("notification server discovery service started") + return nil +} + +// Stop stops all discovery processing loops +func (s *discoveryService) Stop() error { + s.server.whisper.Unsubscribe(s.discoverFilterID) + s.server.whisper.Unsubscribe(s.serverAcceptedFilterID) + + log.Info("notification server discovery service stopped") + return nil +} + +// processDiscoveryRequest processes incoming client requests of type: +// when client tries to discover suitable notification server +func (s *discoveryService) processDiscoveryRequest(msg *whisper.ReceivedMessage) error { + // offer this node as notification server + msgParams := whisper.MessageParams{ + Src: s.server.protocolKey, + Dst: msg.Src, + Topic: MakeTopic([]byte(topicProposeServer)), + Payload: []byte(`{"server": "0x` + s.server.nodeID + `"}`), + TTL: uint32(s.server.config.TTL), + PoW: s.server.config.MinimumPoW, + WorkTime: 5, + } + response, err := whisper.NewSentMessage(&msgParams) + if err != nil { + return fmt.Errorf("failed to create proposal message: %v", err) + } + env, err := response.Wrap(&msgParams) + if err != nil { + return fmt.Errorf("failed to wrap server proposal message: %v", err) + } + + if err := s.server.whisper.Send(env); err != nil { + return fmt.Errorf("failed to send server proposal message: %v", err) + } + + log.Info(fmt.Sprintf("server proposal sent (server: %v, dst: %v, topic: %x)", + s.server.nodeID, common.ToHex(crypto.FromECDSAPub(msgParams.Dst)), msgParams.Topic)) + return nil +} + +// processServerAcceptedRequest processes incoming client requests of type: +// when client is ready to select the given node as its notification server +func (s *discoveryService) processServerAcceptedRequest(msg *whisper.ReceivedMessage) error { + var parsedMessage struct { + ServerID string `json:"server"` + } + if err := json.Unmarshal(msg.Payload, &parsedMessage); err != nil { + return err + } + + if msg.Src == nil { + return errors.New("message 'from' field is required") + } + + // make sure that only requests made to the current node are processed + if parsedMessage.ServerID != `0x`+s.server.nodeID { + return nil + } + + // register client + sessionKey, err := s.server.RegisterClientSession(&ClientSession{ + ClientKey: hex.EncodeToString(crypto.FromECDSAPub(msg.Src)), + }) + if err != nil { + return err + } + + // confirm that client has been successfully subscribed + msgParams := whisper.MessageParams{ + Src: s.server.protocolKey, + Dst: msg.Src, + Topic: MakeTopic([]byte(topicAckClientSubscription)), + Payload: []byte(`{"server": "0x` + s.server.nodeID + `", "key": "0x` + hex.EncodeToString(sessionKey) + `"}`), + TTL: uint32(s.server.config.TTL), + PoW: s.server.config.MinimumPoW, + WorkTime: 5, + } + response, err := whisper.NewSentMessage(&msgParams) + if err != nil { + return fmt.Errorf("failed to create server proposal message: %v", err) + } + env, err := response.Wrap(&msgParams) + if err != nil { + return fmt.Errorf("failed to wrap server proposal message: %v", err) + } + + if err := s.server.whisper.Send(env); err != nil { + return fmt.Errorf("failed to send server proposal message: %v", err) + } + + log.Info(fmt.Sprintf("server confirms client subscription (dst: %v, topic: %x)", msgParams.Dst, msgParams.Topic)) + return nil +} diff --git a/whisper/notifications/provider.go b/whisper/notifications/provider.go new file mode 100644 index 0000000000..032463a6ee --- /dev/null +++ b/whisper/notifications/provider.go @@ -0,0 +1,59 @@ +package notifications + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/ethereum/go-ethereum/log" + "github.com/status-im/status-go/geth/params" +) + +// NotificationDeliveryProvider handles the notification delivery +type NotificationDeliveryProvider interface { + Send(id string, payload string) error +} + +// FirebaseProvider represents FCM provider +type FirebaseProvider struct { + AuthorizationKey string + NotificationTriggerURL string +} + +// NewFirebaseProvider creates new FCM provider +func NewFirebaseProvider(config *params.FirebaseConfig) *FirebaseProvider { + authorizationKey, _ := config.ReadAuthorizationKeyFile() + return &FirebaseProvider{ + NotificationTriggerURL: config.NotificationTriggerURL, + AuthorizationKey: string(authorizationKey), + } +} + +// Send triggers sending of Push Notification to a given device id +func (p *FirebaseProvider) Send(id string, payload string) (err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v", r) + } + }() + + jsonRequest := strings.Replace(payload, "{{ ID }}", id, 3) + req, err := http.NewRequest("POST", p.NotificationTriggerURL, bytes.NewBuffer([]byte(jsonRequest))) + req.Header.Set("Authorization", "key="+p.AuthorizationKey) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + log.Debug("FCM response", "status", resp.Status, "header", resp.Header) + body, _ := ioutil.ReadAll(resp.Body) + log.Debug("FCM response body", "body", string(body)) + + return nil +} diff --git a/whisper/notifications/server.go b/whisper/notifications/server.go new file mode 100644 index 0000000000..7f27f03018 --- /dev/null +++ b/whisper/notifications/server.go @@ -0,0 +1,590 @@ +package notifications + +import ( + "errors" + "fmt" + "sync" + "time" + + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p" + whisper "github.com/ethereum/go-ethereum/whisper/whisperv5" + "github.com/status-im/status-go/geth/params" +) + +const ( + topicSendNotification = "SEND_NOTIFICATION" + topicNewChatSession = "NEW_CHAT_SESSION" + topicAckNewChatSession = "ACK_NEW_CHAT_SESSION" + topicNewDeviceRegistration = "NEW_DEVICE_REGISTRATION" + topicAckDeviceRegistration = "ACK_DEVICE_REGISTRATION" + topicCheckClientSession = "CHECK_CLIENT_SESSION" + topicConfirmClientSession = "CONFIRM_CLIENT_SESSION" + topicDropClientSession = "DROP_CLIENT_SESSION" +) + +var ( + ErrServiceInitError = errors.New("notification service has not been properly initialized") +) + +// NotificationServer service capable of handling Push Notifications +type NotificationServer struct { + whisper *whisper.Whisper + config *params.WhisperConfig + + nodeID string // proposed server will feature this ID + discovery *discoveryService // discovery service handles client/server negotiation, when server is selected + protocolKey *ecdsa.PrivateKey // private key of service, used to encode handshake communication + + clientSessions map[string]*ClientSession + clientSessionsMu sync.RWMutex + + chatSessions map[string]*ChatSession + chatSessionsMu sync.RWMutex + + deviceSubscriptions map[string]*DeviceSubscription + deviceSubscriptionsMu sync.RWMutex + + firebaseProvider NotificationDeliveryProvider + + quit chan struct{} +} + +// ClientSession abstracts notification client, which expects notifications whenever +// some envelope can be decoded with session key (key hash is compared for optimization) +type ClientSession struct { + ClientKey string // public key uniquely identifying a client + SessionKey []byte // actual symkey used for client - server communication + SessionKeyHash common.Hash // The Keccak256Hash of the symmetric key, which is shared between server/client + SessionKeyInput []byte // raw symkey used as input for actual SessionKey +} + +// ChatSession abstracts chat session, which some previously registered client can create. +// ChatSession is used by client for sharing common secret, allowing others to register +// themselves and eventually to trigger notifications. +type ChatSession struct { + ParentKey string // public key uniquely identifying a client session used to create a chat session + ChatKey string // ID that uniquely identifies a chat session + SessionKey []byte // actual symkey used for client - server communication + SessionKeyHash common.Hash // The Keccak256Hash of the symmetric key, which is shared between server/client +} + +// DeviceSubscription stores enough information about a device (or group of devices), +// so that Notification Server can trigger notification on that device(s) +type DeviceSubscription struct { + DeviceID string // ID that will be used as destination + ChatSessionKeyHash common.Hash // The Keccak256Hash of the symmetric key, which is shared between server/client + PubKey *ecdsa.PublicKey // public key of subscriber (to filter out when notification is triggered) +} + +// Init used for service initialization, making sure it is safe to call Start() +func (s *NotificationServer) Init(whisperService *whisper.Whisper, whisperConfig *params.WhisperConfig) { + s.whisper = whisperService + s.config = whisperConfig + + s.discovery = NewDiscoveryService(s) + s.clientSessions = make(map[string]*ClientSession) + s.chatSessions = make(map[string]*ChatSession) + s.deviceSubscriptions = make(map[string]*DeviceSubscription) + s.quit = make(chan struct{}) + + // setup providers (FCM only, for now) + s.firebaseProvider = NewFirebaseProvider(whisperConfig.FirebaseConfig) +} + +// Start begins notification loop, in a separate go routine +func (s *NotificationServer) Start(stack *p2p.Server) error { + if s.whisper == nil { + return ErrServiceInitError + } + + // configure nodeID + if stack != nil { + if nodeInfo := stack.NodeInfo(); nodeInfo != nil { + s.nodeID = nodeInfo.ID + } + } + + // configure keys + identity, err := s.config.ReadIdentityFile() + if err != nil { + return err + } + s.whisper.AddKeyPair(identity) + s.protocolKey = identity + log.Info("protocol pubkey", "key", common.ToHex(crypto.FromECDSAPub(&s.protocolKey.PublicKey))) + + // start discovery protocol + s.discovery.Start() + + // client session status requests + clientSessionStatusFilterID, err := s.installKeyFilter(topicCheckClientSession, s.protocolKey) + if err != nil { + return fmt.Errorf("failed installing filter: %v", err) + } + go s.requestProcessorLoop(clientSessionStatusFilterID, topicDiscoverServer, s.processClientSessionStatusRequest) + + // client session remove requests + dropClientSessionFilterID, err := s.installKeyFilter(topicDropClientSession, s.protocolKey) + if err != nil { + return fmt.Errorf("failed installing filter: %v", err) + } + go s.requestProcessorLoop(dropClientSessionFilterID, topicDropClientSession, s.processDropClientSessionRequest) + + log.Info("Whisper Notification Server started") + return nil +} + +// Stop handles stopping the running notification loop, and all related resources +func (s *NotificationServer) Stop() error { + close(s.quit) + + if s.whisper == nil { + return ErrServiceInitError + } + + if s.discovery != nil { + s.discovery.Stop() + } + + log.Info("Whisper Notification Server stopped") + return nil +} + +// RegisterClientSession forms a cryptographic link between server and client. +// It does so by sharing a session SymKey and installing filter listening for messages +// encrypted with that key. So, both server and client have a secure way to communicate. +func (s *NotificationServer) RegisterClientSession(session *ClientSession) (sessionKey []byte, err error) { + s.clientSessionsMu.Lock() + defer s.clientSessionsMu.Unlock() + + // generate random symmetric session key + keyName := fmt.Sprintf("%s-%s", "ntfy-client", crypto.Keccak256Hash([]byte(session.ClientKey)).Hex()) + sessionKey, sessionKeyDerived, err := s.makeSessionKey(keyName) + if err != nil { + return nil, err + } + + // populate session key hash (will be used to match decrypted message to a given client id) + session.SessionKeyInput = sessionKey + session.SessionKeyHash = crypto.Keccak256Hash(sessionKeyDerived) + session.SessionKey = sessionKeyDerived + + // append to list of known clients + // so that it is trivial to go key hash -> client session info + id := session.SessionKeyHash.Hex() + s.clientSessions[id] = session + + // setup filter, which will get all incoming messages, that are encrypted with SymKey + filterID, err := s.installTopicFilter(topicNewChatSession, sessionKeyDerived) + if err != nil { + return nil, fmt.Errorf("failed installing filter: %v", err) + } + go s.requestProcessorLoop(filterID, topicNewChatSession, s.processNewChatSessionRequest) + return +} + +// RegisterChatSession forms a cryptographic link between server and client. +// This link is meant to be shared with other clients, so that they can use +// the shared SymKey to trigger notifications for devices attached to a given +// chat session. +func (s *NotificationServer) RegisterChatSession(session *ChatSession) (sessionKey []byte, err error) { + s.chatSessionsMu.Lock() + defer s.chatSessionsMu.Unlock() + + // generate random symmetric session key + keyName := fmt.Sprintf("%s-%s", "ntfy-chat", crypto.Keccak256Hash([]byte(session.ParentKey+session.ChatKey)).Hex()) + sessionKey, sessionKeyDerived, err := s.makeSessionKey(keyName) + if err != nil { + return nil, err + } + + // populate session key hash (will be used to match decrypted message to a given client id) + session.SessionKeyHash = crypto.Keccak256Hash(sessionKeyDerived) + session.SessionKey = sessionKeyDerived + + // append to list of known clients + // so that it is trivial to go key hash -> client session info + id := session.SessionKeyHash.Hex() + s.chatSessions[id] = session + + // setup filter, to process incoming device registration requests + filterID1, err := s.installTopicFilter(topicNewDeviceRegistration, sessionKeyDerived) + if err != nil { + return nil, fmt.Errorf("failed installing filter: %v", err) + } + go s.requestProcessorLoop(filterID1, topicNewDeviceRegistration, s.processNewDeviceRegistrationRequest) + + // setup filter, to process incoming notification trigger requests + filterID2, err := s.installTopicFilter(topicSendNotification, sessionKeyDerived) + if err != nil { + return nil, fmt.Errorf("failed installing filter: %v", err) + } + go s.requestProcessorLoop(filterID2, topicSendNotification, s.processSendNotificationRequest) + + return +} + +// RegisterDeviceSubscription persists device id, so that it can be used to trigger notifications. +func (s *NotificationServer) RegisterDeviceSubscription(subscription *DeviceSubscription) error { + s.deviceSubscriptionsMu.Lock() + defer s.deviceSubscriptionsMu.Unlock() + + // if one passes the same id again, we will just overwrite + id := fmt.Sprintf("%s-%s", "ntfy-device", + crypto.Keccak256Hash([]byte(subscription.ChatSessionKeyHash.Hex()+subscription.DeviceID)).Hex()) + s.deviceSubscriptions[id] = subscription + + log.Info("device registered", "device", subscription.DeviceID) + return nil +} + +// DropClientSession uninstalls session +func (s *NotificationServer) DropClientSession(id string) { + dropChatSessions := func(parentKey string) { + s.chatSessionsMu.Lock() + defer s.chatSessionsMu.Unlock() + + for key, chatSession := range s.chatSessions { + if chatSession.ParentKey == parentKey { + delete(s.chatSessions, key) + log.Info("drop chat session", "key", key) + } + } + } + + dropDeviceSubscriptions := func(parentKey string) { + s.deviceSubscriptionsMu.Lock() + defer s.deviceSubscriptionsMu.Unlock() + + for key, subscription := range s.deviceSubscriptions { + if hex.EncodeToString(crypto.FromECDSAPub(subscription.PubKey)) == parentKey { + delete(s.deviceSubscriptions, key) + log.Info("drop device subscription", "key", key) + } + } + } + + s.clientSessionsMu.Lock() + if session, ok := s.clientSessions[id]; ok { + delete(s.clientSessions, id) + log.Info("server drops client session", "id", id) + s.clientSessionsMu.Unlock() + + dropDeviceSubscriptions(session.ClientKey) + dropChatSessions(session.ClientKey) + } +} + +// processNewChatSessionRequest processes incoming client requests of type: +// client has a session key, and ready to create a new chat session (which is +// a bag of subscribed devices, basically) +func (s *NotificationServer) processNewChatSessionRequest(msg *whisper.ReceivedMessage) error { + s.clientSessionsMu.RLock() + defer s.clientSessionsMu.RUnlock() + + var parsedMessage struct { + ChatID string `json:"chat"` + } + if err := json.Unmarshal(msg.Payload, &parsedMessage); err != nil { + return err + } + + if msg.Src == nil { + return errors.New("message 'from' field is required") + } + + clientSession, ok := s.clientSessions[msg.SymKeyHash.Hex()] + if !ok { + return errors.New("client session not found") + } + + // register chat session + parentKey := hex.EncodeToString(crypto.FromECDSAPub(msg.Src)) + sessionKey, err := s.RegisterChatSession(&ChatSession{ + ParentKey: parentKey, + ChatKey: parsedMessage.ChatID, + }) + if err != nil { + return err + } + + // confirm that chat has been successfully created + msgParams := whisper.MessageParams{ + Dst: msg.Src, + KeySym: clientSession.SessionKey, + Topic: MakeTopic([]byte(topicAckNewChatSession)), + Payload: []byte(`{"server": "0x` + s.nodeID + `", "key": "0x` + hex.EncodeToString(sessionKey) + `"}`), + TTL: uint32(s.config.TTL), + PoW: s.config.MinimumPoW, + WorkTime: 5, + } + response, err := whisper.NewSentMessage(&msgParams) + if err != nil { + return fmt.Errorf("failed to create server response message: %v", err) + } + env, err := response.Wrap(&msgParams) + if err != nil { + return fmt.Errorf("failed to wrap server response message: %v", err) + } + + if err := s.whisper.Send(env); err != nil { + return fmt.Errorf("failed to send server response message: %v", err) + } + + log.Info("server confirms chat creation", "dst", + common.ToHex(crypto.FromECDSAPub(msgParams.Dst)), "topic", msgParams.Topic.String()) + return nil +} + +// processNewDeviceRegistrationRequest processes incoming client requests of type: +// client has a session key, creates chat, and obtains chat SymKey (to be shared with +// others). Then using that chat SymKey client registers it's device ID with server. +func (s *NotificationServer) processNewDeviceRegistrationRequest(msg *whisper.ReceivedMessage) error { + s.chatSessionsMu.RLock() + defer s.chatSessionsMu.RUnlock() + + var parsedMessage struct { + DeviceID string `json:"device"` + } + if err := json.Unmarshal(msg.Payload, &parsedMessage); err != nil { + return err + } + + if msg.Src == nil { + return errors.New("message 'from' field is required") + } + + chatSession, ok := s.chatSessions[msg.SymKeyHash.Hex()] + if !ok { + return errors.New("chat session not found") + } + + if len(parsedMessage.DeviceID) <= 0 { + return errors.New("'device' cannot be empty") + } + + // register chat session + err := s.RegisterDeviceSubscription(&DeviceSubscription{ + DeviceID: parsedMessage.DeviceID, + ChatSessionKeyHash: chatSession.SessionKeyHash, + PubKey: msg.Src, + }) + if err != nil { + return err + } + + // confirm that client has been successfully subscribed + msgParams := whisper.MessageParams{ + Dst: msg.Src, + KeySym: chatSession.SessionKey, + Topic: MakeTopic([]byte(topicAckDeviceRegistration)), + Payload: []byte(`{"server": "0x` + s.nodeID + `"}`), + TTL: uint32(s.config.TTL), + PoW: s.config.MinimumPoW, + WorkTime: 5, + } + response, err := whisper.NewSentMessage(&msgParams) + if err != nil { + return fmt.Errorf("failed to create server response message: %v", err) + } + env, err := response.Wrap(&msgParams) + if err != nil { + return fmt.Errorf("failed to wrap server response message: %v", err) + } + + if err := s.whisper.Send(env); err != nil { + return fmt.Errorf("failed to send server response message: %v", err) + } + + log.Info("server confirms device registration", "dst", + common.ToHex(crypto.FromECDSAPub(msgParams.Dst)), "topic", msgParams.Topic.String()) + return nil +} + +// processSendNotificationRequest processes incoming client requests of type: +// when client has session key, and ready to use it to send notifications +func (s *NotificationServer) processSendNotificationRequest(msg *whisper.ReceivedMessage) error { + s.deviceSubscriptionsMu.RLock() + defer s.deviceSubscriptionsMu.RUnlock() + + for _, subscriber := range s.deviceSubscriptions { + if subscriber.ChatSessionKeyHash == msg.SymKeyHash { + if whisper.IsPubKeyEqual(msg.Src, subscriber.PubKey) { + continue // no need to notify ourselves + } + + if s.firebaseProvider != nil { + err := s.firebaseProvider.Send(subscriber.DeviceID, string(msg.Payload)) + if err != nil { + log.Info("cannot send notification", "error", err) + } + } + } + } + + return nil +} + +// processClientSessionStatusRequest processes incoming client requests when: +// client wants to learn whether it is already registered on some of the servers +func (s *NotificationServer) processClientSessionStatusRequest(msg *whisper.ReceivedMessage) error { + s.clientSessionsMu.RLock() + defer s.clientSessionsMu.RUnlock() + + if msg.Src == nil { + return errors.New("message 'from' field is required") + } + + var sessionKey []byte + pubKey := hex.EncodeToString(crypto.FromECDSAPub(msg.Src)) + for _, clientSession := range s.clientSessions { + if clientSession.ClientKey == pubKey { + sessionKey = clientSession.SessionKeyInput + break + } + } + + // session is not found + if sessionKey == nil { + return nil + } + + // let client know that we have session for a given public key + msgParams := whisper.MessageParams{ + Src: s.protocolKey, + Dst: msg.Src, + Topic: MakeTopic([]byte(topicConfirmClientSession)), + Payload: []byte(`{"server": "0x` + s.nodeID + `", "key": "0x` + hex.EncodeToString(sessionKey) + `"}`), + TTL: uint32(s.config.TTL), + PoW: s.config.MinimumPoW, + WorkTime: 5, + } + response, err := whisper.NewSentMessage(&msgParams) + if err != nil { + return fmt.Errorf("failed to create server response message: %v", err) + } + env, err := response.Wrap(&msgParams) + if err != nil { + return fmt.Errorf("failed to wrap server response message: %v", err) + } + + if err := s.whisper.Send(env); err != nil { + return fmt.Errorf("failed to send server response message: %v", err) + } + + log.Info("server confirms client session", "dst", + common.ToHex(crypto.FromECDSAPub(msgParams.Dst)), "topic", msgParams.Topic.String()) + return nil +} + +// processDropClientSessionRequest processes incoming client requests when: +// client wants to drop its sessions with notification servers (if they exist) +func (s *NotificationServer) processDropClientSessionRequest(msg *whisper.ReceivedMessage) error { + if msg.Src == nil { + return errors.New("message 'from' field is required") + } + + s.clientSessionsMu.RLock() + pubKey := hex.EncodeToString(crypto.FromECDSAPub(msg.Src)) + for _, clientSession := range s.clientSessions { + if clientSession.ClientKey == pubKey { + s.clientSessionsMu.RUnlock() + s.DropClientSession(clientSession.SessionKeyHash.Hex()) + break + } + } + return nil +} + +// installTopicFilter installs Whisper filter using symmetric key +func (s *NotificationServer) installTopicFilter(topicName string, topicKey []byte) (filterID string, err error) { + topic := MakeTopicAsBytes([]byte(topicName)) + filter := whisper.Filter{ + KeySym: topicKey, + Topics: [][]byte{topic}, + AllowP2P: true, + } + filterID, err = s.whisper.Subscribe(&filter) + if err != nil { + return "", fmt.Errorf("failed installing filter: %v", err) + } + + log.Debug(fmt.Sprintf("installed topic filter %v for topic %x (%s)", filterID, topic, topicName)) + return +} + +// installKeyFilter installs Whisper filter using asymmetric key +func (s *NotificationServer) installKeyFilter(topicName string, key *ecdsa.PrivateKey) (filterID string, err error) { + topic := MakeTopicAsBytes([]byte(topicName)) + filter := whisper.Filter{ + KeyAsym: key, + Topics: [][]byte{topic}, + AllowP2P: true, + } + filterID, err = s.whisper.Subscribe(&filter) + if err != nil { + return "", fmt.Errorf("failed installing filter: %v", err) + } + + log.Info(fmt.Sprintf("installed key filter %v for topic %x (%s)", filterID, topic, topicName)) + return +} + +// requestProcessorLoop processes incoming client requests, by listening to a given filter, +// and executing process function on each incoming message +func (s *NotificationServer) requestProcessorLoop(filterID string, topicWatched string, fn messageProcessingFn) { + log.Debug(fmt.Sprintf("request processor started: %s", topicWatched)) + + filter := s.whisper.GetFilter(filterID) + if filter == nil { + log.Warn(fmt.Sprintf("filter is not installed: %s (for topic '%s')", filterID, topicWatched)) + return + } + + ticker := time.NewTicker(time.Millisecond * 50) + + for { + select { + case <-ticker.C: + messages := filter.Retrieve() + for _, msg := range messages { + if err := fn(msg); err != nil { + log.Warn("failed processing incoming request", "error", err) + } + } + case <-s.quit: + log.Debug("request processor stopped", "topic", topicWatched) + return + } + } +} + +// makeSessionKey generates and saves random SymKey, allowing to establish secure +// channel between server and client +func (s *NotificationServer) makeSessionKey(keyName string) (sessionKey, sessionKeyDerived []byte, err error) { + // wipe out previous occurrence of symmetric key + s.whisper.DeleteSymKey(keyName) + + sessionKey, err = makeSessionKey() + if err != nil { + return nil, nil, err + } + + keyName, err = s.whisper.AddSymKey(keyName, sessionKey) + if err != nil { + return nil, nil, err + } + + sessionKeyDerived, err = s.whisper.GetSymKey(keyName) + if err != nil { + return nil, nil, err + } + + return +} diff --git a/whisper/notifications/utils.go b/whisper/notifications/utils.go new file mode 100644 index 0000000000..106752186d --- /dev/null +++ b/whisper/notifications/utils.go @@ -0,0 +1,84 @@ +package notifications + +import ( + "crypto/sha512" + "errors" + "crypto/sha256" + + crand "crypto/rand" + whisper "github.com/ethereum/go-ethereum/whisper/whisperv5" + "golang.org/x/crypto/pbkdf2" +) + +// makeSessionKey returns pseudo-random symmetric key, which is used as +// session key between notification client and server +func makeSessionKey() ([]byte, error) { + // generate random key + const keyLen = 32 + buf := make([]byte, keyLen) + _, err := crand.Read(buf) + if err != nil { + return nil, err + } else if !validateSymmetricKey(buf) { + return nil, errors.New("error in GenerateSymKey: crypto/rand failed to generate random data") + } + + key := buf[:keyLen] + derived, err := deriveKeyMaterial(key, whisper.EnvelopeVersion) + if err != nil { + return nil, err + } else if !validateSymmetricKey(derived) { + return nil, errors.New("failed to derive valid key") + } + + return derived, nil +} + +// validateSymmetricKey returns false if the key contains all zeros +func validateSymmetricKey(k []byte) bool { + return len(k) > 0 && !containsOnlyZeros(k) +} + +// containsOnlyZeros checks if data is empty or not +func containsOnlyZeros(data []byte) bool { + for _, b := range data { + if b != 0 { + return false + } + } + return true +} + +// deriveKeyMaterial derives symmetric key material from the key or password./~~~ +// pbkdf2 is used for security, in case people use password instead of randomly generated keys. +func deriveKeyMaterial(key []byte, version uint64) (derivedKey []byte, err error) { + if version == 0 { + // kdf should run no less than 0.1 seconds on average compute, + // because it's a once in a session experience + derivedKey := pbkdf2.Key(key, nil, 65356, 32, sha256.New) + return derivedKey, nil + } else { + return nil, errors.New("unknown version") + } +} + +// MakeTopic returns Whisper topic *as bytes array* by generating cryptographic key from the provided password +func MakeTopicAsBytes(password []byte) ([]byte) { + topic := make([]byte, int(whisper.TopicLength)) + x := pbkdf2.Key(password, password, 8196, 128, sha512.New) + for i := 0; i < len(x); i++ { + topic[i%whisper.TopicLength] ^= x[i] + } + + return topic +} + +// MakeTopic returns Whisper topic by generating cryptographic key from the provided password +func MakeTopic(password []byte) (topic whisper.TopicType) { + x := pbkdf2.Key(password, password, 8196, 128, sha512.New) + for i := 0; i < len(x); i++ { + topic[i%whisper.TopicLength] ^= x[i] + } + + return +} diff --git a/whisper/whisperv2/whisper.go b/whisper/whisperv2/whisper.go index 61c36918d4..9083469999 100644 --- a/whisper/whisperv2/whisper.go +++ b/whisper/whisperv2/whisper.go @@ -134,6 +134,13 @@ func (self *Whisper) NewIdentity() *ecdsa.PrivateKey { return key } +// AddIdentity adds identity into the known identities list (for message decryption). +func (self *Whisper) AddIdentity(key *ecdsa.PrivateKey) { + self.keysMu.Lock() + self.keys[string(crypto.FromECDSAPub(&key.PublicKey))] = key + self.keysMu.Unlock() +} + // 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 { diff --git a/whisper/whisperv5/api.go b/whisper/whisperv5/api.go index 96c4b0e6c4..75ef8b66c0 100644 --- a/whisper/whisperv5/api.go +++ b/whisper/whisperv5/api.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/message" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p/discover" @@ -238,13 +239,17 @@ type newMessageOverride struct { // Post a message on the Whisper network. func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, error) { var ( - symKeyGiven = len(req.SymKeyID) > 0 - pubKeyGiven = len(req.PublicKey) > 0 - err error + symKeyGiven = len(req.SymKeyID) > 0 + pubKeyGiven = len(req.PublicKey) > 0 + isP2PMessage = len(req.TargetPeer) > 0 + err error ) + api.w.traceOutgoingDelivery(isP2PMessage, message.PendingStatus, &req, nil, nil, nil) + // user must specify either a symmetric or an asymmetric key if (symKeyGiven && pubKeyGiven) || (!symKeyGiven && !pubKeyGiven) { + api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, ErrSymAsym) return false, ErrSymAsym } @@ -260,6 +265,7 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er // Set key that is used to sign the message if len(req.Sig) > 0 { if params.Src, err = api.w.GetPrivateKey(req.Sig); err != nil { + api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, err) return false, err } } @@ -267,12 +273,15 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er // Set symmetric key that is used to encrypt the message if symKeyGiven { if params.Topic == (TopicType{}) { // topics are mandatory with symmetric encryption + api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, ErrNoTopics) return false, ErrNoTopics } if params.KeySym, err = api.w.GetSymKey(req.SymKeyID); err != nil { + api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, err) return false, err } if !validateSymmetricKey(params.KeySym) { + api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, ErrInvalidSymmetricKey) return false, ErrInvalidSymmetricKey } } @@ -281,6 +290,7 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er if pubKeyGiven { params.Dst = crypto.ToECDSAPub(req.PublicKey) if !ValidatePublicKey(params.Dst) { + api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, ErrInvalidPublicKey) return false, ErrInvalidPublicKey } } @@ -288,11 +298,13 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er // encrypt and sent message whisperMsg, err := NewSentMessage(params) if err != nil { + api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, err) return false, err } env, err := whisperMsg.Wrap(params) if err != nil { + api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, err) return false, err } @@ -300,19 +312,41 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er if len(req.TargetPeer) > 0 { n, err := discover.ParseNode(req.TargetPeer) if err != nil { + api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, env, nil, err) return false, fmt.Errorf("failed to parse target peer: %s", err) } - return true, api.w.SendP2PMessage(n.ID[:], env) + + api.w.traceOutgoingDelivery(isP2PMessage, message.SentStatus, &req, env, nil, nil) + + if err := api.w.SendP2PMessage(n.ID[:], env); err != nil { + api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, env, nil, err) + return true, err + } + + api.w.traceOutgoingDelivery(isP2PMessage, message.DeliveredStatus, &req, env, nil, err) + return true, nil } // ensure that the message PoW meets the node's minimum accepted PoW if req.PowTarget < api.w.MinPow() { + api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, env, nil, ErrTooLowPoW) return false, ErrTooLowPoW } + api.w.traceOutgoingDelivery(isP2PMessage, message.SentStatus, &req, env, nil, nil) return true, api.w.Send(env) } +// UninstallFilter is alias for Unsubscribe +func (api *PublicWhisperAPI) UninstallFilter(id string) { + api.w.Unsubscribe(id) +} + +// Unsubscribe disables and removes an existing filter. +func (api *PublicWhisperAPI) Unsubscribe(id string) { + api.w.Unsubscribe(id) +} + //go:generate gencodec -type Criteria -field-override criteriaOverride -out gen_criteria_json.go // Criteria holds various filter options for inbound messages. diff --git a/whisper/whisperv5/doc.go b/whisper/whisperv5/doc.go index 7a57488bd7..b3bc9963b9 100644 --- a/whisper/whisperv5/doc.go +++ b/whisper/whisperv5/doc.go @@ -32,6 +32,9 @@ package whisperv5 import ( "fmt" "time" + + "github.com/ethereum/go-ethereum/common/message" + "github.com/ethereum/go-ethereum/p2p" ) const ( @@ -57,7 +60,7 @@ const ( MaxMessageSize = uint32(10 * 1024 * 1024) // maximum accepted size of a message. DefaultMaxMessageSize = uint32(1024 * 1024) - DefaultMinimumPoW = 0.2 + DefaultMinimumPoW = 0.001 padSizeLimit = 256 // just an arbitrary number, could be changed without breaking the protocol (must not exceed 2^24) messageQueueLimit = 1024 @@ -85,3 +88,35 @@ type MailServer interface { Archive(env *Envelope) DeliverMail(whisperPeer *Peer, request *Envelope) } + +// NotificationServer represents a notification server, +// capable of screening incoming envelopes for special +// topics, and once located, subscribe client nodes as +// recipients to notifications (push notifications atm) +type NotificationServer interface { + // Start initializes notification sending loop + Start(server *p2p.Server) error + + // Stop stops notification sending loop, releasing related resources + Stop() error +} + +// MessageState holds the current delivery status of a whisper p2p message. +type MessageState struct { + IsP2P bool `json:"is_p2p"` + Reason error `json:"reason"` + Envelope Envelope `json:"envelope"` + Timestamp time.Time `json:"timestamp"` + Source NewMessage `json:"source"` + Status message.Status `json:"status"` + Direction message.Direction `json:"direction"` + Received ReceivedMessage `json:"received"` +} + +// DeliveryServer represents a small message status +// notification system where a message delivery status +// update event is delivered to it's underline system +// for both rpc messages and p2p messages. +type DeliveryServer interface { + SendState(MessageState) +} diff --git a/whisper/whisperv5/filter.go b/whisper/whisperv5/filter.go index b5e893e0ff..71dc3b2d71 100644 --- a/whisper/whisperv5/filter.go +++ b/whisper/whisperv5/filter.go @@ -18,10 +18,12 @@ package whisperv5 import ( "crypto/ecdsa" + "errors" "fmt" "sync" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/message" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" ) @@ -115,15 +117,20 @@ func (fs *Filters) NotifyWatchers(env *Envelope, p2pMessage bool) { if match { msg = env.Open(watcher) if msg == nil { + err := errors.New("Envelope failed to be opened") + fs.whisper.traceIncomingDelivery(p2pMessage, message.RejectedStatus, nil, env, nil, err) log.Trace("processing message: failed to open", "message", env.Hash().Hex(), "filter", i) } } else { + err := errors.New("processing message: does not match") + fs.whisper.traceIncomingDelivery(p2pMessage, message.RejectedStatus, nil, env, nil, err) log.Trace("processing message: does not match", "message", env.Hash().Hex(), "filter", i) } } if match && msg != nil { log.Trace("processing message: decrypted", "hash", env.Hash().Hex()) + fs.whisper.traceIncomingDelivery(p2pMessage, message.DeliveredStatus, nil, env, msg, nil) if watcher.Src == nil || IsPubKeyEqual(msg.Src, watcher.Src) { watcher.Trigger(msg) } diff --git a/whisper/whisperv5/whisper.go b/whisper/whisperv5/whisper.go index 85849ccce4..9081777cdc 100644 --- a/whisper/whisperv5/whisper.go +++ b/whisper/whisperv5/whisper.go @@ -27,6 +27,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/message" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p" @@ -77,7 +78,9 @@ type Whisper struct { statsMu sync.Mutex // guard stats stats Statistics // Statistics of whisper node - mailServer MailServer // MailServer interface + mailServer MailServer // MailServer interface + deliveryServer DeliveryServer // DeliveryServer interface + notificationServer NotificationServer } // New creates a Whisper client ready to communicate through the Ethereum P2P network. @@ -156,6 +159,16 @@ func (w *Whisper) RegisterServer(server MailServer) { w.mailServer = server } +// RegisterDeliveryServer registers notification server with Whisper +func (w *Whisper) RegisterDeliveryServer(server DeliveryServer) { + w.deliveryServer = server +} + +// RegisterNotificationServer registers notification server with Whisper +func (w *Whisper) RegisterNotificationServer(server NotificationServer) { + w.notificationServer = server +} + // Protocols returns the whisper sub-protocols ran by this particular client. func (w *Whisper) Protocols() []p2p.Protocol { return []p2p.Protocol{w.protocol} @@ -250,9 +263,9 @@ func (w *Whisper) NewKeyPair() (string, error) { return "", fmt.Errorf("failed to generate valid key") } - id, err := GenerateRandomID() + id, err := toDeterministicID(common.ToHex(crypto.FromECDSAPub(&key.PublicKey)), keyIdSize) if err != nil { - return "", fmt.Errorf("failed to generate ID: %s", err) + return "", err } w.keyMu.Lock() @@ -265,45 +278,94 @@ func (w *Whisper) NewKeyPair() (string, error) { return id, nil } -// DeleteKeyPair deletes the specified key if it exists. -func (w *Whisper) DeleteKeyPair(key string) bool { +// AddIdentity adds cryptographic identity into the known +// identities list (for message decryption). +func (w *Whisper) AddKeyPair(key *ecdsa.PrivateKey) (string, error) { + id, err := makeDeterministicID(common.ToHex(crypto.FromECDSAPub(&key.PublicKey)), keyIdSize) + if err != nil { + return "", err + } + if w.HasKeyPair(id) { + return id, nil // no need to re-inject + } + w.keyMu.Lock() defer w.keyMu.Unlock() - if w.privateKeys[key] != nil { - delete(w.privateKeys, key) + w.privateKeys[id] = key + log.Info("Whisper identity added", "id", id, "pubkey", common.ToHex(crypto.FromECDSAPub(&key.PublicKey))) + + return id, nil +} + +// SelectKeyPair adds cryptographic identity, and makes sure +// that it is the only private key known to the node. +func (w *Whisper) SelectKeyPair(key *ecdsa.PrivateKey) error { + id, err := makeDeterministicID(common.ToHex(crypto.FromECDSAPub(&key.PublicKey)), keyIdSize) + if err != nil { + return err + } + + w.keyMu.Lock() + defer w.keyMu.Unlock() + + w.privateKeys = make(map[string]*ecdsa.PrivateKey) // reset key store + w.privateKeys[id] = key + + log.Info("Whisper identity selected", "id", id, "key", common.ToHex(crypto.FromECDSAPub(&key.PublicKey))) + return nil +} + +// DeleteKeyPairs removes all cryptographic identities known to the node +func (w *Whisper) DeleteKeyPairs() error { + w.keyMu.Lock() + defer w.keyMu.Unlock() + + w.privateKeys = make(map[string]*ecdsa.PrivateKey) + + return nil +} + +// DeleteKeyPair deletes the specified key if it exists. +func (w *Whisper) DeleteKeyPair(id string) bool { + deterministicID, err := toDeterministicID(id, keyIdSize) + if err != nil { + return false + } + + w.keyMu.Lock() + defer w.keyMu.Unlock() + + if w.privateKeys[deterministicID] != nil { + delete(w.privateKeys, deterministicID) return true } return false } -// AddKeyPair imports a asymmetric private key and returns it identifier. -func (w *Whisper) AddKeyPair(key *ecdsa.PrivateKey) (string, error) { - id, err := GenerateRandomID() - if err != nil { - return "", fmt.Errorf("failed to generate ID: %s", err) - } - - w.keyMu.Lock() - w.privateKeys[id] = key - w.keyMu.Unlock() - - return id, nil -} - // HasKeyPair checks if the the whisper node is configured with the private key // of the specified public pair. func (w *Whisper) HasKeyPair(id string) bool { + deterministicID, err := toDeterministicID(id, keyIdSize) + if err != nil { + return false + } + w.keyMu.RLock() defer w.keyMu.RUnlock() - return w.privateKeys[id] != nil + return w.privateKeys[deterministicID] != nil } // GetPrivateKey retrieves the private key of the specified identity. func (w *Whisper) GetPrivateKey(id string) (*ecdsa.PrivateKey, error) { + deterministicID, err := toDeterministicID(id, keyIdSize) + if err != nil { + return nil, err + } + w.keyMu.RLock() defer w.keyMu.RUnlock() - key := w.privateKeys[id] + key := w.privateKeys[deterministicID] if key == nil { return nil, fmt.Errorf("invalid id") } @@ -336,6 +398,23 @@ func (w *Whisper) GenerateSymKey() (string, error) { return id, nil } +// AddSymKey stores the key with a given id. +func (w *Whisper) AddSymKey(id string, key []byte) (string, error) { + deterministicID, err := toDeterministicID(id, keyIdSize) + if err != nil { + return "", err + } + + w.keyMu.Lock() + defer w.keyMu.Unlock() + + if w.symKeys[deterministicID] != nil { + return "", fmt.Errorf("key already exists: %v", id) + } + w.symKeys[deterministicID] = key + return deterministicID, nil +} + // AddSymKeyDirect stores the key, and returns its id. func (w *Whisper) AddSymKeyDirect(key []byte) (string, error) { if len(key) != aesKeyLength { @@ -447,7 +526,7 @@ func (w *Whisper) Send(envelope *Envelope) error { // Start implements node.Service, starting the background data propagation thread // of the Whisper protocol. -func (w *Whisper) Start(*p2p.Server) error { +func (w *Whisper) Start(stack *p2p.Server) error { log.Info("started whisper v." + ProtocolVersionStr) go w.update() @@ -456,6 +535,12 @@ func (w *Whisper) Start(*p2p.Server) error { go w.processQueue() } + if w.notificationServer != nil { + if err := w.notificationServer.Start(stack); err != nil { + return err + } + } + return nil } @@ -463,6 +548,13 @@ func (w *Whisper) Start(*p2p.Server) error { // of the Whisper protocol. func (w *Whisper) Stop() error { close(w.quit) + + if w.notificationServer != nil { + if err := w.notificationServer.Stop(); err != nil { + return err + } + } + log.Info("whisper stopped") return nil } @@ -535,8 +627,11 @@ func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error { var envelope Envelope if err := packet.Decode(&envelope); err != nil { log.Warn("failed to decode direct message, peer will be disconnected", "peer", p.peer.ID(), "err", err) + wh.traceIncomingDelivery(true, message.RejectedStatus, nil, &envelope, nil, err) return errors.New("invalid direct message") } + + wh.traceIncomingDelivery(true, message.SentStatus, nil, &envelope, nil, nil) wh.postEvent(&envelope, true) } case p2pRequestCode: @@ -545,6 +640,7 @@ func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error { var request Envelope if err := packet.Decode(&request); err != nil { log.Warn("failed to decode p2p request message, peer will be disconnected", "peer", p.peer.ID(), "err", err) + wh.traceIncomingDelivery(true, message.RejectedStatus, nil, &request, nil, err) return errors.New("invalid p2p request") } wh.mailServer.DeliverMail(p, &request) @@ -615,16 +711,22 @@ func (wh *Whisper) add(envelope *Envelope) (bool, error) { if !wh.expirations[envelope.Expiry].Has(hash) { wh.expirations[envelope.Expiry].Add(hash) } + + wh.traceIncomingDelivery(false, message.CachedStatus, nil, envelope, nil, nil) } wh.poolMu.Unlock() if alreadyCached { log.Trace("whisper envelope already cached", "hash", envelope.Hash().Hex()) + wh.traceIncomingDelivery(false, message.ResentStatus, nil, envelope, nil, nil) } else { log.Trace("cached whisper envelope", "hash", envelope.Hash().Hex()) wh.statsMu.Lock() wh.stats.memoryUsed += envelope.size() wh.statsMu.Unlock() + + wh.traceIncomingDelivery(false, message.QueuedStatus, nil, envelope, nil, nil) + wh.postEvent(envelope, false) // notify the local node about the new message if wh.mailServer != nil { wh.mailServer.Archive(envelope) @@ -633,6 +735,47 @@ func (wh *Whisper) add(envelope *Envelope) (bool, error) { return true, nil } +func (w *Whisper) traceIncomingDelivery(isP2P bool, status message.Status, src *NewMessage, env *Envelope, rec *ReceivedMessage, err error) { + w.traceDelivery(isP2P, message.IncomingMessage, status, src, env, rec, err) +} + +func (w *Whisper) traceOutgoingDelivery(isP2P bool, status message.Status, src *NewMessage, env *Envelope, rec *ReceivedMessage, err error) { + w.traceDelivery(isP2P, message.OutgoingMessage, status, src, env, rec, err) +} + +func (w *Whisper) traceDelivery(isP2P bool, dir message.Direction, status message.Status, newmsg *NewMessage, envelope *Envelope, received *ReceivedMessage, err error) { + if w.deliveryServer == nil { + return + } + + var env Envelope + var rec ReceivedMessage + var src NewMessage + + if newmsg != nil { + src = *newmsg + } + + if envelope != nil { + env = *envelope + } + + if received != nil { + rec = *received + } + + go w.deliveryServer.SendState(MessageState{ + Reason: err, + Source: src, + Received: rec, + IsP2P: isP2P, + Status: status, + Envelope: env, + Direction: dir, + Timestamp: time.Now(), + }) +} + // postEvent queues the message for further processing. func (w *Whisper) postEvent(envelope *Envelope, isP2P bool) { // if the version of incoming message is higher than @@ -645,6 +788,13 @@ func (w *Whisper) postEvent(envelope *Envelope, isP2P bool) { w.checkOverflow() w.messageQueue <- envelope } + + return + } + + if w.deliveryServer != nil { + err := fmt.Errorf("Mismatch Envelope version(%d) to wanted Version(%d)", envelope.Ver(), EnvelopeVersion) + w.traceIncomingDelivery(isP2P, message.RejectedStatus, nil, envelope, nil, err) } } @@ -674,9 +824,11 @@ func (w *Whisper) processQueue() { return case e = <-w.messageQueue: + w.traceIncomingDelivery(false, message.ProcessingStatus, nil, e, nil, nil) w.filters.NotifyWatchers(e, false) case e = <-w.p2pMsgQueue: + w.traceIncomingDelivery(true, message.ProcessingStatus, nil, e, nil, nil) w.filters.NotifyWatchers(e, true) } } @@ -856,3 +1008,30 @@ func GenerateRandomID() (id string, err error) { id = common.Bytes2Hex(buf) return id, err } + +// makeDeterministicID generates a deterministic ID, based on a given input +func makeDeterministicID(input string, keyLen int) (id string, err error) { + buf := pbkdf2.Key([]byte(input), nil, 4096, keyLen, sha256.New) + if !validateSymmetricKey(buf) { + return "", fmt.Errorf("error in GenerateDeterministicID: failed to generate key") + } + id = common.Bytes2Hex(buf) + return id, err +} + +// toDeterministicID reviews incoming id, and transforms it to format +// expected internally be private key store. Originally, public keys +// were used as keys, now random keys are being used. And in order to +// make it easier to consume, we now allow both random IDs and public +// keys to be passed. +func toDeterministicID(id string, expectedLen int) (string, error) { + if len(id) != (expectedLen * 2) { // we received hex key, so number of chars in id is doubled + var err error + id, err = makeDeterministicID(id, expectedLen) + if err != nil { + return "", err + } + } + + return id, nil +}