Apply status-patches

This commit is contained in:
Ivan Danyliuk 2017-12-12 11:20:23 +01:00
parent 4bb3c89d44
commit 805544030a
No known key found for this signature in database
GPG key ID: 97ED33CE024E1DBF
28 changed files with 1783 additions and 49 deletions

View file

@ -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 ## Go Ethereum
Official golang implementation of the Ethereum protocol. Official golang implementation of the Ethereum protocol.

View file

@ -33,6 +33,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/pborman/uuid" "github.com/pborman/uuid"
"github.com/status-im/status-go/extkeys"
) )
const ( const (
@ -46,6 +47,10 @@ type Key struct {
// we only store privkey as pubkey/address can be derived from it // we only store privkey as pubkey/address can be derived from it
// privkey in this struct is always in plaintext // privkey in this struct is always in plaintext
PrivateKey *ecdsa.PrivateKey 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 { type keyStore interface {
@ -65,10 +70,12 @@ type plainKeyJSON struct {
} }
type encryptedKeyJSONV3 struct { type encryptedKeyJSONV3 struct {
Address string `json:"address"` Address string `json:"address"`
Crypto cryptoJSON `json:"crypto"` Crypto cryptoJSON `json:"crypto"`
Id string `json:"id"` Id string `json:"id"`
Version int `json:"version"` Version int `json:"version"`
ExtendedKey cryptoJSON `json:"extendedkey"`
SubAccountIndex uint32 `json:"subaccountindex"`
} }
type encryptedKeyJSONV1 struct { type encryptedKeyJSONV1 struct {
@ -137,6 +144,40 @@ func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key {
return 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 // 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 // into the Direct ICAP spec. for simplicity and easier compatibility with other libs, we
// retry until the first byte is 0. // retry until the first byte is 0.

View file

@ -38,6 +38,7 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/status-im/status-go/extkeys"
) )
var ( var (
@ -228,6 +229,11 @@ func (ks *KeyStore) Accounts() []accounts.Account {
return ks.cache.accounts() 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. // 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. // If the account contains no filename, the address must match a unique key.
func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { 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) 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) { 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))}} 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 { 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 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. // Update changes the passphrase of an existing account.
func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error { func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error {
a, key, err := ks.getDecryptedKey(a, passphrase) 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. // zeroKey zeroes a private key in memory.
func zeroKey(k *ecdsa.PrivateKey) { func zeroKey(k *ecdsa.PrivateKey) {
if k == nil {
return
}
b := k.D.Bits() b := k.D.Bits()
for i := range b { for i := range b {
b[i] = 0 b[i] = 0

View file

@ -41,6 +41,7 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/randentropy" "github.com/ethereum/go-ethereum/crypto/randentropy"
"github.com/pborman/uuid" "github.com/pborman/uuid"
"github.com/status-im/status-go/extkeys"
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/scrypt" "golang.org/x/crypto/scrypt"
) )
@ -151,15 +152,62 @@ func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) {
KDFParams: scryptParamsJSON, KDFParams: scryptParamsJSON,
MAC: hex.EncodeToString(mac), MAC: hex.EncodeToString(mac),
} }
encryptedExtendedKey, err := EncryptExtendedKey(key.ExtendedKey, auth, scryptN, scryptP)
if err != nil {
return nil, err
}
encryptedKeyJSONV3 := encryptedKeyJSONV3{ encryptedKeyJSONV3 := encryptedKeyJSONV3{
hex.EncodeToString(key.Address[:]), hex.EncodeToString(key.Address[:]),
cryptoStruct, cryptoStruct,
key.Id.String(), key.Id.String(),
version, version,
encryptedExtendedKey,
key.SubAccountIndex,
} }
return json.Marshal(encryptedKeyJSONV3) 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. // DecryptKey decrypts a key from a json blob, returning the private key itself.
func DecryptKey(keyjson []byte, auth string) (*Key, error) { func DecryptKey(keyjson []byte, auth string) (*Key, error) {
// Parse the json into a simple map to fetch the key version // 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 ( var (
keyBytes, keyId []byte keyBytes, keyId []byte
err error 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" { if version, ok := m["version"].(string); ok && version == "1" {
k := new(encryptedKeyJSONV1) k := new(encryptedKeyJSONV1)
if err := json.Unmarshal(keyjson, k); err != nil { if err := json.Unmarshal(keyjson, k); err != nil {
return nil, err return nil, err
} }
keyBytes, keyId, err = decryptKeyV1(k, auth) keyBytes, keyId, err = decryptKeyV1(k, auth)
if err != nil {
return nil, err
}
extKey, err = extkeys.NewKeyFromString(extkeys.EmptyExtendedKeyString)
} else { } else {
k := new(encryptedKeyJSONV3) k := new(encryptedKeyJSONV3)
if err := json.Unmarshal(keyjson, k); err != nil { if err := json.Unmarshal(keyjson, k); err != nil {
return nil, err return nil, err
} }
keyBytes, keyId, err = decryptKeyV3(k, auth) 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 // Handle any decryption errors and return the key
if err != nil { if err != nil {
return nil, err return nil, err
@ -192,9 +263,11 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) {
key := crypto.ToECDSAUnsafe(keyBytes) key := crypto.ToECDSAUnsafe(keyBytes)
return &Key{ return &Key{
Id: uuid.UUID(keyId), Id: uuid.UUID(keyId),
Address: crypto.PubkeyToAddress(key.PublicKey), Address: crypto.PubkeyToAddress(key.PublicKey),
PrivateKey: key, PrivateKey: key,
ExtendedKey: extKey,
SubAccountIndex: uint32(subAccountIndex),
}, nil }, nil
} }
@ -274,6 +347,51 @@ func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byt
return plainText, keyId, err return plainText, keyId, err
} }
func decryptExtendedKey(keyProtected *encryptedKeyJSONV3, auth string) (plainText []byte, err error) {
if len(keyProtected.ExtendedKey.CipherText) == 0 {
return []byte(extkeys.EmptyExtendedKeyString), nil
}
if keyProtected.Version != version {
return nil, fmt.Errorf("Version not supported: %v", keyProtected.Version)
}
if keyProtected.ExtendedKey.Cipher != "aes-128-ctr" {
return nil, fmt.Errorf("Cipher not supported: %v", keyProtected.ExtendedKey.Cipher)
}
mac, err := hex.DecodeString(keyProtected.ExtendedKey.MAC)
if err != nil {
return nil, err
}
iv, err := hex.DecodeString(keyProtected.ExtendedKey.CipherParams.IV)
if err != nil {
return nil, err
}
cipherText, err := hex.DecodeString(keyProtected.ExtendedKey.CipherText)
if err != nil {
return nil, err
}
derivedKey, err := getKDFKey(keyProtected.ExtendedKey, auth)
if err != nil {
return nil, err
}
calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText)
if !bytes.Equal(calculatedMAC, mac) {
return nil, ErrDecrypt
}
plainText, err = aesCTRXOR(derivedKey[:16], cipherText, iv)
if err != nil {
return nil, err
}
return plainText, err
}
func getKDFKey(cryptoJSON cryptoJSON, auth string) ([]byte, error) { func getKDFKey(cryptoJSON cryptoJSON, auth string) ([]byte, error) {
authArray := []byte(auth) authArray := []byte(auth)
salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string)) salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string))

View file

@ -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)") 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)") argTopic = flag.String("topic", "", "topic in hexadecimal format (e.g. 70a4beef)")
argSaveDir = flag.String("savedir", "", "directory where incoming messages will be saved as files") 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() { func main() {
@ -146,6 +148,13 @@ func processArgs() {
} else if *fileExMode { } else if *fileExMode {
utils.Fatalf("Parameter 'savedir' is mandatory for file exchange mode") utils.Fatalf("Parameter 'savedir' is mandatory for file exchange mode")
} }
if len(*argSymPass) > 0 {
symPass = *argSymPass
}
if len(*argMsPass) > 0 {
msPassword = *argMsPass
}
if *echoMode { if *echoMode {
echo() echo()
@ -415,10 +424,24 @@ func run() {
} else if *fileExMode { } else if *fileExMode {
sendFilesLoop() sendFilesLoop()
} else { } 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() { func sendLoop() {
for { for {
s := scanLine("") s := scanLine("")

65
common/message/message.go Normal file
View file

@ -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"
}

View file

@ -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"]

View file

@ -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"]

View file

@ -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"]

View file

@ -32,14 +32,20 @@ import (
"github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/eth/gasprice"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
) )
// EthApiBackend implements ethapi.Backend for full nodes // EthApiBackend implements ethapi.Backend for full nodes
type EthApiBackend struct { type EthApiBackend struct {
eth *Ethereum eth *Ethereum
gpo *gasprice.Oracle gpo *gasprice.Oracle
statusBackend *ethapi.StatusBackend
}
func (b *EthApiBackend) GetStatusBackend() *ethapi.StatusBackend {
return b.statusBackend
} }
func (b *EthApiBackend) ChainConfig() *params.ChainConfig { func (b *EthApiBackend) ChainConfig() *params.ChainConfig {

View file

@ -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 = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.engine)
eth.miner.SetExtra(makeExtraData(config.ExtraData)) eth.miner.SetExtra(makeExtraData(config.ExtraData))
eth.ApiBackend = &EthApiBackend{eth, nil} eth.ApiBackend = &EthApiBackend{eth, nil, nil}
gpoParams := config.GPO gpoParams := config.GPO
if gpoParams.Default == nil { if gpoParams.Default == nil {
gpoParams.Default = config.GasPrice gpoParams.Default = config.GasPrice

View file

@ -178,15 +178,24 @@ func (s *PublicTxPoolAPI) Inspect() map[string]map[string]map[string]string {
// It offers only methods that can retrieve accounts. // It offers only methods that can retrieve accounts.
type PublicAccountAPI struct { type PublicAccountAPI struct {
am *accounts.Manager am *accounts.Manager
b Backend
} }
// NewPublicAccountAPI creates a new PublicAccountAPI. // NewPublicAccountAPI creates a new PublicAccountAPI.
func NewPublicAccountAPI(am *accounts.Manager) *PublicAccountAPI { func NewPublicAccountAPI(b Backend) *PublicAccountAPI {
return &PublicAccountAPI{am: am} return &PublicAccountAPI{
am: b.AccountManager(),
b: b,
}
} }
// Accounts returns the collection of accounts this node manages // Accounts returns the collection of accounts this node manages
func (s *PublicAccountAPI) Accounts() []common.Address { 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 addresses := make([]common.Address, 0) // return [] instead of nil if empty
for _, wallet := range s.am.Wallets() { for _, wallet := range s.am.Wallets() {
for _, account := range wallet.Accounts() { 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. // ListAccounts will return a list of addresses for accounts this node manages.
func (s *PrivateAccountAPI) ListAccounts() []common.Address { 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 addresses := make([]common.Address, 0) // return [] instead of nil if empty
for _, wallet := range s.am.Wallets() { for _, wallet := range s.am.Wallets() {
for _, account := range wallet.Accounts() { for _, account := range wallet.Accounts() {
@ -1122,10 +1136,46 @@ func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (c
return tx.Hash(), nil 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. // transaction pool.
func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) {
// Look up the wallet containing the requested signer // Look up the wallet containing the requested signer
account := accounts.Account{Address: args.From} account := accounts.Account{Address: args.From}

View file

@ -68,6 +68,8 @@ type Backend interface {
ChainConfig() *params.ChainConfig ChainConfig() *params.ChainConfig
CurrentBlock() *types.Block CurrentBlock() *types.Block
GetStatusBackend() *StatusBackend
} }
func GetAPIs(apiBackend Backend) []rpc.API { func GetAPIs(apiBackend Backend) []rpc.API {
@ -105,7 +107,7 @@ func GetAPIs(apiBackend Backend) []rpc.API {
}, { }, {
Namespace: "eth", Namespace: "eth",
Version: "1.0", Version: "1.0",
Service: NewPublicAccountAPI(apiBackend.AccountManager()), Service: NewPublicAccountAPI(apiBackend),
Public: true, Public: true,
}, { }, {
Namespace: "personal", Namespace: "personal",

View file

@ -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)
}

View file

@ -32,14 +32,20 @@ import (
"github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/eth/gasprice"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/light" "github.com/ethereum/go-ethereum/light"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
) )
type LesApiBackend struct { type LesApiBackend struct {
eth *LightEthereum eth *LightEthereum
gpo *gasprice.Oracle gpo *gasprice.Oracle
statusBackend *ethapi.StatusBackend
}
func (b *LesApiBackend) GetStatusBackend() *ethapi.StatusBackend {
return b.statusBackend
} }
func (b *LesApiBackend) ChainConfig() *params.ChainConfig { func (b *LesApiBackend) ChainConfig() *params.ChainConfig {

View file

@ -75,6 +75,8 @@ type LightEthereum struct {
netRPCService *ethapi.PublicNetAPI netRPCService *ethapi.PublicNetAPI
wg sync.WaitGroup wg sync.WaitGroup
StatusBackend *ethapi.StatusBackend
} }
func New(ctx *node.ServiceContext, config *eth.Config) (*LightEthereum, error) { 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 { 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 return nil, err
} }
leth.ApiBackend = &LesApiBackend{leth, nil} leth.ApiBackend = &LesApiBackend{leth, nil, nil}
gpoParams := config.GPO gpoParams := config.GPO
if gpoParams.Default == nil { if gpoParams.Default == nil {
gpoParams.Default = config.GasPrice gpoParams.Default = config.GasPrice
} }
leth.ApiBackend.gpo = gasprice.NewOracle(leth.ApiBackend, gpoParams) 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 return leth, nil
} }

45
les/status/accounts.go Normal file
View file

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

17
les/status/types.go Normal file
View file

@ -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"`
}

View file

@ -66,12 +66,20 @@ var (
chtRoot: common.HexToHash("6f56dc61936752cc1f8c84b4addabdbe6a1c19693de3f21cb818362df2117f03"), chtRoot: common.HexToHash("6f56dc61936752cc1f8c84b4addabdbe6a1c19693de3f21cb818362df2117f03"),
bloomTrieRoot: common.HexToHash("aca7d7c504d22737242effc3fdc604a762a0af9ced898036b5986c3a15220208"), 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 // trustedCheckpoints associates each known checkpoint with the genesis hash of the chain it belongs to
var trustedCheckpoints = map[common.Hash]trustedCheckpoint{ var trustedCheckpoints = map[common.Hash]trustedCheckpoint{
params.MainnetGenesisHash: mainnetCheckpoint, params.MainnetGenesisHash: mainnetCheckpoint,
params.TestnetGenesisHash: ropstenCheckpoint, params.TestnetGenesisHash: statusRopstenCheckpoint,
} }
var ( var (

View file

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

View file

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

View file

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

View file

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

View file

@ -134,6 +134,13 @@ func (self *Whisper) NewIdentity() *ecdsa.PrivateKey {
return key 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 // HasIdentity checks if the the whisper node is configured with the private key
// of the specified public pair. // of the specified public pair.
func (self *Whisper) HasIdentity(key *ecdsa.PublicKey) bool { func (self *Whisper) HasIdentity(key *ecdsa.PublicKey) bool {

View file

@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "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/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/discover"
@ -238,13 +239,17 @@ type newMessageOverride struct {
// Post a message on the Whisper network. // Post a message on the Whisper network.
func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, error) { func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, error) {
var ( var (
symKeyGiven = len(req.SymKeyID) > 0 symKeyGiven = len(req.SymKeyID) > 0
pubKeyGiven = len(req.PublicKey) > 0 pubKeyGiven = len(req.PublicKey) > 0
err error 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 // user must specify either a symmetric or an asymmetric key
if (symKeyGiven && pubKeyGiven) || (!symKeyGiven && !pubKeyGiven) { if (symKeyGiven && pubKeyGiven) || (!symKeyGiven && !pubKeyGiven) {
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, ErrSymAsym)
return false, 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 // Set key that is used to sign the message
if len(req.Sig) > 0 { if len(req.Sig) > 0 {
if params.Src, err = api.w.GetPrivateKey(req.Sig); err != nil { if params.Src, err = api.w.GetPrivateKey(req.Sig); err != nil {
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, err)
return false, 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 // Set symmetric key that is used to encrypt the message
if symKeyGiven { if symKeyGiven {
if params.Topic == (TopicType{}) { // topics are mandatory with symmetric encryption if params.Topic == (TopicType{}) { // topics are mandatory with symmetric encryption
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, ErrNoTopics)
return false, ErrNoTopics return false, ErrNoTopics
} }
if params.KeySym, err = api.w.GetSymKey(req.SymKeyID); err != nil { if params.KeySym, err = api.w.GetSymKey(req.SymKeyID); err != nil {
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, err)
return false, err return false, err
} }
if !validateSymmetricKey(params.KeySym) { if !validateSymmetricKey(params.KeySym) {
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, ErrInvalidSymmetricKey)
return false, ErrInvalidSymmetricKey return false, ErrInvalidSymmetricKey
} }
} }
@ -281,6 +290,7 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er
if pubKeyGiven { if pubKeyGiven {
params.Dst = crypto.ToECDSAPub(req.PublicKey) params.Dst = crypto.ToECDSAPub(req.PublicKey)
if !ValidatePublicKey(params.Dst) { if !ValidatePublicKey(params.Dst) {
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, ErrInvalidPublicKey)
return false, ErrInvalidPublicKey return false, ErrInvalidPublicKey
} }
} }
@ -288,11 +298,13 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er
// encrypt and sent message // encrypt and sent message
whisperMsg, err := NewSentMessage(params) whisperMsg, err := NewSentMessage(params)
if err != nil { if err != nil {
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, err)
return false, err return false, err
} }
env, err := whisperMsg.Wrap(params) env, err := whisperMsg.Wrap(params)
if err != nil { if err != nil {
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, nil, nil, err)
return false, err return false, err
} }
@ -300,19 +312,41 @@ func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (bool, er
if len(req.TargetPeer) > 0 { if len(req.TargetPeer) > 0 {
n, err := discover.ParseNode(req.TargetPeer) n, err := discover.ParseNode(req.TargetPeer)
if err != nil { 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 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 // ensure that the message PoW meets the node's minimum accepted PoW
if req.PowTarget < api.w.MinPow() { if req.PowTarget < api.w.MinPow() {
api.w.traceOutgoingDelivery(isP2PMessage, message.RejectedStatus, &req, env, nil, ErrTooLowPoW)
return false, ErrTooLowPoW return false, ErrTooLowPoW
} }
api.w.traceOutgoingDelivery(isP2PMessage, message.SentStatus, &req, env, nil, nil)
return true, api.w.Send(env) 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 //go:generate gencodec -type Criteria -field-override criteriaOverride -out gen_criteria_json.go
// Criteria holds various filter options for inbound messages. // Criteria holds various filter options for inbound messages.

View file

@ -32,6 +32,9 @@ package whisperv5
import ( import (
"fmt" "fmt"
"time" "time"
"github.com/ethereum/go-ethereum/common/message"
"github.com/ethereum/go-ethereum/p2p"
) )
const ( const (
@ -57,7 +60,7 @@ const (
MaxMessageSize = uint32(10 * 1024 * 1024) // maximum accepted size of a message. MaxMessageSize = uint32(10 * 1024 * 1024) // maximum accepted size of a message.
DefaultMaxMessageSize = uint32(1024 * 1024) 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) padSizeLimit = 256 // just an arbitrary number, could be changed without breaking the protocol (must not exceed 2^24)
messageQueueLimit = 1024 messageQueueLimit = 1024
@ -85,3 +88,35 @@ type MailServer interface {
Archive(env *Envelope) Archive(env *Envelope)
DeliverMail(whisperPeer *Peer, request *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)
}

View file

@ -18,10 +18,12 @@ package whisperv5
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"errors"
"fmt" "fmt"
"sync" "sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/message"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
@ -115,15 +117,20 @@ func (fs *Filters) NotifyWatchers(env *Envelope, p2pMessage bool) {
if match { if match {
msg = env.Open(watcher) msg = env.Open(watcher)
if msg == nil { 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) log.Trace("processing message: failed to open", "message", env.Hash().Hex(), "filter", i)
} }
} else { } 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) log.Trace("processing message: does not match", "message", env.Hash().Hex(), "filter", i)
} }
} }
if match && msg != nil { if match && msg != nil {
log.Trace("processing message: decrypted", "hash", env.Hash().Hex()) 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) { if watcher.Src == nil || IsPubKeyEqual(msg.Src, watcher.Src) {
watcher.Trigger(msg) watcher.Trigger(msg)
} }

View file

@ -27,6 +27,7 @@ import (
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/message"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
@ -77,7 +78,9 @@ type Whisper struct {
statsMu sync.Mutex // guard stats statsMu sync.Mutex // guard stats
stats Statistics // Statistics of whisper node 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. // 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 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. // Protocols returns the whisper sub-protocols ran by this particular client.
func (w *Whisper) Protocols() []p2p.Protocol { func (w *Whisper) Protocols() []p2p.Protocol {
return []p2p.Protocol{w.protocol} return []p2p.Protocol{w.protocol}
@ -250,9 +263,9 @@ func (w *Whisper) NewKeyPair() (string, error) {
return "", fmt.Errorf("failed to generate valid key") return "", fmt.Errorf("failed to generate valid key")
} }
id, err := GenerateRandomID() id, err := toDeterministicID(common.ToHex(crypto.FromECDSAPub(&key.PublicKey)), keyIdSize)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to generate ID: %s", err) return "", err
} }
w.keyMu.Lock() w.keyMu.Lock()
@ -265,45 +278,94 @@ func (w *Whisper) NewKeyPair() (string, error) {
return id, nil return id, nil
} }
// DeleteKeyPair deletes the specified key if it exists. // AddIdentity adds cryptographic identity into the known
func (w *Whisper) DeleteKeyPair(key string) bool { // 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() w.keyMu.Lock()
defer w.keyMu.Unlock() defer w.keyMu.Unlock()
if w.privateKeys[key] != nil { w.privateKeys[id] = key
delete(w.privateKeys, 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 true
} }
return false 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 // HasKeyPair checks if the the whisper node is configured with the private key
// of the specified public pair. // of the specified public pair.
func (w *Whisper) HasKeyPair(id string) bool { func (w *Whisper) HasKeyPair(id string) bool {
deterministicID, err := toDeterministicID(id, keyIdSize)
if err != nil {
return false
}
w.keyMu.RLock() w.keyMu.RLock()
defer w.keyMu.RUnlock() defer w.keyMu.RUnlock()
return w.privateKeys[id] != nil return w.privateKeys[deterministicID] != nil
} }
// GetPrivateKey retrieves the private key of the specified identity. // GetPrivateKey retrieves the private key of the specified identity.
func (w *Whisper) GetPrivateKey(id string) (*ecdsa.PrivateKey, error) { func (w *Whisper) GetPrivateKey(id string) (*ecdsa.PrivateKey, error) {
deterministicID, err := toDeterministicID(id, keyIdSize)
if err != nil {
return nil, err
}
w.keyMu.RLock() w.keyMu.RLock()
defer w.keyMu.RUnlock() defer w.keyMu.RUnlock()
key := w.privateKeys[id] key := w.privateKeys[deterministicID]
if key == nil { if key == nil {
return nil, fmt.Errorf("invalid id") return nil, fmt.Errorf("invalid id")
} }
@ -336,6 +398,23 @@ func (w *Whisper) GenerateSymKey() (string, error) {
return id, nil 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. // AddSymKeyDirect stores the key, and returns its id.
func (w *Whisper) AddSymKeyDirect(key []byte) (string, error) { func (w *Whisper) AddSymKeyDirect(key []byte) (string, error) {
if len(key) != aesKeyLength { 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 // Start implements node.Service, starting the background data propagation thread
// of the Whisper protocol. // 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) log.Info("started whisper v." + ProtocolVersionStr)
go w.update() go w.update()
@ -456,6 +535,12 @@ func (w *Whisper) Start(*p2p.Server) error {
go w.processQueue() go w.processQueue()
} }
if w.notificationServer != nil {
if err := w.notificationServer.Start(stack); err != nil {
return err
}
}
return nil return nil
} }
@ -463,6 +548,13 @@ func (w *Whisper) Start(*p2p.Server) error {
// of the Whisper protocol. // of the Whisper protocol.
func (w *Whisper) Stop() error { func (w *Whisper) Stop() error {
close(w.quit) close(w.quit)
if w.notificationServer != nil {
if err := w.notificationServer.Stop(); err != nil {
return err
}
}
log.Info("whisper stopped") log.Info("whisper stopped")
return nil return nil
} }
@ -535,8 +627,11 @@ func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error {
var envelope Envelope var envelope Envelope
if err := packet.Decode(&envelope); err != nil { if err := packet.Decode(&envelope); err != nil {
log.Warn("failed to decode direct message, peer will be disconnected", "peer", p.peer.ID(), "err", err) 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") return errors.New("invalid direct message")
} }
wh.traceIncomingDelivery(true, message.SentStatus, nil, &envelope, nil, nil)
wh.postEvent(&envelope, true) wh.postEvent(&envelope, true)
} }
case p2pRequestCode: case p2pRequestCode:
@ -545,6 +640,7 @@ func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error {
var request Envelope var request Envelope
if err := packet.Decode(&request); err != nil { 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) 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") return errors.New("invalid p2p request")
} }
wh.mailServer.DeliverMail(p, &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) { if !wh.expirations[envelope.Expiry].Has(hash) {
wh.expirations[envelope.Expiry].Add(hash) wh.expirations[envelope.Expiry].Add(hash)
} }
wh.traceIncomingDelivery(false, message.CachedStatus, nil, envelope, nil, nil)
} }
wh.poolMu.Unlock() wh.poolMu.Unlock()
if alreadyCached { if alreadyCached {
log.Trace("whisper envelope already cached", "hash", envelope.Hash().Hex()) log.Trace("whisper envelope already cached", "hash", envelope.Hash().Hex())
wh.traceIncomingDelivery(false, message.ResentStatus, nil, envelope, nil, nil)
} else { } else {
log.Trace("cached whisper envelope", "hash", envelope.Hash().Hex()) log.Trace("cached whisper envelope", "hash", envelope.Hash().Hex())
wh.statsMu.Lock() wh.statsMu.Lock()
wh.stats.memoryUsed += envelope.size() wh.stats.memoryUsed += envelope.size()
wh.statsMu.Unlock() wh.statsMu.Unlock()
wh.traceIncomingDelivery(false, message.QueuedStatus, nil, envelope, nil, nil)
wh.postEvent(envelope, false) // notify the local node about the new message wh.postEvent(envelope, false) // notify the local node about the new message
if wh.mailServer != nil { if wh.mailServer != nil {
wh.mailServer.Archive(envelope) wh.mailServer.Archive(envelope)
@ -633,6 +735,47 @@ func (wh *Whisper) add(envelope *Envelope) (bool, error) {
return true, nil 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. // postEvent queues the message for further processing.
func (w *Whisper) postEvent(envelope *Envelope, isP2P bool) { func (w *Whisper) postEvent(envelope *Envelope, isP2P bool) {
// if the version of incoming message is higher than // if the version of incoming message is higher than
@ -645,6 +788,13 @@ func (w *Whisper) postEvent(envelope *Envelope, isP2P bool) {
w.checkOverflow() w.checkOverflow()
w.messageQueue <- envelope 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 return
case e = <-w.messageQueue: case e = <-w.messageQueue:
w.traceIncomingDelivery(false, message.ProcessingStatus, nil, e, nil, nil)
w.filters.NotifyWatchers(e, false) w.filters.NotifyWatchers(e, false)
case e = <-w.p2pMsgQueue: case e = <-w.p2pMsgQueue:
w.traceIncomingDelivery(true, message.ProcessingStatus, nil, e, nil, nil)
w.filters.NotifyWatchers(e, true) w.filters.NotifyWatchers(e, true)
} }
} }
@ -856,3 +1008,30 @@ func GenerateRandomID() (id string, err error) {
id = common.Bytes2Hex(buf) id = common.Bytes2Hex(buf)
return id, err 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
}