From 22965ba57e3ad4f587821412e0d68125118f115e Mon Sep 17 00:00:00 2001 From: siddharth0a Date: Tue, 2 Apr 2024 20:19:49 +0900 Subject: [PATCH] whiseper: maintain legacy --- whisper/mailserver/mailserver.go | 205 +++++ whisper/mailserver/server_test.go | 212 +++++ whisper/shhclient/client.go | 192 ++++ whisper/whisperv6/api.go | 593 ++++++++++++ whisper/whisperv6/api_test.go | 63 ++ whisper/whisperv6/benchmarks_test.go | 208 +++++ whisper/whisperv6/config.go | 31 + whisper/whisperv6/doc.go | 92 ++ whisper/whisperv6/envelope.go | 280 ++++++ whisper/whisperv6/envelope_test.go | 91 ++ whisper/whisperv6/filter.go | 262 ++++++ whisper/whisperv6/filter_test.go | 831 +++++++++++++++++ whisper/whisperv6/gen_criteria_json.go | 66 ++ whisper/whisperv6/gen_message_json.go | 84 ++ whisper/whisperv6/gen_newmessage_json.go | 90 ++ whisper/whisperv6/message.go | 355 +++++++ whisper/whisperv6/message_test.go | 471 ++++++++++ whisper/whisperv6/peer.go | 259 ++++++ whisper/whisperv6/peer_test.go | 568 ++++++++++++ whisper/whisperv6/topic.go | 57 ++ whisper/whisperv6/topic_test.go | 134 +++ whisper/whisperv6/whisper.go | 1076 ++++++++++++++++++++++ whisper/whisperv6/whisper_test.go | 892 ++++++++++++++++++ 23 files changed, 7112 insertions(+) create mode 100644 whisper/mailserver/mailserver.go create mode 100644 whisper/mailserver/server_test.go create mode 100644 whisper/shhclient/client.go create mode 100644 whisper/whisperv6/api.go create mode 100644 whisper/whisperv6/api_test.go create mode 100644 whisper/whisperv6/benchmarks_test.go create mode 100644 whisper/whisperv6/config.go create mode 100644 whisper/whisperv6/doc.go create mode 100644 whisper/whisperv6/envelope.go create mode 100644 whisper/whisperv6/envelope_test.go create mode 100644 whisper/whisperv6/filter.go create mode 100644 whisper/whisperv6/filter_test.go create mode 100644 whisper/whisperv6/gen_criteria_json.go create mode 100644 whisper/whisperv6/gen_message_json.go create mode 100644 whisper/whisperv6/gen_newmessage_json.go create mode 100644 whisper/whisperv6/message.go create mode 100644 whisper/whisperv6/message_test.go create mode 100644 whisper/whisperv6/peer.go create mode 100644 whisper/whisperv6/peer_test.go create mode 100644 whisper/whisperv6/topic.go create mode 100644 whisper/whisperv6/topic_test.go create mode 100644 whisper/whisperv6/whisper.go create mode 100644 whisper/whisperv6/whisper_test.go diff --git a/whisper/mailserver/mailserver.go b/whisper/mailserver/mailserver.go new file mode 100644 index 0000000000..a7d1058744 --- /dev/null +++ b/whisper/mailserver/mailserver.go @@ -0,0 +1,205 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package mailserver provides a naive, example mailserver implementation +package mailserver + +import ( + "encoding/binary" + "fmt" + + "github.com/cryptoecc/ETH-ECC/common" + "github.com/cryptoecc/ETH-ECC/crypto" + "github.com/cryptoecc/ETH-ECC/log" + "github.com/cryptoecc/ETH-ECC/rlp" + whisper "github.com/cryptoecc/ETH-ECC/whisper/whisperv6" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/opt" + "github.com/syndtr/goleveldb/leveldb/util" +) + +// WMailServer represents the state data of the mailserver. +type WMailServer struct { + db *leveldb.DB + w *whisper.Whisper + pow float64 + key []byte +} + +type DBKey struct { + timestamp uint32 + hash common.Hash + raw []byte +} + +// NewDbKey is a helper function that creates a levelDB +// key from a hash and an integer. +func NewDbKey(t uint32, h common.Hash) *DBKey { + const sz = common.HashLength + 4 + var k DBKey + k.timestamp = t + k.hash = h + k.raw = make([]byte, sz) + binary.BigEndian.PutUint32(k.raw, k.timestamp) + copy(k.raw[4:], k.hash[:]) + return &k +} + +// Init initializes the mail server. +func (s *WMailServer) Init(shh *whisper.Whisper, path string, password string, pow float64) error { + var err error + if len(path) == 0 { + return fmt.Errorf("DB file is not specified") + } + + if len(password) == 0 { + return fmt.Errorf("password is not specified") + } + + s.db, err = leveldb.OpenFile(path, &opt.Options{OpenFilesCacheCapacity: 32}) + if err != nil { + return fmt.Errorf("open DB file: %s", err) + } + + s.w = shh + s.pow = pow + + MailServerKeyID, err := s.w.AddSymKeyFromPassword(password) + if err != nil { + return fmt.Errorf("create symmetric key: %s", err) + } + s.key, err = s.w.GetSymKey(MailServerKeyID) + if err != nil { + return fmt.Errorf("save symmetric key: %s", err) + } + return nil +} + +// Close cleans up before shutdown. +func (s *WMailServer) Close() { + if s.db != nil { + s.db.Close() + } +} + +// Archive stores the +func (s *WMailServer) Archive(env *whisper.Envelope) { + key := NewDbKey(env.Expiry-env.TTL, env.Hash()) + rawEnvelope, err := rlp.EncodeToBytes(env) + if err != nil { + log.Error(fmt.Sprintf("rlp.EncodeToBytes failed: %s", err)) + } else { + err = s.db.Put(key.raw, rawEnvelope, nil) + if err != nil { + log.Error(fmt.Sprintf("Writing to DB failed: %s", err)) + } + } +} + +// DeliverMail responds with saved messages upon request by the +// messages' owner. +func (s *WMailServer) DeliverMail(peer *whisper.Peer, request *whisper.Envelope) { + if peer == nil { + log.Error("Whisper peer is nil") + return + } + + ok, lower, upper, bloom := s.validateRequest(peer.ID(), request) + if ok { + s.processRequest(peer, lower, upper, bloom) + } +} + +func (s *WMailServer) processRequest(peer *whisper.Peer, lower, upper uint32, bloom []byte) []*whisper.Envelope { + ret := make([]*whisper.Envelope, 0) + var err error + var zero common.Hash + kl := NewDbKey(lower, zero) + ku := NewDbKey(upper+1, zero) // LevelDB is exclusive, while the Whisper API is inclusive + i := s.db.NewIterator(&util.Range{Start: kl.raw, Limit: ku.raw}, nil) + defer i.Release() + + for i.Next() { + var envelope whisper.Envelope + err = rlp.DecodeBytes(i.Value(), &envelope) + if err != nil { + log.Error(fmt.Sprintf("RLP decoding failed: %s", err)) + } + + if whisper.BloomFilterMatch(bloom, envelope.Bloom()) { + if peer == nil { + // used for test purposes + ret = append(ret, &envelope) + } else { + err = s.w.SendP2PDirect(peer, &envelope) + if err != nil { + log.Error(fmt.Sprintf("Failed to send direct message to peer: %s", err)) + return nil + } + } + } + } + + err = i.Error() + if err != nil { + log.Error(fmt.Sprintf("Level DB iterator error: %s", err)) + } + + return ret +} + +func (s *WMailServer) validateRequest(peerID []byte, request *whisper.Envelope) (bool, uint32, uint32, []byte) { + if s.pow > 0.0 && request.PoW() < s.pow { + return false, 0, 0, nil + } + + f := whisper.Filter{KeySym: s.key} + decrypted := request.Open(&f) + if decrypted == nil { + log.Warn(fmt.Sprintf("Failed to decrypt p2p request")) + return false, 0, 0, nil + } + + src := crypto.FromECDSAPub(decrypted.Src) + if len(src)-len(peerID) == 1 { + src = src[1:] + } + + // if you want to check the signature, you can do it here. e.g.: + // if !bytes.Equal(peerID, src) { + if src == nil { + log.Warn(fmt.Sprintf("Wrong signature of p2p request")) + return false, 0, 0, nil + } + + var bloom []byte + payloadSize := len(decrypted.Payload) + if payloadSize < 8 { + log.Warn(fmt.Sprintf("Undersized p2p request")) + return false, 0, 0, nil + } else if payloadSize == 8 { + bloom = whisper.MakeFullNodeBloom() + } else if payloadSize < 8+whisper.BloomFilterSize { + log.Warn(fmt.Sprintf("Undersized bloom filter in p2p request")) + return false, 0, 0, nil + } else { + bloom = decrypted.Payload[8 : 8+whisper.BloomFilterSize] + } + + lower := binary.BigEndian.Uint32(decrypted.Payload[:4]) + upper := binary.BigEndian.Uint32(decrypted.Payload[4:8]) + return true, lower, upper, bloom +} diff --git a/whisper/mailserver/server_test.go b/whisper/mailserver/server_test.go new file mode 100644 index 0000000000..c8ffe6137c --- /dev/null +++ b/whisper/mailserver/server_test.go @@ -0,0 +1,212 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package mailserver + +import ( + "bytes" + "crypto/ecdsa" + "encoding/binary" + "io/ioutil" + "math/rand" + "testing" + "time" + + "github.com/cryptoecc/ETH-ECC/common" + "github.com/cryptoecc/ETH-ECC/crypto" + whisper "github.com/cryptoecc/ETH-ECC/whisper/whisperv6" +) + +const powRequirement = 0.00001 + +var keyID string +var shh *whisper.Whisper +var seed = time.Now().Unix() + +type ServerTestParams struct { + topic whisper.TopicType + low uint32 + upp uint32 + key *ecdsa.PrivateKey +} + +func assert(statement bool, text string, t *testing.T) { + if !statement { + t.Fatal(text) + } +} + +func TestDBKey(t *testing.T) { + var h common.Hash + i := uint32(time.Now().Unix()) + k := NewDbKey(i, h) + assert(len(k.raw) == common.HashLength+4, "wrong DB key length", t) + assert(byte(i%0x100) == k.raw[3], "raw representation should be big endian", t) + assert(byte(i/0x1000000) == k.raw[0], "big endian expected", t) +} + +func generateEnvelope(t *testing.T) *whisper.Envelope { + h := crypto.Keccak256Hash([]byte("test sample data")) + params := &whisper.MessageParams{ + KeySym: h[:], + Topic: whisper.TopicType{0x1F, 0x7E, 0xA1, 0x7F}, + Payload: []byte("test payload"), + PoW: powRequirement, + WorkTime: 2, + } + + msg, err := whisper.NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed to wrap with seed %d: %s.", seed, err) + } + return env +} + +func TestMailServer(t *testing.T) { + const password = "password_for_this_test" + const dbPath = "whisper-server-test" + + dir, err := ioutil.TempDir("", dbPath) + if err != nil { + t.Fatal(err) + } + + var server WMailServer + shh = whisper.New(&whisper.DefaultConfig) + shh.RegisterServer(&server) + + err = server.Init(shh, dir, password, powRequirement) + if err != nil { + t.Fatal(err) + } + defer server.Close() + + keyID, err = shh.AddSymKeyFromPassword(password) + if err != nil { + t.Fatalf("Failed to create symmetric key for mail request: %s", err) + } + + rand.Seed(seed) + env := generateEnvelope(t) + server.Archive(env) + deliverTest(t, &server, env) +} + +func deliverTest(t *testing.T, server *WMailServer, env *whisper.Envelope) { + id, err := shh.NewKeyPair() + if err != nil { + t.Fatalf("failed to generate new key pair with seed %d: %s.", seed, err) + } + testPeerID, err := shh.GetPrivateKey(id) + if err != nil { + t.Fatalf("failed to retrieve new key pair with seed %d: %s.", seed, err) + } + birth := env.Expiry - env.TTL + p := &ServerTestParams{ + topic: env.Topic, + low: birth - 1, + upp: birth + 1, + key: testPeerID, + } + + singleRequest(t, server, env, p, true) + + p.low, p.upp = birth+1, 0xffffffff + singleRequest(t, server, env, p, false) + + p.low, p.upp = 0, birth-1 + singleRequest(t, server, env, p, false) + + p.low = birth - 1 + p.upp = birth + 1 + p.topic[0] = 0xFF + singleRequest(t, server, env, p, false) +} + +func singleRequest(t *testing.T, server *WMailServer, env *whisper.Envelope, p *ServerTestParams, expect bool) { + request := createRequest(t, p) + src := crypto.FromECDSAPub(&p.key.PublicKey) + ok, lower, upper, bloom := server.validateRequest(src, request) + if !ok { + t.Fatalf("request validation failed, seed: %d.", seed) + } + if lower != p.low { + t.Fatalf("request validation failed (lower bound), seed: %d.", seed) + } + if upper != p.upp { + t.Fatalf("request validation failed (upper bound), seed: %d.", seed) + } + expectedBloom := whisper.TopicToBloom(p.topic) + if !bytes.Equal(bloom, expectedBloom) { + t.Fatalf("request validation failed (topic), seed: %d.", seed) + } + + var exist bool + mail := server.processRequest(nil, p.low, p.upp, bloom) + for _, msg := range mail { + if msg.Hash() == env.Hash() { + exist = true + break + } + } + + if exist != expect { + t.Fatalf("error: exist = %v, seed: %d.", exist, seed) + } + + src[0]++ + ok, lower, upper, _ = server.validateRequest(src, request) + if !ok { + // request should be valid regardless of signature + t.Fatalf("request validation false negative, seed: %d (lower: %d, upper: %d).", seed, lower, upper) + } +} + +func createRequest(t *testing.T, p *ServerTestParams) *whisper.Envelope { + bloom := whisper.TopicToBloom(p.topic) + data := make([]byte, 8) + binary.BigEndian.PutUint32(data, p.low) + binary.BigEndian.PutUint32(data[4:], p.upp) + data = append(data, bloom...) + + key, err := shh.GetSymKey(keyID) + if err != nil { + t.Fatalf("failed to retrieve sym key with seed %d: %s.", seed, err) + } + + params := &whisper.MessageParams{ + KeySym: key, + Topic: p.topic, + Payload: data, + PoW: powRequirement * 2, + WorkTime: 2, + Src: p.key, + } + + msg, err := whisper.NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed to wrap with seed %d: %s.", seed, err) + } + return env +} diff --git a/whisper/shhclient/client.go b/whisper/shhclient/client.go new file mode 100644 index 0000000000..824ce0e610 --- /dev/null +++ b/whisper/shhclient/client.go @@ -0,0 +1,192 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package shhclient + +import ( + "context" + + "github.com/cryptoecc/ETH-ECC/common/hexutil" + "github.com/cryptoecc/ETH-ECC/rpc" + whisper "github.com/cryptoecc/ETH-ECC/whisper/whisperv6" +) + +// Client defines typed wrappers for the Whisper v6 RPC API. +type Client struct { + c *rpc.Client +} + +// Dial connects a client to the given URL. +func Dial(rawurl string) (*Client, error) { + c, err := rpc.Dial(rawurl) + if err != nil { + return nil, err + } + return NewClient(c), nil +} + +// NewClient creates a client that uses the given RPC client. +func NewClient(c *rpc.Client) *Client { + return &Client{c} +} + +// Version returns the Whisper sub-protocol version. +func (sc *Client) Version(ctx context.Context) (string, error) { + var result string + err := sc.c.CallContext(ctx, &result, "shh_version") + return result, err +} + +// Info returns diagnostic information about the whisper node. +func (sc *Client) Info(ctx context.Context) (whisper.Info, error) { + var info whisper.Info + err := sc.c.CallContext(ctx, &info, "shh_info") + return info, err +} + +// SetMaxMessageSize sets the maximal message size allowed by this node. Incoming +// and outgoing messages with a larger size will be rejected. Whisper message size +// can never exceed the limit imposed by the underlying P2P protocol (10 Mb). +func (sc *Client) SetMaxMessageSize(ctx context.Context, size uint32) error { + var ignored bool + return sc.c.CallContext(ctx, &ignored, "shh_setMaxMessageSize", size) +} + +// SetMinimumPoW (experimental) sets the minimal PoW required by this node. +// This experimental function was introduced for the future dynamic adjustment of +// PoW requirement. If the node is overwhelmed with messages, it should raise the +// PoW requirement and notify the peers. The new value should be set relative to +// the old value (e.g. double). The old value could be obtained via shh_info call. +func (sc *Client) SetMinimumPoW(ctx context.Context, pow float64) error { + var ignored bool + return sc.c.CallContext(ctx, &ignored, "shh_setMinPoW", pow) +} + +// MarkTrustedPeer marks specific peer trusted, which will allow it to send historic (expired) messages. +// Note This function is not adding new nodes, the node needs to exists as a peer. +func (sc *Client) MarkTrustedPeer(ctx context.Context, enode string) error { + var ignored bool + return sc.c.CallContext(ctx, &ignored, "shh_markTrustedPeer", enode) +} + +// NewKeyPair generates a new public and private key pair for message decryption and encryption. +// It returns an identifier that can be used to refer to the key. +func (sc *Client) NewKeyPair(ctx context.Context) (string, error) { + var id string + return id, sc.c.CallContext(ctx, &id, "shh_newKeyPair") +} + +// AddPrivateKey stored the key pair, and returns its ID. +func (sc *Client) AddPrivateKey(ctx context.Context, key []byte) (string, error) { + var id string + return id, sc.c.CallContext(ctx, &id, "shh_addPrivateKey", hexutil.Bytes(key)) +} + +// DeleteKeyPair delete the specifies key. +func (sc *Client) DeleteKeyPair(ctx context.Context, id string) (string, error) { + var ignored bool + return id, sc.c.CallContext(ctx, &ignored, "shh_deleteKeyPair", id) +} + +// HasKeyPair returns an indication if the node has a private key or +// key pair matching the given ID. +func (sc *Client) HasKeyPair(ctx context.Context, id string) (bool, error) { + var has bool + return has, sc.c.CallContext(ctx, &has, "shh_hasKeyPair", id) +} + +// PublicKey return the public key for a key ID. +func (sc *Client) PublicKey(ctx context.Context, id string) ([]byte, error) { + var key hexutil.Bytes + return []byte(key), sc.c.CallContext(ctx, &key, "shh_getPublicKey", id) +} + +// PrivateKey return the private key for a key ID. +func (sc *Client) PrivateKey(ctx context.Context, id string) ([]byte, error) { + var key hexutil.Bytes + return []byte(key), sc.c.CallContext(ctx, &key, "shh_getPrivateKey", id) +} + +// NewSymmetricKey generates a random symmetric key and returns its identifier. +// Can be used encrypting and decrypting messages where the key is known to both parties. +func (sc *Client) NewSymmetricKey(ctx context.Context) (string, error) { + var id string + return id, sc.c.CallContext(ctx, &id, "shh_newSymKey") +} + +// AddSymmetricKey stores the key, and returns its identifier. +func (sc *Client) AddSymmetricKey(ctx context.Context, key []byte) (string, error) { + var id string + return id, sc.c.CallContext(ctx, &id, "shh_addSymKey", hexutil.Bytes(key)) +} + +// GenerateSymmetricKeyFromPassword generates the key from password, stores it, and returns its identifier. +func (sc *Client) GenerateSymmetricKeyFromPassword(ctx context.Context, passwd string) (string, error) { + var id string + return id, sc.c.CallContext(ctx, &id, "shh_generateSymKeyFromPassword", passwd) +} + +// HasSymmetricKey returns an indication if the key associated with the given id is stored in the node. +func (sc *Client) HasSymmetricKey(ctx context.Context, id string) (bool, error) { + var found bool + return found, sc.c.CallContext(ctx, &found, "shh_hasSymKey", id) +} + +// GetSymmetricKey returns the symmetric key associated with the given identifier. +func (sc *Client) GetSymmetricKey(ctx context.Context, id string) ([]byte, error) { + var key hexutil.Bytes + return []byte(key), sc.c.CallContext(ctx, &key, "shh_getSymKey", id) +} + +// DeleteSymmetricKey deletes the symmetric key associated with the given identifier. +func (sc *Client) DeleteSymmetricKey(ctx context.Context, id string) error { + var ignored bool + return sc.c.CallContext(ctx, &ignored, "shh_deleteSymKey", id) +} + +// Post a message onto the network. +func (sc *Client) Post(ctx context.Context, message whisper.NewMessage) (string, error) { + var hash string + return hash, sc.c.CallContext(ctx, &hash, "shh_post", message) +} + +// SubscribeMessages subscribes to messages that match the given criteria. This method +// is only supported on bi-directional connections such as websockets and IPC. +// NewMessageFilter uses polling and is supported over HTTP. +func (sc *Client) SubscribeMessages(ctx context.Context, criteria whisper.Criteria, ch chan<- *whisper.Message) (ethereum.Subscription, error) { + return sc.c.ShhSubscribe(ctx, ch, "messages", criteria) +} + +// NewMessageFilter creates a filter within the node. This filter can be used to poll +// for new messages (see FilterMessages) that satisfy the given criteria. A filter can +// timeout when it was polled for in whisper.filterTimeout. +func (sc *Client) NewMessageFilter(ctx context.Context, criteria whisper.Criteria) (string, error) { + var id string + return id, sc.c.CallContext(ctx, &id, "shh_newMessageFilter", criteria) +} + +// DeleteMessageFilter removes the filter associated with the given id. +func (sc *Client) DeleteMessageFilter(ctx context.Context, id string) error { + var ignored bool + return sc.c.CallContext(ctx, &ignored, "shh_deleteMessageFilter", id) +} + +// FilterMessages retrieves all messages that are received between the last call to +// this function and match the criteria that where given when the filter was created. +func (sc *Client) FilterMessages(ctx context.Context, id string) ([]*whisper.Message, error) { + var messages []*whisper.Message + return messages, sc.c.CallContext(ctx, &messages, "shh_getFilterMessages", id) +} diff --git a/whisper/whisperv6/api.go b/whisper/whisperv6/api.go new file mode 100644 index 0000000000..97ace655f3 --- /dev/null +++ b/whisper/whisperv6/api.go @@ -0,0 +1,593 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +import ( + "context" + "crypto/ecdsa" + "errors" + "fmt" + "sync" + "time" + + "github.com/cryptoecc/ETH-ECC/common" + "github.com/cryptoecc/ETH-ECC/common/hexutil" + "github.com/cryptoecc/ETH-ECC/crypto" + "github.com/cryptoecc/ETH-ECC/log" + "github.com/cryptoecc/ETH-ECC/p2p/enode" + "github.com/cryptoecc/ETH-ECC/rpc" +) + +// List of errors +var ( + ErrSymAsym = errors.New("specify either a symmetric or an asymmetric key") + ErrInvalidSymmetricKey = errors.New("invalid symmetric key") + ErrInvalidPublicKey = errors.New("invalid public key") + ErrInvalidSigningPubKey = errors.New("invalid signing public key") + ErrTooLowPoW = errors.New("message rejected, PoW too low") + ErrNoTopics = errors.New("missing topic(s)") +) + +// PublicWhisperAPI provides the whisper RPC service that can be +// use publicly without security implications. +type PublicWhisperAPI struct { + w *Whisper + + mu sync.Mutex + lastUsed map[string]time.Time // keeps track when a filter was polled for the last time. +} + +// NewPublicWhisperAPI create a new RPC whisper service. +func NewPublicWhisperAPI(w *Whisper) *PublicWhisperAPI { + api := &PublicWhisperAPI{ + w: w, + lastUsed: make(map[string]time.Time), + } + return api +} + +// Version returns the Whisper sub-protocol version. +func (api *PublicWhisperAPI) Version(ctx context.Context) string { + return ProtocolVersionStr +} + +// Info contains diagnostic information. +type Info struct { + Memory int `json:"memory"` // Memory size of the floating messages in bytes. + Messages int `json:"messages"` // Number of floating messages. + MinPow float64 `json:"minPow"` // Minimal accepted PoW + MaxMessageSize uint32 `json:"maxMessageSize"` // Maximum accepted message size +} + +// Info returns diagnostic information about the whisper node. +func (api *PublicWhisperAPI) Info(ctx context.Context) Info { + stats := api.w.Stats() + return Info{ + Memory: stats.memoryUsed, + Messages: len(api.w.messageQueue) + len(api.w.p2pMsgQueue), + MinPow: api.w.MinPow(), + MaxMessageSize: api.w.MaxMessageSize(), + } +} + +// SetMaxMessageSize sets the maximum message size that is accepted. +// Upper limit is defined by MaxMessageSize. +func (api *PublicWhisperAPI) SetMaxMessageSize(ctx context.Context, size uint32) (bool, error) { + return true, api.w.SetMaxMessageSize(size) +} + +// SetMinPoW sets the minimum PoW, and notifies the peers. +func (api *PublicWhisperAPI) SetMinPoW(ctx context.Context, pow float64) (bool, error) { + return true, api.w.SetMinimumPoW(pow) +} + +// SetBloomFilter sets the new value of bloom filter, and notifies the peers. +func (api *PublicWhisperAPI) SetBloomFilter(ctx context.Context, bloom hexutil.Bytes) (bool, error) { + return true, api.w.SetBloomFilter(bloom) +} + +// MarkTrustedPeer marks a peer trusted, which will allow it to send historic (expired) messages. +// Note: This function is not adding new nodes, the node needs to exists as a peer. +func (api *PublicWhisperAPI) MarkTrustedPeer(ctx context.Context, url string) (bool, error) { + n, err := enode.Parse(enode.ValidSchemes, url) + if err != nil { + return false, err + } + return true, api.w.AllowP2PMessagesFromPeer(n.ID().Bytes()) +} + +// NewKeyPair generates a new public and private key pair for message decryption and encryption. +// It returns an ID that can be used to refer to the keypair. +func (api *PublicWhisperAPI) NewKeyPair(ctx context.Context) (string, error) { + return api.w.NewKeyPair() +} + +// AddPrivateKey imports the given private key. +func (api *PublicWhisperAPI) AddPrivateKey(ctx context.Context, privateKey hexutil.Bytes) (string, error) { + key, err := crypto.ToECDSA(privateKey) + if err != nil { + return "", err + } + return api.w.AddKeyPair(key) +} + +// DeleteKeyPair removes the key with the given key if it exists. +func (api *PublicWhisperAPI) DeleteKeyPair(ctx context.Context, key string) (bool, error) { + if ok := api.w.DeleteKeyPair(key); ok { + return true, nil + } + return false, fmt.Errorf("key pair %s not found", key) +} + +// HasKeyPair returns an indication if the node has a key pair that is associated with the given id. +func (api *PublicWhisperAPI) HasKeyPair(ctx context.Context, id string) bool { + return api.w.HasKeyPair(id) +} + +// GetPublicKey returns the public key associated with the given key. The key is the hex +// encoded representation of a key in the form specified in section 4.3.6 of ANSI X9.62. +func (api *PublicWhisperAPI) GetPublicKey(ctx context.Context, id string) (hexutil.Bytes, error) { + key, err := api.w.GetPrivateKey(id) + if err != nil { + return hexutil.Bytes{}, err + } + return crypto.FromECDSAPub(&key.PublicKey), nil +} + +// GetPrivateKey returns the private key associated with the given key. The key is the hex +// encoded representation of a key in the form specified in section 4.3.6 of ANSI X9.62. +func (api *PublicWhisperAPI) GetPrivateKey(ctx context.Context, id string) (hexutil.Bytes, error) { + key, err := api.w.GetPrivateKey(id) + if err != nil { + return hexutil.Bytes{}, err + } + return crypto.FromECDSA(key), nil +} + +// NewSymKey generate a random symmetric key. +// It returns an ID that can be used to refer to the key. +// Can be used encrypting and decrypting messages where the key is known to both parties. +func (api *PublicWhisperAPI) NewSymKey(ctx context.Context) (string, error) { + return api.w.GenerateSymKey() +} + +// AddSymKey import a symmetric key. +// It returns an ID that can be used to refer to the key. +// Can be used encrypting and decrypting messages where the key is known to both parties. +func (api *PublicWhisperAPI) AddSymKey(ctx context.Context, key hexutil.Bytes) (string, error) { + return api.w.AddSymKeyDirect([]byte(key)) +} + +// GenerateSymKeyFromPassword derive a key from the given password, stores it, and returns its ID. +func (api *PublicWhisperAPI) GenerateSymKeyFromPassword(ctx context.Context, passwd string) (string, error) { + return api.w.AddSymKeyFromPassword(passwd) +} + +// HasSymKey returns an indication if the node has a symmetric key associated with the given key. +func (api *PublicWhisperAPI) HasSymKey(ctx context.Context, id string) bool { + return api.w.HasSymKey(id) +} + +// GetSymKey returns the symmetric key associated with the given id. +func (api *PublicWhisperAPI) GetSymKey(ctx context.Context, id string) (hexutil.Bytes, error) { + return api.w.GetSymKey(id) +} + +// DeleteSymKey deletes the symmetric key that is associated with the given id. +func (api *PublicWhisperAPI) DeleteSymKey(ctx context.Context, id string) bool { + return api.w.DeleteSymKey(id) +} + +// MakeLightClient turns the node into light client, which does not forward +// any incoming messages, and sends only messages originated in this node. +func (api *PublicWhisperAPI) MakeLightClient(ctx context.Context) bool { + api.w.SetLightClientMode(true) + return api.w.LightClientMode() +} + +// CancelLightClient cancels light client mode. +func (api *PublicWhisperAPI) CancelLightClient(ctx context.Context) bool { + api.w.SetLightClientMode(false) + return !api.w.LightClientMode() +} + +//go:generate gencodec -type NewMessage -field-override newMessageOverride -out gen_newmessage_json.go + +// NewMessage represents a new whisper message that is posted through the RPC. +type NewMessage struct { + SymKeyID string `json:"symKeyID"` + PublicKey []byte `json:"pubKey"` + Sig string `json:"sig"` + TTL uint32 `json:"ttl"` + Topic TopicType `json:"topic"` + Payload []byte `json:"payload"` + Padding []byte `json:"padding"` + PowTime uint32 `json:"powTime"` + PowTarget float64 `json:"powTarget"` + TargetPeer string `json:"targetPeer"` +} + +type newMessageOverride struct { + PublicKey hexutil.Bytes + Payload hexutil.Bytes + Padding hexutil.Bytes +} + +// Post posts a message on the Whisper network. +// returns the hash of the message in case of success. +func (api *PublicWhisperAPI) Post(ctx context.Context, req NewMessage) (hexutil.Bytes, error) { + var ( + symKeyGiven = len(req.SymKeyID) > 0 + pubKeyGiven = len(req.PublicKey) > 0 + err error + ) + + // user must specify either a symmetric or an asymmetric key + if (symKeyGiven && pubKeyGiven) || (!symKeyGiven && !pubKeyGiven) { + return nil, ErrSymAsym + } + + params := &MessageParams{ + TTL: req.TTL, + Payload: req.Payload, + Padding: req.Padding, + WorkTime: req.PowTime, + PoW: req.PowTarget, + Topic: req.Topic, + } + + // 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 { + return nil, err + } + } + + // Set symmetric key that is used to encrypt the message + if symKeyGiven { + if params.Topic == (TopicType{}) { // topics are mandatory with symmetric encryption + return nil, ErrNoTopics + } + if params.KeySym, err = api.w.GetSymKey(req.SymKeyID); err != nil { + return nil, err + } + if !validateDataIntegrity(params.KeySym, aesKeyLength) { + return nil, ErrInvalidSymmetricKey + } + } + + // Set asymmetric key that is used to encrypt the message + if pubKeyGiven { + if params.Dst, err = crypto.UnmarshalPubkey(req.PublicKey); err != nil { + return nil, ErrInvalidPublicKey + } + } + + // encrypt and sent message + whisperMsg, err := NewSentMessage(params) + if err != nil { + return nil, err + } + + var result []byte + env, err := whisperMsg.Wrap(params) + if err != nil { + return nil, err + } + + // send to specific node (skip PoW check) + if len(req.TargetPeer) > 0 { + n, err := enode.Parse(enode.ValidSchemes, req.TargetPeer) + if err != nil { + return nil, fmt.Errorf("failed to parse target peer: %s", err) + } + err = api.w.SendP2PMessage(n.ID().Bytes(), env) + if err == nil { + hash := env.Hash() + result = hash[:] + } + return result, err + } + + // ensure that the message PoW meets the node's minimum accepted PoW + if req.PowTarget < api.w.MinPow() { + return nil, ErrTooLowPoW + } + + err = api.w.Send(env) + if err == nil { + hash := env.Hash() + result = hash[:] + } + return result, err +} + +//go:generate gencodec -type Criteria -field-override criteriaOverride -out gen_criteria_json.go + +// Criteria holds various filter options for inbound messages. +type Criteria struct { + SymKeyID string `json:"symKeyID"` + PrivateKeyID string `json:"privateKeyID"` + Sig []byte `json:"sig"` + MinPow float64 `json:"minPow"` + Topics []TopicType `json:"topics"` + AllowP2P bool `json:"allowP2P"` +} + +type criteriaOverride struct { + Sig hexutil.Bytes +} + +// Messages set up a subscription that fires events when messages arrive that match +// the given set of criteria. +func (api *PublicWhisperAPI) Messages(ctx context.Context, crit Criteria) (*rpc.Subscription, error) { + var ( + symKeyGiven = len(crit.SymKeyID) > 0 + pubKeyGiven = len(crit.PrivateKeyID) > 0 + err error + ) + + // ensure that the RPC connection supports subscriptions + notifier, supported := rpc.NotifierFromContext(ctx) + if !supported { + return nil, rpc.ErrNotificationsUnsupported + } + + // user must specify either a symmetric or an asymmetric key + if (symKeyGiven && pubKeyGiven) || (!symKeyGiven && !pubKeyGiven) { + return nil, ErrSymAsym + } + + filter := Filter{ + PoW: crit.MinPow, + Messages: make(map[common.Hash]*ReceivedMessage), + AllowP2P: crit.AllowP2P, + } + + if len(crit.Sig) > 0 { + if filter.Src, err = crypto.UnmarshalPubkey(crit.Sig); err != nil { + return nil, ErrInvalidSigningPubKey + } + } + + for i, bt := range crit.Topics { + if len(bt) == 0 || len(bt) > 4 { + return nil, fmt.Errorf("subscribe: topic %d has wrong size: %d", i, len(bt)) + } + filter.Topics = append(filter.Topics, bt[:]) + } + + // listen for message that are encrypted with the given symmetric key + if symKeyGiven { + if len(filter.Topics) == 0 { + return nil, ErrNoTopics + } + key, err := api.w.GetSymKey(crit.SymKeyID) + if err != nil { + return nil, err + } + if !validateDataIntegrity(key, aesKeyLength) { + return nil, ErrInvalidSymmetricKey + } + filter.KeySym = key + filter.SymKeyHash = crypto.Keccak256Hash(filter.KeySym) + } + + // listen for messages that are encrypted with the given public key + if pubKeyGiven { + filter.KeyAsym, err = api.w.GetPrivateKey(crit.PrivateKeyID) + if err != nil || filter.KeyAsym == nil { + return nil, ErrInvalidPublicKey + } + } + + id, err := api.w.Subscribe(&filter) + if err != nil { + return nil, err + } + + // create subscription and start waiting for message events + rpcSub := notifier.CreateSubscription() + go func() { + // for now poll internally, refactor whisper internal for channel support + ticker := time.NewTicker(250 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if filter := api.w.GetFilter(id); filter != nil { + for _, rpcMessage := range toMessage(filter.Retrieve()) { + if err := notifier.Notify(rpcSub.ID, rpcMessage); err != nil { + log.Error("Failed to send notification", "err", err) + } + } + } + case <-rpcSub.Err(): + api.w.Unsubscribe(id) + return + case <-notifier.Closed(): + api.w.Unsubscribe(id) + return + } + } + }() + + return rpcSub, nil +} + +//go:generate gencodec -type Message -field-override messageOverride -out gen_message_json.go + +// Message is the RPC representation of a whisper message. +type Message struct { + Sig []byte `json:"sig,omitempty"` + TTL uint32 `json:"ttl"` + Timestamp uint32 `json:"timestamp"` + Topic TopicType `json:"topic"` + Payload []byte `json:"payload"` + Padding []byte `json:"padding"` + PoW float64 `json:"pow"` + Hash []byte `json:"hash"` + Dst []byte `json:"recipientPublicKey,omitempty"` +} + +type messageOverride struct { + Sig hexutil.Bytes + Payload hexutil.Bytes + Padding hexutil.Bytes + Hash hexutil.Bytes + Dst hexutil.Bytes +} + +// ToWhisperMessage converts an internal message into an API version. +func ToWhisperMessage(message *ReceivedMessage) *Message { + msg := Message{ + Payload: message.Payload, + Padding: message.Padding, + Timestamp: message.Sent, + TTL: message.TTL, + PoW: message.PoW, + Hash: message.EnvelopeHash.Bytes(), + Topic: message.Topic, + } + + if message.Dst != nil { + b := crypto.FromECDSAPub(message.Dst) + if b != nil { + msg.Dst = b + } + } + + if isMessageSigned(message.Raw[0]) { + b := crypto.FromECDSAPub(message.SigToPubKey()) + if b != nil { + msg.Sig = b + } + } + + return &msg +} + +// toMessage converts a set of messages to its RPC representation. +func toMessage(messages []*ReceivedMessage) []*Message { + msgs := make([]*Message, len(messages)) + for i, msg := range messages { + msgs[i] = ToWhisperMessage(msg) + } + return msgs +} + +// GetFilterMessages returns the messages that match the filter criteria and +// are received between the last poll and now. +func (api *PublicWhisperAPI) GetFilterMessages(id string) ([]*Message, error) { + api.mu.Lock() + f := api.w.GetFilter(id) + if f == nil { + api.mu.Unlock() + return nil, fmt.Errorf("filter not found") + } + api.lastUsed[id] = time.Now() + api.mu.Unlock() + + receivedMessages := f.Retrieve() + messages := make([]*Message, 0, len(receivedMessages)) + for _, msg := range receivedMessages { + messages = append(messages, ToWhisperMessage(msg)) + } + + return messages, nil +} + +// DeleteMessageFilter deletes a filter. +func (api *PublicWhisperAPI) DeleteMessageFilter(id string) (bool, error) { + api.mu.Lock() + defer api.mu.Unlock() + + delete(api.lastUsed, id) + return true, api.w.Unsubscribe(id) +} + +// NewMessageFilter creates a new filter that can be used to poll for +// (new) messages that satisfy the given criteria. +func (api *PublicWhisperAPI) NewMessageFilter(req Criteria) (string, error) { + var ( + src *ecdsa.PublicKey + keySym []byte + keyAsym *ecdsa.PrivateKey + topics [][]byte + + symKeyGiven = len(req.SymKeyID) > 0 + asymKeyGiven = len(req.PrivateKeyID) > 0 + + err error + ) + + // user must specify either a symmetric or an asymmetric key + if (symKeyGiven && asymKeyGiven) || (!symKeyGiven && !asymKeyGiven) { + return "", ErrSymAsym + } + + if len(req.Sig) > 0 { + if src, err = crypto.UnmarshalPubkey(req.Sig); err != nil { + return "", ErrInvalidSigningPubKey + } + } + + if symKeyGiven { + if keySym, err = api.w.GetSymKey(req.SymKeyID); err != nil { + return "", err + } + if !validateDataIntegrity(keySym, aesKeyLength) { + return "", ErrInvalidSymmetricKey + } + } + + if asymKeyGiven { + if keyAsym, err = api.w.GetPrivateKey(req.PrivateKeyID); err != nil { + return "", err + } + } + + if len(req.Topics) > 0 { + topics = make([][]byte, len(req.Topics)) + for i, topic := range req.Topics { + topics[i] = make([]byte, TopicLength) + copy(topics[i], topic[:]) + } + } + + f := &Filter{ + Src: src, + KeySym: keySym, + KeyAsym: keyAsym, + PoW: req.MinPow, + AllowP2P: req.AllowP2P, + Topics: topics, + Messages: make(map[common.Hash]*ReceivedMessage), + } + + id, err := api.w.Subscribe(f) + if err != nil { + return "", err + } + + api.mu.Lock() + api.lastUsed[id] = time.Now() + api.mu.Unlock() + + return id, nil +} diff --git a/whisper/whisperv6/api_test.go b/whisper/whisperv6/api_test.go new file mode 100644 index 0000000000..6d7157f57f --- /dev/null +++ b/whisper/whisperv6/api_test.go @@ -0,0 +1,63 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +import ( + "bytes" + "testing" + "time" +) + +func TestMultipleTopicCopyInNewMessageFilter(t *testing.T) { + w := New(nil) + + keyID, err := w.GenerateSymKey() + if err != nil { + t.Fatalf("Error generating symmetric key: %v", err) + } + api := PublicWhisperAPI{ + w: w, + lastUsed: make(map[string]time.Time), + } + + t1 := [4]byte{0xde, 0xea, 0xbe, 0xef} + t2 := [4]byte{0xca, 0xfe, 0xde, 0xca} + + crit := Criteria{ + SymKeyID: keyID, + Topics: []TopicType{TopicType(t1), TopicType(t2)}, + } + + _, err = api.NewMessageFilter(crit) + if err != nil { + t.Fatalf("Error creating the filter: %v", err) + } + + found := false + candidates := w.filters.getWatchersByTopic(TopicType(t1)) + for _, f := range candidates { + if len(f.Topics) == 2 { + if bytes.Equal(f.Topics[0], t1[:]) && bytes.Equal(f.Topics[1], t2[:]) { + found = true + } + } + } + + if !found { + t.Fatalf("Could not find filter with both topics") + } +} diff --git a/whisper/whisperv6/benchmarks_test.go b/whisper/whisperv6/benchmarks_test.go new file mode 100644 index 0000000000..43dfca99e4 --- /dev/null +++ b/whisper/whisperv6/benchmarks_test.go @@ -0,0 +1,208 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +import ( + "crypto/sha256" + "testing" + + "github.com/cryptoecc/ETH-ECC/crypto" + "golang.org/x/crypto/pbkdf2" +) + +func BenchmarkDeriveKeyMaterial(b *testing.B) { + for i := 0; i < b.N; i++ { + pbkdf2.Key([]byte("test"), nil, 65356, aesKeyLength, sha256.New) + } +} + +func BenchmarkEncryptionSym(b *testing.B) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + b.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + + for i := 0; i < b.N; i++ { + msg, _ := NewSentMessage(params) + _, err := msg.Wrap(params) + if err != nil { + b.Errorf("failed Wrap with seed %d: %s.", seed, err) + b.Errorf("i = %d, len(msg.Raw) = %d, params.Payload = %d.", i, len(msg.Raw), len(params.Payload)) + return + } + } +} + +func BenchmarkEncryptionAsym(b *testing.B) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + b.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + key, err := crypto.GenerateKey() + if err != nil { + b.Fatalf("failed GenerateKey with seed %d: %s.", seed, err) + } + params.KeySym = nil + params.Dst = &key.PublicKey + + for i := 0; i < b.N; i++ { + msg, _ := NewSentMessage(params) + _, err := msg.Wrap(params) + if err != nil { + b.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + } +} + +func BenchmarkDecryptionSymValid(b *testing.B) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + b.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + msg, _ := NewSentMessage(params) + env, err := msg.Wrap(params) + if err != nil { + b.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + f := Filter{KeySym: params.KeySym} + + for i := 0; i < b.N; i++ { + msg := env.Open(&f) + if msg == nil { + b.Fatalf("failed to open with seed %d.", seed) + } + } +} + +func BenchmarkDecryptionSymInvalid(b *testing.B) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + b.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + msg, _ := NewSentMessage(params) + env, err := msg.Wrap(params) + if err != nil { + b.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + f := Filter{KeySym: []byte("arbitrary stuff here")} + + for i := 0; i < b.N; i++ { + msg := env.Open(&f) + if msg != nil { + b.Fatalf("opened envelope with invalid key, seed: %d.", seed) + } + } +} + +func BenchmarkDecryptionAsymValid(b *testing.B) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + b.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + key, err := crypto.GenerateKey() + if err != nil { + b.Fatalf("failed GenerateKey with seed %d: %s.", seed, err) + } + f := Filter{KeyAsym: key} + params.KeySym = nil + params.Dst = &key.PublicKey + msg, _ := NewSentMessage(params) + env, err := msg.Wrap(params) + if err != nil { + b.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + for i := 0; i < b.N; i++ { + msg := env.Open(&f) + if msg == nil { + b.Fatalf("fail to open, seed: %d.", seed) + } + } +} + +func BenchmarkDecryptionAsymInvalid(b *testing.B) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + b.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + key, err := crypto.GenerateKey() + if err != nil { + b.Fatalf("failed GenerateKey with seed %d: %s.", seed, err) + } + params.KeySym = nil + params.Dst = &key.PublicKey + msg, _ := NewSentMessage(params) + env, err := msg.Wrap(params) + if err != nil { + b.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + key, err = crypto.GenerateKey() + if err != nil { + b.Fatalf("failed GenerateKey with seed %d: %s.", seed, err) + } + f := Filter{KeyAsym: key} + + for i := 0; i < b.N; i++ { + msg := env.Open(&f) + if msg != nil { + b.Fatalf("opened envelope with invalid key, seed: %d.", seed) + } + } +} + +func increment(x []byte) { + for i := 0; i < len(x); i++ { + x[i]++ + if x[i] != 0 { + break + } + } +} + +func BenchmarkPoW(b *testing.B) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + b.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + params.Payload = make([]byte, 32) + params.PoW = 10.0 + params.TTL = 1 + + for i := 0; i < b.N; i++ { + increment(params.Payload) + msg, _ := NewSentMessage(params) + _, err := msg.Wrap(params) + if err != nil { + b.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + } +} diff --git a/whisper/whisperv6/config.go b/whisper/whisperv6/config.go new file mode 100644 index 0000000000..38eb9551cc --- /dev/null +++ b/whisper/whisperv6/config.go @@ -0,0 +1,31 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +// Config represents the configuration state of a whisper node. +type Config struct { + MaxMessageSize uint32 `toml:",omitempty"` + MinimumAcceptedPOW float64 `toml:",omitempty"` + RestrictConnectionBetweenLightClients bool `toml:",omitempty"` +} + +// DefaultConfig represents (shocker!) the default configuration. +var DefaultConfig = Config{ + MaxMessageSize: DefaultMaxMessageSize, + MinimumAcceptedPOW: DefaultMinimumPoW, + RestrictConnectionBetweenLightClients: true, +} diff --git a/whisper/whisperv6/doc.go b/whisper/whisperv6/doc.go new file mode 100644 index 0000000000..02c836414c --- /dev/null +++ b/whisper/whisperv6/doc.go @@ -0,0 +1,92 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +/* +Package whisper implements the Whisper protocol (version 6). + +Whisper combines aspects of both DHTs and datagram messaging systems (e.g. UDP). +As such it may be likened and compared to both, not dissimilar to the +matter/energy duality (apologies to physicists for the blatant abuse of a +fundamental and beautiful natural principle). + +Whisper is a pure identity-based messaging system. Whisper provides a low-level +(non-application-specific) but easily-accessible API without being based upon +or prejudiced by the low-level hardware attributes and characteristics, +particularly the notion of singular endpoints. +*/ + +// Contains the Whisper protocol constant definitions + +package whisperv6 + +import ( + "time" + + "github.com/cryptoecc/ETH-ECC/crypto" +) + +// Whisper protocol parameters +const ( + ProtocolVersion = uint64(6) // Protocol version number + ProtocolVersionStr = "6.0" // The same, as a string + ProtocolName = "shh" // Nickname of the protocol in geth + + // whisper protocol message codes, according to EIP-627 + statusCode = 0 // used by whisper protocol + messagesCode = 1 // normal whisper message + powRequirementCode = 2 // PoW requirement + bloomFilterExCode = 3 // bloom filter exchange + p2pRequestCode = 126 // peer-to-peer message, used by Dapp protocol + p2pMessageCode = 127 // peer-to-peer message (to be consumed by the peer, but not forwarded any further) + NumberOfMessageCodes = 128 + + SizeMask = byte(3) // mask used to extract the size of payload size field from the flags + signatureFlag = byte(4) + + TopicLength = 4 // in bytes + signatureLength = crypto.SignatureLength // in bytes + aesKeyLength = 32 // in bytes + aesNonceLength = 12 // in bytes; for more info please see cipher.gcmStandardNonceSize & aesgcm.NonceSize() + keyIDSize = 32 // in bytes + BloomFilterSize = 64 // in bytes + flagsLength = 1 + + EnvelopeHeaderLength = 20 + + MaxMessageSize = uint32(10 * 1024 * 1024) // maximum accepted size of a message. + DefaultMaxMessageSize = uint32(1024 * 1024) + DefaultMinimumPoW = 0.2 + + padSizeLimit = 256 // just an arbitrary number, could be changed without breaking the protocol + messageQueueLimit = 1024 + + expirationCycle = time.Second + transmissionCycle = 300 * time.Millisecond + + DefaultTTL = 50 // seconds + DefaultSyncAllowance = 10 // seconds +) + +// MailServer represents a mail server, capable of +// archiving the old messages for subsequent delivery +// to the peers. Any implementation must ensure that both +// functions are thread-safe. Also, they must return ASAP. +// DeliverMail should use directMessagesCode for delivery, +// in order to bypass the expiry checks. +type MailServer interface { + Archive(env *Envelope) + DeliverMail(whisperPeer *Peer, request *Envelope) +} diff --git a/whisper/whisperv6/envelope.go b/whisper/whisperv6/envelope.go new file mode 100644 index 0000000000..8e04735c28 --- /dev/null +++ b/whisper/whisperv6/envelope.go @@ -0,0 +1,280 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Contains the Whisper protocol Envelope element. + +package whisperv6 + +import ( + "crypto/ecdsa" + "encoding/binary" + "fmt" + gmath "math" + "math/big" + "time" + + "github.com/cryptoecc/ETH-ECC/common" + "github.com/cryptoecc/ETH-ECC/crypto" + "github.com/cryptoecc/ETH-ECC/crypto/ecies" + "github.com/cryptoecc/ETH-ECC/rlp" +) + +// Envelope represents a clear-text data packet to transmit through the Whisper +// network. Its contents may or may not be encrypted and signed. +type Envelope struct { + Expiry uint32 + TTL uint32 + Topic TopicType + Data []byte + Nonce uint64 + + pow float64 // Message-specific PoW as described in the Whisper specification. + + // the following variables should not be accessed directly, use the corresponding function instead: Hash(), Bloom() + hash common.Hash // Cached hash of the envelope to avoid rehashing every time. + bloom []byte +} + +// size returns the size of envelope as it is sent (i.e. public fields only) +func (e *Envelope) size() int { + return EnvelopeHeaderLength + len(e.Data) +} + +// rlpWithoutNonce returns the RLP encoded envelope contents, except the nonce. +func (e *Envelope) rlpWithoutNonce() []byte { + res, _ := rlp.EncodeToBytes([]interface{}{e.Expiry, e.TTL, e.Topic, e.Data}) + return res +} + +// NewEnvelope wraps a Whisper message with expiration and destination data +// included into an envelope for network forwarding. +func NewEnvelope(ttl uint32, topic TopicType, msg *sentMessage) *Envelope { + env := Envelope{ + Expiry: uint32(time.Now().Add(time.Second * time.Duration(ttl)).Unix()), + TTL: ttl, + Topic: topic, + Data: msg.Raw, + Nonce: 0, + } + + return &env +} + +// Seal closes the envelope by spending the requested amount of time as a proof +// of work on hashing the data. +func (e *Envelope) Seal(options *MessageParams) error { + if options.PoW == 0 { + // PoW is not required + return nil + } + + var target, bestLeadingZeros int + if options.PoW < 0 { + // target is not set - the function should run for a period + // of time specified in WorkTime param. Since we can predict + // the execution time, we can also adjust Expiry. + e.Expiry += options.WorkTime + } else { + target = e.powToFirstBit(options.PoW) + } + + rlp := e.rlpWithoutNonce() + buf := make([]byte, len(rlp)+8) + copy(buf, rlp) + asAnInt := new(big.Int) + + finish := time.Now().Add(time.Duration(options.WorkTime) * time.Second).UnixNano() + for nonce := uint64(0); time.Now().UnixNano() < finish; { + for i := 0; i < 1024; i++ { + binary.BigEndian.PutUint64(buf[len(rlp):], nonce) + h := crypto.Keccak256(buf) + asAnInt.SetBytes(h) + leadingZeros := 256 - asAnInt.BitLen() + if leadingZeros > bestLeadingZeros { + e.Nonce, bestLeadingZeros = nonce, leadingZeros + if target > 0 && bestLeadingZeros >= target { + return nil + } + } + nonce++ + } + } + + if target > 0 && bestLeadingZeros < target { + return fmt.Errorf("failed to reach the PoW target, specified pow time (%d seconds) was insufficient", options.WorkTime) + } + + return nil +} + +// PoW computes (if necessary) and returns the proof of work target +// of the envelope. +func (e *Envelope) PoW() float64 { + if e.pow == 0 { + e.calculatePoW(0) + } + return e.pow +} + +func (e *Envelope) calculatePoW(diff uint32) { + rlp := e.rlpWithoutNonce() + buf := make([]byte, len(rlp)+8) + copy(buf, rlp) + binary.BigEndian.PutUint64(buf[len(rlp):], e.Nonce) + powHash := new(big.Int).SetBytes(crypto.Keccak256(buf)) + leadingZeroes := 256 - powHash.BitLen() + x := gmath.Pow(2, float64(leadingZeroes)) + x /= float64(len(rlp)) + x /= float64(e.TTL + diff) + e.pow = x +} + +func (e *Envelope) powToFirstBit(pow float64) int { + x := pow + x *= float64(e.size()) + x *= float64(e.TTL) + bits := gmath.Log2(x) + bits = gmath.Ceil(bits) + res := int(bits) + if res < 1 { + res = 1 + } + return res +} + +// Hash returns the SHA3 hash of the envelope, calculating it if not yet done. +func (e *Envelope) Hash() common.Hash { + if (e.hash == common.Hash{}) { + encoded, _ := rlp.EncodeToBytes(e) + e.hash = crypto.Keccak256Hash(encoded) + } + return e.hash +} + +// DecodeRLP decodes an Envelope from an RLP data stream. +func (e *Envelope) DecodeRLP(s *rlp.Stream) error { + raw, err := s.Raw() + if err != nil { + return err + } + // The decoding of Envelope uses the struct fields but also needs + // to compute the hash of the whole RLP-encoded envelope. This + // type has the same structure as Envelope but is not an + // rlp.Decoder (does not implement DecodeRLP function). + // Only public members will be encoded. + type rlpenv Envelope + if err := rlp.DecodeBytes(raw, (*rlpenv)(e)); err != nil { + return err + } + e.hash = crypto.Keccak256Hash(raw) + return nil +} + +// OpenAsymmetric tries to decrypt an envelope, potentially encrypted with a particular key. +func (e *Envelope) OpenAsymmetric(key *ecdsa.PrivateKey) (*ReceivedMessage, error) { + message := &ReceivedMessage{Raw: e.Data} + err := message.decryptAsymmetric(key) + switch err { + case nil: + return message, nil + case ecies.ErrInvalidPublicKey: // addressed to somebody else + return nil, err + default: + return nil, fmt.Errorf("unable to open envelope, decrypt failed: %v", err) + } +} + +// OpenSymmetric tries to decrypt an envelope, potentially encrypted with a particular key. +func (e *Envelope) OpenSymmetric(key []byte) (msg *ReceivedMessage, err error) { + msg = &ReceivedMessage{Raw: e.Data} + err = msg.decryptSymmetric(key) + if err != nil { + msg = nil + } + return msg, err +} + +// Open tries to decrypt an envelope, and populates the message fields in case of success. +func (e *Envelope) Open(watcher *Filter) (msg *ReceivedMessage) { + if watcher == nil { + return nil + } + + // The API interface forbids filters doing both symmetric and asymmetric encryption. + if watcher.expectsAsymmetricEncryption() && watcher.expectsSymmetricEncryption() { + return nil + } + + if watcher.expectsAsymmetricEncryption() { + msg, _ = e.OpenAsymmetric(watcher.KeyAsym) + if msg != nil { + msg.Dst = &watcher.KeyAsym.PublicKey + } + } else if watcher.expectsSymmetricEncryption() { + msg, _ = e.OpenSymmetric(watcher.KeySym) + if msg != nil { + msg.SymKeyHash = crypto.Keccak256Hash(watcher.KeySym) + } + } + + if msg != nil { + ok := msg.ValidateAndParse() + if !ok { + return nil + } + msg.Topic = e.Topic + msg.PoW = e.PoW() + msg.TTL = e.TTL + msg.Sent = e.Expiry - e.TTL + msg.EnvelopeHash = e.Hash() + } + return msg +} + +// Bloom maps 4-bytes Topic into 64-byte bloom filter with 3 bits set (at most). +func (e *Envelope) Bloom() []byte { + if e.bloom == nil { + e.bloom = TopicToBloom(e.Topic) + } + return e.bloom +} + +// TopicToBloom converts the topic (4 bytes) to the bloom filter (64 bytes) +func TopicToBloom(topic TopicType) []byte { + b := make([]byte, BloomFilterSize) + var index [3]int + for j := 0; j < 3; j++ { + index[j] = int(topic[j]) + if (topic[3] & (1 << uint(j))) != 0 { + index[j] += 256 + } + } + + for j := 0; j < 3; j++ { + byteIndex := index[j] / 8 + bitIndex := index[j] % 8 + b[byteIndex] = (1 << uint(bitIndex)) + } + return b +} + +// GetEnvelope retrieves an envelope from the message queue by its hash. +// It returns nil if the envelope can not be found. +func (w *Whisper) GetEnvelope(hash common.Hash) *Envelope { + w.poolMu.RLock() + defer w.poolMu.RUnlock() + return w.envelopes[hash] +} diff --git a/whisper/whisperv6/envelope_test.go b/whisper/whisperv6/envelope_test.go new file mode 100644 index 0000000000..062937a843 --- /dev/null +++ b/whisper/whisperv6/envelope_test.go @@ -0,0 +1,91 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Contains the tests associated with the Whisper protocol Envelope object. + +package whisperv6 + +import ( + mrand "math/rand" + "testing" + + "github.com/cryptoecc/ETH-ECC/crypto" +) + +func TestPoWCalculationsWithNoLeadingZeros(t *testing.T) { + e := Envelope{ + TTL: 1, + Data: []byte{0xde, 0xad, 0xbe, 0xef}, + Nonce: 100000, + } + + e.calculatePoW(0) + + if e.pow != 0.07692307692307693 { + t.Fatalf("invalid PoW calculation. Expected 0.07692307692307693, got %v", e.pow) + } +} + +func TestPoWCalculationsWith8LeadingZeros(t *testing.T) { + e := Envelope{ + TTL: 1, + Data: []byte{0xde, 0xad, 0xbe, 0xef}, + Nonce: 276, + } + e.calculatePoW(0) + + if e.pow != 19.692307692307693 { + t.Fatalf("invalid PoW calculation. Expected 19.692307692307693, got %v", e.pow) + } +} + +func TestEnvelopeOpenAcceptsOnlyOneKeyTypeInFilter(t *testing.T) { + symKey := make([]byte, aesKeyLength) + mrand.Read(symKey) + + asymKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed GenerateKey with seed %d: %s.", seed, err) + } + + params := MessageParams{ + PoW: 0.01, + WorkTime: 1, + TTL: uint32(mrand.Intn(1024)), + Payload: make([]byte, 50), + KeySym: symKey, + Dst: nil, + } + + mrand.Read(params.Payload) + + msg, err := NewSentMessage(¶ms) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + + e, err := msg.Wrap(¶ms) + if err != nil { + t.Fatalf("Failed to Wrap the message in an envelope with seed %d: %s", seed, err) + } + + f := Filter{KeySym: symKey, KeyAsym: asymKey} + + decrypted := e.Open(&f) + if decrypted != nil { + t.Fatalf("Managed to decrypt a message with an invalid filter, seed %d", seed) + } +} diff --git a/whisper/whisperv6/filter.go b/whisper/whisperv6/filter.go new file mode 100644 index 0000000000..1be18a3693 --- /dev/null +++ b/whisper/whisperv6/filter.go @@ -0,0 +1,262 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +import ( + "crypto/ecdsa" + "fmt" + "sync" + + "github.com/cryptoecc/ETH-ECC/common" + "github.com/cryptoecc/ETH-ECC/crypto" + "github.com/cryptoecc/ETH-ECC/log" +) + +// Filter represents a Whisper message filter +type Filter struct { + Src *ecdsa.PublicKey // Sender of the message + KeyAsym *ecdsa.PrivateKey // Private Key of recipient + KeySym []byte // Key associated with the Topic + Topics [][]byte // Topics to filter messages with + PoW float64 // Proof of work as described in the Whisper spec + AllowP2P bool // Indicates whether this filter is interested in direct peer-to-peer messages + SymKeyHash common.Hash // The Keccak256Hash of the symmetric key, needed for optimization + id string // unique identifier + + Messages map[common.Hash]*ReceivedMessage + mutex sync.RWMutex +} + +// Filters represents a collection of filters +type Filters struct { + watchers map[string]*Filter + + topicMatcher map[TopicType]map[*Filter]struct{} // map a topic to the filters that are interested in being notified when a message matches that topic + allTopicsMatcher map[*Filter]struct{} // list all the filters that will be notified of a new message, no matter what its topic is + + whisper *Whisper + mutex sync.RWMutex +} + +// NewFilters returns a newly created filter collection +func NewFilters(w *Whisper) *Filters { + return &Filters{ + watchers: make(map[string]*Filter), + topicMatcher: make(map[TopicType]map[*Filter]struct{}), + allTopicsMatcher: make(map[*Filter]struct{}), + whisper: w, + } +} + +// Install will add a new filter to the filter collection +func (fs *Filters) Install(watcher *Filter) (string, error) { + if watcher.KeySym != nil && watcher.KeyAsym != nil { + return "", fmt.Errorf("filters must choose between symmetric and asymmetric keys") + } + + if watcher.Messages == nil { + watcher.Messages = make(map[common.Hash]*ReceivedMessage) + } + + id, err := GenerateRandomID() + if err != nil { + return "", err + } + + fs.mutex.Lock() + defer fs.mutex.Unlock() + + if fs.watchers[id] != nil { + return "", fmt.Errorf("failed to generate unique ID") + } + + if watcher.expectsSymmetricEncryption() { + watcher.SymKeyHash = crypto.Keccak256Hash(watcher.KeySym) + } + + watcher.id = id + fs.watchers[id] = watcher + fs.addTopicMatcher(watcher) + return id, err +} + +// Uninstall will remove a filter whose id has been specified from +// the filter collection +func (fs *Filters) Uninstall(id string) bool { + fs.mutex.Lock() + defer fs.mutex.Unlock() + if fs.watchers[id] != nil { + fs.removeFromTopicMatchers(fs.watchers[id]) + delete(fs.watchers, id) + return true + } + return false +} + +// addTopicMatcher adds a filter to the topic matchers. +// If the filter's Topics array is empty, it will be tried on every topic. +// Otherwise, it will be tried on the topics specified. +func (fs *Filters) addTopicMatcher(watcher *Filter) { + if len(watcher.Topics) == 0 { + fs.allTopicsMatcher[watcher] = struct{}{} + } else { + for _, t := range watcher.Topics { + topic := BytesToTopic(t) + if fs.topicMatcher[topic] == nil { + fs.topicMatcher[topic] = make(map[*Filter]struct{}) + } + fs.topicMatcher[topic][watcher] = struct{}{} + } + } +} + +// removeFromTopicMatchers removes a filter from the topic matchers +func (fs *Filters) removeFromTopicMatchers(watcher *Filter) { + delete(fs.allTopicsMatcher, watcher) + for _, topic := range watcher.Topics { + delete(fs.topicMatcher[BytesToTopic(topic)], watcher) + } +} + +// getWatchersByTopic returns a slice containing the filters that +// match a specific topic +func (fs *Filters) getWatchersByTopic(topic TopicType) []*Filter { + res := make([]*Filter, 0, len(fs.allTopicsMatcher)) + for watcher := range fs.allTopicsMatcher { + res = append(res, watcher) + } + for watcher := range fs.topicMatcher[topic] { + res = append(res, watcher) + } + return res +} + +// Get returns a filter from the collection with a specific ID +func (fs *Filters) Get(id string) *Filter { + fs.mutex.RLock() + defer fs.mutex.RUnlock() + return fs.watchers[id] +} + +// NotifyWatchers notifies any filter that has declared interest +// for the envelope's topic. +func (fs *Filters) NotifyWatchers(env *Envelope, p2pMessage bool) { + var msg *ReceivedMessage + + fs.mutex.RLock() + defer fs.mutex.RUnlock() + + candidates := fs.getWatchersByTopic(env.Topic) + for _, watcher := range candidates { + if p2pMessage && !watcher.AllowP2P { + log.Trace(fmt.Sprintf("msg [%x], filter [%s]: p2p messages are not allowed", env.Hash(), watcher.id)) + continue + } + + var match bool + if msg != nil { + match = watcher.MatchMessage(msg) + } else { + match = watcher.MatchEnvelope(env) + if match { + msg = env.Open(watcher) + if msg == nil { + log.Trace("processing message: failed to open", "message", env.Hash().Hex(), "filter", watcher.id) + } + } else { + log.Trace("processing message: does not match", "message", env.Hash().Hex(), "filter", watcher.id) + } + } + + if match && msg != nil { + log.Trace("processing message: decrypted", "hash", env.Hash().Hex()) + if watcher.Src == nil || IsPubKeyEqual(msg.Src, watcher.Src) { + watcher.Trigger(msg) + } + } + } +} + +func (f *Filter) expectsAsymmetricEncryption() bool { + return f.KeyAsym != nil +} + +func (f *Filter) expectsSymmetricEncryption() bool { + return f.KeySym != nil +} + +// Trigger adds a yet-unknown message to the filter's list of +// received messages. +func (f *Filter) Trigger(msg *ReceivedMessage) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if _, exist := f.Messages[msg.EnvelopeHash]; !exist { + f.Messages[msg.EnvelopeHash] = msg + } +} + +// Retrieve will return the list of all received messages associated +// to a filter. +func (f *Filter) Retrieve() (all []*ReceivedMessage) { + f.mutex.Lock() + defer f.mutex.Unlock() + + all = make([]*ReceivedMessage, 0, len(f.Messages)) + for _, msg := range f.Messages { + all = append(all, msg) + } + + f.Messages = make(map[common.Hash]*ReceivedMessage) // delete old messages + return all +} + +// MatchMessage checks if the filter matches an already decrypted +// message (i.e. a Message that has already been handled by +// MatchEnvelope when checked by a previous filter). +// Topics are not checked here, since this is done by topic matchers. +func (f *Filter) MatchMessage(msg *ReceivedMessage) bool { + if f.PoW > 0 && msg.PoW < f.PoW { + return false + } + + if f.expectsAsymmetricEncryption() && msg.isAsymmetricEncryption() { + return IsPubKeyEqual(&f.KeyAsym.PublicKey, msg.Dst) + } else if f.expectsSymmetricEncryption() && msg.isSymmetricEncryption() { + return f.SymKeyHash == msg.SymKeyHash + } + return false +} + +// MatchEnvelope checks if it's worth decrypting the message. If +// it returns `true`, client code is expected to attempt decrypting +// the message and subsequently call MatchMessage. +// Topics are not checked here, since this is done by topic matchers. +func (f *Filter) MatchEnvelope(envelope *Envelope) bool { + return f.PoW <= 0 || envelope.pow >= f.PoW +} + +// IsPubKeyEqual checks that two public keys are equal +func IsPubKeyEqual(a, b *ecdsa.PublicKey) bool { + if !ValidatePublicKey(a) { + return false + } else if !ValidatePublicKey(b) { + return false + } + // the curve is always the same, just compare the points + return a.X.Cmp(b.X) == 0 && a.Y.Cmp(b.Y) == 0 +} diff --git a/whisper/whisperv6/filter_test.go b/whisper/whisperv6/filter_test.go new file mode 100644 index 0000000000..572ff75127 --- /dev/null +++ b/whisper/whisperv6/filter_test.go @@ -0,0 +1,831 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +import ( + "math/big" + mrand "math/rand" + "testing" + "time" + + "github.com/cryptoecc/ETH-ECC/common" + "github.com/cryptoecc/ETH-ECC/crypto" +) + +var seed int64 + +// InitSingleTest should be called in the beginning of every +// test, which uses RNG, in order to make the tests +// reproduciblity independent of their sequence. +func InitSingleTest() { + seed = time.Now().Unix() + mrand.Seed(seed) +} + +func InitDebugTest(i int64) { + seed = i + mrand.Seed(seed) +} + +type FilterTestCase struct { + f *Filter + id string + alive bool + msgCnt int +} + +func generateFilter(t *testing.T, symmetric bool) (*Filter, error) { + var f Filter + f.Messages = make(map[common.Hash]*ReceivedMessage) + + const topicNum = 8 + f.Topics = make([][]byte, topicNum) + for i := 0; i < topicNum; i++ { + f.Topics[i] = make([]byte, 4) + mrand.Read(f.Topics[i]) + f.Topics[i][0] = 0x01 + } + + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("generateFilter 1 failed with seed %d.", seed) + return nil, err + } + f.Src = &key.PublicKey + + if symmetric { + f.KeySym = make([]byte, aesKeyLength) + mrand.Read(f.KeySym) + f.SymKeyHash = crypto.Keccak256Hash(f.KeySym) + } else { + f.KeyAsym, err = crypto.GenerateKey() + if err != nil { + t.Fatalf("generateFilter 2 failed with seed %d.", seed) + return nil, err + } + } + + // AcceptP2P & PoW are not set + return &f, nil +} + +func generateTestCases(t *testing.T, SizeTestFilters int) []FilterTestCase { + cases := make([]FilterTestCase, SizeTestFilters) + for i := 0; i < SizeTestFilters; i++ { + f, _ := generateFilter(t, true) + cases[i].f = f + cases[i].alive = mrand.Int()&int(1) == 0 + } + return cases +} + +func TestInstallFilters(t *testing.T) { + InitSingleTest() + + const SizeTestFilters = 256 + w := New(&Config{}) + filters := NewFilters(w) + tst := generateTestCases(t, SizeTestFilters) + + var err error + var j string + for i := 0; i < SizeTestFilters; i++ { + j, err = filters.Install(tst[i].f) + if err != nil { + t.Fatalf("seed %d: failed to install filter: %s", seed, err) + } + tst[i].id = j + if len(j) != keyIDSize*2 { + t.Fatalf("seed %d: wrong filter id size [%d]", seed, len(j)) + } + } + + for _, testCase := range tst { + if !testCase.alive { + filters.Uninstall(testCase.id) + } + } + + for i, testCase := range tst { + fil := filters.Get(testCase.id) + exist := fil != nil + if exist != testCase.alive { + t.Fatalf("seed %d: failed alive: %d, %v, %v", seed, i, exist, testCase.alive) + } + if exist && fil.PoW != testCase.f.PoW { + t.Fatalf("seed %d: failed Get: %d, %v, %v", seed, i, exist, testCase.alive) + } + } +} + +func TestInstallSymKeyGeneratesHash(t *testing.T) { + InitSingleTest() + + w := New(&Config{}) + filters := NewFilters(w) + filter, _ := generateFilter(t, true) + + // save the current SymKeyHash for comparison + initialSymKeyHash := filter.SymKeyHash + + // ensure the SymKeyHash is invalid, for Install to recreate it + var invalid common.Hash + filter.SymKeyHash = invalid + + _, err := filters.Install(filter) + + if err != nil { + t.Fatalf("Error installing the filter: %s", err) + } + + for i, b := range filter.SymKeyHash { + if b != initialSymKeyHash[i] { + t.Fatalf("The filter's symmetric key hash was not properly generated by Install") + } + } +} + +func TestInstallIdenticalFilters(t *testing.T) { + InitSingleTest() + + w := New(&Config{}) + filters := NewFilters(w) + filter1, _ := generateFilter(t, true) + + // Copy the first filter since some of its fields + // are randomly gnerated. + filter2 := &Filter{ + KeySym: filter1.KeySym, + Topics: filter1.Topics, + PoW: filter1.PoW, + AllowP2P: filter1.AllowP2P, + Messages: make(map[common.Hash]*ReceivedMessage), + } + + _, err := filters.Install(filter1) + + if err != nil { + t.Fatalf("Error installing the first filter with seed %d: %s", seed, err) + } + + _, err = filters.Install(filter2) + + if err != nil { + t.Fatalf("Error installing the second filter with seed %d: %s", seed, err) + } + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("Error generating message parameters with seed %d: %s", seed, err) + } + + params.KeySym = filter1.KeySym + params.Topic = BytesToTopic(filter1.Topics[0]) + + filter1.Src = ¶ms.Src.PublicKey + filter2.Src = ¶ms.Src.PublicKey + + sentMessage, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := sentMessage.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + msg := env.Open(filter1) + if msg == nil { + t.Fatalf("failed to Open with filter1") + } + + if !filter1.MatchEnvelope(env) { + t.Fatalf("failed matching with the first filter") + } + + if !filter2.MatchEnvelope(env) { + t.Fatalf("failed matching with the first filter") + } + + if !filter1.MatchMessage(msg) { + t.Fatalf("failed matching with the second filter") + } + + if !filter2.MatchMessage(msg) { + t.Fatalf("failed matching with the second filter") + } +} + +func TestInstallFilterWithSymAndAsymKeys(t *testing.T) { + InitSingleTest() + + w := New(&Config{}) + filters := NewFilters(w) + filter1, _ := generateFilter(t, true) + + asymKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("Unable to create asymetric keys: %v", err) + } + + // Copy the first filter since some of its fields + // are randomly gnerated. + filter := &Filter{ + KeySym: filter1.KeySym, + KeyAsym: asymKey, + Topics: filter1.Topics, + PoW: filter1.PoW, + AllowP2P: filter1.AllowP2P, + Messages: make(map[common.Hash]*ReceivedMessage), + } + + _, err = filters.Install(filter) + + if err == nil { + t.Fatalf("Error detecting that a filter had both an asymmetric and symmetric key, with seed %d", seed) + } +} + +func TestComparePubKey(t *testing.T) { + InitSingleTest() + + key1, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to generate first key with seed %d: %s.", seed, err) + } + key2, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to generate second key with seed %d: %s.", seed, err) + } + if IsPubKeyEqual(&key1.PublicKey, &key2.PublicKey) { + t.Fatalf("public keys are equal, seed %d.", seed) + } + + // generate key3 == key1 + mrand.Seed(seed) + key3, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to generate third key with seed %d: %s.", seed, err) + } + if IsPubKeyEqual(&key1.PublicKey, &key3.PublicKey) { + t.Fatalf("key1 == key3, seed %d.", seed) + } +} + +func TestMatchEnvelope(t *testing.T) { + InitSingleTest() + + fsym, err := generateFilter(t, true) + if err != nil { + t.Fatalf("failed generateFilter with seed %d: %s.", seed, err) + } + + fasym, err := generateFilter(t, false) + if err != nil { + t.Fatalf("failed generateFilter() with seed %d: %s.", seed, err) + } + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + + params.Topic[0] = 0xFF // topic mismatch + + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + // encrypt symmetrically + i := mrand.Int() % 4 + fsym.Topics[i] = params.Topic[:] + fasym.Topics[i] = params.Topic[:] + msg, err = NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err = msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap() with seed %d: %s.", seed, err) + } + + // symmetric + matching topic: match + match := fsym.MatchEnvelope(env) + if !match { + t.Fatalf("failed MatchEnvelope() symmetric with seed %d.", seed) + } + + // symmetric + matching topic + insufficient PoW: mismatch + fsym.PoW = env.PoW() + 1.0 + match = fsym.MatchEnvelope(env) + if match { + t.Fatalf("failed MatchEnvelope(symmetric + matching topic + insufficient PoW) asymmetric with seed %d.", seed) + } + + // symmetric + matching topic + sufficient PoW: match + fsym.PoW = env.PoW() / 2 + match = fsym.MatchEnvelope(env) + if !match { + t.Fatalf("failed MatchEnvelope(symmetric + matching topic + sufficient PoW) with seed %d.", seed) + } + + // symmetric + topics are nil (wildcard): match + prevTopics := fsym.Topics + fsym.Topics = nil + match = fsym.MatchEnvelope(env) + if !match { + t.Fatalf("failed MatchEnvelope(symmetric + topics are nil) with seed %d.", seed) + } + fsym.Topics = prevTopics + + // encrypt asymmetrically + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed GenerateKey with seed %d: %s.", seed, err) + } + params.KeySym = nil + params.Dst = &key.PublicKey + msg, err = NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err = msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap() with seed %d: %s.", seed, err) + } + + // encryption method mismatch + match = fsym.MatchEnvelope(env) + if match { + t.Fatalf("failed MatchEnvelope(encryption method mismatch) with seed %d.", seed) + } + + // asymmetric + mismatching topic: mismatch + match = fasym.MatchEnvelope(env) + if !match { + t.Fatalf("failed MatchEnvelope(asymmetric + mismatching topic) with seed %d.", seed) + } + + // asymmetric + matching topic: match + fasym.Topics[i] = fasym.Topics[i+1] + match = fasym.MatchEnvelope(env) + if !match { + t.Fatalf("failed MatchEnvelope(asymmetric + matching topic) with seed %d.", seed) + } + + // asymmetric + filter without topic (wildcard): match + fasym.Topics = nil + match = fasym.MatchEnvelope(env) + if !match { + t.Fatalf("failed MatchEnvelope(asymmetric + filter without topic) with seed %d.", seed) + } + + // asymmetric + insufficient PoW: mismatch + fasym.PoW = env.PoW() + 1.0 + match = fasym.MatchEnvelope(env) + if match { + t.Fatalf("failed MatchEnvelope(asymmetric + insufficient PoW) with seed %d.", seed) + } + + // asymmetric + sufficient PoW: match + fasym.PoW = env.PoW() / 2 + match = fasym.MatchEnvelope(env) + if !match { + t.Fatalf("failed MatchEnvelope(asymmetric + sufficient PoW) with seed %d.", seed) + } + + // filter without topic + envelope without topic: match + env.Topic = TopicType{} + match = fasym.MatchEnvelope(env) + if !match { + t.Fatalf("failed MatchEnvelope(filter without topic + envelope without topic) with seed %d.", seed) + } + + // filter with topic + envelope without topic: mismatch + fasym.Topics = fsym.Topics + match = fasym.MatchEnvelope(env) + if !match { + // topic mismatch should have no affect, as topics are handled by topic matchers + t.Fatalf("failed MatchEnvelope(filter without topic + envelope without topic) with seed %d.", seed) + } +} + +func TestMatchMessageSym(t *testing.T) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + + f, err := generateFilter(t, true) + if err != nil { + t.Fatalf("failed generateFilter with seed %d: %s.", seed, err) + } + + const index = 1 + params.KeySym = f.KeySym + params.Topic = BytesToTopic(f.Topics[index]) + + sentMessage, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := sentMessage.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + msg := env.Open(f) + if msg == nil { + t.Fatalf("failed Open with seed %d.", seed) + } + + // Src: match + *f.Src.X = *params.Src.PublicKey.X + *f.Src.Y = *params.Src.PublicKey.Y + if !f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(src match) with seed %d.", seed) + } + + // insufficient PoW: mismatch + f.PoW = msg.PoW + 1.0 + if f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(insufficient PoW) with seed %d.", seed) + } + + // sufficient PoW: match + f.PoW = msg.PoW / 2 + if !f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(sufficient PoW) with seed %d.", seed) + } + + // topic mismatch + f.Topics[index][0]++ + if !f.MatchMessage(msg) { + // topic mismatch should have no affect, as topics are handled by topic matchers + t.Fatalf("failed MatchEnvelope(topic mismatch) with seed %d.", seed) + } + f.Topics[index][0]-- + + // key mismatch + f.SymKeyHash[0]++ + if f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(key mismatch) with seed %d.", seed) + } + f.SymKeyHash[0]-- + + // Src absent: match + f.Src = nil + if !f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(src absent) with seed %d.", seed) + } + + // key hash mismatch + h := f.SymKeyHash + f.SymKeyHash = common.Hash{} + if f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(key hash mismatch) with seed %d.", seed) + } + f.SymKeyHash = h + if !f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(key hash match) with seed %d.", seed) + } + + // encryption method mismatch + f.KeySym = nil + f.KeyAsym, err = crypto.GenerateKey() + if err != nil { + t.Fatalf("failed GenerateKey with seed %d: %s.", seed, err) + } + if f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(encryption method mismatch) with seed %d.", seed) + } +} + +func TestMatchMessageAsym(t *testing.T) { + InitSingleTest() + + f, err := generateFilter(t, false) + if err != nil { + t.Fatalf("failed generateFilter with seed %d: %s.", seed, err) + } + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + + const index = 1 + params.Topic = BytesToTopic(f.Topics[index]) + params.Dst = &f.KeyAsym.PublicKey + keySymOrig := params.KeySym + params.KeySym = nil + + sentMessage, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := sentMessage.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + msg := env.Open(f) + if msg == nil { + t.Fatalf("failed to open with seed %d.", seed) + } + + // Src: match + *f.Src.X = *params.Src.PublicKey.X + *f.Src.Y = *params.Src.PublicKey.Y + if !f.MatchMessage(msg) { + t.Fatalf("failed MatchMessage(src match) with seed %d.", seed) + } + + // insufficient PoW: mismatch + f.PoW = msg.PoW + 1.0 + if f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(insufficient PoW) with seed %d.", seed) + } + + // sufficient PoW: match + f.PoW = msg.PoW / 2 + if !f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(sufficient PoW) with seed %d.", seed) + } + + // topic mismatch + f.Topics[index][0]++ + if !f.MatchMessage(msg) { + // topic mismatch should have no affect, as topics are handled by topic matchers + t.Fatalf("failed MatchEnvelope(topic mismatch) with seed %d.", seed) + } + f.Topics[index][0]-- + + // key mismatch + prev := *f.KeyAsym.PublicKey.X + zero := *big.NewInt(0) + *f.KeyAsym.PublicKey.X = zero + if f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(key mismatch) with seed %d.", seed) + } + *f.KeyAsym.PublicKey.X = prev + + // Src absent: match + f.Src = nil + if !f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(src absent) with seed %d.", seed) + } + + // encryption method mismatch + f.KeySym = keySymOrig + f.KeyAsym = nil + if f.MatchMessage(msg) { + t.Fatalf("failed MatchEnvelope(encryption method mismatch) with seed %d.", seed) + } +} + +func cloneFilter(orig *Filter) *Filter { + var clone Filter + clone.Messages = make(map[common.Hash]*ReceivedMessage) + clone.Src = orig.Src + clone.KeyAsym = orig.KeyAsym + clone.KeySym = orig.KeySym + clone.Topics = orig.Topics + clone.PoW = orig.PoW + clone.AllowP2P = orig.AllowP2P + clone.SymKeyHash = orig.SymKeyHash + return &clone +} + +func generateCompatibeEnvelope(t *testing.T, f *Filter) *Envelope { + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + return nil + } + + params.KeySym = f.KeySym + params.Topic = BytesToTopic(f.Topics[2]) + sentMessage, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := sentMessage.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + return nil + } + return env +} + +func TestWatchers(t *testing.T) { + InitSingleTest() + + const NumFilters = 16 + const NumMessages = 256 + var i int + var j uint32 + var e *Envelope + var x, firstID string + var err error + + w := New(&Config{}) + filters := NewFilters(w) + tst := generateTestCases(t, NumFilters) + for i = 0; i < NumFilters; i++ { + tst[i].f.Src = nil + x, err = filters.Install(tst[i].f) + if err != nil { + t.Fatalf("failed to install filter with seed %d: %s.", seed, err) + } + tst[i].id = x + if len(firstID) == 0 { + firstID = x + } + } + + lastID := x + + var envelopes [NumMessages]*Envelope + for i = 0; i < NumMessages; i++ { + j = mrand.Uint32() % NumFilters + e = generateCompatibeEnvelope(t, tst[j].f) + envelopes[i] = e + tst[j].msgCnt++ + } + + for i = 0; i < NumMessages; i++ { + filters.NotifyWatchers(envelopes[i], false) + } + + var total int + var mail []*ReceivedMessage + var count [NumFilters]int + + for i = 0; i < NumFilters; i++ { + mail = tst[i].f.Retrieve() + count[i] = len(mail) + total += len(mail) + } + + if total != NumMessages { + t.Fatalf("failed with seed %d: total = %d, want: %d.", seed, total, NumMessages) + } + + for i = 0; i < NumFilters; i++ { + mail = tst[i].f.Retrieve() + if len(mail) != 0 { + t.Fatalf("failed with seed %d: i = %d.", seed, i) + } + + if tst[i].msgCnt != count[i] { + t.Fatalf("failed with seed %d: count[%d]: get %d, want %d.", seed, i, tst[i].msgCnt, count[i]) + } + } + + // another round with a cloned filter + + clone := cloneFilter(tst[0].f) + filters.Uninstall(lastID) + total = 0 + last := NumFilters - 1 + tst[last].f = clone + filters.Install(clone) + for i = 0; i < NumFilters; i++ { + tst[i].msgCnt = 0 + count[i] = 0 + } + + // make sure that the first watcher receives at least one message + e = generateCompatibeEnvelope(t, tst[0].f) + envelopes[0] = e + tst[0].msgCnt++ + for i = 1; i < NumMessages; i++ { + j = mrand.Uint32() % NumFilters + e = generateCompatibeEnvelope(t, tst[j].f) + envelopes[i] = e + tst[j].msgCnt++ + } + + for i = 0; i < NumMessages; i++ { + filters.NotifyWatchers(envelopes[i], false) + } + + for i = 0; i < NumFilters; i++ { + mail = tst[i].f.Retrieve() + count[i] = len(mail) + total += len(mail) + } + + combined := tst[0].msgCnt + tst[last].msgCnt + if total != NumMessages+count[0] { + t.Fatalf("failed with seed %d: total = %d, count[0] = %d.", seed, total, count[0]) + } + + if combined != count[0] { + t.Fatalf("failed with seed %d: combined = %d, count[0] = %d.", seed, combined, count[0]) + } + + if combined != count[last] { + t.Fatalf("failed with seed %d: combined = %d, count[last] = %d.", seed, combined, count[last]) + } + + for i = 1; i < NumFilters-1; i++ { + mail = tst[i].f.Retrieve() + if len(mail) != 0 { + t.Fatalf("failed with seed %d: i = %d.", seed, i) + } + + if tst[i].msgCnt != count[i] { + t.Fatalf("failed with seed %d: i = %d, get %d, want %d.", seed, i, tst[i].msgCnt, count[i]) + } + } + + // test AcceptP2P + + total = 0 + filters.NotifyWatchers(envelopes[0], true) + + for i = 0; i < NumFilters; i++ { + mail = tst[i].f.Retrieve() + total += len(mail) + } + + if total != 0 { + t.Fatalf("failed with seed %d: total: got %d, want 0.", seed, total) + } + + f := filters.Get(firstID) + if f == nil { + t.Fatalf("failed to get the filter with seed %d.", seed) + } + f.AllowP2P = true + total = 0 + filters.NotifyWatchers(envelopes[0], true) + + for i = 0; i < NumFilters; i++ { + mail = tst[i].f.Retrieve() + total += len(mail) + } + + if total != 1 { + t.Fatalf("failed with seed %d: total: got %d, want 1.", seed, total) + } +} + +func TestVariableTopics(t *testing.T) { + InitSingleTest() + + const lastTopicByte = 3 + var match bool + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + f, err := generateFilter(t, true) + if err != nil { + t.Fatalf("failed generateFilter with seed %d: %s.", seed, err) + } + + for i := 0; i < 4; i++ { + env.Topic = BytesToTopic(f.Topics[i]) + match = f.MatchEnvelope(env) + if !match { + t.Fatalf("failed MatchEnvelope symmetric with seed %d, step %d.", seed, i) + } + + f.Topics[i][lastTopicByte]++ + match = f.MatchEnvelope(env) + if !match { + // topic mismatch should have no affect, as topics are handled by topic matchers + t.Fatalf("MatchEnvelope symmetric with seed %d, step %d.", seed, i) + } + } +} diff --git a/whisper/whisperv6/gen_criteria_json.go b/whisper/whisperv6/gen_criteria_json.go new file mode 100644 index 0000000000..dd8b69feda --- /dev/null +++ b/whisper/whisperv6/gen_criteria_json.go @@ -0,0 +1,66 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package whisperv6 + +import ( + "encoding/json" + + "github.com/cryptoecc/ETH-ECC/common/hexutil" +) + +var _ = (*criteriaOverride)(nil) + +// MarshalJSON marshals type Criteria to a json string +func (c Criteria) MarshalJSON() ([]byte, error) { + type Criteria struct { + SymKeyID string `json:"symKeyID"` + PrivateKeyID string `json:"privateKeyID"` + Sig hexutil.Bytes `json:"sig"` + MinPow float64 `json:"minPow"` + Topics []TopicType `json:"topics"` + AllowP2P bool `json:"allowP2P"` + } + var enc Criteria + enc.SymKeyID = c.SymKeyID + enc.PrivateKeyID = c.PrivateKeyID + enc.Sig = c.Sig + enc.MinPow = c.MinPow + enc.Topics = c.Topics + enc.AllowP2P = c.AllowP2P + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals type Criteria to a json string +func (c *Criteria) UnmarshalJSON(input []byte) error { + type Criteria struct { + SymKeyID *string `json:"symKeyID"` + PrivateKeyID *string `json:"privateKeyID"` + Sig *hexutil.Bytes `json:"sig"` + MinPow *float64 `json:"minPow"` + Topics []TopicType `json:"topics"` + AllowP2P *bool `json:"allowP2P"` + } + var dec Criteria + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.SymKeyID != nil { + c.SymKeyID = *dec.SymKeyID + } + if dec.PrivateKeyID != nil { + c.PrivateKeyID = *dec.PrivateKeyID + } + if dec.Sig != nil { + c.Sig = *dec.Sig + } + if dec.MinPow != nil { + c.MinPow = *dec.MinPow + } + if dec.Topics != nil { + c.Topics = dec.Topics + } + if dec.AllowP2P != nil { + c.AllowP2P = *dec.AllowP2P + } + return nil +} diff --git a/whisper/whisperv6/gen_message_json.go b/whisper/whisperv6/gen_message_json.go new file mode 100644 index 0000000000..01e949839f --- /dev/null +++ b/whisper/whisperv6/gen_message_json.go @@ -0,0 +1,84 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package whisperv6 + +import ( + "encoding/json" + + "github.com/cryptoecc/ETH-ECC/common/hexutil" +) + +var _ = (*messageOverride)(nil) + +// MarshalJSON marshals type Message to a json string +func (m Message) MarshalJSON() ([]byte, error) { + type Message struct { + Sig hexutil.Bytes `json:"sig,omitempty"` + TTL uint32 `json:"ttl"` + Timestamp uint32 `json:"timestamp"` + Topic TopicType `json:"topic"` + Payload hexutil.Bytes `json:"payload"` + Padding hexutil.Bytes `json:"padding"` + PoW float64 `json:"pow"` + Hash hexutil.Bytes `json:"hash"` + Dst hexutil.Bytes `json:"recipientPublicKey,omitempty"` + } + var enc Message + enc.Sig = m.Sig + enc.TTL = m.TTL + enc.Timestamp = m.Timestamp + enc.Topic = m.Topic + enc.Payload = m.Payload + enc.Padding = m.Padding + enc.PoW = m.PoW + enc.Hash = m.Hash + enc.Dst = m.Dst + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals type Message to a json string +func (m *Message) UnmarshalJSON(input []byte) error { + type Message struct { + Sig *hexutil.Bytes `json:"sig,omitempty"` + TTL *uint32 `json:"ttl"` + Timestamp *uint32 `json:"timestamp"` + Topic *TopicType `json:"topic"` + Payload *hexutil.Bytes `json:"payload"` + Padding *hexutil.Bytes `json:"padding"` + PoW *float64 `json:"pow"` + Hash *hexutil.Bytes `json:"hash"` + Dst *hexutil.Bytes `json:"recipientPublicKey,omitempty"` + } + var dec Message + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Sig != nil { + m.Sig = *dec.Sig + } + if dec.TTL != nil { + m.TTL = *dec.TTL + } + if dec.Timestamp != nil { + m.Timestamp = *dec.Timestamp + } + if dec.Topic != nil { + m.Topic = *dec.Topic + } + if dec.Payload != nil { + m.Payload = *dec.Payload + } + if dec.Padding != nil { + m.Padding = *dec.Padding + } + if dec.PoW != nil { + m.PoW = *dec.PoW + } + if dec.Hash != nil { + m.Hash = *dec.Hash + } + if dec.Dst != nil { + m.Dst = *dec.Dst + } + return nil +} diff --git a/whisper/whisperv6/gen_newmessage_json.go b/whisper/whisperv6/gen_newmessage_json.go new file mode 100644 index 0000000000..c7f0f6824a --- /dev/null +++ b/whisper/whisperv6/gen_newmessage_json.go @@ -0,0 +1,90 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package whisperv6 + +import ( + "encoding/json" + + "github.com/cryptoecc/ETH-ECC/common/hexutil" +) + +var _ = (*newMessageOverride)(nil) + +// MarshalJSON marshals type NewMessage to a json string +func (n NewMessage) MarshalJSON() ([]byte, error) { + type NewMessage struct { + SymKeyID string `json:"symKeyID"` + PublicKey hexutil.Bytes `json:"pubKey"` + Sig string `json:"sig"` + TTL uint32 `json:"ttl"` + Topic TopicType `json:"topic"` + Payload hexutil.Bytes `json:"payload"` + Padding hexutil.Bytes `json:"padding"` + PowTime uint32 `json:"powTime"` + PowTarget float64 `json:"powTarget"` + TargetPeer string `json:"targetPeer"` + } + var enc NewMessage + enc.SymKeyID = n.SymKeyID + enc.PublicKey = n.PublicKey + enc.Sig = n.Sig + enc.TTL = n.TTL + enc.Topic = n.Topic + enc.Payload = n.Payload + enc.Padding = n.Padding + enc.PowTime = n.PowTime + enc.PowTarget = n.PowTarget + enc.TargetPeer = n.TargetPeer + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals type NewMessage to a json string +func (n *NewMessage) UnmarshalJSON(input []byte) error { + type NewMessage struct { + SymKeyID *string `json:"symKeyID"` + PublicKey *hexutil.Bytes `json:"pubKey"` + Sig *string `json:"sig"` + TTL *uint32 `json:"ttl"` + Topic *TopicType `json:"topic"` + Payload *hexutil.Bytes `json:"payload"` + Padding *hexutil.Bytes `json:"padding"` + PowTime *uint32 `json:"powTime"` + PowTarget *float64 `json:"powTarget"` + TargetPeer *string `json:"targetPeer"` + } + var dec NewMessage + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.SymKeyID != nil { + n.SymKeyID = *dec.SymKeyID + } + if dec.PublicKey != nil { + n.PublicKey = *dec.PublicKey + } + if dec.Sig != nil { + n.Sig = *dec.Sig + } + if dec.TTL != nil { + n.TTL = *dec.TTL + } + if dec.Topic != nil { + n.Topic = *dec.Topic + } + if dec.Payload != nil { + n.Payload = *dec.Payload + } + if dec.Padding != nil { + n.Padding = *dec.Padding + } + if dec.PowTime != nil { + n.PowTime = *dec.PowTime + } + if dec.PowTarget != nil { + n.PowTarget = *dec.PowTarget + } + if dec.TargetPeer != nil { + n.TargetPeer = *dec.TargetPeer + } + return nil +} diff --git a/whisper/whisperv6/message.go b/whisper/whisperv6/message.go new file mode 100644 index 0000000000..1a4bce1e29 --- /dev/null +++ b/whisper/whisperv6/message.go @@ -0,0 +1,355 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Contains the Whisper protocol Message element. + +package whisperv6 + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + crand "crypto/rand" + "encoding/binary" + "errors" + mrand "math/rand" + "strconv" + + "github.com/cryptoecc/ETH-ECC/common" + "github.com/cryptoecc/ETH-ECC/crypto" + "github.com/cryptoecc/ETH-ECC/crypto/ecies" + "github.com/cryptoecc/ETH-ECC/log" +) + +// MessageParams specifies the exact way a message should be wrapped +// into an Envelope. +type MessageParams struct { + TTL uint32 + Src *ecdsa.PrivateKey + Dst *ecdsa.PublicKey + KeySym []byte + Topic TopicType + WorkTime uint32 + PoW float64 + Payload []byte + Padding []byte +} + +// SentMessage represents an end-user data packet to transmit through the +// Whisper protocol. These are wrapped into Envelopes that need not be +// understood by intermediate nodes, just forwarded. +type sentMessage struct { + Raw []byte +} + +// ReceivedMessage represents a data packet to be received through the +// Whisper protocol and successfully decrypted. +type ReceivedMessage struct { + Raw []byte + + Payload []byte + Padding []byte + Signature []byte + Salt []byte + + PoW float64 // Proof of work as described in the Whisper spec + Sent uint32 // Time when the message was posted into the network + TTL uint32 // Maximum time to live allowed for the message + Src *ecdsa.PublicKey // Message recipient (identity used to decode the message) + Dst *ecdsa.PublicKey // Message recipient (identity used to decode the message) + Topic TopicType + + SymKeyHash common.Hash // The Keccak256Hash of the key + EnvelopeHash common.Hash // Message envelope hash to act as a unique id +} + +func isMessageSigned(flags byte) bool { + return (flags & signatureFlag) != 0 +} + +func (msg *ReceivedMessage) isSymmetricEncryption() bool { + return msg.SymKeyHash != common.Hash{} +} + +func (msg *ReceivedMessage) isAsymmetricEncryption() bool { + return msg.Dst != nil +} + +// NewSentMessage creates and initializes a non-signed, non-encrypted Whisper message. +func NewSentMessage(params *MessageParams) (*sentMessage, error) { + const payloadSizeFieldMaxSize = 4 + msg := sentMessage{} + msg.Raw = make([]byte, 1, + flagsLength+payloadSizeFieldMaxSize+len(params.Payload)+len(params.Padding)+signatureLength+padSizeLimit) + msg.Raw[0] = 0 // set all the flags to zero + msg.addPayloadSizeField(params.Payload) + msg.Raw = append(msg.Raw, params.Payload...) + err := msg.appendPadding(params) + return &msg, err +} + +// addPayloadSizeField appends the auxiliary field containing the size of payload +func (msg *sentMessage) addPayloadSizeField(payload []byte) { + fieldSize := getSizeOfPayloadSizeField(payload) + field := make([]byte, 4) + binary.LittleEndian.PutUint32(field, uint32(len(payload))) + field = field[:fieldSize] + msg.Raw = append(msg.Raw, field...) + msg.Raw[0] |= byte(fieldSize) +} + +// getSizeOfPayloadSizeField returns the number of bytes necessary to encode the size of payload +func getSizeOfPayloadSizeField(payload []byte) int { + s := 1 + for i := len(payload); i >= 256; i /= 256 { + s++ + } + return s +} + +// appendPadding appends the padding specified in params. +// If no padding is provided in params, then random padding is generated. +func (msg *sentMessage) appendPadding(params *MessageParams) error { + if len(params.Padding) != 0 { + // padding data was provided by the Dapp, just use it as is + msg.Raw = append(msg.Raw, params.Padding...) + return nil + } + + rawSize := flagsLength + getSizeOfPayloadSizeField(params.Payload) + len(params.Payload) + if params.Src != nil { + rawSize += signatureLength + } + odd := rawSize % padSizeLimit + paddingSize := padSizeLimit - odd + pad := make([]byte, paddingSize) + _, err := crand.Read(pad) + if err != nil { + return err + } + if !validateDataIntegrity(pad, paddingSize) { + return errors.New("failed to generate random padding of size " + strconv.Itoa(paddingSize)) + } + msg.Raw = append(msg.Raw, pad...) + return nil +} + +// sign calculates and sets the cryptographic signature for the message, +// also setting the sign flag. +func (msg *sentMessage) sign(key *ecdsa.PrivateKey) error { + if isMessageSigned(msg.Raw[0]) { + // this should not happen, but no reason to panic + log.Error("failed to sign the message: already signed") + return nil + } + + msg.Raw[0] |= signatureFlag // it is important to set this flag before signing + hash := crypto.Keccak256(msg.Raw) + signature, err := crypto.Sign(hash, key) + if err != nil { + msg.Raw[0] &= (0xFF ^ signatureFlag) // clear the flag + return err + } + msg.Raw = append(msg.Raw, signature...) + return nil +} + +// encryptAsymmetric encrypts a message with a public key. +func (msg *sentMessage) encryptAsymmetric(key *ecdsa.PublicKey) error { + if !ValidatePublicKey(key) { + return errors.New("invalid public key provided for asymmetric encryption") + } + encrypted, err := ecies.Encrypt(crand.Reader, ecies.ImportECDSAPublic(key), msg.Raw, nil, nil) + if err == nil { + msg.Raw = encrypted + } + return err +} + +// encryptSymmetric encrypts a message with a topic key, using AES-GCM-256. +// nonce size should be 12 bytes (see cipher.gcmStandardNonceSize). +func (msg *sentMessage) encryptSymmetric(key []byte) (err error) { + if !validateDataIntegrity(key, aesKeyLength) { + return errors.New("invalid key provided for symmetric encryption, size: " + strconv.Itoa(len(key))) + } + block, err := aes.NewCipher(key) + if err != nil { + return err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return err + } + salt, err := generateSecureRandomData(aesNonceLength) // never use more than 2^32 random nonces with a given key + if err != nil { + return err + } + encrypted := aesgcm.Seal(nil, salt, msg.Raw, nil) + msg.Raw = append(encrypted, salt...) + return nil +} + +// generateSecureRandomData generates random data where extra security is required. +// The purpose of this function is to prevent some bugs in software or in hardware +// from delivering not-very-random data. This is especially useful for AES nonce, +// where true randomness does not really matter, but it is very important to have +// a unique nonce for every message. +func generateSecureRandomData(length int) ([]byte, error) { + x := make([]byte, length) + y := make([]byte, length) + res := make([]byte, length) + + _, err := crand.Read(x) + if err != nil { + return nil, err + } else if !validateDataIntegrity(x, length) { + return nil, errors.New("crypto/rand failed to generate secure random data") + } + _, err = mrand.Read(y) + if err != nil { + return nil, err + } else if !validateDataIntegrity(y, length) { + return nil, errors.New("math/rand failed to generate secure random data") + } + for i := 0; i < length; i++ { + res[i] = x[i] ^ y[i] + } + if !validateDataIntegrity(res, length) { + return nil, errors.New("failed to generate secure random data") + } + return res, nil +} + +// Wrap bundles the message into an Envelope to transmit over the network. +func (msg *sentMessage) Wrap(options *MessageParams) (envelope *Envelope, err error) { + if options.TTL == 0 { + options.TTL = DefaultTTL + } + if options.Src != nil { + if err = msg.sign(options.Src); err != nil { + return nil, err + } + } + if options.Dst != nil { + err = msg.encryptAsymmetric(options.Dst) + } else if options.KeySym != nil { + err = msg.encryptSymmetric(options.KeySym) + } else { + err = errors.New("unable to encrypt the message: neither symmetric nor assymmetric key provided") + } + if err != nil { + return nil, err + } + + envelope = NewEnvelope(options.TTL, options.Topic, msg) + if err = envelope.Seal(options); err != nil { + return nil, err + } + return envelope, nil +} + +// decryptSymmetric decrypts a message with a topic key, using AES-GCM-256. +// nonce size should be 12 bytes (see cipher.gcmStandardNonceSize). +func (msg *ReceivedMessage) decryptSymmetric(key []byte) error { + // symmetric messages are expected to contain the 12-byte nonce at the end of the payload + if len(msg.Raw) < aesNonceLength { + return errors.New("missing salt or invalid payload in symmetric message") + } + salt := msg.Raw[len(msg.Raw)-aesNonceLength:] + + block, err := aes.NewCipher(key) + if err != nil { + return err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return err + } + decrypted, err := aesgcm.Open(nil, salt, msg.Raw[:len(msg.Raw)-aesNonceLength], nil) + if err != nil { + return err + } + msg.Raw = decrypted + msg.Salt = salt + return nil +} + +// decryptAsymmetric decrypts an encrypted payload with a private key. +func (msg *ReceivedMessage) decryptAsymmetric(key *ecdsa.PrivateKey) error { + decrypted, err := ecies.ImportECDSA(key).Decrypt(msg.Raw, nil, nil) + if err == nil { + msg.Raw = decrypted + } + return err +} + +// ValidateAndParse checks the message validity and extracts the fields in case of success. +func (msg *ReceivedMessage) ValidateAndParse() bool { + end := len(msg.Raw) + if end < 1 { + return false + } + + if isMessageSigned(msg.Raw[0]) { + end -= signatureLength + if end <= 1 { + return false + } + msg.Signature = msg.Raw[end : end+signatureLength] + msg.Src = msg.SigToPubKey() + if msg.Src == nil { + return false + } + } + + beg := 1 + payloadSize := 0 + sizeOfPayloadSizeField := int(msg.Raw[0] & SizeMask) // number of bytes indicating the size of payload + if sizeOfPayloadSizeField != 0 { + payloadSize = int(bytesToUintLittleEndian(msg.Raw[beg : beg+sizeOfPayloadSizeField])) + if payloadSize+1 > end { + return false + } + beg += sizeOfPayloadSizeField + msg.Payload = msg.Raw[beg : beg+payloadSize] + } + + beg += payloadSize + msg.Padding = msg.Raw[beg:end] + return true +} + +// SigToPubKey returns the public key associated to the message's +// signature. +func (msg *ReceivedMessage) SigToPubKey() *ecdsa.PublicKey { + defer func() { recover() }() // in case of invalid signature + + pub, err := crypto.SigToPub(msg.hash(), msg.Signature) + if err != nil { + log.Error("failed to recover public key from signature", "err", err) + return nil + } + return pub +} + +// hash calculates the SHA3 checksum of the message flags, payload size field, payload and padding. +func (msg *ReceivedMessage) hash() []byte { + if isMessageSigned(msg.Raw[0]) { + sz := len(msg.Raw) - signatureLength + return crypto.Keccak256(msg.Raw[:sz]) + } + return crypto.Keccak256(msg.Raw) +} diff --git a/whisper/whisperv6/message_test.go b/whisper/whisperv6/message_test.go new file mode 100644 index 0000000000..9e5e0b60bc --- /dev/null +++ b/whisper/whisperv6/message_test.go @@ -0,0 +1,471 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + mrand "math/rand" + "testing" + + "github.com/cryptoecc/ETH-ECC/common/hexutil" + "github.com/cryptoecc/ETH-ECC/crypto" + "github.com/cryptoecc/ETH-ECC/rlp" +) + +func generateMessageParams() (*MessageParams, error) { + // set all the parameters except p.Dst and p.Padding + + buf := make([]byte, 4) + mrand.Read(buf) + sz := mrand.Intn(400) + + var p MessageParams + p.PoW = 0.01 + p.WorkTime = 1 + p.TTL = uint32(mrand.Intn(1024)) + p.Payload = make([]byte, sz) + p.KeySym = make([]byte, aesKeyLength) + mrand.Read(p.Payload) + mrand.Read(p.KeySym) + p.Topic = BytesToTopic(buf) + + var err error + p.Src, err = crypto.GenerateKey() + if err != nil { + return nil, err + } + + return &p, nil +} + +func singleMessageTest(t *testing.T, symmetric bool) { + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed GenerateKey with seed %d: %s.", seed, err) + } + + if !symmetric { + params.KeySym = nil + params.Dst = &key.PublicKey + } + + text := make([]byte, 0, 512) + text = append(text, params.Payload...) + + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + var decrypted *ReceivedMessage + if symmetric { + decrypted, err = env.OpenSymmetric(params.KeySym) + } else { + decrypted, err = env.OpenAsymmetric(key) + } + + if err != nil { + t.Fatalf("failed to encrypt with seed %d: %s.", seed, err) + } + + if !decrypted.ValidateAndParse() { + t.Fatalf("failed to validate with seed %d, symmetric = %v.", seed, symmetric) + } + + if !bytes.Equal(text, decrypted.Payload) { + t.Fatalf("failed with seed %d: compare payload.", seed) + } + if !isMessageSigned(decrypted.Raw[0]) { + t.Fatalf("failed with seed %d: unsigned.", seed) + } + if len(decrypted.Signature) != signatureLength { + t.Fatalf("failed with seed %d: signature len %d.", seed, len(decrypted.Signature)) + } + if !IsPubKeyEqual(decrypted.Src, ¶ms.Src.PublicKey) { + t.Fatalf("failed with seed %d: signature mismatch.", seed) + } +} + +func TestMessageEncryption(t *testing.T) { + InitSingleTest() + + var symmetric bool + for i := 0; i < 256; i++ { + singleMessageTest(t, symmetric) + symmetric = !symmetric + } +} + +func TestMessageWrap(t *testing.T) { + seed = int64(1777444222) + mrand.Seed(seed) + target := 128.0 + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + params.TTL = 1 + params.WorkTime = 12 + params.PoW = target + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + pow := env.PoW() + if pow < target { + t.Fatalf("failed Wrap with seed %d: pow < target (%f vs. %f).", seed, pow, target) + } + + // set PoW target too high, expect error + msg2, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + params.TTL = 1000000 + params.WorkTime = 1 + params.PoW = 10000000.0 + _, err = msg2.Wrap(params) + if err == nil { + t.Fatalf("unexpectedly reached the PoW target with seed %d.", seed) + } +} + +func TestMessageSeal(t *testing.T) { + // this test depends on deterministic choice of seed (1976726903) + seed = int64(1976726903) + mrand.Seed(seed) + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + params.TTL = 1 + + env := NewEnvelope(params.TTL, params.Topic, msg) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + env.Expiry = uint32(seed) // make it deterministic + target := 32.0 + params.WorkTime = 4 + params.PoW = target + env.Seal(params) + + env.calculatePoW(0) + pow := env.PoW() + if pow < target { + t.Fatalf("failed Wrap with seed %d: pow < target (%f vs. %f).", seed, pow, target) + } + + params.WorkTime = 1 + params.PoW = 1000000000.0 + env.Seal(params) + env.calculatePoW(0) + pow = env.PoW() + if pow < 2*target { + t.Fatalf("failed Wrap with seed %d: pow too small %f.", seed, pow) + } +} + +func TestEnvelopeOpen(t *testing.T) { + InitSingleTest() + + var symmetric bool + for i := 0; i < 32; i++ { + singleEnvelopeOpenTest(t, symmetric) + symmetric = !symmetric + } +} + +func singleEnvelopeOpenTest(t *testing.T, symmetric bool) { + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed GenerateKey with seed %d: %s.", seed, err) + } + + if !symmetric { + params.KeySym = nil + params.Dst = &key.PublicKey + } + + text := make([]byte, 0, 512) + text = append(text, params.Payload...) + + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + var f Filter + if symmetric { + f = Filter{KeySym: params.KeySym} + } else { + f = Filter{KeyAsym: key} + } + decrypted := env.Open(&f) + if decrypted == nil { + t.Fatalf("failed to open with seed %d.", seed) + } + + if !bytes.Equal(text, decrypted.Payload) { + t.Fatalf("failed with seed %d: compare payload.", seed) + } + if !isMessageSigned(decrypted.Raw[0]) { + t.Fatalf("failed with seed %d: unsigned.", seed) + } + if len(decrypted.Signature) != signatureLength { + t.Fatalf("failed with seed %d: signature len %d.", seed, len(decrypted.Signature)) + } + if !IsPubKeyEqual(decrypted.Src, ¶ms.Src.PublicKey) { + t.Fatalf("failed with seed %d: signature mismatch.", seed) + } + if decrypted.isAsymmetricEncryption() == symmetric { + t.Fatalf("failed with seed %d: asymmetric %v vs. %v.", seed, decrypted.isAsymmetricEncryption(), symmetric) + } + if decrypted.isSymmetricEncryption() != symmetric { + t.Fatalf("failed with seed %d: symmetric %v vs. %v.", seed, decrypted.isSymmetricEncryption(), symmetric) + } + if !symmetric { + if decrypted.Dst == nil { + t.Fatalf("failed with seed %d: dst is nil.", seed) + } + if !IsPubKeyEqual(decrypted.Dst, &key.PublicKey) { + t.Fatalf("failed with seed %d: Dst.", seed) + } + } +} + +func TestEncryptWithZeroKey(t *testing.T) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + params.KeySym = make([]byte, aesKeyLength) + _, err = msg.Wrap(params) + if err == nil { + t.Fatalf("wrapped with zero key, seed: %d.", seed) + } + + params, err = generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + msg, err = NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + params.KeySym = make([]byte, 0) + _, err = msg.Wrap(params) + if err == nil { + t.Fatalf("wrapped with empty key, seed: %d.", seed) + } + + params, err = generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + msg, err = NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + params.KeySym = nil + _, err = msg.Wrap(params) + if err == nil { + t.Fatalf("wrapped with nil key, seed: %d.", seed) + } +} + +func TestRlpEncode(t *testing.T) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("wrapped with zero key, seed: %d.", seed) + } + + raw, err := rlp.EncodeToBytes(env) + if err != nil { + t.Fatalf("RLP encode failed: %s.", err) + } + + var decoded Envelope + rlp.DecodeBytes(raw, &decoded) + if err != nil { + t.Fatalf("RLP decode failed: %s.", err) + } + + he := env.Hash() + hd := decoded.Hash() + + if he != hd { + t.Fatalf("Hashes are not equal: %x vs. %x", he, hd) + } +} + +func singlePaddingTest(t *testing.T, padSize int) { + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d and sz=%d: %s.", seed, padSize, err) + } + params.Padding = make([]byte, padSize) + params.PoW = 0.0000000001 + pad := make([]byte, padSize) + _, err = mrand.Read(pad) + if err != nil { + t.Fatalf("padding is not generated (seed %d): %s", seed, err) + } + n := copy(params.Padding, pad) + if n != padSize { + t.Fatalf("padding is not copied (seed %d): %s", seed, err) + } + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed to wrap, seed: %d and sz=%d.", seed, padSize) + } + f := Filter{KeySym: params.KeySym} + decrypted := env.Open(&f) + if decrypted == nil { + t.Fatalf("failed to open, seed and sz=%d: %d.", seed, padSize) + } + if !bytes.Equal(pad, decrypted.Padding) { + t.Fatalf("padding is not retireved as expected with seed %d and sz=%d:\n[%x]\n[%x].", seed, padSize, pad, decrypted.Padding) + } +} + +func TestPadding(t *testing.T) { + InitSingleTest() + + for i := 1; i < 260; i++ { + singlePaddingTest(t, i) + } + + lim := 256 * 256 + for i := lim - 5; i < lim+2; i++ { + singlePaddingTest(t, i) + } + + for i := 0; i < 256; i++ { + n := mrand.Intn(256*254) + 256 + singlePaddingTest(t, n) + } + + for i := 0; i < 256; i++ { + n := mrand.Intn(256*1024) + 256*256 + singlePaddingTest(t, n) + } +} + +func TestPaddingAppendedToSymMessagesWithSignature(t *testing.T) { + params := &MessageParams{ + Payload: make([]byte, 246), + KeySym: make([]byte, aesKeyLength), + } + + pSrc, err := crypto.GenerateKey() + + if err != nil { + t.Fatalf("Error creating the signature key %v", err) + return + } + params.Src = pSrc + + // Simulate a message with a payload just under 256 so that + // payload + flag + signature > 256. Check that the result + // is padded on the next 256 boundary. + msg := sentMessage{} + const payloadSizeFieldMinSize = 1 + msg.Raw = make([]byte, flagsLength+payloadSizeFieldMinSize+len(params.Payload)) + + err = msg.appendPadding(params) + + if err != nil { + t.Fatalf("Error appending padding to message %v", err) + return + } + + if len(msg.Raw) != 512-signatureLength { + t.Errorf("Invalid size %d != 512", len(msg.Raw)) + } +} + +func TestAesNonce(t *testing.T) { + key := hexutil.MustDecode("0x03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31") + block, err := aes.NewCipher(key) + if err != nil { + t.Fatalf("NewCipher failed: %s", err) + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + t.Fatalf("NewGCM failed: %s", err) + } + // This is the most important single test in this package. + // If it fails, whisper will not be working. + if aesgcm.NonceSize() != aesNonceLength { + t.Fatalf("Nonce size is wrong. This is a critical error. Apparently AES nonce size have changed in the new version of AES GCM package. Whisper will not be working until this problem is resolved.") + } +} diff --git a/whisper/whisperv6/peer.go b/whisper/whisperv6/peer.go new file mode 100644 index 0000000000..7ff43eb83e --- /dev/null +++ b/whisper/whisperv6/peer.go @@ -0,0 +1,259 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +import ( + "fmt" + "math" + "sync" + "time" + + "github.com/cryptoecc/ETH-ECC/common" + "github.com/cryptoecc/ETH-ECC/log" + "github.com/cryptoecc/ETH-ECC/p2p" + "github.com/cryptoecc/ETH-ECC/rlp" + mapset "github.com/deckarep/golang-set" +) + +// Peer represents a whisper protocol peer connection. +type Peer struct { + host *Whisper + peer *p2p.Peer + ws p2p.MsgReadWriter + + trusted bool + powRequirement float64 + bloomMu sync.Mutex + bloomFilter []byte + fullNode bool + + known mapset.Set // Messages already known by the peer to avoid wasting bandwidth + + quit chan struct{} +} + +// newPeer creates a new whisper peer object, but does not run the handshake itself. +func newPeer(host *Whisper, remote *p2p.Peer, rw p2p.MsgReadWriter) *Peer { + return &Peer{ + host: host, + peer: remote, + ws: rw, + trusted: false, + powRequirement: 0.0, + known: mapset.NewSet(), + quit: make(chan struct{}), + bloomFilter: MakeFullNodeBloom(), + fullNode: true, + } +} + +// start initiates the peer updater, periodically broadcasting the whisper packets +// into the network. +func (peer *Peer) start() { + go peer.update() + log.Trace("start", "peer", peer.ID()) +} + +// stop terminates the peer updater, stopping message forwarding to it. +func (peer *Peer) stop() { + close(peer.quit) + log.Trace("stop", "peer", peer.ID()) +} + +// handshake sends the protocol initiation status message to the remote peer and +// verifies the remote status too. +func (peer *Peer) handshake() error { + // Send the handshake status message asynchronously + errc := make(chan error, 1) + isLightNode := peer.host.LightClientMode() + isRestrictedLightNodeConnection := peer.host.LightClientModeConnectionRestricted() + go func() { + pow := peer.host.MinPow() + powConverted := math.Float64bits(pow) + bloom := peer.host.BloomFilter() + + errc <- p2p.SendItems(peer.ws, statusCode, ProtocolVersion, powConverted, bloom, isLightNode) + }() + + // Fetch the remote status packet and verify protocol match + packet, err := peer.ws.ReadMsg() + if err != nil { + return err + } + if packet.Code != statusCode { + return fmt.Errorf("peer [%x] sent packet %x before status packet", peer.ID(), packet.Code) + } + s := rlp.NewStream(packet.Payload, uint64(packet.Size)) + _, err = s.List() + if err != nil { + return fmt.Errorf("peer [%x] sent bad status message: %v", peer.ID(), err) + } + peerVersion, err := s.Uint() + if err != nil { + return fmt.Errorf("peer [%x] sent bad status message (unable to decode version): %v", peer.ID(), err) + } + if peerVersion != ProtocolVersion { + return fmt.Errorf("peer [%x]: protocol version mismatch %d != %d", peer.ID(), peerVersion, ProtocolVersion) + } + + // only version is mandatory, subsequent parameters are optional + powRaw, err := s.Uint() + if err == nil { + pow := math.Float64frombits(powRaw) + if math.IsInf(pow, 0) || math.IsNaN(pow) || pow < 0.0 { + return fmt.Errorf("peer [%x] sent bad status message: invalid pow", peer.ID()) + } + peer.powRequirement = pow + + var bloom []byte + err = s.Decode(&bloom) + if err == nil { + sz := len(bloom) + if sz != BloomFilterSize && sz != 0 { + return fmt.Errorf("peer [%x] sent bad status message: wrong bloom filter size %d", peer.ID(), sz) + } + peer.setBloomFilter(bloom) + } + } + + isRemotePeerLightNode, _ := s.Bool() + if isRemotePeerLightNode && isLightNode && isRestrictedLightNodeConnection { + return fmt.Errorf("peer [%x] is useless: two light client communication restricted", peer.ID()) + } + + if err := <-errc; err != nil { + return fmt.Errorf("peer [%x] failed to send status packet: %v", peer.ID(), err) + } + return nil +} + +// update executes periodic operations on the peer, including message transmission +// and expiration. +func (peer *Peer) update() { + // Start the tickers for the updates + expire := time.NewTicker(expirationCycle) + transmit := time.NewTicker(transmissionCycle) + + // Loop and transmit until termination is requested + for { + select { + case <-expire.C: + peer.expire() + + case <-transmit.C: + if err := peer.broadcast(); err != nil { + log.Trace("broadcast failed", "reason", err, "peer", peer.ID()) + return + } + + case <-peer.quit: + return + } + } +} + +// mark marks an envelope known to the peer so that it won't be sent back. +func (peer *Peer) mark(envelope *Envelope) { + peer.known.Add(envelope.Hash()) +} + +// marked checks if an envelope is already known to the remote peer. +func (peer *Peer) marked(envelope *Envelope) bool { + return peer.known.Contains(envelope.Hash()) +} + +// expire iterates over all the known envelopes in the host and removes all +// expired (unknown) ones from the known list. +func (peer *Peer) expire() { + unmark := make(map[common.Hash]struct{}) + peer.known.Each(func(v interface{}) bool { + if !peer.host.isEnvelopeCached(v.(common.Hash)) { + unmark[v.(common.Hash)] = struct{}{} + } + return true + }) + // Dump all known but no longer cached + for hash := range unmark { + peer.known.Remove(hash) + } +} + +// broadcast iterates over the collection of envelopes and transmits yet unknown +// ones over the network. +func (peer *Peer) broadcast() error { + envelopes := peer.host.Envelopes() + bundle := make([]*Envelope, 0, len(envelopes)) + for _, envelope := range envelopes { + if !peer.marked(envelope) && envelope.PoW() >= peer.powRequirement && peer.bloomMatch(envelope) { + bundle = append(bundle, envelope) + } + } + + if len(bundle) > 0 { + // transmit the batch of envelopes + if err := p2p.Send(peer.ws, messagesCode, bundle); err != nil { + return err + } + + // mark envelopes only if they were successfully sent + for _, e := range bundle { + peer.mark(e) + } + + log.Trace("broadcast", "num. messages", len(bundle)) + } + return nil +} + +// ID returns a peer's id +func (peer *Peer) ID() []byte { + id := peer.peer.ID() + return id[:] +} + +func (peer *Peer) notifyAboutPowRequirementChange(pow float64) error { + i := math.Float64bits(pow) + return p2p.Send(peer.ws, powRequirementCode, i) +} + +func (peer *Peer) notifyAboutBloomFilterChange(bloom []byte) error { + return p2p.Send(peer.ws, bloomFilterExCode, bloom) +} + +func (peer *Peer) bloomMatch(env *Envelope) bool { + peer.bloomMu.Lock() + defer peer.bloomMu.Unlock() + return peer.fullNode || BloomFilterMatch(peer.bloomFilter, env.Bloom()) +} + +func (peer *Peer) setBloomFilter(bloom []byte) { + peer.bloomMu.Lock() + defer peer.bloomMu.Unlock() + peer.bloomFilter = bloom + peer.fullNode = isFullNode(bloom) + if peer.fullNode && peer.bloomFilter == nil { + peer.bloomFilter = MakeFullNodeBloom() + } +} + +func MakeFullNodeBloom() []byte { + bloom := make([]byte, BloomFilterSize) + for i := 0; i < BloomFilterSize; i++ { + bloom[i] = 0xFF + } + return bloom +} diff --git a/whisper/whisperv6/peer_test.go b/whisper/whisperv6/peer_test.go new file mode 100644 index 0000000000..f1295d2f76 --- /dev/null +++ b/whisper/whisperv6/peer_test.go @@ -0,0 +1,568 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +import ( + "bytes" + "crypto/ecdsa" + "fmt" + mrand "math/rand" + "sync" + "sync/atomic" + "testing" + "time" + + "net" + + "github.com/cryptoecc/ETH-ECC/common" + "github.com/cryptoecc/ETH-ECC/common/hexutil" + "github.com/cryptoecc/ETH-ECC/crypto" + "github.com/cryptoecc/ETH-ECC/p2p" + "github.com/cryptoecc/ETH-ECC/p2p/enode" + "github.com/cryptoecc/ETH-ECC/p2p/nat" + "github.com/cryptoecc/ETH-ECC/rlp" +) + +var keys = []string{ + "d49dcf37238dc8a7aac57dc61b9fee68f0a97f062968978b9fafa7d1033d03a9", + "73fd6143c48e80ed3c56ea159fe7494a0b6b393a392227b422f4c3e8f1b54f98", + "119dd32adb1daa7a4c7bf77f847fb28730785aa92947edf42fdd997b54de40dc", + "deeda8709dea935bb772248a3144dea449ffcc13e8e5a1fd4ef20ce4e9c87837", + "5bd208a079633befa349441bdfdc4d85ba9bd56081525008380a63ac38a407cf", + "1d27fb4912002d58a2a42a50c97edb05c1b3dffc665dbaa42df1fe8d3d95c9b5", + "15def52800c9d6b8ca6f3066b7767a76afc7b611786c1276165fbc61636afb68", + "51be6ab4b2dc89f251ff2ace10f3c1cc65d6855f3e083f91f6ff8efdfd28b48c", + "ef1ef7441bf3c6419b162f05da6037474664f198b58db7315a6f4de52414b4a0", + "09bdf6985aabc696dc1fbeb5381aebd7a6421727343872eb2fadfc6d82486fd9", + "15d811bf2e01f99a224cdc91d0cf76cea08e8c67905c16fee9725c9be71185c4", + "2f83e45cf1baaea779789f755b7da72d8857aeebff19362dd9af31d3c9d14620", + "73f04e34ac6532b19c2aae8f8e52f38df1ac8f5cd10369f92325b9b0494b0590", + "1e2e07b69e5025537fb73770f483dc8d64f84ae3403775ef61cd36e3faf162c1", + "8963d9bbb3911aac6d30388c786756b1c423c4fbbc95d1f96ddbddf39809e43a", + "0422da85abc48249270b45d8de38a4cc3c02032ede1fcf0864a51092d58a2f1f", + "8ae5c15b0e8c7cade201fdc149831aa9b11ff626a7ffd27188886cc108ad0fa8", + "acd8f5a71d4aecfcb9ad00d32aa4bcf2a602939b6a9dd071bab443154184f805", + "a285a922125a7481600782ad69debfbcdb0316c1e97c267aff29ef50001ec045", + "28fd4eee78c6cd4bf78f39f8ab30c32c67c24a6223baa40e6f9c9a0e1de7cef5", + "c5cca0c9e6f043b288c6f1aef448ab59132dab3e453671af5d0752961f013fc7", + "46df99b051838cb6f8d1b73f232af516886bd8c4d0ee07af9a0a033c391380fd", + "c6a06a53cbaadbb432884f36155c8f3244e244881b5ee3e92e974cfa166d793f", + "783b90c75c63dc72e2f8d11b6f1b4de54d63825330ec76ee8db34f06b38ea211", + "9450038f10ca2c097a8013e5121b36b422b95b04892232f930a29292d9935611", + "e215e6246ed1cfdcf7310d4d8cdbe370f0d6a8371e4eb1089e2ae05c0e1bc10f", + "487110939ed9d64ebbc1f300adeab358bc58875faf4ca64990fbd7fe03b78f2b", + "824a70ea76ac81366da1d4f4ac39de851c8ac49dca456bb3f0a186ceefa269a5", + "ba8f34fa40945560d1006a328fe70c42e35cc3d1017e72d26864cd0d1b150f15", + "30a5dfcfd144997f428901ea88a43c8d176b19c79dde54cc58eea001aa3d246c", + "de59f7183aca39aa245ce66a05245fecfc7e2c75884184b52b27734a4a58efa2", + "92629e2ff5f0cb4f5f08fffe0f64492024d36f045b901efb271674b801095c5a", + "7184c1701569e3a4c4d2ddce691edd983b81e42e09196d332e1ae2f1e062cff4", +} + +type TestData struct { + started int64 + counter [NumNodes]int + mutex sync.RWMutex +} + +type TestNode struct { + shh *Whisper + id *ecdsa.PrivateKey + server *p2p.Server + filerID string +} + +const NumNodes = 8 // must not exceed the number of keys (32) + +var result TestData +var nodes [NumNodes]*TestNode +var sharedKey = hexutil.MustDecode("0x03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31") +var wrongKey = hexutil.MustDecode("0xf91156714d7ec88d3edc1c652c2181dbb3044e8771c683f3b30d33c12b986b11") +var sharedTopic = TopicType{0xF, 0x1, 0x2, 0} +var wrongTopic = TopicType{0, 0, 0, 0} +var expectedMessage = []byte("per aspera ad astra") +var unexpectedMessage = []byte("per rectum ad astra") +var masterBloomFilter []byte +var masterPow = 0.00000001 +var round = 1 +var debugMode = false +var prevTime time.Time +var cntPrev int + +func TestSimulation(t *testing.T) { + // create a chain of whisper nodes, + // installs the filters with shared (predefined) parameters + initialize(t) + + // each node sends one random (not decryptable) message + for i := 0; i < NumNodes; i++ { + sendMsg(t, false, i) + } + + // node #0 sends one expected (decryptable) message + sendMsg(t, true, 0) + + // check if each node have received and decrypted exactly one message + checkPropagation(t, true) + + // check if Status message was correctly decoded + checkBloomFilterExchange(t) + checkPowExchange(t) + + // send new pow and bloom exchange messages + resetParams(t) + + // node #1 sends one expected (decryptable) message + sendMsg(t, true, 1) + + // check if each node (except node #0) have received and decrypted exactly one message + checkPropagation(t, false) + + // check if corresponding protocol-level messages were correctly decoded + checkPowExchangeForNodeZero(t) + checkBloomFilterExchange(t) + + stopServers() +} + +func resetParams(t *testing.T) { + // change pow only for node zero + masterPow = 7777777.0 + nodes[0].shh.SetMinimumPoW(masterPow) + + // change bloom for all nodes + masterBloomFilter = TopicToBloom(sharedTopic) + for i := 0; i < NumNodes; i++ { + nodes[i].shh.SetBloomFilter(masterBloomFilter) + } + + round++ +} + +func initBloom(t *testing.T) { + masterBloomFilter = make([]byte, BloomFilterSize) + _, err := mrand.Read(masterBloomFilter) + if err != nil { + t.Fatalf("rand failed: %s.", err) + } + + msgBloom := TopicToBloom(sharedTopic) + masterBloomFilter = addBloom(masterBloomFilter, msgBloom) + for i := 0; i < 32; i++ { + masterBloomFilter[i] = 0xFF + } + + if !BloomFilterMatch(masterBloomFilter, msgBloom) { + t.Fatalf("bloom mismatch on initBloom.") + } +} + +func initialize(t *testing.T) { + initBloom(t) + + var err error + + for i := 0; i < NumNodes; i++ { + var node TestNode + b := make([]byte, BloomFilterSize) + copy(b, masterBloomFilter) + node.shh = New(&DefaultConfig) + node.shh.SetMinimumPoW(masterPow) + node.shh.SetBloomFilter(b) + if !bytes.Equal(node.shh.BloomFilter(), masterBloomFilter) { + t.Fatalf("bloom mismatch on init.") + } + node.shh.Start(nil) + topics := make([]TopicType, 0) + topics = append(topics, sharedTopic) + f := Filter{KeySym: sharedKey} + f.Topics = [][]byte{topics[0][:]} + node.filerID, err = node.shh.Subscribe(&f) + if err != nil { + t.Fatalf("failed to install the filter: %s.", err) + } + node.id, err = crypto.HexToECDSA(keys[i]) + if err != nil { + t.Fatalf("failed convert the key: %s.", keys[i]) + } + name := common.MakeName("whisper-go", "2.0") + + node.server = &p2p.Server{ + Config: p2p.Config{ + PrivateKey: node.id, + MaxPeers: NumNodes/2 + 1, + Name: name, + Protocols: node.shh.Protocols(), + ListenAddr: "127.0.0.1:0", + NAT: nat.Any(), + }, + } + + go startServer(t, node.server) + + nodes[i] = &node + } + + waitForServersToStart(t) + + for i := 0; i < NumNodes; i++ { + for j := 0; j < i; j++ { + peerNodeId := nodes[j].id + address, _ := net.ResolveTCPAddr("tcp", nodes[j].server.ListenAddr) + peer := enode.NewV4(&peerNodeId.PublicKey, address.IP, address.Port, address.Port) + nodes[i].server.AddPeer(peer) + } + } +} + +func startServer(t *testing.T, s *p2p.Server) { + err := s.Start() + if err != nil { + t.Fatalf("failed to start the first server. err: %v", err) + } + + atomic.AddInt64(&result.started, 1) +} + +func stopServers() { + for i := 0; i < NumNodes; i++ { + n := nodes[i] + if n != nil { + n.shh.Unsubscribe(n.filerID) + n.shh.Stop() + n.server.Stop() + } + } +} + +func checkPropagation(t *testing.T, includingNodeZero bool) { + if t.Failed() { + return + } + + prevTime = time.Now() + // (cycle * iterations) should not exceed 50 seconds, since TTL=50 + const cycle = 200 // time in milliseconds + const iterations = 250 + + first := 0 + if !includingNodeZero { + first = 1 + } + + for j := 0; j < iterations; j++ { + for i := first; i < NumNodes; i++ { + f := nodes[i].shh.GetFilter(nodes[i].filerID) + if f == nil { + t.Fatalf("failed to get filterId %s from node %d, round %d.", nodes[i].filerID, i, round) + } + + mail := f.Retrieve() + validateMail(t, i, mail) + + if isTestComplete() { + checkTestStatus() + return + } + } + + checkTestStatus() + time.Sleep(cycle * time.Millisecond) + } + + if !includingNodeZero { + f := nodes[0].shh.GetFilter(nodes[0].filerID) + if f != nil { + t.Fatalf("node zero received a message with low PoW.") + } + } + + t.Fatalf("Test was not complete (%d round): timeout %d seconds. nodes=%v", round, iterations*cycle/1000, nodes) +} + +func validateMail(t *testing.T, index int, mail []*ReceivedMessage) { + var cnt int + for _, m := range mail { + if bytes.Equal(m.Payload, expectedMessage) { + cnt++ + } + } + + if cnt == 0 { + // no messages received yet: nothing is wrong + return + } + if cnt > 1 { + t.Fatalf("node %d received %d.", index, cnt) + } + + if cnt == 1 { + result.mutex.Lock() + defer result.mutex.Unlock() + result.counter[index] += cnt + if result.counter[index] > 1 { + t.Fatalf("node %d accumulated %d.", index, result.counter[index]) + } + } +} + +func checkTestStatus() { + var cnt int + var arr [NumNodes]int + + for i := 0; i < NumNodes; i++ { + arr[i] = nodes[i].server.PeerCount() + envelopes := nodes[i].shh.Envelopes() + if len(envelopes) >= NumNodes { + cnt++ + } + } + + if debugMode { + if cntPrev != cnt { + fmt.Printf(" %v \t number of nodes that have received all msgs: %d, number of peers per node: %v \n", + time.Since(prevTime), cnt, arr) + prevTime = time.Now() + cntPrev = cnt + } + } +} + +func isTestComplete() bool { + result.mutex.RLock() + defer result.mutex.RUnlock() + + for i := 0; i < NumNodes; i++ { + if result.counter[i] < 1 { + return false + } + } + + for i := 0; i < NumNodes; i++ { + envelopes := nodes[i].shh.Envelopes() + if len(envelopes) < NumNodes+1 { + return false + } + } + + return true +} + +func sendMsg(t *testing.T, expected bool, id int) { + if t.Failed() { + return + } + + opt := MessageParams{KeySym: sharedKey, Topic: sharedTopic, Payload: expectedMessage, PoW: 0.00000001, WorkTime: 1} + if !expected { + opt.KeySym = wrongKey + opt.Topic = wrongTopic + opt.Payload = unexpectedMessage + opt.Payload[0] = byte(id) + } + + msg, err := NewSentMessage(&opt) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + envelope, err := msg.Wrap(&opt) + if err != nil { + t.Fatalf("failed to seal message: %s", err) + } + + err = nodes[id].shh.Send(envelope) + if err != nil { + t.Fatalf("failed to send message: %s", err) + } +} + +func TestPeerBasic(t *testing.T) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d.", seed) + } + + params.PoW = 0.001 + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d.", seed) + } + + p := newPeer(nil, nil, nil) + p.mark(env) + if !p.marked(env) { + t.Fatalf("failed mark with seed %d.", seed) + } +} + +func checkPowExchangeForNodeZero(t *testing.T) { + const iterations = 200 + for j := 0; j < iterations; j++ { + lastCycle := (j == iterations-1) + ok := checkPowExchangeForNodeZeroOnce(t, lastCycle) + if ok { + break + } + time.Sleep(50 * time.Millisecond) + } +} + +func checkPowExchangeForNodeZeroOnce(t *testing.T, mustPass bool) bool { + cnt := 0 + for i, node := range nodes { + for peer := range node.shh.peers { + if peer.peer.ID() == nodes[0].server.Self().ID() { + cnt++ + if peer.powRequirement != masterPow { + if mustPass { + t.Fatalf("node %d: failed to set the new pow requirement for node zero.", i) + } else { + return false + } + } + } + } + } + if cnt == 0 { + t.Fatalf("looking for node zero: no matching peers found.") + } + return true +} + +func checkPowExchange(t *testing.T) { + for i, node := range nodes { + for peer := range node.shh.peers { + if peer.peer.ID() != nodes[0].server.Self().ID() { + if peer.powRequirement != masterPow { + t.Fatalf("node %d: failed to exchange pow requirement in round %d; expected %f, got %f", + i, round, masterPow, peer.powRequirement) + } + } + } + } +} + +func checkBloomFilterExchangeOnce(t *testing.T, mustPass bool) bool { + for i, node := range nodes { + for peer := range node.shh.peers { + peer.bloomMu.Lock() + equals := bytes.Equal(peer.bloomFilter, masterBloomFilter) + peer.bloomMu.Unlock() + if !equals { + if mustPass { + t.Fatalf("node %d: failed to exchange bloom filter requirement in round %d. \n%x expected \n%x got", + i, round, masterBloomFilter, peer.bloomFilter) + } else { + return false + } + } + } + } + + return true +} + +func checkBloomFilterExchange(t *testing.T) { + const iterations = 200 + for j := 0; j < iterations; j++ { + lastCycle := (j == iterations-1) + ok := checkBloomFilterExchangeOnce(t, lastCycle) + if ok { + break + } + time.Sleep(50 * time.Millisecond) + } +} + +func waitForServersToStart(t *testing.T) { + const iterations = 200 + var started int64 + for j := 0; j < iterations; j++ { + time.Sleep(50 * time.Millisecond) + started = atomic.LoadInt64(&result.started) + if started == NumNodes { + return + } + } + t.Fatalf("Failed to start all the servers, running: %d", started) +} + +//two generic whisper node handshake +func TestPeerHandshakeWithTwoFullNode(t *testing.T) { + w1 := Whisper{} + p1 := newPeer(&w1, p2p.NewPeer(enode.ID{}, "test", []p2p.Cap{}), &rwStub{[]interface{}{ProtocolVersion, uint64(123), make([]byte, BloomFilterSize), false}}) + err := p1.handshake() + if err != nil { + t.Fatal() + } +} + +//two generic whisper node handshake. one don't send light flag +func TestHandshakeWithOldVersionWithoutLightModeFlag(t *testing.T) { + w1 := Whisper{} + p1 := newPeer(&w1, p2p.NewPeer(enode.ID{}, "test", []p2p.Cap{}), &rwStub{[]interface{}{ProtocolVersion, uint64(123), make([]byte, BloomFilterSize)}}) + err := p1.handshake() + if err != nil { + t.Fatal() + } +} + +//two light nodes handshake. restriction disabled +func TestTwoLightPeerHandshakeRestrictionOff(t *testing.T) { + w1 := Whisper{} + w1.settings.Store(restrictConnectionBetweenLightClientsIdx, false) + w1.SetLightClientMode(true) + p1 := newPeer(&w1, p2p.NewPeer(enode.ID{}, "test", []p2p.Cap{}), &rwStub{[]interface{}{ProtocolVersion, uint64(123), make([]byte, BloomFilterSize), true}}) + err := p1.handshake() + if err != nil { + t.FailNow() + } +} + +//two light nodes handshake. restriction enabled +func TestTwoLightPeerHandshakeError(t *testing.T) { + w1 := Whisper{} + w1.settings.Store(restrictConnectionBetweenLightClientsIdx, true) + w1.SetLightClientMode(true) + p1 := newPeer(&w1, p2p.NewPeer(enode.ID{}, "test", []p2p.Cap{}), &rwStub{[]interface{}{ProtocolVersion, uint64(123), make([]byte, BloomFilterSize), true}}) + err := p1.handshake() + if err == nil { + t.FailNow() + } +} + +type rwStub struct { + payload []interface{} +} + +func (stub *rwStub) ReadMsg() (p2p.Msg, error) { + size, r, err := rlp.EncodeToReader(stub.payload) + if err != nil { + return p2p.Msg{}, err + } + return p2p.Msg{Code: statusCode, Size: uint32(size), Payload: r}, nil +} + +func (stub *rwStub) WriteMsg(m p2p.Msg) error { + return nil +} diff --git a/whisper/whisperv6/topic.go b/whisper/whisperv6/topic.go new file mode 100644 index 0000000000..fe1ca847c6 --- /dev/null +++ b/whisper/whisperv6/topic.go @@ -0,0 +1,57 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Contains the Whisper protocol Topic element. + +package whisperv6 + +import ( + "github.com/cryptoecc/ETH-ECC/common" + "github.com/cryptoecc/ETH-ECC/common/hexutil" +) + +// TopicType represents a cryptographically secure, probabilistic partial +// classifications of a message, determined as the first (left) 4 bytes of the +// SHA3 hash of some arbitrary data given by the original author of the message. +type TopicType [TopicLength]byte + +// BytesToTopic converts from the byte array representation of a topic +// into the TopicType type. +func BytesToTopic(b []byte) (t TopicType) { + sz := TopicLength + if x := len(b); x < TopicLength { + sz = x + } + for i := 0; i < sz; i++ { + t[i] = b[i] + } + return t +} + +// String converts a topic byte array to a string representation. +func (t *TopicType) String() string { + return common.ToHex(t[:]) +} + +// MarshalText returns the hex representation of t. +func (t TopicType) MarshalText() ([]byte, error) { + return hexutil.Bytes(t[:]).MarshalText() +} + +// UnmarshalText parses a hex representation to a topic. +func (t *TopicType) UnmarshalText(input []byte) error { + return hexutil.UnmarshalFixedText("Topic", input, t[:]) +} diff --git a/whisper/whisperv6/topic_test.go b/whisper/whisperv6/topic_test.go new file mode 100644 index 0000000000..454afe0de1 --- /dev/null +++ b/whisper/whisperv6/topic_test.go @@ -0,0 +1,134 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +import ( + "encoding/json" + "testing" +) + +var topicStringTests = []struct { + topic TopicType + str string +}{ + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, str: "0x00000000"}, + {topic: TopicType{0x00, 0x7f, 0x80, 0xff}, str: "0x007f80ff"}, + {topic: TopicType{0xff, 0x80, 0x7f, 0x00}, str: "0xff807f00"}, + {topic: TopicType{0xf2, 0x6e, 0x77, 0x79}, str: "0xf26e7779"}, +} + +func TestTopicString(t *testing.T) { + for i, tst := range topicStringTests { + s := tst.topic.String() + if s != tst.str { + t.Fatalf("failed test %d: have %s, want %s.", i, s, tst.str) + } + } +} + +var bytesToTopicTests = []struct { + data []byte + topic TopicType +}{ + {topic: TopicType{0x8f, 0x9a, 0x2b, 0x7d}, data: []byte{0x8f, 0x9a, 0x2b, 0x7d}}, + {topic: TopicType{0x00, 0x7f, 0x80, 0xff}, data: []byte{0x00, 0x7f, 0x80, 0xff}}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte{0x00, 0x00, 0x00, 0x00}}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte{0x00, 0x00, 0x00}}, + {topic: TopicType{0x01, 0x00, 0x00, 0x00}, data: []byte{0x01}}, + {topic: TopicType{0x00, 0xfe, 0x00, 0x00}, data: []byte{0x00, 0xfe}}, + {topic: TopicType{0xea, 0x1d, 0x43, 0x00}, data: []byte{0xea, 0x1d, 0x43}}, + {topic: TopicType{0x6f, 0x3c, 0xb0, 0xdd}, data: []byte{0x6f, 0x3c, 0xb0, 0xdd, 0x0f, 0x00, 0x90}}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte{}}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: nil}, +} + +var unmarshalTestsGood = []struct { + topic TopicType + data []byte +}{ + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0x00000000"`)}, + {topic: TopicType{0x00, 0x7f, 0x80, 0xff}, data: []byte(`"0x007f80ff"`)}, + {topic: TopicType{0xff, 0x80, 0x7f, 0x00}, data: []byte(`"0xff807f00"`)}, + {topic: TopicType{0xf2, 0x6e, 0x77, 0x79}, data: []byte(`"0xf26e7779"`)}, +} + +var unmarshalTestsBad = []struct { + topic TopicType + data []byte +}{ + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0x000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0x0000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0x000000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0x0000000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"000000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"0000000000"`)}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte(`"abcdefg0"`)}, +} + +var unmarshalTestsUgly = []struct { + topic TopicType + data []byte +}{ + {topic: TopicType{0x01, 0x00, 0x00, 0x00}, data: []byte(`"0x00000001"`)}, +} + +func TestBytesToTopic(t *testing.T) { + for i, tst := range bytesToTopicTests { + top := BytesToTopic(tst.data) + if top != tst.topic { + t.Fatalf("failed test %d: have %v, want %v.", i, t, tst.topic) + } + } +} + +func TestUnmarshalTestsGood(t *testing.T) { + for i, tst := range unmarshalTestsGood { + var top TopicType + err := json.Unmarshal(tst.data, &top) + if err != nil { + t.Errorf("failed test %d. input: %v. err: %v", i, tst.data, err) + } else if top != tst.topic { + t.Errorf("failed test %d: have %v, want %v.", i, t, tst.topic) + } + } +} + +func TestUnmarshalTestsBad(t *testing.T) { + // in this test UnmarshalJSON() is supposed to fail + for i, tst := range unmarshalTestsBad { + var top TopicType + err := json.Unmarshal(tst.data, &top) + if err == nil { + t.Fatalf("failed test %d. input: %v.", i, tst.data) + } + } +} + +func TestUnmarshalTestsUgly(t *testing.T) { + // in this test UnmarshalJSON() is NOT supposed to fail, but result should be wrong + for i, tst := range unmarshalTestsUgly { + var top TopicType + err := json.Unmarshal(tst.data, &top) + if err != nil { + t.Errorf("failed test %d. input: %v.", i, tst.data) + } else if top == tst.topic { + t.Errorf("failed test %d: have %v, want %v.", i, top, tst.topic) + } + } +} diff --git a/whisper/whisperv6/whisper.go b/whisper/whisperv6/whisper.go new file mode 100644 index 0000000000..eb119daa7b --- /dev/null +++ b/whisper/whisperv6/whisper.go @@ -0,0 +1,1076 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +import ( + "bytes" + "crypto/ecdsa" + "crypto/sha256" + "fmt" + "math" + "runtime" + "sync" + "time" + + "github.com/cryptoecc/ETH-ECC/common" + "github.com/cryptoecc/ETH-ECC/crypto" + "github.com/cryptoecc/ETH-ECC/log" + "github.com/cryptoecc/ETH-ECC/p2p" + "github.com/cryptoecc/ETH-ECC/rlp" + "github.com/cryptoecc/ETH-ECC/rpc" + mapset "github.com/deckarep/golang-set" + "github.com/syndtr/goleveldb/leveldb/errors" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/sync/syncmap" +) + +// Statistics holds several message-related counter for analytics +// purposes. +type Statistics struct { + messagesCleared int + memoryCleared int + memoryUsed int + cycles int + totalMessagesCleared int +} + +const ( + maxMsgSizeIdx = iota // Maximal message length allowed by the whisper node + overflowIdx // Indicator of message queue overflow + minPowIdx // Minimal PoW required by the whisper node + minPowToleranceIdx // Minimal PoW tolerated by the whisper node for a limited time + bloomFilterIdx // Bloom filter for topics of interest for this node + bloomFilterToleranceIdx // Bloom filter tolerated by the whisper node for a limited time + lightClientModeIdx // Light client mode. (does not forward any messages) + restrictConnectionBetweenLightClientsIdx // Restrict connection between two light clients +) + +// Whisper represents a dark communication interface through the Ethereum +// network, using its very own P2P communication layer. +type Whisper struct { + protocol p2p.Protocol // Protocol description and parameters + filters *Filters // Message filters installed with Subscribe function + + privateKeys map[string]*ecdsa.PrivateKey // Private key storage + symKeys map[string][]byte // Symmetric key storage + keyMu sync.RWMutex // Mutex associated with key storages + + poolMu sync.RWMutex // Mutex to sync the message and expiration pools + envelopes map[common.Hash]*Envelope // Pool of envelopes currently tracked by this node + expirations map[uint32]mapset.Set // Message expiration pool + + peerMu sync.RWMutex // Mutex to sync the active peer set + peers map[*Peer]struct{} // Set of currently active peers + + messageQueue chan *Envelope // Message queue for normal whisper messages + p2pMsgQueue chan *Envelope // Message queue for peer-to-peer messages (not to be forwarded any further) + quit chan struct{} // Channel used for graceful exit + + settings syncmap.Map // holds configuration settings that can be dynamically changed + + syncAllowance int // maximum time in seconds allowed to process the whisper-related messages + + statsMu sync.Mutex // guard stats + stats Statistics // Statistics of whisper node + + mailServer MailServer // MailServer interface +} + +// New creates a Whisper client ready to communicate through the Ethereum P2P network. +func New(cfg *Config) *Whisper { + if cfg == nil { + cfg = &DefaultConfig + } + + whisper := &Whisper{ + privateKeys: make(map[string]*ecdsa.PrivateKey), + symKeys: make(map[string][]byte), + envelopes: make(map[common.Hash]*Envelope), + expirations: make(map[uint32]mapset.Set), + peers: make(map[*Peer]struct{}), + messageQueue: make(chan *Envelope, messageQueueLimit), + p2pMsgQueue: make(chan *Envelope, messageQueueLimit), + quit: make(chan struct{}), + syncAllowance: DefaultSyncAllowance, + } + + whisper.filters = NewFilters(whisper) + + whisper.settings.Store(minPowIdx, cfg.MinimumAcceptedPOW) + whisper.settings.Store(maxMsgSizeIdx, cfg.MaxMessageSize) + whisper.settings.Store(overflowIdx, false) + whisper.settings.Store(restrictConnectionBetweenLightClientsIdx, cfg.RestrictConnectionBetweenLightClients) + + // p2p whisper sub protocol handler + whisper.protocol = p2p.Protocol{ + Name: ProtocolName, + Version: uint(ProtocolVersion), + Length: NumberOfMessageCodes, + Run: whisper.HandlePeer, + NodeInfo: func() interface{} { + return map[string]interface{}{ + "version": ProtocolVersionStr, + "maxMessageSize": whisper.MaxMessageSize(), + "minimumPoW": whisper.MinPow(), + } + }, + } + + return whisper +} + +// MinPow returns the PoW value required by this node. +func (whisper *Whisper) MinPow() float64 { + val, exist := whisper.settings.Load(minPowIdx) + if !exist || val == nil { + return DefaultMinimumPoW + } + v, ok := val.(float64) + if !ok { + log.Error("Error loading minPowIdx, using default") + return DefaultMinimumPoW + } + return v +} + +// MinPowTolerance returns the value of minimum PoW which is tolerated for a limited +// time after PoW was changed. If sufficient time have elapsed or no change of PoW +// have ever occurred, the return value will be the same as return value of MinPow(). +func (whisper *Whisper) MinPowTolerance() float64 { + val, exist := whisper.settings.Load(minPowToleranceIdx) + if !exist || val == nil { + return DefaultMinimumPoW + } + return val.(float64) +} + +// BloomFilter returns the aggregated bloom filter for all the topics of interest. +// The nodes are required to send only messages that match the advertised bloom filter. +// If a message does not match the bloom, it will tantamount to spam, and the peer will +// be disconnected. +func (whisper *Whisper) BloomFilter() []byte { + val, exist := whisper.settings.Load(bloomFilterIdx) + if !exist || val == nil { + return nil + } + return val.([]byte) +} + +// BloomFilterTolerance returns the bloom filter which is tolerated for a limited +// time after new bloom was advertised to the peers. If sufficient time have elapsed +// or no change of bloom filter have ever occurred, the return value will be the same +// as return value of BloomFilter(). +func (whisper *Whisper) BloomFilterTolerance() []byte { + val, exist := whisper.settings.Load(bloomFilterToleranceIdx) + if !exist || val == nil { + return nil + } + return val.([]byte) +} + +// MaxMessageSize returns the maximum accepted message size. +func (whisper *Whisper) MaxMessageSize() uint32 { + val, _ := whisper.settings.Load(maxMsgSizeIdx) + return val.(uint32) +} + +// Overflow returns an indication if the message queue is full. +func (whisper *Whisper) Overflow() bool { + val, _ := whisper.settings.Load(overflowIdx) + return val.(bool) +} + +// APIs returns the RPC descriptors the Whisper implementation offers +func (whisper *Whisper) APIs() []rpc.API { + return []rpc.API{ + { + Namespace: ProtocolName, + Version: ProtocolVersionStr, + Service: NewPublicWhisperAPI(whisper), + Public: true, + }, + } +} + +// RegisterServer registers MailServer interface. +// MailServer will process all the incoming messages with p2pRequestCode. +func (whisper *Whisper) RegisterServer(server MailServer) { + whisper.mailServer = server +} + +// Protocols returns the whisper sub-protocols ran by this particular client. +func (whisper *Whisper) Protocols() []p2p.Protocol { + return []p2p.Protocol{whisper.protocol} +} + +// Version returns the whisper sub-protocols version number. +func (whisper *Whisper) Version() uint { + return whisper.protocol.Version +} + +// SetMaxMessageSize sets the maximal message size allowed by this node +func (whisper *Whisper) SetMaxMessageSize(size uint32) error { + if size > MaxMessageSize { + return fmt.Errorf("message size too large [%d>%d]", size, MaxMessageSize) + } + whisper.settings.Store(maxMsgSizeIdx, size) + return nil +} + +// SetBloomFilter sets the new bloom filter +func (whisper *Whisper) SetBloomFilter(bloom []byte) error { + if len(bloom) != BloomFilterSize { + return fmt.Errorf("invalid bloom filter size: %d", len(bloom)) + } + + b := make([]byte, BloomFilterSize) + copy(b, bloom) + + whisper.settings.Store(bloomFilterIdx, b) + whisper.notifyPeersAboutBloomFilterChange(b) + + go func() { + // allow some time before all the peers have processed the notification + time.Sleep(time.Duration(whisper.syncAllowance) * time.Second) + whisper.settings.Store(bloomFilterToleranceIdx, b) + }() + + return nil +} + +// SetMinimumPoW sets the minimal PoW required by this node +func (whisper *Whisper) SetMinimumPoW(val float64) error { + if val < 0.0 { + return fmt.Errorf("invalid PoW: %f", val) + } + + whisper.settings.Store(minPowIdx, val) + whisper.notifyPeersAboutPowRequirementChange(val) + + go func() { + // allow some time before all the peers have processed the notification + time.Sleep(time.Duration(whisper.syncAllowance) * time.Second) + whisper.settings.Store(minPowToleranceIdx, val) + }() + + return nil +} + +// SetMinimumPowTest sets the minimal PoW in test environment +func (whisper *Whisper) SetMinimumPowTest(val float64) { + whisper.settings.Store(minPowIdx, val) + whisper.notifyPeersAboutPowRequirementChange(val) + whisper.settings.Store(minPowToleranceIdx, val) +} + +//SetLightClientMode makes node light client (does not forward any messages) +func (whisper *Whisper) SetLightClientMode(v bool) { + whisper.settings.Store(lightClientModeIdx, v) +} + +//LightClientMode indicates is this node is light client (does not forward any messages) +func (whisper *Whisper) LightClientMode() bool { + val, exist := whisper.settings.Load(lightClientModeIdx) + if !exist || val == nil { + return false + } + v, ok := val.(bool) + return v && ok +} + +//LightClientModeConnectionRestricted indicates that connection to light client in light client mode not allowed +func (whisper *Whisper) LightClientModeConnectionRestricted() bool { + val, exist := whisper.settings.Load(restrictConnectionBetweenLightClientsIdx) + if !exist || val == nil { + return false + } + v, ok := val.(bool) + return v && ok +} + +func (whisper *Whisper) notifyPeersAboutPowRequirementChange(pow float64) { + arr := whisper.getPeers() + for _, p := range arr { + err := p.notifyAboutPowRequirementChange(pow) + if err != nil { + // allow one retry + err = p.notifyAboutPowRequirementChange(pow) + } + if err != nil { + log.Warn("failed to notify peer about new pow requirement", "peer", p.ID(), "error", err) + } + } +} + +func (whisper *Whisper) notifyPeersAboutBloomFilterChange(bloom []byte) { + arr := whisper.getPeers() + for _, p := range arr { + err := p.notifyAboutBloomFilterChange(bloom) + if err != nil { + // allow one retry + err = p.notifyAboutBloomFilterChange(bloom) + } + if err != nil { + log.Warn("failed to notify peer about new bloom filter", "peer", p.ID(), "error", err) + } + } +} + +func (whisper *Whisper) getPeers() []*Peer { + arr := make([]*Peer, len(whisper.peers)) + i := 0 + whisper.peerMu.Lock() + for p := range whisper.peers { + arr[i] = p + i++ + } + whisper.peerMu.Unlock() + return arr +} + +// getPeer retrieves peer by ID +func (whisper *Whisper) getPeer(peerID []byte) (*Peer, error) { + whisper.peerMu.Lock() + defer whisper.peerMu.Unlock() + for p := range whisper.peers { + id := p.peer.ID() + if bytes.Equal(peerID, id[:]) { + return p, nil + } + } + return nil, fmt.Errorf("Could not find peer with ID: %x", peerID) +} + +// AllowP2PMessagesFromPeer marks specific peer trusted, +// which will allow it to send historic (expired) messages. +func (whisper *Whisper) AllowP2PMessagesFromPeer(peerID []byte) error { + p, err := whisper.getPeer(peerID) + if err != nil { + return err + } + p.trusted = true + return nil +} + +// RequestHistoricMessages sends a message with p2pRequestCode to a specific peer, +// which is known to implement MailServer interface, and is supposed to process this +// request and respond with a number of peer-to-peer messages (possibly expired), +// which are not supposed to be forwarded any further. +// The whisper protocol is agnostic of the format and contents of envelope. +func (whisper *Whisper) RequestHistoricMessages(peerID []byte, envelope *Envelope) error { + p, err := whisper.getPeer(peerID) + if err != nil { + return err + } + p.trusted = true + return p2p.Send(p.ws, p2pRequestCode, envelope) +} + +// SendP2PMessage sends a peer-to-peer message to a specific peer. +func (whisper *Whisper) SendP2PMessage(peerID []byte, envelope *Envelope) error { + p, err := whisper.getPeer(peerID) + if err != nil { + return err + } + return whisper.SendP2PDirect(p, envelope) +} + +// SendP2PDirect sends a peer-to-peer message to a specific peer. +func (whisper *Whisper) SendP2PDirect(peer *Peer, envelope *Envelope) error { + return p2p.Send(peer.ws, p2pMessageCode, envelope) +} + +// NewKeyPair generates a new cryptographic identity for the client, and injects +// it into the known identities for message decryption. Returns ID of the new key pair. +func (whisper *Whisper) NewKeyPair() (string, error) { + key, err := crypto.GenerateKey() + if err != nil || !validatePrivateKey(key) { + key, err = crypto.GenerateKey() // retry once + } + if err != nil { + return "", err + } + if !validatePrivateKey(key) { + return "", fmt.Errorf("failed to generate valid key") + } + + id, err := GenerateRandomID() + if err != nil { + return "", fmt.Errorf("failed to generate ID: %s", err) + } + + whisper.keyMu.Lock() + defer whisper.keyMu.Unlock() + + if whisper.privateKeys[id] != nil { + return "", fmt.Errorf("failed to generate unique ID") + } + whisper.privateKeys[id] = key + return id, nil +} + +// DeleteKeyPair deletes the specified key if it exists. +func (whisper *Whisper) DeleteKeyPair(key string) bool { + whisper.keyMu.Lock() + defer whisper.keyMu.Unlock() + + if whisper.privateKeys[key] != nil { + delete(whisper.privateKeys, key) + return true + } + return false +} + +// AddKeyPair imports a asymmetric private key and returns it identifier. +func (whisper *Whisper) AddKeyPair(key *ecdsa.PrivateKey) (string, error) { + id, err := GenerateRandomID() + if err != nil { + return "", fmt.Errorf("failed to generate ID: %s", err) + } + + whisper.keyMu.Lock() + whisper.privateKeys[id] = key + whisper.keyMu.Unlock() + + return id, nil +} + +// HasKeyPair checks if the whisper node is configured with the private key +// of the specified public pair. +func (whisper *Whisper) HasKeyPair(id string) bool { + whisper.keyMu.RLock() + defer whisper.keyMu.RUnlock() + return whisper.privateKeys[id] != nil +} + +// GetPrivateKey retrieves the private key of the specified identity. +func (whisper *Whisper) GetPrivateKey(id string) (*ecdsa.PrivateKey, error) { + whisper.keyMu.RLock() + defer whisper.keyMu.RUnlock() + key := whisper.privateKeys[id] + if key == nil { + return nil, fmt.Errorf("invalid id") + } + return key, nil +} + +// GenerateSymKey generates a random symmetric key and stores it under id, +// which is then returned. Will be used in the future for session key exchange. +func (whisper *Whisper) GenerateSymKey() (string, error) { + key, err := generateSecureRandomData(aesKeyLength) + if err != nil { + return "", err + } else if !validateDataIntegrity(key, aesKeyLength) { + return "", fmt.Errorf("error in GenerateSymKey: crypto/rand failed to generate random data") + } + + id, err := GenerateRandomID() + if err != nil { + return "", fmt.Errorf("failed to generate ID: %s", err) + } + + whisper.keyMu.Lock() + defer whisper.keyMu.Unlock() + + if whisper.symKeys[id] != nil { + return "", fmt.Errorf("failed to generate unique ID") + } + whisper.symKeys[id] = key + return id, nil +} + +// AddSymKeyDirect stores the key, and returns its id. +func (whisper *Whisper) AddSymKeyDirect(key []byte) (string, error) { + if len(key) != aesKeyLength { + return "", fmt.Errorf("wrong key size: %d", len(key)) + } + + id, err := GenerateRandomID() + if err != nil { + return "", fmt.Errorf("failed to generate ID: %s", err) + } + + whisper.keyMu.Lock() + defer whisper.keyMu.Unlock() + + if whisper.symKeys[id] != nil { + return "", fmt.Errorf("failed to generate unique ID") + } + whisper.symKeys[id] = key + return id, nil +} + +// AddSymKeyFromPassword generates the key from password, stores it, and returns its id. +func (whisper *Whisper) AddSymKeyFromPassword(password string) (string, error) { + id, err := GenerateRandomID() + if err != nil { + return "", fmt.Errorf("failed to generate ID: %s", err) + } + if whisper.HasSymKey(id) { + return "", fmt.Errorf("failed to generate unique ID") + } + + // kdf should run no less than 0.1 seconds on an average computer, + // because it's an once in a session experience + derived := pbkdf2.Key([]byte(password), nil, 65356, aesKeyLength, sha256.New) + if err != nil { + return "", err + } + + whisper.keyMu.Lock() + defer whisper.keyMu.Unlock() + + // double check is necessary, because deriveKeyMaterial() is very slow + if whisper.symKeys[id] != nil { + return "", fmt.Errorf("critical error: failed to generate unique ID") + } + whisper.symKeys[id] = derived + return id, nil +} + +// HasSymKey returns true if there is a key associated with the given id. +// Otherwise returns false. +func (whisper *Whisper) HasSymKey(id string) bool { + whisper.keyMu.RLock() + defer whisper.keyMu.RUnlock() + return whisper.symKeys[id] != nil +} + +// DeleteSymKey deletes the key associated with the name string if it exists. +func (whisper *Whisper) DeleteSymKey(id string) bool { + whisper.keyMu.Lock() + defer whisper.keyMu.Unlock() + if whisper.symKeys[id] != nil { + delete(whisper.symKeys, id) + return true + } + return false +} + +// GetSymKey returns the symmetric key associated with the given id. +func (whisper *Whisper) GetSymKey(id string) ([]byte, error) { + whisper.keyMu.RLock() + defer whisper.keyMu.RUnlock() + if whisper.symKeys[id] != nil { + return whisper.symKeys[id], nil + } + return nil, fmt.Errorf("non-existent key ID") +} + +// Subscribe installs a new message handler used for filtering, decrypting +// and subsequent storing of incoming messages. +func (whisper *Whisper) Subscribe(f *Filter) (string, error) { + s, err := whisper.filters.Install(f) + if err == nil { + whisper.updateBloomFilter(f) + } + return s, err +} + +// updateBloomFilter recalculates the new value of bloom filter, +// and informs the peers if necessary. +func (whisper *Whisper) updateBloomFilter(f *Filter) { + aggregate := make([]byte, BloomFilterSize) + for _, t := range f.Topics { + top := BytesToTopic(t) + b := TopicToBloom(top) + aggregate = addBloom(aggregate, b) + } + + if !BloomFilterMatch(whisper.BloomFilter(), aggregate) { + // existing bloom filter must be updated + aggregate = addBloom(whisper.BloomFilter(), aggregate) + whisper.SetBloomFilter(aggregate) + } +} + +// GetFilter returns the filter by id. +func (whisper *Whisper) GetFilter(id string) *Filter { + return whisper.filters.Get(id) +} + +// Unsubscribe removes an installed message handler. +func (whisper *Whisper) Unsubscribe(id string) error { + ok := whisper.filters.Uninstall(id) + if !ok { + return fmt.Errorf("Unsubscribe: Invalid ID") + } + return nil +} + +// Send injects a message into the whisper send queue, to be distributed in the +// network in the coming cycles. +func (whisper *Whisper) Send(envelope *Envelope) error { + ok, err := whisper.add(envelope, false) + if err == nil && !ok { + return fmt.Errorf("failed to add envelope") + } + return err +} + +// Start implements node.Service, starting the background data propagation thread +// of the Whisper protocol. +func (whisper *Whisper) Start(*p2p.Server) error { + log.Info("started whisper v." + ProtocolVersionStr) + go whisper.update() + + numCPU := runtime.NumCPU() + for i := 0; i < numCPU; i++ { + go whisper.processQueue() + } + + return nil +} + +// Stop implements node.Service, stopping the background data propagation thread +// of the Whisper protocol. +func (whisper *Whisper) Stop() error { + close(whisper.quit) + log.Info("whisper stopped") + return nil +} + +// HandlePeer is called by the underlying P2P layer when the whisper sub-protocol +// connection is negotiated. +func (whisper *Whisper) HandlePeer(peer *p2p.Peer, rw p2p.MsgReadWriter) error { + // Create the new peer and start tracking it + whisperPeer := newPeer(whisper, peer, rw) + + whisper.peerMu.Lock() + whisper.peers[whisperPeer] = struct{}{} + whisper.peerMu.Unlock() + + defer func() { + whisper.peerMu.Lock() + delete(whisper.peers, whisperPeer) + whisper.peerMu.Unlock() + }() + + // Run the peer handshake and state updates + if err := whisperPeer.handshake(); err != nil { + return err + } + whisperPeer.start() + defer whisperPeer.stop() + + return whisper.runMessageLoop(whisperPeer, rw) +} + +// runMessageLoop reads and processes inbound messages directly to merge into client-global state. +func (whisper *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error { + for { + // fetch the next packet + packet, err := rw.ReadMsg() + if err != nil { + log.Info("message loop", "peer", p.peer.ID(), "err", err) + return err + } + if packet.Size > whisper.MaxMessageSize() { + log.Warn("oversized message received", "peer", p.peer.ID()) + return errors.New("oversized message received") + } + + switch packet.Code { + case statusCode: + // this should not happen, but no need to panic; just ignore this message. + log.Warn("unxepected status message received", "peer", p.peer.ID()) + case messagesCode: + // decode the contained envelopes + var envelopes []*Envelope + if err := packet.Decode(&envelopes); err != nil { + log.Warn("failed to decode envelopes, peer will be disconnected", "peer", p.peer.ID(), "err", err) + return errors.New("invalid envelopes") + } + + trouble := false + for _, env := range envelopes { + cached, err := whisper.add(env, whisper.LightClientMode()) + if err != nil { + trouble = true + log.Error("bad envelope received, peer will be disconnected", "peer", p.peer.ID(), "err", err) + } + if cached { + p.mark(env) + } + } + + if trouble { + return errors.New("invalid envelope") + } + case powRequirementCode: + s := rlp.NewStream(packet.Payload, uint64(packet.Size)) + i, err := s.Uint() + if err != nil { + log.Warn("failed to decode powRequirementCode message, peer will be disconnected", "peer", p.peer.ID(), "err", err) + return errors.New("invalid powRequirementCode message") + } + f := math.Float64frombits(i) + if math.IsInf(f, 0) || math.IsNaN(f) || f < 0.0 { + log.Warn("invalid value in powRequirementCode message, peer will be disconnected", "peer", p.peer.ID(), "err", err) + return errors.New("invalid value in powRequirementCode message") + } + p.powRequirement = f + case bloomFilterExCode: + var bloom []byte + err := packet.Decode(&bloom) + if err == nil && len(bloom) != BloomFilterSize { + err = fmt.Errorf("wrong bloom filter size %d", len(bloom)) + } + + if err != nil { + log.Warn("failed to decode bloom filter exchange message, peer will be disconnected", "peer", p.peer.ID(), "err", err) + return errors.New("invalid bloom filter exchange message") + } + p.setBloomFilter(bloom) + case p2pMessageCode: + // peer-to-peer message, sent directly to peer bypassing PoW checks, etc. + // this message is not supposed to be forwarded to other peers, and + // therefore might not satisfy the PoW, expiry and other requirements. + // these messages are only accepted from the trusted peer. + if p.trusted { + 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) + return errors.New("invalid direct message") + } + whisper.postEvent(&envelope, true) + } + case p2pRequestCode: + // Must be processed if mail server is implemented. Otherwise ignore. + if whisper.mailServer != nil { + 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) + return errors.New("invalid p2p request") + } + whisper.mailServer.DeliverMail(p, &request) + } + default: + // New message types might be implemented in the future versions of Whisper. + // For forward compatibility, just ignore. + } + + packet.Discard() + } +} + +// add inserts a new envelope into the message pool to be distributed within the +// whisper network. It also inserts the envelope into the expiration pool at the +// appropriate time-stamp. In case of error, connection should be dropped. +// param isP2P indicates whether the message is peer-to-peer (should not be forwarded). +func (whisper *Whisper) add(envelope *Envelope, isP2P bool) (bool, error) { + now := uint32(time.Now().Unix()) + sent := envelope.Expiry - envelope.TTL + + if sent > now { + if sent-DefaultSyncAllowance > now { + return false, fmt.Errorf("envelope created in the future [%x]", envelope.Hash()) + } + // recalculate PoW, adjusted for the time difference, plus one second for latency + envelope.calculatePoW(sent - now + 1) + } + + if envelope.Expiry < now { + if envelope.Expiry+DefaultSyncAllowance*2 < now { + return false, fmt.Errorf("very old message") + } + log.Debug("expired envelope dropped", "hash", envelope.Hash().Hex()) + return false, nil // drop envelope without error + } + + if uint32(envelope.size()) > whisper.MaxMessageSize() { + return false, fmt.Errorf("huge messages are not allowed [%x]", envelope.Hash()) + } + + if envelope.PoW() < whisper.MinPow() { + // maybe the value was recently changed, and the peers did not adjust yet. + // in this case the previous value is retrieved by MinPowTolerance() + // for a short period of peer synchronization. + if envelope.PoW() < whisper.MinPowTolerance() { + return false, fmt.Errorf("envelope with low PoW received: PoW=%f, hash=[%v]", envelope.PoW(), envelope.Hash().Hex()) + } + } + + if !BloomFilterMatch(whisper.BloomFilter(), envelope.Bloom()) { + // maybe the value was recently changed, and the peers did not adjust yet. + // in this case the previous value is retrieved by BloomFilterTolerance() + // for a short period of peer synchronization. + if !BloomFilterMatch(whisper.BloomFilterTolerance(), envelope.Bloom()) { + return false, fmt.Errorf("envelope does not match bloom filter, hash=[%v], bloom: \n%x \n%x \n%x", + envelope.Hash().Hex(), whisper.BloomFilter(), envelope.Bloom(), envelope.Topic) + } + } + + hash := envelope.Hash() + + whisper.poolMu.Lock() + _, alreadyCached := whisper.envelopes[hash] + if !alreadyCached { + whisper.envelopes[hash] = envelope + if whisper.expirations[envelope.Expiry] == nil { + whisper.expirations[envelope.Expiry] = mapset.NewThreadUnsafeSet() + } + if !whisper.expirations[envelope.Expiry].Contains(hash) { + whisper.expirations[envelope.Expiry].Add(hash) + } + } + whisper.poolMu.Unlock() + + if alreadyCached { + log.Trace("whisper envelope already cached", "hash", envelope.Hash().Hex()) + } else { + log.Trace("cached whisper envelope", "hash", envelope.Hash().Hex()) + whisper.statsMu.Lock() + whisper.stats.memoryUsed += envelope.size() + whisper.statsMu.Unlock() + whisper.postEvent(envelope, isP2P) // notify the local node about the new message + if whisper.mailServer != nil { + whisper.mailServer.Archive(envelope) + } + } + return true, nil +} + +// postEvent queues the message for further processing. +func (whisper *Whisper) postEvent(envelope *Envelope, isP2P bool) { + if isP2P { + whisper.p2pMsgQueue <- envelope + } else { + whisper.checkOverflow() + whisper.messageQueue <- envelope + } +} + +// checkOverflow checks if message queue overflow occurs and reports it if necessary. +func (whisper *Whisper) checkOverflow() { + queueSize := len(whisper.messageQueue) + + if queueSize == messageQueueLimit { + if !whisper.Overflow() { + whisper.settings.Store(overflowIdx, true) + log.Warn("message queue overflow") + } + } else if queueSize <= messageQueueLimit/2 { + if whisper.Overflow() { + whisper.settings.Store(overflowIdx, false) + log.Warn("message queue overflow fixed (back to normal)") + } + } +} + +// processQueue delivers the messages to the watchers during the lifetime of the whisper node. +func (whisper *Whisper) processQueue() { + var e *Envelope + for { + select { + case <-whisper.quit: + return + + case e = <-whisper.messageQueue: + whisper.filters.NotifyWatchers(e, false) + + case e = <-whisper.p2pMsgQueue: + whisper.filters.NotifyWatchers(e, true) + } + } +} + +// update loops until the lifetime of the whisper node, updating its internal +// state by expiring stale messages from the pool. +func (whisper *Whisper) update() { + // Start a ticker to check for expirations + expire := time.NewTicker(expirationCycle) + + // Repeat updates until termination is requested + for { + select { + case <-expire.C: + whisper.expire() + + case <-whisper.quit: + return + } + } +} + +// expire iterates over all the expiration timestamps, removing all stale +// messages from the pools. +func (whisper *Whisper) expire() { + whisper.poolMu.Lock() + defer whisper.poolMu.Unlock() + + whisper.statsMu.Lock() + defer whisper.statsMu.Unlock() + whisper.stats.reset() + now := uint32(time.Now().Unix()) + for expiry, hashSet := range whisper.expirations { + if expiry < now { + // Dump all expired messages and remove timestamp + hashSet.Each(func(v interface{}) bool { + sz := whisper.envelopes[v.(common.Hash)].size() + delete(whisper.envelopes, v.(common.Hash)) + whisper.stats.messagesCleared++ + whisper.stats.memoryCleared += sz + whisper.stats.memoryUsed -= sz + return false + }) + whisper.expirations[expiry].Clear() + delete(whisper.expirations, expiry) + } + } +} + +// Stats returns the whisper node statistics. +func (whisper *Whisper) Stats() Statistics { + whisper.statsMu.Lock() + defer whisper.statsMu.Unlock() + + return whisper.stats +} + +// Envelopes retrieves all the messages currently pooled by the node. +func (whisper *Whisper) Envelopes() []*Envelope { + whisper.poolMu.RLock() + defer whisper.poolMu.RUnlock() + + all := make([]*Envelope, 0, len(whisper.envelopes)) + for _, envelope := range whisper.envelopes { + all = append(all, envelope) + } + return all +} + +// isEnvelopeCached checks if envelope with specific hash has already been received and cached. +func (whisper *Whisper) isEnvelopeCached(hash common.Hash) bool { + whisper.poolMu.Lock() + defer whisper.poolMu.Unlock() + + _, exist := whisper.envelopes[hash] + return exist +} + +// reset resets the node's statistics after each expiry cycle. +func (s *Statistics) reset() { + s.cycles++ + s.totalMessagesCleared += s.messagesCleared + + s.memoryCleared = 0 + s.messagesCleared = 0 +} + +// ValidatePublicKey checks the format of the given public key. +func ValidatePublicKey(k *ecdsa.PublicKey) bool { + return k != nil && k.X != nil && k.Y != nil && k.X.Sign() != 0 && k.Y.Sign() != 0 +} + +// validatePrivateKey checks the format of the given private key. +func validatePrivateKey(k *ecdsa.PrivateKey) bool { + if k == nil || k.D == nil || k.D.Sign() == 0 { + return false + } + return ValidatePublicKey(&k.PublicKey) +} + +// validateDataIntegrity returns false if the data have the wrong or contains all zeros, +// which is the simplest and the most common bug. +func validateDataIntegrity(k []byte, expectedSize int) bool { + if len(k) != expectedSize { + return false + } + if expectedSize > 3 && containsOnlyZeros(k) { + return false + } + return true +} + +// containsOnlyZeros checks if the data contain only zeros. +func containsOnlyZeros(data []byte) bool { + for _, b := range data { + if b != 0 { + return false + } + } + return true +} + +// bytesToUintLittleEndian converts the slice to 64-bit unsigned integer. +func bytesToUintLittleEndian(b []byte) (res uint64) { + mul := uint64(1) + for i := 0; i < len(b); i++ { + res += uint64(b[i]) * mul + mul *= 256 + } + return res +} + +// BytesToUintBigEndian converts the slice to 64-bit unsigned integer. +func BytesToUintBigEndian(b []byte) (res uint64) { + for i := 0; i < len(b); i++ { + res *= 256 + res += uint64(b[i]) + } + return res +} + +// GenerateRandomID generates a random string, which is then returned to be used as a key id +func GenerateRandomID() (id string, err error) { + buf, err := generateSecureRandomData(keyIDSize) + if err != nil { + return "", err + } + if !validateDataIntegrity(buf, keyIDSize) { + return "", fmt.Errorf("error in generateRandomID: crypto/rand failed to generate random data") + } + id = common.Bytes2Hex(buf) + return id, err +} + +func isFullNode(bloom []byte) bool { + if bloom == nil { + return true + } + for _, b := range bloom { + if b != 255 { + return false + } + } + return true +} + +func BloomFilterMatch(filter, sample []byte) bool { + if filter == nil { + return true + } + + for i := 0; i < BloomFilterSize; i++ { + f := filter[i] + s := sample[i] + if (f | s) != f { + return false + } + } + + return true +} + +func addBloom(a, b []byte) []byte { + c := make([]byte, BloomFilterSize) + for i := 0; i < BloomFilterSize; i++ { + c[i] = a[i] | b[i] + } + return c +} diff --git a/whisper/whisperv6/whisper_test.go b/whisper/whisperv6/whisper_test.go new file mode 100644 index 0000000000..2b7322b7a7 --- /dev/null +++ b/whisper/whisperv6/whisper_test.go @@ -0,0 +1,892 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package whisperv6 + +import ( + "bytes" + "crypto/ecdsa" + "crypto/sha256" + mrand "math/rand" + "testing" + "time" + + "github.com/cryptoecc/ETH-ECC/common" + "golang.org/x/crypto/pbkdf2" +) + +func TestWhisperBasic(t *testing.T) { + w := New(&DefaultConfig) + p := w.Protocols() + shh := p[0] + if shh.Name != ProtocolName { + t.Fatalf("failed Protocol Name: %v.", shh.Name) + } + if uint64(shh.Version) != ProtocolVersion { + t.Fatalf("failed Protocol Version: %v.", shh.Version) + } + if shh.Length != NumberOfMessageCodes { + t.Fatalf("failed Protocol Length: %v.", shh.Length) + } + if shh.Run == nil { + t.Fatalf("failed shh.Run.") + } + if uint64(w.Version()) != ProtocolVersion { + t.Fatalf("failed whisper Version: %v.", shh.Version) + } + if w.GetFilter("non-existent") != nil { + t.Fatalf("failed GetFilter.") + } + + peerID := make([]byte, 64) + mrand.Read(peerID) + peer, _ := w.getPeer(peerID) + if peer != nil { + t.Fatal("found peer for random key.") + } + if err := w.AllowP2PMessagesFromPeer(peerID); err == nil { + t.Fatalf("failed MarkPeerTrusted.") + } + exist := w.HasSymKey("non-existing") + if exist { + t.Fatalf("failed HasSymKey.") + } + key, err := w.GetSymKey("non-existing") + if err == nil { + t.Fatalf("failed GetSymKey(non-existing): false positive.") + } + if key != nil { + t.Fatalf("failed GetSymKey: false positive.") + } + mail := w.Envelopes() + if len(mail) != 0 { + t.Fatalf("failed w.Envelopes().") + } + + derived := pbkdf2.Key(peerID, nil, 65356, aesKeyLength, sha256.New) + if !validateDataIntegrity(derived, aesKeyLength) { + t.Fatalf("failed validateSymmetricKey with param = %v.", derived) + } + if containsOnlyZeros(derived) { + t.Fatalf("failed containsOnlyZeros with param = %v.", derived) + } + + buf := []byte{0xFF, 0xE5, 0x80, 0x2, 0} + le := bytesToUintLittleEndian(buf) + be := BytesToUintBigEndian(buf) + if le != uint64(0x280e5ff) { + t.Fatalf("failed bytesToIntLittleEndian: %d.", le) + } + if be != uint64(0xffe5800200) { + t.Fatalf("failed BytesToIntBigEndian: %d.", be) + } + + id, err := w.NewKeyPair() + if err != nil { + t.Fatalf("failed to generate new key pair: %s.", err) + } + pk, err := w.GetPrivateKey(id) + if err != nil { + t.Fatalf("failed to retrieve new key pair: %s.", err) + } + if !validatePrivateKey(pk) { + t.Fatalf("failed validatePrivateKey: %v.", pk) + } + if !ValidatePublicKey(&pk.PublicKey) { + t.Fatalf("failed ValidatePublicKey: %v.", pk) + } +} + +func TestWhisperAsymmetricKeyImport(t *testing.T) { + var ( + w = New(&DefaultConfig) + privateKeys []*ecdsa.PrivateKey + ) + + for i := 0; i < 50; i++ { + id, err := w.NewKeyPair() + if err != nil { + t.Fatalf("could not generate key: %v", err) + } + + pk, err := w.GetPrivateKey(id) + if err != nil { + t.Fatalf("could not export private key: %v", err) + } + + privateKeys = append(privateKeys, pk) + + if !w.DeleteKeyPair(id) { + t.Fatalf("could not delete private key") + } + } + + for _, pk := range privateKeys { + if _, err := w.AddKeyPair(pk); err != nil { + t.Fatalf("could not import private key: %v", err) + } + } +} + +func TestWhisperIdentityManagement(t *testing.T) { + w := New(&DefaultConfig) + id1, err := w.NewKeyPair() + if err != nil { + t.Fatalf("failed to generate new key pair: %s.", err) + } + id2, err := w.NewKeyPair() + if err != nil { + t.Fatalf("failed to generate new key pair: %s.", err) + } + pk1, err := w.GetPrivateKey(id1) + if err != nil { + t.Fatalf("failed to retrieve the key pair: %s.", err) + } + pk2, err := w.GetPrivateKey(id2) + if err != nil { + t.Fatalf("failed to retrieve the key pair: %s.", err) + } + + if !w.HasKeyPair(id1) { + t.Fatalf("failed HasIdentity(pk1).") + } + if !w.HasKeyPair(id2) { + t.Fatalf("failed HasIdentity(pk2).") + } + if pk1 == nil { + t.Fatalf("failed GetIdentity(pk1).") + } + if pk2 == nil { + t.Fatalf("failed GetIdentity(pk2).") + } + + if !validatePrivateKey(pk1) { + t.Fatalf("pk1 is invalid.") + } + if !validatePrivateKey(pk2) { + t.Fatalf("pk2 is invalid.") + } + + // Delete one identity + done := w.DeleteKeyPair(id1) + if !done { + t.Fatalf("failed to delete id1.") + } + pk1, err = w.GetPrivateKey(id1) + if err == nil { + t.Fatalf("retrieve the key pair: false positive.") + } + pk2, err = w.GetPrivateKey(id2) + if err != nil { + t.Fatalf("failed to retrieve the key pair: %s.", err) + } + if w.HasKeyPair(id1) { + t.Fatalf("failed DeleteIdentity(pub1): still exist.") + } + if !w.HasKeyPair(id2) { + t.Fatalf("failed DeleteIdentity(pub1): pub2 does not exist.") + } + if pk1 != nil { + t.Fatalf("failed DeleteIdentity(pub1): first key still exist.") + } + if pk2 == nil { + t.Fatalf("failed DeleteIdentity(pub1): second key does not exist.") + } + + // Delete again non-existing identity + done = w.DeleteKeyPair(id1) + if done { + t.Fatalf("delete id1: false positive.") + } + pk1, err = w.GetPrivateKey(id1) + if err == nil { + t.Fatalf("retrieve the key pair: false positive.") + } + pk2, err = w.GetPrivateKey(id2) + if err != nil { + t.Fatalf("failed to retrieve the key pair: %s.", err) + } + if w.HasKeyPair(id1) { + t.Fatalf("failed delete non-existing identity: exist.") + } + if !w.HasKeyPair(id2) { + t.Fatalf("failed delete non-existing identity: pub2 does not exist.") + } + if pk1 != nil { + t.Fatalf("failed delete non-existing identity: first key exist.") + } + if pk2 == nil { + t.Fatalf("failed delete non-existing identity: second key does not exist.") + } + + // Delete second identity + done = w.DeleteKeyPair(id2) + if !done { + t.Fatalf("failed to delete id2.") + } + pk1, err = w.GetPrivateKey(id1) + if err == nil { + t.Fatalf("retrieve the key pair: false positive.") + } + pk2, err = w.GetPrivateKey(id2) + if err == nil { + t.Fatalf("retrieve the key pair: false positive.") + } + if w.HasKeyPair(id1) { + t.Fatalf("failed delete second identity: first identity exist.") + } + if w.HasKeyPair(id2) { + t.Fatalf("failed delete second identity: still exist.") + } + if pk1 != nil { + t.Fatalf("failed delete second identity: first key exist.") + } + if pk2 != nil { + t.Fatalf("failed delete second identity: second key exist.") + } +} + +func TestWhisperSymKeyManagement(t *testing.T) { + InitSingleTest() + + var err error + var k1, k2 []byte + w := New(&DefaultConfig) + id1 := string("arbitrary-string-1") + id2 := string("arbitrary-string-2") + + id1, err = w.GenerateSymKey() + if err != nil { + t.Fatalf("failed GenerateSymKey with seed %d: %s.", seed, err) + } + + k1, err = w.GetSymKey(id1) + if err != nil { + t.Fatalf("failed GetSymKey(id1).") + } + k2, err = w.GetSymKey(id2) + if err == nil { + t.Fatalf("failed GetSymKey(id2): false positive.") + } + if !w.HasSymKey(id1) { + t.Fatalf("failed HasSymKey(id1).") + } + if w.HasSymKey(id2) { + t.Fatalf("failed HasSymKey(id2): false positive.") + } + if k1 == nil { + t.Fatalf("first key does not exist.") + } + if k2 != nil { + t.Fatalf("second key still exist.") + } + + // add existing id, nothing should change + randomKey := make([]byte, aesKeyLength) + mrand.Read(randomKey) + id1, err = w.AddSymKeyDirect(randomKey) + if err != nil { + t.Fatalf("failed AddSymKey with seed %d: %s.", seed, err) + } + + k1, err = w.GetSymKey(id1) + if err != nil { + t.Fatalf("failed w.GetSymKey(id1).") + } + k2, err = w.GetSymKey(id2) + if err == nil { + t.Fatalf("failed w.GetSymKey(id2): false positive.") + } + if !w.HasSymKey(id1) { + t.Fatalf("failed w.HasSymKey(id1).") + } + if w.HasSymKey(id2) { + t.Fatalf("failed w.HasSymKey(id2): false positive.") + } + if k1 == nil { + t.Fatalf("first key does not exist.") + } + if !bytes.Equal(k1, randomKey) { + t.Fatalf("k1 != randomKey.") + } + if k2 != nil { + t.Fatalf("second key already exist.") + } + + id2, err = w.AddSymKeyDirect(randomKey) + if err != nil { + t.Fatalf("failed AddSymKey(id2) with seed %d: %s.", seed, err) + } + k1, err = w.GetSymKey(id1) + if err != nil { + t.Fatalf("failed w.GetSymKey(id1).") + } + k2, err = w.GetSymKey(id2) + if err != nil { + t.Fatalf("failed w.GetSymKey(id2).") + } + if !w.HasSymKey(id1) { + t.Fatalf("HasSymKey(id1) failed.") + } + if !w.HasSymKey(id2) { + t.Fatalf("HasSymKey(id2) failed.") + } + if k1 == nil { + t.Fatalf("k1 does not exist.") + } + if k2 == nil { + t.Fatalf("k2 does not exist.") + } + if !bytes.Equal(k1, k2) { + t.Fatalf("k1 != k2.") + } + if !bytes.Equal(k1, randomKey) { + t.Fatalf("k1 != randomKey.") + } + if len(k1) != aesKeyLength { + t.Fatalf("wrong length of k1.") + } + if len(k2) != aesKeyLength { + t.Fatalf("wrong length of k2.") + } + + w.DeleteSymKey(id1) + k1, err = w.GetSymKey(id1) + if err == nil { + t.Fatalf("failed w.GetSymKey(id1): false positive.") + } + if k1 != nil { + t.Fatalf("failed GetSymKey(id1): false positive.") + } + k2, err = w.GetSymKey(id2) + if err != nil { + t.Fatalf("failed w.GetSymKey(id2).") + } + if w.HasSymKey(id1) { + t.Fatalf("failed to delete first key: still exist.") + } + if !w.HasSymKey(id2) { + t.Fatalf("failed to delete first key: second key does not exist.") + } + if k1 != nil { + t.Fatalf("failed to delete first key.") + } + if k2 == nil { + t.Fatalf("failed to delete first key: second key is nil.") + } + + w.DeleteSymKey(id1) + w.DeleteSymKey(id2) + k1, err = w.GetSymKey(id1) + if err == nil { + t.Fatalf("failed w.GetSymKey(id1): false positive.") + } + k2, err = w.GetSymKey(id2) + if err == nil { + t.Fatalf("failed w.GetSymKey(id2): false positive.") + } + if k1 != nil || k2 != nil { + t.Fatalf("k1 or k2 is not nil") + } + if w.HasSymKey(id1) { + t.Fatalf("failed to delete second key: first key exist.") + } + if w.HasSymKey(id2) { + t.Fatalf("failed to delete second key: still exist.") + } + if k1 != nil { + t.Fatalf("failed to delete second key: first key is not nil.") + } + if k2 != nil { + t.Fatalf("failed to delete second key: second key is not nil.") + } + + randomKey = make([]byte, aesKeyLength+1) + mrand.Read(randomKey) + _, err = w.AddSymKeyDirect(randomKey) + if err == nil { + t.Fatalf("added the key with wrong size, seed %d.", seed) + } + + const password = "arbitrary data here" + id1, err = w.AddSymKeyFromPassword(password) + if err != nil { + t.Fatalf("failed AddSymKeyFromPassword(id1) with seed %d: %s.", seed, err) + } + id2, err = w.AddSymKeyFromPassword(password) + if err != nil { + t.Fatalf("failed AddSymKeyFromPassword(id2) with seed %d: %s.", seed, err) + } + k1, err = w.GetSymKey(id1) + if err != nil { + t.Fatalf("failed w.GetSymKey(id1).") + } + k2, err = w.GetSymKey(id2) + if err != nil { + t.Fatalf("failed w.GetSymKey(id2).") + } + if !w.HasSymKey(id1) { + t.Fatalf("HasSymKey(id1) failed.") + } + if !w.HasSymKey(id2) { + t.Fatalf("HasSymKey(id2) failed.") + } + if !validateDataIntegrity(k2, aesKeyLength) { + t.Fatalf("key validation failed.") + } + if !bytes.Equal(k1, k2) { + t.Fatalf("k1 != k2.") + } +} + +func TestExpiry(t *testing.T) { + InitSingleTest() + + w := New(&DefaultConfig) + w.SetMinimumPowTest(0.0000001) + defer w.SetMinimumPowTest(DefaultMinimumPoW) + w.Start(nil) + defer w.Stop() + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + params.TTL = 1 + + messagesCount := 5 + + // Send a few messages one after another. Due to low PoW and expiration buckets + // with one second resolution, it covers a case when there are multiple items + // in a single expiration bucket. + for i := 0; i < messagesCount; i++ { + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + err = w.Send(env) + if err != nil { + t.Fatalf("failed to send envelope with seed %d: %s.", seed, err) + } + } + + // wait till received or timeout + var received, expired bool + for j := 0; j < 20; j++ { + time.Sleep(100 * time.Millisecond) + if len(w.Envelopes()) == messagesCount { + received = true + break + } + } + + if !received { + t.Fatalf("did not receive the sent envelope, seed: %d.", seed) + } + + // wait till expired or timeout + for j := 0; j < 20; j++ { + time.Sleep(100 * time.Millisecond) + if len(w.Envelopes()) == 0 { + expired = true + break + } + } + + if !expired { + t.Fatalf("expire failed, seed: %d.", seed) + } +} + +func TestCustomization(t *testing.T) { + InitSingleTest() + + w := New(&DefaultConfig) + defer w.SetMinimumPowTest(DefaultMinimumPoW) + defer w.SetMaxMessageSize(DefaultMaxMessageSize) + w.Start(nil) + defer w.Stop() + + const smallPoW = 0.00001 + + f, err := generateFilter(t, true) + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + + params.KeySym = f.KeySym + params.Topic = BytesToTopic(f.Topics[2]) + params.PoW = smallPoW + params.TTL = 3600 * 24 // one day + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + err = w.Send(env) + if err == nil { + t.Fatalf("successfully sent envelope with PoW %.06f, false positive (seed %d).", env.PoW(), seed) + } + + w.SetMinimumPowTest(smallPoW / 2) + err = w.Send(env) + if err != nil { + t.Fatalf("failed to send envelope with seed %d: %s.", seed, err) + } + + params.TTL++ + msg, err = NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err = msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + w.SetMaxMessageSize(uint32(env.size() - 1)) + err = w.Send(env) + if err == nil { + t.Fatalf("successfully sent oversized envelope (seed %d): false positive.", seed) + } + + w.SetMaxMessageSize(DefaultMaxMessageSize) + err = w.Send(env) + if err != nil { + t.Fatalf("failed to send second envelope with seed %d: %s.", seed, err) + } + + // wait till received or timeout + var received bool + for j := 0; j < 20; j++ { + time.Sleep(100 * time.Millisecond) + if len(w.Envelopes()) > 1 { + received = true + break + } + } + + if !received { + t.Fatalf("did not receive the sent envelope, seed: %d.", seed) + } + + // check w.messages() + _, err = w.Subscribe(f) + if err != nil { + t.Fatalf("failed subscribe with seed %d: %s.", seed, err) + } + time.Sleep(5 * time.Millisecond) + mail := f.Retrieve() + if len(mail) > 0 { + t.Fatalf("received premature mail") + } +} + +func TestSymmetricSendCycle(t *testing.T) { + InitSingleTest() + + w := New(&DefaultConfig) + defer w.SetMinimumPowTest(DefaultMinimumPoW) + defer w.SetMaxMessageSize(DefaultMaxMessageSize) + w.Start(nil) + defer w.Stop() + + filter1, err := generateFilter(t, true) + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + filter1.PoW = DefaultMinimumPoW + + // Copy the first filter since some of its fields + // are randomly gnerated. + filter2 := &Filter{ + KeySym: filter1.KeySym, + Topics: filter1.Topics, + PoW: filter1.PoW, + AllowP2P: filter1.AllowP2P, + Messages: make(map[common.Hash]*ReceivedMessage), + } + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + + filter1.Src = ¶ms.Src.PublicKey + filter2.Src = ¶ms.Src.PublicKey + + params.KeySym = filter1.KeySym + params.Topic = BytesToTopic(filter1.Topics[2]) + params.PoW = filter1.PoW + params.WorkTime = 10 + params.TTL = 50 + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + _, err = w.Subscribe(filter1) + if err != nil { + t.Fatalf("failed subscribe 1 with seed %d: %s.", seed, err) + } + + _, err = w.Subscribe(filter2) + if err != nil { + t.Fatalf("failed subscribe 2 with seed %d: %s.", seed, err) + } + + err = w.Send(env) + if err != nil { + t.Fatalf("Failed sending envelope with PoW %.06f (seed %d): %s", env.PoW(), seed, err) + } + + // wait till received or timeout + var received bool + for j := 0; j < 200; j++ { + time.Sleep(10 * time.Millisecond) + if len(w.Envelopes()) > 0 { + received = true + break + } + } + + if !received { + t.Fatalf("did not receive the sent envelope, seed: %d.", seed) + } + + // check w.messages() + time.Sleep(5 * time.Millisecond) + mail1 := filter1.Retrieve() + mail2 := filter2.Retrieve() + if len(mail2) == 0 { + t.Fatalf("did not receive any email for filter 2") + } + if len(mail1) == 0 { + t.Fatalf("did not receive any email for filter 1") + } + +} + +func TestSymmetricSendWithoutAKey(t *testing.T) { + InitSingleTest() + + w := New(&DefaultConfig) + defer w.SetMinimumPowTest(DefaultMinimumPoW) + defer w.SetMaxMessageSize(DefaultMaxMessageSize) + w.Start(nil) + defer w.Stop() + + filter, err := generateFilter(t, true) + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + filter.PoW = DefaultMinimumPoW + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + + filter.Src = nil + + params.KeySym = filter.KeySym + params.Topic = BytesToTopic(filter.Topics[2]) + params.PoW = filter.PoW + params.WorkTime = 10 + params.TTL = 50 + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + _, err = w.Subscribe(filter) + if err != nil { + t.Fatalf("failed subscribe 1 with seed %d: %s.", seed, err) + } + + err = w.Send(env) + if err != nil { + t.Fatalf("Failed sending envelope with PoW %.06f (seed %d): %s", env.PoW(), seed, err) + } + + // wait till received or timeout + var received bool + for j := 0; j < 200; j++ { + time.Sleep(10 * time.Millisecond) + if len(w.Envelopes()) > 0 { + received = true + break + } + } + + if !received { + t.Fatalf("did not receive the sent envelope, seed: %d.", seed) + } + + // check w.messages() + time.Sleep(5 * time.Millisecond) + mail := filter.Retrieve() + if len(mail) == 0 { + t.Fatalf("did not receive message in spite of not setting a public key") + } +} + +func TestSymmetricSendKeyMismatch(t *testing.T) { + InitSingleTest() + + w := New(&DefaultConfig) + defer w.SetMinimumPowTest(DefaultMinimumPoW) + defer w.SetMaxMessageSize(DefaultMaxMessageSize) + w.Start(nil) + defer w.Stop() + + filter, err := generateFilter(t, true) + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + filter.PoW = DefaultMinimumPoW + + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + + params.KeySym = filter.KeySym + params.Topic = BytesToTopic(filter.Topics[2]) + params.PoW = filter.PoW + params.WorkTime = 10 + params.TTL = 50 + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed Wrap with seed %d: %s.", seed, err) + } + + _, err = w.Subscribe(filter) + if err != nil { + t.Fatalf("failed subscribe 1 with seed %d: %s.", seed, err) + } + + err = w.Send(env) + if err != nil { + t.Fatalf("Failed sending envelope with PoW %.06f (seed %d): %s", env.PoW(), seed, err) + } + + // wait till received or timeout + var received bool + for j := 0; j < 200; j++ { + time.Sleep(10 * time.Millisecond) + if len(w.Envelopes()) > 0 { + received = true + break + } + } + + if !received { + t.Fatalf("did not receive the sent envelope, seed: %d.", seed) + } + + // check w.messages() + time.Sleep(5 * time.Millisecond) + mail := filter.Retrieve() + if len(mail) > 0 { + t.Fatalf("received a message when keys weren't matching") + } +} + +func TestBloom(t *testing.T) { + topic := TopicType{0, 0, 255, 6} + b := TopicToBloom(topic) + x := make([]byte, BloomFilterSize) + x[0] = byte(1) + x[32] = byte(1) + x[BloomFilterSize-1] = byte(128) + if !BloomFilterMatch(x, b) || !BloomFilterMatch(b, x) { + t.Fatalf("bloom filter does not match the mask") + } + + _, err := mrand.Read(b) + if err != nil { + t.Fatalf("math rand error") + } + _, err = mrand.Read(x) + if err != nil { + t.Fatalf("math rand error") + } + if !BloomFilterMatch(b, b) { + t.Fatalf("bloom filter does not match self") + } + x = addBloom(x, b) + if !BloomFilterMatch(x, b) { + t.Fatalf("bloom filter does not match combined bloom") + } + if !isFullNode(nil) { + t.Fatalf("isFullNode did not recognize nil as full node") + } + x[17] = 254 + if isFullNode(x) { + t.Fatalf("isFullNode false positive") + } + for i := 0; i < BloomFilterSize; i++ { + b[i] = byte(255) + } + if !isFullNode(b) { + t.Fatalf("isFullNode false negative") + } + if BloomFilterMatch(x, b) { + t.Fatalf("bloomFilterMatch false positive") + } + if !BloomFilterMatch(b, x) { + t.Fatalf("bloomFilterMatch false negative") + } + + w := New(&DefaultConfig) + f := w.BloomFilter() + if f != nil { + t.Fatalf("wrong bloom on creation") + } + err = w.SetBloomFilter(x) + if err != nil { + t.Fatalf("failed to set bloom filter: %s", err) + } + f = w.BloomFilter() + if !BloomFilterMatch(f, x) || !BloomFilterMatch(x, f) { + t.Fatalf("retireved wrong bloom filter") + } +}