From 79789af2e7fce8807d21a8eedbf42d41a7c55848 Mon Sep 17 00:00:00 2001 From: gluk256 Date: Sat, 29 Oct 2016 14:11:37 +0200 Subject: whisper: project restructured, version 5 introduced (#3022) whisper: project restructured, version 5 introduced This commits adds a draft version of the new shh v5 protocol. The new version is not on by default, --shh still selects version 2. --- cmd/gethrpctest/main.go | 2 +- cmd/utils/flags.go | 2 +- whisper/api.go | 413 -------------------- whisper/doc.go | 32 -- whisper/envelope.go | 147 -------- whisper/envelope_test.go | 158 -------- whisper/filter.go | 132 ------- whisper/filter_test.go | 215 ----------- whisper/main.go | 106 ------ whisper/message.go | 156 -------- whisper/message_test.go | 159 -------- whisper/peer.go | 175 --------- whisper/peer_test.go | 261 ------------- whisper/shhapi/api.go | 504 +++++++++++++++++++++++++ whisper/shhapi/api_test.go | 170 +++++++++ whisper/topic.go | 140 ------- whisper/topic_test.go | 215 ----------- whisper/whisper.go | 378 ------------------- whisper/whisper_test.go | 216 ----------- whisper/whisperv2/api.go | 413 ++++++++++++++++++++ whisper/whisperv2/doc.go | 32 ++ whisper/whisperv2/envelope.go | 147 ++++++++ whisper/whisperv2/envelope_test.go | 158 ++++++++ whisper/whisperv2/filter.go | 132 +++++++ whisper/whisperv2/filter_test.go | 215 +++++++++++ whisper/whisperv2/main.go | 106 ++++++ whisper/whisperv2/message.go | 156 ++++++++ whisper/whisperv2/message_test.go | 159 ++++++++ whisper/whisperv2/peer.go | 175 +++++++++ whisper/whisperv2/peer_test.go | 261 +++++++++++++ whisper/whisperv2/topic.go | 140 +++++++ whisper/whisperv2/topic_test.go | 215 +++++++++++ whisper/whisperv2/whisper.go | 378 +++++++++++++++++++ whisper/whisperv2/whisper_test.go | 216 +++++++++++ whisper/whisperv5/benchmarks_test.go | 202 ++++++++++ whisper/whisperv5/doc.go | 87 +++++ whisper/whisperv5/envelope.go | 233 ++++++++++++ whisper/whisperv5/filter.go | 197 ++++++++++ whisper/whisperv5/filter_test.go | 707 +++++++++++++++++++++++++++++++++++ whisper/whisperv5/message.go | 378 +++++++++++++++++++ whisper/whisperv5/message_test.go | 306 +++++++++++++++ whisper/whisperv5/peer.go | 174 +++++++++ whisper/whisperv5/peer_test.go | 307 +++++++++++++++ whisper/whisperv5/topic.go | 70 ++++ whisper/whisperv5/topic_test.go | 136 +++++++ whisper/whisperv5/whisper.go | 585 +++++++++++++++++++++++++++++ whisper/whisperv5/whisper_test.go | 377 +++++++++++++++++++ 47 files changed, 7338 insertions(+), 2905 deletions(-) delete mode 100644 whisper/api.go delete mode 100644 whisper/doc.go delete mode 100644 whisper/envelope.go delete mode 100644 whisper/envelope_test.go delete mode 100644 whisper/filter.go delete mode 100644 whisper/filter_test.go delete mode 100644 whisper/main.go delete mode 100644 whisper/message.go delete mode 100644 whisper/message_test.go delete mode 100644 whisper/peer.go delete mode 100644 whisper/peer_test.go create mode 100644 whisper/shhapi/api.go create mode 100644 whisper/shhapi/api_test.go delete mode 100644 whisper/topic.go delete mode 100644 whisper/topic_test.go delete mode 100644 whisper/whisper.go delete mode 100644 whisper/whisper_test.go create mode 100644 whisper/whisperv2/api.go create mode 100644 whisper/whisperv2/doc.go create mode 100644 whisper/whisperv2/envelope.go create mode 100644 whisper/whisperv2/envelope_test.go create mode 100644 whisper/whisperv2/filter.go create mode 100644 whisper/whisperv2/filter_test.go create mode 100644 whisper/whisperv2/main.go create mode 100644 whisper/whisperv2/message.go create mode 100644 whisper/whisperv2/message_test.go create mode 100644 whisper/whisperv2/peer.go create mode 100644 whisper/whisperv2/peer_test.go create mode 100644 whisper/whisperv2/topic.go create mode 100644 whisper/whisperv2/topic_test.go create mode 100644 whisper/whisperv2/whisper.go create mode 100644 whisper/whisperv2/whisper_test.go create mode 100644 whisper/whisperv5/benchmarks_test.go create mode 100644 whisper/whisperv5/doc.go create mode 100644 whisper/whisperv5/envelope.go create mode 100644 whisper/whisperv5/filter.go create mode 100644 whisper/whisperv5/filter_test.go create mode 100644 whisper/whisperv5/message.go create mode 100644 whisper/whisperv5/message_test.go create mode 100644 whisper/whisperv5/peer.go create mode 100644 whisper/whisperv5/peer_test.go create mode 100644 whisper/whisperv5/topic.go create mode 100644 whisper/whisperv5/topic_test.go create mode 100644 whisper/whisperv5/whisper.go create mode 100644 whisper/whisperv5/whisper_test.go diff --git a/cmd/gethrpctest/main.go b/cmd/gethrpctest/main.go index d0d6e1618..343aedc6f 100644 --- a/cmd/gethrpctest/main.go +++ b/cmd/gethrpctest/main.go @@ -31,7 +31,7 @@ import ( "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/tests" - "github.com/ethereum/go-ethereum/whisper" + whisper "github.com/ethereum/go-ethereum/whisper/whisperv2" ) const defaultTestKey = "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 5e2dcf8c9..abfb4d1e7 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -48,7 +48,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/pow" "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/whisper" + whisper "github.com/ethereum/go-ethereum/whisper/whisperv2" "gopkg.in/urfave/cli.v1" ) diff --git a/whisper/api.go b/whisper/api.go deleted file mode 100644 index d1f454a1f..000000000 --- a/whisper/api.go +++ /dev/null @@ -1,413 +0,0 @@ -// Copyright 2015 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 - -import ( - "encoding/json" - "fmt" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/rpc" -) - -// PublicWhisperAPI provides the whisper RPC service. -type PublicWhisperAPI struct { - w *Whisper - - messagesMu sync.RWMutex - messages map[int]*whisperFilter -} - -type whisperOfflineError struct{} - -func (e *whisperOfflineError) Error() string { - return "whisper is offline" -} - -// whisperOffLineErr is returned when the node doesn't offer the shh service. -var whisperOffLineErr = new(whisperOfflineError) - -// NewPublicWhisperAPI create a new RPC whisper service. -func NewPublicWhisperAPI(w *Whisper) *PublicWhisperAPI { - return &PublicWhisperAPI{w: w, messages: make(map[int]*whisperFilter)} -} - -// Version returns the Whisper version this node offers. -func (s *PublicWhisperAPI) Version() (*rpc.HexNumber, error) { - if s.w == nil { - return rpc.NewHexNumber(0), whisperOffLineErr - } - return rpc.NewHexNumber(s.w.Version()), nil -} - -// HasIdentity checks if the the whisper node is configured with the private key -// of the specified public pair. -func (s *PublicWhisperAPI) HasIdentity(identity string) (bool, error) { - if s.w == nil { - return false, whisperOffLineErr - } - return s.w.HasIdentity(crypto.ToECDSAPub(common.FromHex(identity))), nil -} - -// NewIdentity generates a new cryptographic identity for the client, and injects -// it into the known identities for message decryption. -func (s *PublicWhisperAPI) NewIdentity() (string, error) { - if s.w == nil { - return "", whisperOffLineErr - } - - identity := s.w.NewIdentity() - return common.ToHex(crypto.FromECDSAPub(&identity.PublicKey)), nil -} - -type NewFilterArgs struct { - To string - From string - Topics [][][]byte -} - -// NewWhisperFilter creates and registers a new message filter to watch for inbound whisper messages. -func (s *PublicWhisperAPI) NewFilter(args NewFilterArgs) (*rpc.HexNumber, error) { - if s.w == nil { - return nil, whisperOffLineErr - } - - var id int - filter := Filter{ - To: crypto.ToECDSAPub(common.FromHex(args.To)), - From: crypto.ToECDSAPub(common.FromHex(args.From)), - Topics: NewFilterTopics(args.Topics...), - Fn: func(message *Message) { - wmsg := NewWhisperMessage(message) - s.messagesMu.RLock() // Only read lock to the filter pool - defer s.messagesMu.RUnlock() - if s.messages[id] != nil { - s.messages[id].insert(wmsg) - } - }, - } - - id = s.w.Watch(filter) - - s.messagesMu.Lock() - s.messages[id] = newWhisperFilter(id, s.w) - s.messagesMu.Unlock() - - return rpc.NewHexNumber(id), nil -} - -// GetFilterChanges retrieves all the new messages matched by a filter since the last retrieval. -func (s *PublicWhisperAPI) GetFilterChanges(filterId rpc.HexNumber) []WhisperMessage { - s.messagesMu.RLock() - defer s.messagesMu.RUnlock() - - if s.messages[filterId.Int()] != nil { - if changes := s.messages[filterId.Int()].retrieve(); changes != nil { - return changes - } - } - return returnWhisperMessages(nil) -} - -// UninstallFilter disables and removes an existing filter. -func (s *PublicWhisperAPI) UninstallFilter(filterId rpc.HexNumber) bool { - s.messagesMu.Lock() - defer s.messagesMu.Unlock() - - if _, ok := s.messages[filterId.Int()]; ok { - delete(s.messages, filterId.Int()) - return true - } - return false -} - -// GetMessages retrieves all the known messages that match a specific filter. -func (s *PublicWhisperAPI) GetMessages(filterId rpc.HexNumber) []WhisperMessage { - // Retrieve all the cached messages matching a specific, existing filter - s.messagesMu.RLock() - defer s.messagesMu.RUnlock() - - var messages []*Message - if s.messages[filterId.Int()] != nil { - messages = s.messages[filterId.Int()].messages() - } - - return returnWhisperMessages(messages) -} - -// returnWhisperMessages converts aNhisper message to a RPC whisper message. -func returnWhisperMessages(messages []*Message) []WhisperMessage { - msgs := make([]WhisperMessage, len(messages)) - for i, msg := range messages { - msgs[i] = NewWhisperMessage(msg) - } - return msgs -} - -type PostArgs struct { - From string `json:"from"` - To string `json:"to"` - Topics [][]byte `json:"topics"` - Payload string `json:"payload"` - Priority int64 `json:"priority"` - TTL int64 `json:"ttl"` -} - -// Post injects a message into the whisper network for distribution. -func (s *PublicWhisperAPI) Post(args PostArgs) (bool, error) { - if s.w == nil { - return false, whisperOffLineErr - } - - // construct whisper message with transmission options - message := NewMessage(common.FromHex(args.Payload)) - options := Options{ - To: crypto.ToECDSAPub(common.FromHex(args.To)), - TTL: time.Duration(args.TTL) * time.Second, - Topics: NewTopics(args.Topics...), - } - - // set sender identity - if len(args.From) > 0 { - if key := s.w.GetIdentity(crypto.ToECDSAPub(common.FromHex(args.From))); key != nil { - options.From = key - } else { - return false, fmt.Errorf("unknown identity to send from: %s", args.From) - } - } - - // Wrap and send the message - pow := time.Duration(args.Priority) * time.Millisecond - envelope, err := message.Wrap(pow, options) - if err != nil { - return false, err - } - - return true, s.w.Send(envelope) -} - -// WhisperMessage is the RPC representation of a whisper message. -type WhisperMessage struct { - ref *Message - - Payload string `json:"payload"` - To string `json:"to"` - From string `json:"from"` - Sent int64 `json:"sent"` - TTL int64 `json:"ttl"` - Hash string `json:"hash"` -} - -func (args *PostArgs) UnmarshalJSON(data []byte) (err error) { - var obj struct { - From string `json:"from"` - To string `json:"to"` - Topics []string `json:"topics"` - Payload string `json:"payload"` - Priority rpc.HexNumber `json:"priority"` - TTL rpc.HexNumber `json:"ttl"` - } - - if err := json.Unmarshal(data, &obj); err != nil { - return err - } - - args.From = obj.From - args.To = obj.To - args.Payload = obj.Payload - args.Priority = obj.Priority.Int64() - args.TTL = obj.TTL.Int64() - - // decode topic strings - args.Topics = make([][]byte, len(obj.Topics)) - for i, topic := range obj.Topics { - args.Topics[i] = common.FromHex(topic) - } - - return nil -} - -// UnmarshalJSON implements the json.Unmarshaler interface, invoked to convert a -// JSON message blob into a WhisperFilterArgs structure. -func (args *NewFilterArgs) UnmarshalJSON(b []byte) (err error) { - // Unmarshal the JSON message and sanity check - var obj struct { - To interface{} `json:"to"` - From interface{} `json:"from"` - Topics interface{} `json:"topics"` - } - if err := json.Unmarshal(b, &obj); err != nil { - return err - } - - // Retrieve the simple data contents of the filter arguments - if obj.To == nil { - args.To = "" - } else { - argstr, ok := obj.To.(string) - if !ok { - return fmt.Errorf("to is not a string") - } - args.To = argstr - } - if obj.From == nil { - args.From = "" - } else { - argstr, ok := obj.From.(string) - if !ok { - return fmt.Errorf("from is not a string") - } - args.From = argstr - } - // Construct the nested topic array - if obj.Topics != nil { - // Make sure we have an actual topic array - list, ok := obj.Topics.([]interface{}) - if !ok { - return fmt.Errorf("topics is not an array") - } - // Iterate over each topic and handle nil, string or array - topics := make([][]string, len(list)) - for idx, field := range list { - switch value := field.(type) { - case nil: - topics[idx] = []string{} - - case string: - topics[idx] = []string{value} - - case []interface{}: - topics[idx] = make([]string, len(value)) - for i, nested := range value { - switch value := nested.(type) { - case nil: - topics[idx][i] = "" - - case string: - topics[idx][i] = value - - default: - return fmt.Errorf("topic[%d][%d] is not a string", idx, i) - } - } - default: - return fmt.Errorf("topic[%d] not a string or array", idx) - } - } - - topicsDecoded := make([][][]byte, len(topics)) - for i, condition := range topics { - topicsDecoded[i] = make([][]byte, len(condition)) - for j, topic := range condition { - topicsDecoded[i][j] = common.FromHex(topic) - } - } - - args.Topics = topicsDecoded - } - return nil -} - -// whisperFilter is the message cache matching a specific filter, accumulating -// inbound messages until the are requested by the client. -type whisperFilter struct { - id int // Filter identifier for old message retrieval - ref *Whisper // Whisper reference for old message retrieval - - cache []WhisperMessage // Cache of messages not yet polled - skip map[common.Hash]struct{} // List of retrieved messages to avoid duplication - update time.Time // Time of the last message query - - lock sync.RWMutex // Lock protecting the filter internals -} - -// messages retrieves all the cached messages from the entire pool matching the -// filter, resetting the filter's change buffer. -func (w *whisperFilter) messages() []*Message { - w.lock.Lock() - defer w.lock.Unlock() - - w.cache = nil - w.update = time.Now() - - w.skip = make(map[common.Hash]struct{}) - messages := w.ref.Messages(w.id) - for _, message := range messages { - w.skip[message.Hash] = struct{}{} - } - return messages -} - -// insert injects a new batch of messages into the filter cache. -func (w *whisperFilter) insert(messages ...WhisperMessage) { - w.lock.Lock() - defer w.lock.Unlock() - - for _, message := range messages { - if _, ok := w.skip[message.ref.Hash]; !ok { - w.cache = append(w.cache, messages...) - } - } -} - -// retrieve fetches all the cached messages from the filter. -func (w *whisperFilter) retrieve() (messages []WhisperMessage) { - w.lock.Lock() - defer w.lock.Unlock() - - messages, w.cache = w.cache, nil - w.update = time.Now() - - return -} - -// activity returns the last time instance when client requests were executed on -// the filter. -func (w *whisperFilter) activity() time.Time { - w.lock.RLock() - defer w.lock.RUnlock() - - return w.update -} - -// newWhisperFilter creates a new serialized, poll based whisper topic filter. -func newWhisperFilter(id int, ref *Whisper) *whisperFilter { - return &whisperFilter{ - id: id, - ref: ref, - - update: time.Now(), - skip: make(map[common.Hash]struct{}), - } -} - -// NewWhisperMessage converts an internal message into an API version. -func NewWhisperMessage(message *Message) WhisperMessage { - return WhisperMessage{ - ref: message, - - Payload: common.ToHex(message.Payload), - From: common.ToHex(crypto.FromECDSAPub(message.Recover())), - To: common.ToHex(crypto.FromECDSAPub(message.To)), - Sent: message.Sent.Unix(), - TTL: int64(message.TTL / time.Second), - Hash: common.ToHex(message.Hash.Bytes()), - } -} diff --git a/whisper/doc.go b/whisper/doc.go deleted file mode 100644 index cfb0b5117..000000000 --- a/whisper/doc.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2014 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 PoC-1. - -(https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec) - -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. -*/ -package whisper diff --git a/whisper/envelope.go b/whisper/envelope.go deleted file mode 100644 index 97d489b96..000000000 --- a/whisper/envelope.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2014 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. For formal details please see -// the specs at https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec#envelopes. - -package whisper - -import ( - "crypto/ecdsa" - "encoding/binary" - "fmt" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/ecies" - "github.com/ethereum/go-ethereum/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 // Whisper protocol specifies int32, really should be int64 - TTL uint32 // ^^^^^^ - Topics []Topic - Data []byte - Nonce uint32 - - hash common.Hash // Cached hash of the envelope to avoid rehashing every time -} - -// NewEnvelope wraps a Whisper message with expiration and destination data -// included into an envelope for network forwarding. -func NewEnvelope(ttl time.Duration, topics []Topic, msg *Message) *Envelope { - return &Envelope{ - Expiry: uint32(time.Now().Add(ttl).Unix()), - TTL: uint32(ttl.Seconds()), - Topics: topics, - Data: msg.bytes(), - Nonce: 0, - } -} - -// Seal closes the envelope by spending the requested amount of time as a proof -// of work on hashing the data. -func (self *Envelope) Seal(pow time.Duration) { - d := make([]byte, 64) - copy(d[:32], self.rlpWithoutNonce()) - - finish, bestBit := time.Now().Add(pow).UnixNano(), 0 - for nonce := uint32(0); time.Now().UnixNano() < finish; { - for i := 0; i < 1024; i++ { - binary.BigEndian.PutUint32(d[60:], nonce) - - firstBit := common.FirstBitSet(common.BigD(crypto.Keccak256(d))) - if firstBit > bestBit { - self.Nonce, bestBit = nonce, firstBit - } - nonce++ - } - } -} - -// rlpWithoutNonce returns the RLP encoded envelope contents, except the nonce. -func (self *Envelope) rlpWithoutNonce() []byte { - enc, _ := rlp.EncodeToBytes([]interface{}{self.Expiry, self.TTL, self.Topics, self.Data}) - return enc -} - -// Open extracts the message contained within a potentially encrypted envelope. -func (self *Envelope) Open(key *ecdsa.PrivateKey) (msg *Message, err error) { - // Split open the payload into a message construct - data := self.Data - - message := &Message{ - Flags: data[0], - Sent: time.Unix(int64(self.Expiry-self.TTL), 0), - TTL: time.Duration(self.TTL) * time.Second, - Hash: self.Hash(), - } - data = data[1:] - - if message.Flags&signatureFlag == signatureFlag { - if len(data) < signatureLength { - return nil, fmt.Errorf("unable to open envelope. First bit set but len(data) < len(signature)") - } - message.Signature, data = data[:signatureLength], data[signatureLength:] - } - message.Payload = data - - // Decrypt the message, if requested - if key == nil { - return message, nil - } - err = message.decrypt(key) - switch err { - case nil: - return message, nil - - case ecies.ErrInvalidPublicKey: // Payload isn't encrypted - return message, err - - default: - return nil, fmt.Errorf("unable to open envelope, decrypt failed: %v", err) - } -} - -// Hash returns the SHA3 hash of the envelope, calculating it if not yet done. -func (self *Envelope) Hash() common.Hash { - if (self.hash == common.Hash{}) { - enc, _ := rlp.EncodeToBytes(self) - self.hash = crypto.Keccak256Hash(enc) - } - return self.hash -} - -// DecodeRLP decodes an Envelope from an RLP data stream. -func (self *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 so we can reuse the Envelope struct definition. - type rlpenv Envelope - if err := rlp.DecodeBytes(raw, (*rlpenv)(self)); err != nil { - return err - } - self.hash = crypto.Keccak256Hash(raw) - return nil -} diff --git a/whisper/envelope_test.go b/whisper/envelope_test.go deleted file mode 100644 index 3bfe52737..000000000 --- a/whisper/envelope_test.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2015 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 - -import ( - "bytes" - "testing" - "time" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/ecies" -) - -func TestEnvelopeOpen(t *testing.T) { - payload := []byte("hello world") - message := NewMessage(payload) - - envelope, err := message.Wrap(DefaultPoW, Options{}) - if err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - opened, err := envelope.Open(nil) - if err != nil { - t.Fatalf("failed to open envelope: %v", err) - } - if opened.Flags != message.Flags { - t.Fatalf("flags mismatch: have %d, want %d", opened.Flags, message.Flags) - } - if bytes.Compare(opened.Signature, message.Signature) != 0 { - t.Fatalf("signature mismatch: have 0x%x, want 0x%x", opened.Signature, message.Signature) - } - if bytes.Compare(opened.Payload, message.Payload) != 0 { - t.Fatalf("payload mismatch: have 0x%x, want 0x%x", opened.Payload, message.Payload) - } - if opened.Sent.Unix() != message.Sent.Unix() { - t.Fatalf("send time mismatch: have %d, want %d", opened.Sent, message.Sent) - } - if opened.TTL/time.Second != DefaultTTL/time.Second { - t.Fatalf("message TTL mismatch: have %v, want %v", opened.TTL, DefaultTTL) - } - - if opened.Hash != envelope.Hash() { - t.Fatalf("message hash mismatch: have 0x%x, want 0x%x", opened.Hash, envelope.Hash()) - } -} - -func TestEnvelopeAnonymousOpenUntargeted(t *testing.T) { - payload := []byte("hello envelope") - envelope, err := NewMessage(payload).Wrap(DefaultPoW, Options{}) - if err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - opened, err := envelope.Open(nil) - if err != nil { - t.Fatalf("failed to open envelope: %v", err) - } - if opened.To != nil { - t.Fatalf("recipient mismatch: have 0x%x, want nil", opened.To) - } - if bytes.Compare(opened.Payload, payload) != 0 { - t.Fatalf("payload mismatch: have 0x%x, want 0x%x", opened.Payload, payload) - } -} - -func TestEnvelopeAnonymousOpenTargeted(t *testing.T) { - key, err := crypto.GenerateKey() - if err != nil { - t.Fatalf("failed to generate test identity: %v", err) - } - - payload := []byte("hello envelope") - envelope, err := NewMessage(payload).Wrap(DefaultPoW, Options{ - To: &key.PublicKey, - }) - if err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - opened, err := envelope.Open(nil) - if err != nil { - t.Fatalf("failed to open envelope: %v", err) - } - if opened.To != nil { - t.Fatalf("recipient mismatch: have 0x%x, want nil", opened.To) - } - if bytes.Compare(opened.Payload, payload) == 0 { - t.Fatalf("payload match, should have been encrypted: 0x%x", opened.Payload) - } -} - -func TestEnvelopeIdentifiedOpenUntargeted(t *testing.T) { - key, err := crypto.GenerateKey() - if err != nil { - t.Fatalf("failed to generate test identity: %v", err) - } - - payload := []byte("hello envelope") - envelope, err := NewMessage(payload).Wrap(DefaultPoW, Options{}) - if err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - opened, err := envelope.Open(key) - switch err { - case nil: - t.Fatalf("envelope opened with bad key: %v", opened) - - case ecies.ErrInvalidPublicKey: - // Ok, key mismatch but opened - - default: - t.Fatalf("failed to open envelope: %v", err) - } - - if opened.To != nil { - t.Fatalf("recipient mismatch: have 0x%x, want nil", opened.To) - } - if bytes.Compare(opened.Payload, payload) != 0 { - t.Fatalf("payload mismatch: have 0x%x, want 0x%x", opened.Payload, payload) - } -} - -func TestEnvelopeIdentifiedOpenTargeted(t *testing.T) { - key, err := crypto.GenerateKey() - if err != nil { - t.Fatalf("failed to generate test identity: %v", err) - } - - payload := []byte("hello envelope") - envelope, err := NewMessage(payload).Wrap(DefaultPoW, Options{ - To: &key.PublicKey, - }) - if err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - opened, err := envelope.Open(key) - if err != nil { - t.Fatalf("failed to open envelope: %v", err) - } - if opened.To != nil { - t.Fatalf("recipient mismatch: have 0x%x, want nil", opened.To) - } - if bytes.Compare(opened.Payload, payload) != 0 { - t.Fatalf("payload mismatch: have 0x%x, want 0x%x", opened.Payload, payload) - } -} diff --git a/whisper/filter.go b/whisper/filter.go deleted file mode 100644 index 9f6d6b781..000000000 --- a/whisper/filter.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2014 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 message filter for fine grained subscriptions. - -package whisper - -import ( - "crypto/ecdsa" - - "github.com/ethereum/go-ethereum/event/filter" -) - -// Filter is used to subscribe to specific types of whisper messages. -type Filter struct { - To *ecdsa.PublicKey // Recipient of the message - From *ecdsa.PublicKey // Sender of the message - Topics [][]Topic // Topics to filter messages with - Fn func(msg *Message) // Handler in case of a match -} - -// NewFilterTopics creates a 2D topic array used by whisper.Filter from binary -// data elements. -func NewFilterTopics(data ...[][]byte) [][]Topic { - filter := make([][]Topic, len(data)) - for i, condition := range data { - // Handle the special case of condition == [[]byte{}] - if len(condition) == 1 && len(condition[0]) == 0 { - filter[i] = []Topic{} - continue - } - // Otherwise flatten normally - filter[i] = NewTopics(condition...) - } - return filter -} - -// NewFilterTopicsFlat creates a 2D topic array used by whisper.Filter from flat -// binary data elements. -func NewFilterTopicsFlat(data ...[]byte) [][]Topic { - filter := make([][]Topic, len(data)) - for i, element := range data { - // Only add non-wildcard topics - filter[i] = make([]Topic, 0, 1) - if len(element) > 0 { - filter[i] = append(filter[i], NewTopic(element)) - } - } - return filter -} - -// NewFilterTopicsFromStrings creates a 2D topic array used by whisper.Filter -// from textual data elements. -func NewFilterTopicsFromStrings(data ...[]string) [][]Topic { - filter := make([][]Topic, len(data)) - for i, condition := range data { - // Handle the special case of condition == [""] - if len(condition) == 1 && condition[0] == "" { - filter[i] = []Topic{} - continue - } - // Otherwise flatten normally - filter[i] = NewTopicsFromStrings(condition...) - } - return filter -} - -// NewFilterTopicsFromStringsFlat creates a 2D topic array used by whisper.Filter from flat -// binary data elements. -func NewFilterTopicsFromStringsFlat(data ...string) [][]Topic { - filter := make([][]Topic, len(data)) - for i, element := range data { - // Only add non-wildcard topics - filter[i] = make([]Topic, 0, 1) - if element != "" { - filter[i] = append(filter[i], NewTopicFromString(element)) - } - } - return filter -} - -// filterer is the internal, fully initialized filter ready to match inbound -// messages to a variety of criteria. -type filterer struct { - to string // Recipient of the message - from string // Sender of the message - matcher *topicMatcher // Topics to filter messages with - fn func(data interface{}) // Handler in case of a match -} - -// Compare checks if the specified filter matches the current one. -func (self filterer) Compare(f filter.Filter) bool { - filter := f.(filterer) - - // Check the message sender and recipient - if len(self.to) > 0 && self.to != filter.to { - return false - } - if len(self.from) > 0 && self.from != filter.from { - return false - } - // Check the topic filtering - topics := make([]Topic, len(filter.matcher.conditions)) - for i, group := range filter.matcher.conditions { - // Message should contain a single topic entry, extract - for topics[i], _ = range group { - break - } - } - if !self.matcher.Matches(topics) { - return false - } - return true -} - -// Trigger is called when a filter successfully matches an inbound message. -func (self filterer) Trigger(data interface{}) { - self.fn(data) -} diff --git a/whisper/filter_test.go b/whisper/filter_test.go deleted file mode 100644 index b805b2baf..000000000 --- a/whisper/filter_test.go +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2015 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 - -import ( - "bytes" - - "testing" -) - -var filterTopicsCreationTests = []struct { - topics [][]string - filter [][][4]byte -}{ - { // Simple topic filter - topics: [][]string{ - {"abc", "def", "ghi"}, - {"def"}, - {"ghi", "abc"}, - }, - filter: [][][4]byte{ - {{0x4e, 0x03, 0x65, 0x7a}, {0x34, 0x60, 0x7c, 0x9b}, {0x21, 0x41, 0x7d, 0xf9}}, - {{0x34, 0x60, 0x7c, 0x9b}}, - {{0x21, 0x41, 0x7d, 0xf9}, {0x4e, 0x03, 0x65, 0x7a}}, - }, - }, - { // Wild-carded topic filter - topics: [][]string{ - {"abc", "def", "ghi"}, - {}, - {""}, - {"def"}, - }, - filter: [][][4]byte{ - {{0x4e, 0x03, 0x65, 0x7a}, {0x34, 0x60, 0x7c, 0x9b}, {0x21, 0x41, 0x7d, 0xf9}}, - {}, - {}, - {{0x34, 0x60, 0x7c, 0x9b}}, - }, - }, -} - -var filterTopicsCreationFlatTests = []struct { - topics []string - filter [][][4]byte -}{ - { // Simple topic list - topics: []string{"abc", "def", "ghi"}, - filter: [][][4]byte{ - {{0x4e, 0x03, 0x65, 0x7a}}, - {{0x34, 0x60, 0x7c, 0x9b}}, - {{0x21, 0x41, 0x7d, 0xf9}}, - }, - }, - { // Wild-carded topic list - topics: []string{"abc", "", "ghi"}, - filter: [][][4]byte{ - {{0x4e, 0x03, 0x65, 0x7a}}, - {}, - {{0x21, 0x41, 0x7d, 0xf9}}, - }, - }, -} - -func TestFilterTopicsCreation(t *testing.T) { - // Check full filter creation - for i, tt := range filterTopicsCreationTests { - // Check the textual creation - filter := NewFilterTopicsFromStrings(tt.topics...) - if len(filter) != len(tt.topics) { - t.Errorf("test %d: condition count mismatch: have %v, want %v", i, len(filter), len(tt.topics)) - continue - } - for j, condition := range filter { - if len(condition) != len(tt.filter[j]) { - t.Errorf("test %d, condition %d: size mismatch: have %v, want %v", i, j, len(condition), len(tt.filter[j])) - continue - } - for k := 0; k < len(condition); k++ { - if bytes.Compare(condition[k][:], tt.filter[j][k][:]) != 0 { - t.Errorf("test %d, condition %d, segment %d: filter mismatch: have 0x%x, want 0x%x", i, j, k, condition[k], tt.filter[j][k]) - } - } - } - // Check the binary creation - binary := make([][][]byte, len(tt.topics)) - for j, condition := range tt.topics { - binary[j] = make([][]byte, len(condition)) - for k, segment := range condition { - binary[j][k] = []byte(segment) - } - } - filter = NewFilterTopics(binary...) - if len(filter) != len(tt.topics) { - t.Errorf("test %d: condition count mismatch: have %v, want %v", i, len(filter), len(tt.topics)) - continue - } - for j, condition := range filter { - if len(condition) != len(tt.filter[j]) { - t.Errorf("test %d, condition %d: size mismatch: have %v, want %v", i, j, len(condition), len(tt.filter[j])) - continue - } - for k := 0; k < len(condition); k++ { - if bytes.Compare(condition[k][:], tt.filter[j][k][:]) != 0 { - t.Errorf("test %d, condition %d, segment %d: filter mismatch: have 0x%x, want 0x%x", i, j, k, condition[k], tt.filter[j][k]) - } - } - } - } - // Check flat filter creation - for i, tt := range filterTopicsCreationFlatTests { - // Check the textual creation - filter := NewFilterTopicsFromStringsFlat(tt.topics...) - if len(filter) != len(tt.topics) { - t.Errorf("test %d: condition count mismatch: have %v, want %v", i, len(filter), len(tt.topics)) - continue - } - for j, condition := range filter { - if len(condition) != len(tt.filter[j]) { - t.Errorf("test %d, condition %d: size mismatch: have %v, want %v", i, j, len(condition), len(tt.filter[j])) - continue - } - for k := 0; k < len(condition); k++ { - if bytes.Compare(condition[k][:], tt.filter[j][k][:]) != 0 { - t.Errorf("test %d, condition %d, segment %d: filter mismatch: have 0x%x, want 0x%x", i, j, k, condition[k], tt.filter[j][k]) - } - } - } - // Check the binary creation - binary := make([][]byte, len(tt.topics)) - for j, topic := range tt.topics { - binary[j] = []byte(topic) - } - filter = NewFilterTopicsFlat(binary...) - if len(filter) != len(tt.topics) { - t.Errorf("test %d: condition count mismatch: have %v, want %v", i, len(filter), len(tt.topics)) - continue - } - for j, condition := range filter { - if len(condition) != len(tt.filter[j]) { - t.Errorf("test %d, condition %d: size mismatch: have %v, want %v", i, j, len(condition), len(tt.filter[j])) - continue - } - for k := 0; k < len(condition); k++ { - if bytes.Compare(condition[k][:], tt.filter[j][k][:]) != 0 { - t.Errorf("test %d, condition %d, segment %d: filter mismatch: have 0x%x, want 0x%x", i, j, k, condition[k], tt.filter[j][k]) - } - } - } - } -} - -var filterCompareTests = []struct { - matcher filterer - message filterer - match bool -}{ - { // Wild-card filter matching anything - matcher: filterer{to: "", from: "", matcher: newTopicMatcher()}, - message: filterer{to: "to", from: "from", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, - match: true, - }, - { // Filter matching the to field - matcher: filterer{to: "to", from: "", matcher: newTopicMatcher()}, - message: filterer{to: "to", from: "from", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, - match: true, - }, - { // Filter rejecting the to field - matcher: filterer{to: "to", from: "", matcher: newTopicMatcher()}, - message: filterer{to: "", from: "from", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, - match: false, - }, - { // Filter matching the from field - matcher: filterer{to: "", from: "from", matcher: newTopicMatcher()}, - message: filterer{to: "to", from: "from", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, - match: true, - }, - { // Filter rejecting the from field - matcher: filterer{to: "", from: "from", matcher: newTopicMatcher()}, - message: filterer{to: "to", from: "", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, - match: false, - }, - { // Filter matching the topic field - matcher: filterer{to: "", from: "from", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, - message: filterer{to: "to", from: "from", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, - match: true, - }, - { // Filter rejecting the topic field - matcher: filterer{to: "", from: "", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, - message: filterer{to: "to", from: "from", matcher: newTopicMatcher()}, - match: false, - }, -} - -func TestFilterCompare(t *testing.T) { - for i, tt := range filterCompareTests { - if match := tt.matcher.Compare(tt.message); match != tt.match { - t.Errorf("test %d: match mismatch: have %v, want %v", i, match, tt.match) - } - } -} diff --git a/whisper/main.go b/whisper/main.go deleted file mode 100644 index be4160489..000000000 --- a/whisper/main.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2014 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 . - -// +build none - -// Contains a simple whisper peer setup and self messaging to allow playing -// around with the protocol and API without a fancy client implementation. - -package main - -import ( - "fmt" - "log" - "os" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/logger" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/nat" - "github.com/ethereum/go-ethereum/whisper" -) - -func main() { - logger.AddLogSystem(logger.NewStdLogSystem(os.Stdout, log.LstdFlags, logger.InfoLevel)) - - // Generate the peer identity - key, err := crypto.GenerateKey() - if err != nil { - fmt.Printf("Failed to generate peer key: %v.\n", err) - os.Exit(-1) - } - name := common.MakeName("whisper-go", "1.0") - shh := whisper.New() - - // Create an Ethereum peer to communicate through - server := p2p.Server{ - PrivateKey: key, - MaxPeers: 10, - Name: name, - Protocols: []p2p.Protocol{shh.Protocol()}, - ListenAddr: ":30300", - NAT: nat.Any(), - } - fmt.Println("Starting Ethereum peer...") - if err := server.Start(); err != nil { - fmt.Printf("Failed to start Ethereum peer: %v.\n", err) - os.Exit(1) - } - - // Send a message to self to check that something works - payload := fmt.Sprintf("Hello world, this is %v. In case you're wondering, the time is %v", name, time.Now()) - if err := selfSend(shh, []byte(payload)); err != nil { - fmt.Printf("Failed to self message: %v.\n", err) - os.Exit(-1) - } -} - -// SendSelf wraps a payload into a Whisper envelope and forwards it to itself. -func selfSend(shh *whisper.Whisper, payload []byte) error { - ok := make(chan struct{}) - - // Start watching for self messages, output any arrivals - id := shh.NewIdentity() - shh.Watch(whisper.Filter{ - To: &id.PublicKey, - Fn: func(msg *whisper.Message) { - fmt.Printf("Message received: %s, signed with 0x%x.\n", string(msg.Payload), msg.Signature) - close(ok) - }, - }) - // Wrap the payload and encrypt it - msg := whisper.NewMessage(payload) - envelope, err := msg.Wrap(whisper.DefaultPoW, whisper.Options{ - From: id, - To: &id.PublicKey, - TTL: whisper.DefaultTTL, - }) - if err != nil { - return fmt.Errorf("failed to seal message: %v", err) - } - // Dump the message into the system and wait for it to pop back out - if err := shh.Send(envelope); err != nil { - return fmt.Errorf("failed to send self-message: %v", err) - } - select { - case <-ok: - case <-time.After(time.Second): - return fmt.Errorf("failed to receive message in time") - } - return nil -} diff --git a/whisper/message.go b/whisper/message.go deleted file mode 100644 index f05b5d8b5..000000000 --- a/whisper/message.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2014 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. For formal details please see -// the specs at https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec#messages. - -package whisper - -import ( - "crypto/ecdsa" - "math/rand" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/logger" - "github.com/ethereum/go-ethereum/logger/glog" -) - -// Message 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 Message struct { - Flags byte // First bit is signature presence, rest reserved and should be random - Signature []byte - Payload []byte - - Sent time.Time // Time when the message was posted into the network - TTL time.Duration // Maximum time to live allowed for the message - - To *ecdsa.PublicKey // Message recipient (identity used to decode the message) - Hash common.Hash // Message envelope hash to act as a unique id -} - -// Options specifies the exact way a message should be wrapped into an Envelope. -type Options struct { - From *ecdsa.PrivateKey - To *ecdsa.PublicKey - TTL time.Duration - Topics []Topic -} - -// NewMessage creates and initializes a non-signed, non-encrypted Whisper message. -func NewMessage(payload []byte) *Message { - // Construct an initial flag set: no signature, rest random - flags := byte(rand.Intn(256)) - flags &= ^signatureFlag - - // Assemble and return the message - return &Message{ - Flags: flags, - Payload: payload, - Sent: time.Now(), - } -} - -// Wrap bundles the message into an Envelope to transmit over the network. -// -// pow (Proof Of Work) controls how much time to spend on hashing the message, -// inherently controlling its priority through the network (smaller hash, bigger -// priority). -// -// The user can control the amount of identity, privacy and encryption through -// the options parameter as follows: -// - options.From == nil && options.To == nil: anonymous broadcast -// - options.From != nil && options.To == nil: signed broadcast (known sender) -// - options.From == nil && options.To != nil: encrypted anonymous message -// - options.From != nil && options.To != nil: encrypted signed message -func (self *Message) Wrap(pow time.Duration, options Options) (*Envelope, error) { - // Use the default TTL if non was specified - if options.TTL == 0 { - options.TTL = DefaultTTL - } - self.TTL = options.TTL - - // Sign and encrypt the message if requested - if options.From != nil { - if err := self.sign(options.From); err != nil { - return nil, err - } - } - if options.To != nil { - if err := self.encrypt(options.To); err != nil { - return nil, err - } - } - // Wrap the processed message, seal it and return - envelope := NewEnvelope(options.TTL, options.Topics, self) - envelope.Seal(pow) - - return envelope, nil -} - -// sign calculates and sets the cryptographic signature for the message , also -// setting the sign flag. -func (self *Message) sign(key *ecdsa.PrivateKey) (err error) { - self.Flags |= signatureFlag - self.Signature, err = crypto.Sign(self.hash(), key) - return -} - -// Recover retrieves the public key of the message signer. -func (self *Message) Recover() *ecdsa.PublicKey { - defer func() { recover() }() // in case of invalid signature - - // Short circuit if no signature is present - if self.Signature == nil { - return nil - } - // Otherwise try and recover the signature - pub, err := crypto.SigToPub(self.hash(), self.Signature) - if err != nil { - glog.V(logger.Error).Infof("Could not get public key from signature: %v", err) - return nil - } - return pub -} - -// encrypt encrypts a message payload with a public key. -func (self *Message) encrypt(key *ecdsa.PublicKey) (err error) { - self.Payload, err = crypto.Encrypt(key, self.Payload) - return -} - -// decrypt decrypts an encrypted payload with a private key. -func (self *Message) decrypt(key *ecdsa.PrivateKey) error { - cleartext, err := crypto.Decrypt(key, self.Payload) - if err == nil { - self.Payload = cleartext - } - return err -} - -// hash calculates the SHA3 checksum of the message flags and payload. -func (self *Message) hash() []byte { - return crypto.Keccak256(append([]byte{self.Flags}, self.Payload...)) -} - -// bytes flattens the message contents (flags, signature and payload) into a -// single binary blob. -func (self *Message) bytes() []byte { - return append([]byte{self.Flags}, append(self.Signature, self.Payload...)...) -} diff --git a/whisper/message_test.go b/whisper/message_test.go deleted file mode 100644 index 921c967a9..000000000 --- a/whisper/message_test.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2014 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 - -import ( - "bytes" - "crypto/elliptic" - "testing" - "time" - - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/secp256k1" -) - -// Tests whether a message can be wrapped without any identity or encryption. -func TestMessageSimpleWrap(t *testing.T) { - payload := []byte("hello world") - - msg := NewMessage(payload) - if _, err := msg.Wrap(DefaultPoW, Options{}); err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - if msg.Flags&signatureFlag != 0 { - t.Fatalf("signature flag mismatch: have %d, want %d", msg.Flags&signatureFlag, 0) - } - if len(msg.Signature) != 0 { - t.Fatalf("signature found for simple wrapping: 0x%x", msg.Signature) - } - if bytes.Compare(msg.Payload, payload) != 0 { - t.Fatalf("payload mismatch after wrapping: have 0x%x, want 0x%x", msg.Payload, payload) - } - if msg.TTL/time.Second != DefaultTTL/time.Second { - t.Fatalf("message TTL mismatch: have %v, want %v", msg.TTL, DefaultTTL) - } -} - -// Tests whether a message can be signed, and wrapped in plain-text. -func TestMessageCleartextSignRecover(t *testing.T) { - key, err := crypto.GenerateKey() - if err != nil { - t.Fatalf("failed to create crypto key: %v", err) - } - payload := []byte("hello world") - - msg := NewMessage(payload) - if _, err := msg.Wrap(DefaultPoW, Options{ - From: key, - }); err != nil { - t.Fatalf("failed to sign message: %v", err) - } - if msg.Flags&signatureFlag != signatureFlag { - t.Fatalf("signature flag mismatch: have %d, want %d", msg.Flags&signatureFlag, signatureFlag) - } - if bytes.Compare(msg.Payload, payload) != 0 { - t.Fatalf("payload mismatch after signing: have 0x%x, want 0x%x", msg.Payload, payload) - } - - pubKey := msg.Recover() - if pubKey == nil { - t.Fatalf("failed to recover public key") - } - p1 := elliptic.Marshal(secp256k1.S256(), key.PublicKey.X, key.PublicKey.Y) - p2 := elliptic.Marshal(secp256k1.S256(), pubKey.X, pubKey.Y) - if !bytes.Equal(p1, p2) { - t.Fatalf("public key mismatch: have 0x%x, want 0x%x", p2, p1) - } -} - -// Tests whether a message can be encrypted and decrypted using an anonymous -// sender (i.e. no signature). -func TestMessageAnonymousEncryptDecrypt(t *testing.T) { - key, err := crypto.GenerateKey() - if err != nil { - t.Fatalf("failed to create recipient crypto key: %v", err) - } - payload := []byte("hello world") - - msg := NewMessage(payload) - envelope, err := msg.Wrap(DefaultPoW, Options{ - To: &key.PublicKey, - }) - if err != nil { - t.Fatalf("failed to encrypt message: %v", err) - } - if msg.Flags&signatureFlag != 0 { - t.Fatalf("signature flag mismatch: have %d, want %d", msg.Flags&signatureFlag, 0) - } - if len(msg.Signature) != 0 { - t.Fatalf("signature found for anonymous message: 0x%x", msg.Signature) - } - - out, err := envelope.Open(key) - if err != nil { - t.Fatalf("failed to open encrypted message: %v", err) - } - if !bytes.Equal(out.Payload, payload) { - t.Errorf("payload mismatch: have 0x%x, want 0x%x", out.Payload, payload) - } -} - -// Tests whether a message can be properly signed and encrypted. -func TestMessageFullCrypto(t *testing.T) { - fromKey, err := crypto.GenerateKey() - if err != nil { - t.Fatalf("failed to create sender crypto key: %v", err) - } - toKey, err := crypto.GenerateKey() - if err != nil { - t.Fatalf("failed to create recipient crypto key: %v", err) - } - - payload := []byte("hello world") - msg := NewMessage(payload) - envelope, err := msg.Wrap(DefaultPoW, Options{ - From: fromKey, - To: &toKey.PublicKey, - }) - if err != nil { - t.Fatalf("failed to encrypt message: %v", err) - } - if msg.Flags&signatureFlag != signatureFlag { - t.Fatalf("signature flag mismatch: have %d, want %d", msg.Flags&signatureFlag, signatureFlag) - } - if len(msg.Signature) == 0 { - t.Fatalf("no signature found for signed message") - } - - out, err := envelope.Open(toKey) - if err != nil { - t.Fatalf("failed to open encrypted message: %v", err) - } - if !bytes.Equal(out.Payload, payload) { - t.Errorf("payload mismatch: have 0x%x, want 0x%x", out.Payload, payload) - } - - pubKey := out.Recover() - if pubKey == nil { - t.Fatalf("failed to recover public key") - } - p1 := elliptic.Marshal(secp256k1.S256(), fromKey.PublicKey.X, fromKey.PublicKey.Y) - p2 := elliptic.Marshal(secp256k1.S256(), pubKey.X, pubKey.Y) - if !bytes.Equal(p1, p2) { - t.Fatalf("public key mismatch: have 0x%x, want 0x%x", p2, p1) - } -} diff --git a/whisper/peer.go b/whisper/peer.go deleted file mode 100644 index ee10e66e7..000000000 --- a/whisper/peer.go +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright 2014 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 - -import ( - "fmt" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/logger" - "github.com/ethereum/go-ethereum/logger/glog" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/rlp" - "gopkg.in/fatih/set.v0" -) - -// peer represents a whisper protocol peer connection. -type peer struct { - host *Whisper - peer *p2p.Peer - ws p2p.MsgReadWriter - - known *set.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, - known: set.New(), - quit: make(chan struct{}), - } -} - -// start initiates the peer updater, periodically broadcasting the whisper packets -// into the network. -func (self *peer) start() { - go self.update() - glog.V(logger.Debug).Infof("%v: whisper started", self.peer) -} - -// stop terminates the peer updater, stopping message forwarding to it. -func (self *peer) stop() { - close(self.quit) - glog.V(logger.Debug).Infof("%v: whisper stopped", self.peer) -} - -// handshake sends the protocol initiation status message to the remote peer and -// verifies the remote status too. -func (self *peer) handshake() error { - // Send the handshake status message asynchronously - errc := make(chan error, 1) - go func() { - errc <- p2p.SendItems(self.ws, statusCode, protocolVersion) - }() - // Fetch the remote status packet and verify protocol match - packet, err := self.ws.ReadMsg() - if err != nil { - return err - } - if packet.Code != statusCode { - return fmt.Errorf("peer sent %x before status packet", packet.Code) - } - s := rlp.NewStream(packet.Payload, uint64(packet.Size)) - if _, err := s.List(); err != nil { - return fmt.Errorf("bad status message: %v", err) - } - peerVersion, err := s.Uint() - if err != nil { - return fmt.Errorf("bad status message: %v", err) - } - if peerVersion != protocolVersion { - return fmt.Errorf("protocol version mismatch %d != %d", peerVersion, protocolVersion) - } - // Wait until out own status is consumed too - if err := <-errc; err != nil { - return fmt.Errorf("failed to send status packet: %v", err) - } - return nil -} - -// update executes periodic operations on the peer, including message transmission -// and expiration. -func (self *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: - self.expire() - - case <-transmit.C: - if err := self.broadcast(); err != nil { - glog.V(logger.Info).Infof("%v: broadcast failed: %v", self.peer, err) - return - } - - case <-self.quit: - return - } - } -} - -// mark marks an envelope known to the peer so that it won't be sent back. -func (self *peer) mark(envelope *Envelope) { - self.known.Add(envelope.Hash()) -} - -// marked checks if an envelope is already known to the remote peer. -func (self *peer) marked(envelope *Envelope) bool { - return self.known.Has(envelope.Hash()) -} - -// expire iterates over all the known envelopes in the host and removes all -// expired (unknown) ones from the known list. -func (self *peer) expire() { - // Assemble the list of available envelopes - available := set.NewNonTS() - for _, envelope := range self.host.envelopes() { - available.Add(envelope.Hash()) - } - // Cross reference availability with known status - unmark := make(map[common.Hash]struct{}) - self.known.Each(func(v interface{}) bool { - if !available.Has(v.(common.Hash)) { - unmark[v.(common.Hash)] = struct{}{} - } - return true - }) - // Dump all known but unavailable - for hash, _ := range unmark { - self.known.Remove(hash) - } -} - -// broadcast iterates over the collection of envelopes and transmits yet unknown -// ones over the network. -func (self *peer) broadcast() error { - // Fetch the envelopes and collect the unknown ones - envelopes := self.host.envelopes() - transmit := make([]*Envelope, 0, len(envelopes)) - for _, envelope := range envelopes { - if !self.marked(envelope) { - transmit = append(transmit, envelope) - self.mark(envelope) - } - } - // Transmit the unknown batch (potentially empty) - if err := p2p.Send(self.ws, messagesCode, transmit); err != nil { - return err - } - glog.V(logger.Detail).Infoln(self.peer, "broadcasted", len(transmit), "message(s)") - return nil -} diff --git a/whisper/peer_test.go b/whisper/peer_test.go deleted file mode 100644 index 636bd8ca1..000000000 --- a/whisper/peer_test.go +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright 2015 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 - -import ( - "testing" - "time" - - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/discover" -) - -type testPeer struct { - client *Whisper - stream *p2p.MsgPipeRW - termed chan struct{} -} - -func startTestPeer() *testPeer { - // Create a simulated P2P remote peer and data streams to it - remote := p2p.NewPeer(discover.NodeID{}, "", nil) - tester, tested := p2p.MsgPipe() - - // Create a whisper client and connect with it to the tester peer - client := New() - client.Start(nil) - - termed := make(chan struct{}) - go func() { - defer client.Stop() - defer close(termed) - defer tested.Close() - - client.handlePeer(remote, tested) - }() - - return &testPeer{ - client: client, - stream: tester, - termed: termed, - } -} - -func startTestPeerInited() (*testPeer, error) { - peer := startTestPeer() - - if err := p2p.ExpectMsg(peer.stream, statusCode, []uint64{protocolVersion}); err != nil { - peer.stream.Close() - return nil, err - } - if err := p2p.SendItems(peer.stream, statusCode, protocolVersion); err != nil { - peer.stream.Close() - return nil, err - } - return peer, nil -} - -func TestPeerStatusMessage(t *testing.T) { - tester := startTestPeer() - - // Wait for the handshake status message and check it - if err := p2p.ExpectMsg(tester.stream, statusCode, []uint64{protocolVersion}); err != nil { - t.Fatalf("status message mismatch: %v", err) - } - // Terminate the node - tester.stream.Close() - - select { - case <-tester.termed: - case <-time.After(time.Second): - t.Fatalf("local close timed out") - } -} - -func TestPeerHandshakeFail(t *testing.T) { - tester := startTestPeer() - - // Wait for and check the handshake - if err := p2p.ExpectMsg(tester.stream, statusCode, []uint64{protocolVersion}); err != nil { - t.Fatalf("status message mismatch: %v", err) - } - // Send an invalid handshake status and verify disconnect - if err := p2p.SendItems(tester.stream, messagesCode); err != nil { - t.Fatalf("failed to send malformed status: %v", err) - } - select { - case <-tester.termed: - case <-time.After(time.Second): - t.Fatalf("remote close timed out") - } -} - -func TestPeerHandshakeSuccess(t *testing.T) { - tester := startTestPeer() - - // Wait for and check the handshake - if err := p2p.ExpectMsg(tester.stream, statusCode, []uint64{protocolVersion}); err != nil { - t.Fatalf("status message mismatch: %v", err) - } - // Send a valid handshake status and make sure connection stays live - if err := p2p.SendItems(tester.stream, statusCode, protocolVersion); err != nil { - t.Fatalf("failed to send status: %v", err) - } - select { - case <-tester.termed: - t.Fatalf("valid handshake disconnected") - - case <-time.After(100 * time.Millisecond): - } - // Clean up the test - tester.stream.Close() - - select { - case <-tester.termed: - case <-time.After(time.Second): - t.Fatalf("local close timed out") - } -} - -func TestPeerSend(t *testing.T) { - // Start a tester and execute the handshake - tester, err := startTestPeerInited() - if err != nil { - t.Fatalf("failed to start initialized peer: %v", err) - } - defer tester.stream.Close() - - // Construct a message and inject into the tester - message := NewMessage([]byte("peer broadcast test message")) - envelope, err := message.Wrap(DefaultPoW, Options{ - TTL: DefaultTTL, - }) - if err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - if err := tester.client.Send(envelope); err != nil { - t.Fatalf("failed to send message: %v", err) - } - // Check that the message is eventually forwarded - payload := []interface{}{envelope} - if err := p2p.ExpectMsg(tester.stream, messagesCode, payload); err != nil { - t.Fatalf("message mismatch: %v", err) - } - // Make sure that even with a re-insert, an empty batch is received - if err := tester.client.Send(envelope); err != nil { - t.Fatalf("failed to send message: %v", err) - } - if err := p2p.ExpectMsg(tester.stream, messagesCode, []interface{}{}); err != nil { - t.Fatalf("message mismatch: %v", err) - } -} - -func TestPeerDeliver(t *testing.T) { - // Start a tester and execute the handshake - tester, err := startTestPeerInited() - if err != nil { - t.Fatalf("failed to start initialized peer: %v", err) - } - defer tester.stream.Close() - - // Watch for all inbound messages - arrived := make(chan struct{}, 1) - tester.client.Watch(Filter{ - Fn: func(message *Message) { - arrived <- struct{}{} - }, - }) - // Construct a message and deliver it to the tester peer - message := NewMessage([]byte("peer broadcast test message")) - envelope, err := message.Wrap(DefaultPoW, Options{ - TTL: DefaultTTL, - }) - if err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - if err := p2p.Send(tester.stream, messagesCode, []*Envelope{envelope}); err != nil { - t.Fatalf("failed to transfer message: %v", err) - } - // Check that the message is delivered upstream - select { - case <-arrived: - case <-time.After(time.Second): - t.Fatalf("message delivery timeout") - } - // Check that a resend is not delivered - if err := p2p.Send(tester.stream, messagesCode, []*Envelope{envelope}); err != nil { - t.Fatalf("failed to transfer message: %v", err) - } - select { - case <-time.After(2 * transmissionCycle): - case <-arrived: - t.Fatalf("repeating message arrived") - } -} - -func TestPeerMessageExpiration(t *testing.T) { - // Start a tester and execute the handshake - tester, err := startTestPeerInited() - if err != nil { - t.Fatalf("failed to start initialized peer: %v", err) - } - defer tester.stream.Close() - - // Fetch the peer instance for later inspection - tester.client.peerMu.RLock() - if peers := len(tester.client.peers); peers != 1 { - t.Fatalf("peer pool size mismatch: have %v, want %v", peers, 1) - } - var peer *peer - for peer, _ = range tester.client.peers { - break - } - tester.client.peerMu.RUnlock() - - // Construct a message and pass it through the tester - message := NewMessage([]byte("peer test message")) - envelope, err := message.Wrap(DefaultPoW, Options{ - TTL: time.Second, - }) - if err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - if err := tester.client.Send(envelope); err != nil { - t.Fatalf("failed to send message: %v", err) - } - payload := []interface{}{envelope} - if err := p2p.ExpectMsg(tester.stream, messagesCode, payload); err != nil { - // A premature empty message may have been broadcast, check the next too - if err := p2p.ExpectMsg(tester.stream, messagesCode, payload); err != nil { - t.Fatalf("message mismatch: %v", err) - } - } - // Check that the message is inside the cache - if !peer.known.Has(envelope.Hash()) { - t.Fatalf("message not found in cache") - } - // Discard messages until expiration and check cache again - exp := time.Now().Add(time.Second + 2*expirationCycle + 100*time.Millisecond) - for time.Now().Before(exp) { - if err := p2p.ExpectMsg(tester.stream, messagesCode, []interface{}{}); err != nil { - t.Fatalf("message mismatch: %v", err) - } - } - if peer.known.Has(envelope.Hash()) { - t.Fatalf("message not expired from cache") - } -} diff --git a/whisper/shhapi/api.go b/whisper/shhapi/api.go new file mode 100644 index 000000000..50a8eb34a --- /dev/null +++ b/whisper/shhapi/api.go @@ -0,0 +1,504 @@ +// 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 shhapi + +import ( + "encoding/json" + "errors" + "fmt" + mathrand "math/rand" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/whisper/whisperv5" +) + +var whisperOffLineErr = errors.New("whisper is offline") + +// PublicWhisperAPI provides the whisper RPC service. +type PublicWhisperAPI struct { + whisper *whisperv5.Whisper +} + +// NewPublicWhisperAPI create a new RPC whisper service. +func NewPublicWhisperAPI() *PublicWhisperAPI { + w := whisperv5.NewWhisper(nil) + return &PublicWhisperAPI{whisper: w} +} + +// APIs returns the RPC descriptors the Whisper implementation offers +func APIs() []rpc.API { + return []rpc.API{ + { + Namespace: whisperv5.ProtocolName, + Version: whisperv5.ProtocolVersionStr, + Service: NewPublicWhisperAPI(), + Public: true, + }, + } +} + +// Version returns the Whisper version this node offers. +func (api *PublicWhisperAPI) Version() (*rpc.HexNumber, error) { + if api.whisper == nil { + return rpc.NewHexNumber(0), whisperOffLineErr + } + return rpc.NewHexNumber(api.whisper.Version()), nil +} + +// MarkPeerTrusted marks specific peer trusted, which will allow it +// to send historic (expired) messages. +func (api *PublicWhisperAPI) MarkPeerTrusted(peerID rpc.HexBytes) error { + if api.whisper == nil { + return whisperOffLineErr + } + return api.whisper.MarkPeerTrusted(peerID) +} + +// RequestHistoricMessages requests the peer to deliver the old (expired) messages. +// data contains parameters (time frame, payment details, etc.), required +// by the remote email-like server. Whisper is not aware about the data format, +// it will just forward the raw data to the server. +func (api *PublicWhisperAPI) RequestHistoricMessages(peerID rpc.HexBytes, data rpc.HexBytes) error { + if api.whisper == nil { + return whisperOffLineErr + } + return api.whisper.RequestHistoricMessages(peerID, data) +} + +// HasIdentity checks if the whisper node is configured with the private key +// of the specified public pair. +func (api *PublicWhisperAPI) HasIdentity(identity string) (bool, error) { + if api.whisper == nil { + return false, whisperOffLineErr + } + return api.whisper.HasIdentity(identity), nil +} + +// DeleteIdentity deletes the specifies key if it exists. +func (api *PublicWhisperAPI) DeleteIdentity(identity string) error { + if api.whisper == nil { + return whisperOffLineErr + } + api.whisper.DeleteIdentity(identity) + return nil +} + +// NewIdentity generates a new cryptographic identity for the client, and injects +// it into the known identities for message decryption. +func (api *PublicWhisperAPI) NewIdentity() (string, error) { + if api.whisper == nil { + return "", whisperOffLineErr + } + identity := api.whisper.NewIdentity() + return common.ToHex(crypto.FromECDSAPub(&identity.PublicKey)), nil +} + +// GenerateSymKey generates a random symmetric key and stores it under +// the 'name' id. Will be used in the future for session key exchange. +func (api *PublicWhisperAPI) GenerateSymKey(name string) error { + if api.whisper == nil { + return whisperOffLineErr + } + return api.whisper.GenerateSymKey(name) +} + +// AddSymKey stores the key under the 'name' id. +func (api *PublicWhisperAPI) AddSymKey(name string, key []byte) error { + if api.whisper == nil { + return whisperOffLineErr + } + return api.whisper.AddSymKey(name, key) +} + +// HasSymKey returns true if there is a key associated with the name string. +// Otherwise returns false. +func (api *PublicWhisperAPI) HasSymKey(name string) (bool, error) { + if api.whisper == nil { + return false, whisperOffLineErr + } + res := api.whisper.HasSymKey(name) + return res, nil +} + +// DeleteSymKey deletes the key associated with the name string if it exists. +func (api *PublicWhisperAPI) DeleteSymKey(name string) error { + if api.whisper == nil { + return whisperOffLineErr + } + api.whisper.DeleteSymKey(name) + return nil +} + +// NewWhisperFilter creates and registers a new message filter to watch for inbound whisper messages. +// Returns the ID of the newly created Filter. +func (api *PublicWhisperAPI) NewFilter(args WhisperFilterArgs) (*rpc.HexNumber, error) { + if api.whisper == nil { + return nil, whisperOffLineErr + } + + filter := whisperv5.Filter{ + Src: crypto.ToECDSAPub(args.From), + KeySym: api.whisper.GetSymKey(args.KeyName), + PoW: args.PoW, + Messages: make(map[common.Hash]*whisperv5.ReceivedMessage), + AcceptP2P: args.AcceptP2P, + } + + if len(filter.KeySym) > 0 { + filter.SymKeyHash = crypto.Keccak256Hash(filter.KeySym) + } + + for _, t := range args.Topics { + filter.Topics = append(filter.Topics, t) + } + + if len(args.Topics) == 0 { + info := "NewFilter: at least one topic must be specified" + glog.V(logger.Error).Infof(info) + return nil, errors.New(info) + } + + if len(args.KeyName) != 0 && len(filter.KeySym) == 0 { + info := "NewFilter: key was not found by name: " + args.KeyName + glog.V(logger.Error).Infof(info) + return nil, errors.New(info) + } + + if len(args.To) == 0 && len(filter.KeySym) == 0 { + info := "NewFilter: filter must contain either symmetric or asymmetric key" + glog.V(logger.Error).Infof(info) + return nil, errors.New(info) + } + + if len(args.To) != 0 && len(filter.KeySym) != 0 { + info := "NewFilter: filter must not contain both symmetric and asymmetric key" + glog.V(logger.Error).Infof(info) + return nil, errors.New(info) + } + + if len(args.To) > 0 { + dst := crypto.ToECDSAPub(args.To) + if !whisperv5.ValidatePublicKey(dst) { + info := "NewFilter: Invalid 'To' address" + glog.V(logger.Error).Infof(info) + return nil, errors.New(info) + } + filter.KeyAsym = api.whisper.GetIdentity(string(args.To)) + if filter.KeyAsym == nil { + info := "NewFilter: non-existent identity provided" + glog.V(logger.Error).Infof(info) + return nil, errors.New(info) + } + } + + if len(args.From) > 0 { + if !whisperv5.ValidatePublicKey(filter.Src) { + info := "NewFilter: Invalid 'From' address" + glog.V(logger.Error).Infof(info) + return nil, errors.New(info) + } + } + + id := api.whisper.Watch(&filter) + return rpc.NewHexNumber(id), nil +} + +// UninstallFilter disables and removes an existing filter. +func (api *PublicWhisperAPI) UninstallFilter(filterId rpc.HexNumber) { + api.whisper.Unwatch(filterId.Int()) +} + +// GetFilterChanges retrieves all the new messages matched by a filter since the last retrieval. +func (api *PublicWhisperAPI) GetFilterChanges(filterId rpc.HexNumber) []WhisperMessage { + f := api.whisper.GetFilter(filterId.Int()) + if f != nil { + newMail := f.Retrieve() + return toWhisperMessages(newMail) + } + return toWhisperMessages(nil) +} + +// GetMessages retrieves all the known messages that match a specific filter. +func (api *PublicWhisperAPI) GetMessages(filterId rpc.HexNumber) []WhisperMessage { + all := api.whisper.Messages(filterId.Int()) + return toWhisperMessages(all) +} + +// toWhisperMessages converts a Whisper message to a RPC whisper message. +func toWhisperMessages(messages []*whisperv5.ReceivedMessage) []WhisperMessage { + msgs := make([]WhisperMessage, len(messages)) + for i, msg := range messages { + msgs[i] = NewWhisperMessage(msg) + } + return msgs +} + +// Post creates a whisper message and injects it into the network for distribution. +func (api *PublicWhisperAPI) Post(args PostArgs) error { + if api.whisper == nil { + return whisperOffLineErr + } + + params := whisperv5.MessageParams{ + TTL: args.TTL, + Dst: crypto.ToECDSAPub(args.To), + KeySym: api.whisper.GetSymKey(args.KeyName), + Topic: args.Topic, + Payload: args.Payload, + Padding: args.Padding, + WorkTime: args.WorkTime, + PoW: args.PoW, + } + + if len(args.From) > 0 { + pub := crypto.ToECDSAPub(args.From) + if !whisperv5.ValidatePublicKey(pub) { + info := "Post: Invalid 'From' address" + glog.V(logger.Error).Infof(info) + return errors.New(info) + } + params.Src = api.whisper.GetIdentity(string(args.From)) + if params.Src == nil { + info := "Post: non-existent identity provided" + glog.V(logger.Error).Infof(info) + return errors.New(info) + } + } + + filter := api.whisper.GetFilter(args.FilterID) + if filter == nil && args.FilterID > -1 { + info := fmt.Sprintf("Post: wrong filter id %d", args.FilterID) + glog.V(logger.Error).Infof(info) + return errors.New(info) + } + + if filter != nil { + // get the missing fields from the filter + if params.KeySym == nil && filter.KeySym != nil { + params.KeySym = filter.KeySym + } + if params.Src == nil && filter.Src != nil { + params.Src = filter.KeyAsym + } + if (params.Topic == whisperv5.TopicType{}) { + sz := len(filter.Topics) + if sz < 1 { + info := fmt.Sprintf("Post: no topics in filter # %d", args.FilterID) + glog.V(logger.Error).Infof(info) + return errors.New(info) + } else if sz == 1 { + params.Topic = filter.Topics[0] + } else { + // choose randomly + rnd := mathrand.Intn(sz) + params.Topic = filter.Topics[rnd] + } + } + } + + // validate + if len(args.KeyName) != 0 && len(params.KeySym) == 0 { + info := "Post: key was not found by name: " + args.KeyName + glog.V(logger.Error).Infof(info) + return errors.New(info) + } + + if len(args.To) == 0 && len(args.KeyName) == 0 { + info := "Post: message must be encrypted either symmetrically or asymmetrically" + glog.V(logger.Error).Infof(info) + return errors.New(info) + } + + if len(args.To) != 0 && len(args.KeyName) != 0 { + info := "Post: ambigous encryption method requested" + glog.V(logger.Error).Infof(info) + return errors.New(info) + } + + if len(args.To) > 0 { + if !whisperv5.ValidatePublicKey(params.Dst) { + info := "Post: Invalid 'To' address" + glog.V(logger.Error).Infof(info) + return errors.New(info) + } + } + + // encrypt and send + message := whisperv5.NewSentMessage(¶ms) + envelope, err := message.Wrap(¶ms) + if err != nil { + glog.V(logger.Error).Infof(err.Error()) + return err + } + if len(envelope.Data) > whisperv5.MaxMessageLength { + info := "Post: message is too big" + glog.V(logger.Error).Infof(info) + return errors.New(info) + } + if (envelope.Topic == whisperv5.TopicType{} && envelope.IsSymmetric()) { + info := "Post: topic is missing for symmetric encryption" + glog.V(logger.Error).Infof(info) + return errors.New(info) + } + + if args.PeerID != nil { + return api.whisper.SendP2PMessage(args.PeerID, envelope) + } + + return api.whisper.Send(envelope) +} + +type PostArgs struct { + TTL uint32 `json:"ttl"` + From rpc.HexBytes `json:"from"` + To rpc.HexBytes `json:"to"` + KeyName string `json:"keyname"` + Topic whisperv5.TopicType `json:"topic"` + Padding rpc.HexBytes `json:"padding"` + Payload rpc.HexBytes `json:"payload"` + WorkTime uint32 `json:"worktime"` + PoW float64 `json:"pow"` + FilterID int `json:"filter"` + PeerID rpc.HexBytes `json:"directP2P"` +} + +func (args *PostArgs) UnmarshalJSON(data []byte) (err error) { + var obj struct { + TTL uint32 `json:"ttl"` + From rpc.HexBytes `json:"from"` + To rpc.HexBytes `json:"to"` + KeyName string `json:"keyname"` + Topic whisperv5.TopicType `json:"topic"` + Payload rpc.HexBytes `json:"payload"` + Padding rpc.HexBytes `json:"padding"` + WorkTime uint32 `json:"worktime"` + PoW float64 `json:"pow"` + FilterID rpc.HexBytes `json:"filter"` + PeerID rpc.HexBytes `json:"directP2P"` + } + + if err := json.Unmarshal(data, &obj); err != nil { + return err + } + + args.TTL = obj.TTL + args.From = obj.From + args.To = obj.To + args.KeyName = obj.KeyName + args.Topic = obj.Topic + args.Payload = obj.Payload + args.Padding = obj.Padding + args.WorkTime = obj.WorkTime + args.PoW = obj.PoW + args.FilterID = -1 + args.PeerID = obj.PeerID + + if obj.FilterID != nil { + x := whisperv5.BytesToIntBigEndian(obj.FilterID) + args.FilterID = int(x) + } + + return nil +} + +type WhisperFilterArgs struct { + To []byte + From []byte + KeyName string + PoW float64 + Topics []whisperv5.TopicType + AcceptP2P bool +} + +// UnmarshalJSON implements the json.Unmarshaler interface, invoked to convert a +// JSON message blob into a WhisperFilterArgs structure. +func (args *WhisperFilterArgs) UnmarshalJSON(b []byte) (err error) { + // Unmarshal the JSON message and sanity check + var obj struct { + To rpc.HexBytes `json:"to"` + From rpc.HexBytes `json:"from"` + KeyName string `json:"keyname"` + PoW float64 `json:"pow"` + Topics []interface{} `json:"topics"` + AcceptP2P bool `json:"acceptP2P"` + } + if err := json.Unmarshal(b, &obj); err != nil { + return err + } + + args.To = obj.To + args.From = obj.From + args.KeyName = obj.KeyName + args.PoW = obj.PoW + args.AcceptP2P = obj.AcceptP2P + + // Construct the topic array + if obj.Topics != nil { + topics := make([]string, len(obj.Topics)) + for i, field := range obj.Topics { + switch value := field.(type) { + case string: + topics[i] = value + case nil: + return fmt.Errorf("topic[%d] is empty", i) + default: + return fmt.Errorf("topic[%d] is not a string", i) + } + } + topicsDecoded := make([]whisperv5.TopicType, len(topics)) + for j, s := range topics { + x := common.FromHex(s) + if x == nil || len(x) != whisperv5.TopicLength { + return fmt.Errorf("topic[%d] is invalid", j) + } + topicsDecoded[j] = whisperv5.BytesToTopic(x) + } + args.Topics = topicsDecoded + } + + return nil +} + +// WhisperMessage is the RPC representation of a whisper message. +type WhisperMessage struct { + Payload string `json:"payload"` + Padding string `json:"padding"` + From string `json:"from"` + To string `json:"to"` + Sent uint32 `json:"sent"` + TTL uint32 `json:"ttl"` + PoW float64 `json:"pow"` + Hash string `json:"hash"` +} + +// NewWhisperMessage converts an internal message into an API version. +func NewWhisperMessage(message *whisperv5.ReceivedMessage) WhisperMessage { + return WhisperMessage{ + Payload: common.ToHex(message.Payload), + Padding: common.ToHex(message.Padding), + From: common.ToHex(crypto.FromECDSAPub(message.SigToPubKey())), + To: common.ToHex(crypto.FromECDSAPub(message.Dst)), + Sent: message.Sent, + TTL: message.TTL, + PoW: message.PoW, + Hash: common.ToHex(message.EnvelopeHash.Bytes()), + } +} diff --git a/whisper/shhapi/api_test.go b/whisper/shhapi/api_test.go new file mode 100644 index 000000000..8ce198595 --- /dev/null +++ b/whisper/shhapi/api_test.go @@ -0,0 +1,170 @@ +// 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 shhapi + +import ( + "testing" + + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/whisper/whisperv5" +) + +func TestBasic(x *testing.T) { + var id string = "test" + api := NewPublicWhisperAPI() + if api == nil { + x.Errorf("failed to create API.") + return + } + + ver, err := api.Version() + if err != nil { + x.Errorf("failed generateFilter: %s.", err) + return + } + + if ver.Uint64() != whisperv5.ProtocolVersion { + x.Errorf("wrong version: %d.", ver.Uint64()) + return + } + + var hexnum rpc.HexNumber + mail := api.GetFilterChanges(hexnum) + if len(mail) != 0 { + x.Errorf("failed GetFilterChanges") + return + } + + exist, err := api.HasIdentity(id) + if err != nil { + x.Errorf("failed 1 HasIdentity: %s.", err) + return + } + if exist { + x.Errorf("failed 2 HasIdentity: false positive.") + return + } + + err = api.DeleteIdentity(id) + if err != nil { + x.Errorf("failed 3 DeleteIdentity: %s.", err) + return + } + + pub, err := api.NewIdentity() + if err != nil { + x.Errorf("failed 4 NewIdentity: %s.", err) + return + } + if len(pub) == 0 { + x.Errorf("NewIdentity 5: empty") + return + } + + exist, err = api.HasIdentity(pub) + if err != nil { + x.Errorf("failed 6 HasIdentity: %s.", err) + return + } + if !exist { + x.Errorf("failed 7 HasIdentity: false negative.") + return + } + + err = api.DeleteIdentity(pub) + if err != nil { + x.Errorf("failed 8 DeleteIdentity: %s.", err) + return + } + + exist, err = api.HasIdentity(pub) + if err != nil { + x.Errorf("failed 9 HasIdentity: %s.", err) + return + } + if exist { + x.Errorf("failed 10 HasIdentity: false positive.") + return + } + + id = "arbitrary text" + id2 := "another arbitrary string" + + exist, err = api.HasSymKey(id) + if err != nil { + x.Errorf("failed 11 HasSymKey: %s.", err) + return + } + if exist { + x.Errorf("failed 12 HasSymKey: false positive.") + return + } + + err = api.GenerateSymKey(id) + if err != nil { + x.Errorf("failed 13 GenerateSymKey: %s.", err) + return + } + + exist, err = api.HasSymKey(id) + if err != nil { + x.Errorf("failed 14 HasSymKey: %s.", err) + return + } + if !exist { + x.Errorf("failed 15 HasSymKey: false negative.") + return + } + + err = api.AddSymKey(id, []byte("some stuff here")) + if err == nil { + x.Errorf("failed 16 AddSymKey: %s.", err) + return + } + + err = api.AddSymKey(id2, []byte("some stuff here")) + if err != nil { + x.Errorf("failed 17 AddSymKey: %s.", err) + return + } + + exist, err = api.HasSymKey(id2) + if err != nil { + x.Errorf("failed 18 HasSymKey: %s.", err) + return + } + if !exist { + x.Errorf("failed 19 HasSymKey: false negative.") + return + } + + err = api.DeleteSymKey(id) + if err != nil { + x.Errorf("failed 20 DeleteSymKey: %s.", err) + return + } + + exist, err = api.HasSymKey(id) + if err != nil { + x.Errorf("failed 21 HasSymKey: %s.", err) + return + } + if exist { + x.Errorf("failed 22 HasSymKey: false positive.") + return + } +} diff --git a/whisper/topic.go b/whisper/topic.go deleted file mode 100644 index d37eb25ee..000000000 --- a/whisper/topic.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2015 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. For formal details please see -// the specs at https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec#topics. - -package whisper - -import "github.com/ethereum/go-ethereum/crypto" - -// Topic 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 Topic [4]byte - -// NewTopic creates a topic from the 4 byte prefix of the SHA3 hash of the data. -// -// Note, empty topics are considered the wildcard, and cannot be used in messages. -func NewTopic(data []byte) Topic { - prefix := [4]byte{} - copy(prefix[:], crypto.Keccak256(data)[:4]) - return Topic(prefix) -} - -// NewTopics creates a list of topics from a list of binary data elements, by -// iteratively calling NewTopic on each of them. -func NewTopics(data ...[]byte) []Topic { - topics := make([]Topic, len(data)) - for i, element := range data { - topics[i] = NewTopic(element) - } - return topics -} - -// NewTopicFromString creates a topic using the binary data contents of the -// specified string. -func NewTopicFromString(data string) Topic { - return NewTopic([]byte(data)) -} - -// NewTopicsFromStrings creates a list of topics from a list of textual data -// elements, by iteratively calling NewTopicFromString on each of them. -func NewTopicsFromStrings(data ...string) []Topic { - topics := make([]Topic, len(data)) - for i, element := range data { - topics[i] = NewTopicFromString(element) - } - return topics -} - -// String converts a topic byte array to a string representation. -func (self *Topic) String() string { - return string(self[:]) -} - -// topicMatcher is a filter expression to verify if a list of topics contained -// in an arriving message matches some topic conditions. The topic matcher is -// built up of a list of conditions, each of which must be satisfied by the -// corresponding topic in the message. Each condition may require: a) an exact -// topic match; b) a match from a set of topics; or c) a wild-card matching all. -// -// If a message contains more topics than required by the matcher, those beyond -// the condition count are ignored and assumed to match. -// -// Consider the following sample topic matcher: -// sample := { -// {TopicA1, TopicA2, TopicA3}, -// {TopicB}, -// nil, -// {TopicD1, TopicD2} -// } -// In order for a message to pass this filter, it should enumerate at least 4 -// topics, the first any of [TopicA1, TopicA2, TopicA3], the second mandatory -// "TopicB", the third is ignored by the filter and the fourth either "TopicD1" -// or "TopicD2". If the message contains further topics, the filter will match -// them too. -type topicMatcher struct { - conditions []map[Topic]struct{} -} - -// newTopicMatcher create a topic matcher from a list of topic conditions. -func newTopicMatcher(topics ...[]Topic) *topicMatcher { - matcher := make([]map[Topic]struct{}, len(topics)) - for i, condition := range topics { - matcher[i] = make(map[Topic]struct{}) - for _, topic := range condition { - matcher[i][topic] = struct{}{} - } - } - return &topicMatcher{conditions: matcher} -} - -// newTopicMatcherFromBinary create a topic matcher from a list of binary conditions. -func newTopicMatcherFromBinary(data ...[][]byte) *topicMatcher { - topics := make([][]Topic, len(data)) - for i, condition := range data { - topics[i] = NewTopics(condition...) - } - return newTopicMatcher(topics...) -} - -// newTopicMatcherFromStrings creates a topic matcher from a list of textual -// conditions. -func newTopicMatcherFromStrings(data ...[]string) *topicMatcher { - topics := make([][]Topic, len(data)) - for i, condition := range data { - topics[i] = NewTopicsFromStrings(condition...) - } - return newTopicMatcher(topics...) -} - -// Matches checks if a list of topics matches this particular condition set. -func (self *topicMatcher) Matches(topics []Topic) bool { - // Mismatch if there aren't enough topics - if len(self.conditions) > len(topics) { - return false - } - // Check each topic condition for existence (skip wild-cards) - for i := 0; i < len(topics) && i < len(self.conditions); i++ { - if len(self.conditions[i]) > 0 { - if _, ok := self.conditions[i][topics[i]]; !ok { - return false - } - } - } - return true -} diff --git a/whisper/topic_test.go b/whisper/topic_test.go deleted file mode 100644 index 9c45f6740..000000000 --- a/whisper/topic_test.go +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2015 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 - -import ( - "bytes" - "testing" -) - -var topicCreationTests = []struct { - data []byte - hash [4]byte -}{ - {hash: [4]byte{0x8f, 0x9a, 0x2b, 0x7d}, data: []byte("test name")}, - {hash: [4]byte{0xf2, 0x6e, 0x77, 0x79}, data: []byte("some other test")}, -} - -func TestTopicCreation(t *testing.T) { - // Create the topics individually - for i, tt := range topicCreationTests { - topic := NewTopic(tt.data) - if bytes.Compare(topic[:], tt.hash[:]) != 0 { - t.Errorf("binary test %d: hash mismatch: have %v, want %v.", i, topic, tt.hash) - } - } - for i, tt := range topicCreationTests { - topic := NewTopicFromString(string(tt.data)) - if bytes.Compare(topic[:], tt.hash[:]) != 0 { - t.Errorf("textual test %d: hash mismatch: have %v, want %v.", i, topic, tt.hash) - } - } - // Create the topics in batches - binaryData := make([][]byte, len(topicCreationTests)) - for i, tt := range topicCreationTests { - binaryData[i] = tt.data - } - textualData := make([]string, len(topicCreationTests)) - for i, tt := range topicCreationTests { - textualData[i] = string(tt.data) - } - - topics := NewTopics(binaryData...) - for i, tt := range topicCreationTests { - if bytes.Compare(topics[i][:], tt.hash[:]) != 0 { - t.Errorf("binary batch test %d: hash mismatch: have %v, want %v.", i, topics[i], tt.hash) - } - } - topics = NewTopicsFromStrings(textualData...) - for i, tt := range topicCreationTests { - if bytes.Compare(topics[i][:], tt.hash[:]) != 0 { - t.Errorf("textual batch test %d: hash mismatch: have %v, want %v.", i, topics[i], tt.hash) - } - } -} - -var topicMatcherCreationTest = struct { - binary [][][]byte - textual [][]string - matcher []map[[4]byte]struct{} -}{ - binary: [][][]byte{ - [][]byte{}, - [][]byte{ - []byte("Topic A"), - }, - [][]byte{ - []byte("Topic B1"), - []byte("Topic B2"), - []byte("Topic B3"), - }, - }, - textual: [][]string{ - []string{}, - []string{"Topic A"}, - []string{"Topic B1", "Topic B2", "Topic B3"}, - }, - matcher: []map[[4]byte]struct{}{ - map[[4]byte]struct{}{}, - map[[4]byte]struct{}{ - [4]byte{0x25, 0xfc, 0x95, 0x66}: struct{}{}, - }, - map[[4]byte]struct{}{ - [4]byte{0x93, 0x6d, 0xec, 0x09}: struct{}{}, - [4]byte{0x25, 0x23, 0x34, 0xd3}: struct{}{}, - [4]byte{0x6b, 0xc2, 0x73, 0xd1}: struct{}{}, - }, - }, -} - -func TestTopicMatcherCreation(t *testing.T) { - test := topicMatcherCreationTest - - matcher := newTopicMatcherFromBinary(test.binary...) - for i, cond := range matcher.conditions { - for topic, _ := range cond { - if _, ok := test.matcher[i][topic]; !ok { - t.Errorf("condition %d; extra topic found: 0x%x", i, topic[:]) - } - } - } - for i, cond := range test.matcher { - for topic, _ := range cond { - if _, ok := matcher.conditions[i][topic]; !ok { - t.Errorf("condition %d; topic not found: 0x%x", i, topic[:]) - } - } - } - - matcher = newTopicMatcherFromStrings(test.textual...) - for i, cond := range matcher.conditions { - for topic, _ := range cond { - if _, ok := test.matcher[i][topic]; !ok { - t.Errorf("condition %d; extra topic found: 0x%x", i, topic[:]) - } - } - } - for i, cond := range test.matcher { - for topic, _ := range cond { - if _, ok := matcher.conditions[i][topic]; !ok { - t.Errorf("condition %d; topic not found: 0x%x", i, topic[:]) - } - } - } -} - -var topicMatcherTests = []struct { - filter [][]string - topics []string - match bool -}{ - // Empty topic matcher should match everything - { - filter: [][]string{}, - topics: []string{}, - match: true, - }, - { - filter: [][]string{}, - topics: []string{"a", "b", "c"}, - match: true, - }, - // Fixed topic matcher should match strictly, but only prefix - { - filter: [][]string{[]string{"a"}, []string{"b"}}, - topics: []string{"a"}, - match: false, - }, - { - filter: [][]string{[]string{"a"}, []string{"b"}}, - topics: []string{"a", "b"}, - match: true, - }, - { - filter: [][]string{[]string{"a"}, []string{"b"}}, - topics: []string{"a", "b", "c"}, - match: true, - }, - // Multi-matcher should match any from a sub-group - { - filter: [][]string{[]string{"a1", "a2"}}, - topics: []string{"a"}, - match: false, - }, - { - filter: [][]string{[]string{"a1", "a2"}}, - topics: []string{"a1"}, - match: true, - }, - { - filter: [][]string{[]string{"a1", "a2"}}, - topics: []string{"a2"}, - match: true, - }, - // Wild-card condition should match anything - { - filter: [][]string{[]string{}, []string{"b"}}, - topics: []string{"a"}, - match: false, - }, - { - filter: [][]string{[]string{}, []string{"b"}}, - topics: []string{"a", "b"}, - match: true, - }, - { - filter: [][]string{[]string{}, []string{"b"}}, - topics: []string{"b", "b"}, - match: true, - }, -} - -func TestTopicMatcher(t *testing.T) { - for i, tt := range topicMatcherTests { - topics := NewTopicsFromStrings(tt.topics...) - - matcher := newTopicMatcherFromStrings(tt.filter...) - if match := matcher.Matches(topics); match != tt.match { - t.Errorf("test %d: match mismatch: have %v, want %v", i, match, tt.match) - } - } -} diff --git a/whisper/whisper.go b/whisper/whisper.go deleted file mode 100644 index 0a49c1000..000000000 --- a/whisper/whisper.go +++ /dev/null @@ -1,378 +0,0 @@ -// Copyright 2014 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 - -import ( - "crypto/ecdsa" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/ecies" - "github.com/ethereum/go-ethereum/event/filter" - "github.com/ethereum/go-ethereum/logger" - "github.com/ethereum/go-ethereum/logger/glog" - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/rpc" - - "gopkg.in/fatih/set.v0" -) - -const ( - statusCode = 0x00 - messagesCode = 0x01 - - protocolVersion uint64 = 0x02 - protocolName = "shh" - - signatureFlag = byte(1 << 7) - signatureLength = 65 - - expirationCycle = 800 * time.Millisecond - transmissionCycle = 300 * time.Millisecond -) - -const ( - DefaultTTL = 50 * time.Second - DefaultPoW = 50 * time.Millisecond -) - -type MessageEvent struct { - To *ecdsa.PrivateKey - From *ecdsa.PublicKey - Message *Message -} - -// Whisper represents a dark communication interface through the Ethereum -// network, using its very own P2P communication layer. -type Whisper struct { - protocol p2p.Protocol - filters *filter.Filters - - keys map[string]*ecdsa.PrivateKey - - messages map[common.Hash]*Envelope // Pool of messages currently tracked by this node - expirations map[uint32]*set.SetNonTS // Message expiration pool (TODO: something lighter) - poolMu sync.RWMutex // Mutex to sync the message and expiration pools - - peers map[*peer]struct{} // Set of currently active peers - peerMu sync.RWMutex // Mutex to sync the active peer set - - quit chan struct{} -} - -// New creates a Whisper client ready to communicate through the Ethereum P2P -// network. -func New() *Whisper { - whisper := &Whisper{ - filters: filter.New(), - keys: make(map[string]*ecdsa.PrivateKey), - messages: make(map[common.Hash]*Envelope), - expirations: make(map[uint32]*set.SetNonTS), - peers: make(map[*peer]struct{}), - quit: make(chan struct{}), - } - whisper.filters.Start() - - // p2p whisper sub protocol handler - whisper.protocol = p2p.Protocol{ - Name: protocolName, - Version: uint(protocolVersion), - Length: 2, - Run: whisper.handlePeer, - } - - return whisper -} - -// APIs returns the RPC descriptors the Whisper implementation offers -func (s *Whisper) APIs() []rpc.API { - return []rpc.API{ - { - Namespace: "shh", - Version: "1.0", - Service: NewPublicWhisperAPI(s), - Public: true, - }, - } -} - -// Protocols returns the whisper sub-protocols ran by this particular client. -func (self *Whisper) Protocols() []p2p.Protocol { - return []p2p.Protocol{self.protocol} -} - -// Version returns the whisper sub-protocols version number. -func (self *Whisper) Version() uint { - return self.protocol.Version -} - -// NewIdentity generates a new cryptographic identity for the client, and injects -// it into the known identities for message decryption. -func (self *Whisper) NewIdentity() *ecdsa.PrivateKey { - key, err := crypto.GenerateKey() - if err != nil { - panic(err) - } - self.keys[string(crypto.FromECDSAPub(&key.PublicKey))] = key - - return key -} - -// HasIdentity checks if the the whisper node is configured with the private key -// of the specified public pair. -func (self *Whisper) HasIdentity(key *ecdsa.PublicKey) bool { - return self.keys[string(crypto.FromECDSAPub(key))] != nil -} - -// GetIdentity retrieves the private key of the specified public identity. -func (self *Whisper) GetIdentity(key *ecdsa.PublicKey) *ecdsa.PrivateKey { - return self.keys[string(crypto.FromECDSAPub(key))] -} - -// Watch installs a new message handler to run in case a matching packet arrives -// from the whisper network. -func (self *Whisper) Watch(options Filter) int { - filter := filterer{ - to: string(crypto.FromECDSAPub(options.To)), - from: string(crypto.FromECDSAPub(options.From)), - matcher: newTopicMatcher(options.Topics...), - fn: func(data interface{}) { - options.Fn(data.(*Message)) - }, - } - return self.filters.Install(filter) -} - -// Unwatch removes an installed message handler. -func (self *Whisper) Unwatch(id int) { - self.filters.Uninstall(id) -} - -// Send injects a message into the whisper send queue, to be distributed in the -// network in the coming cycles. -func (self *Whisper) Send(envelope *Envelope) error { - return self.add(envelope) -} - -// Start implements node.Service, starting the background data propagation thread -// of the Whisper protocol. -func (self *Whisper) Start(*p2p.Server) error { - glog.V(logger.Info).Infoln("Whisper started") - go self.update() - return nil -} - -// Stop implements node.Service, stopping the background data propagation thread -// of the Whisper protocol. -func (self *Whisper) Stop() error { - close(self.quit) - glog.V(logger.Info).Infoln("Whisper stopped") - return nil -} - -// Messages retrieves all the currently pooled messages matching a filter id. -func (self *Whisper) Messages(id int) []*Message { - messages := make([]*Message, 0) - if filter := self.filters.Get(id); filter != nil { - for _, envelope := range self.messages { - if message := self.open(envelope); message != nil { - if self.filters.Match(filter, createFilter(message, envelope.Topics)) { - messages = append(messages, message) - } - } - } - } - return messages -} - -// handlePeer is called by the underlying P2P layer when the whisper sub-protocol -// connection is negotiated. -func (self *Whisper) handlePeer(peer *p2p.Peer, rw p2p.MsgReadWriter) error { - // Create the new peer and start tracking it - whisperPeer := newPeer(self, peer, rw) - - self.peerMu.Lock() - self.peers[whisperPeer] = struct{}{} - self.peerMu.Unlock() - - defer func() { - self.peerMu.Lock() - delete(self.peers, whisperPeer) - self.peerMu.Unlock() - }() - - // Run the peer handshake and state updates - if err := whisperPeer.handshake(); err != nil { - return err - } - whisperPeer.start() - defer whisperPeer.stop() - - // Read and process inbound messages directly to merge into client-global state - for { - // Fetch the next packet and decode the contained envelopes - packet, err := rw.ReadMsg() - if err != nil { - return err - } - var envelopes []*Envelope - if err := packet.Decode(&envelopes); err != nil { - glog.V(logger.Info).Infof("%v: failed to decode envelope: %v", peer, err) - continue - } - // Inject all envelopes into the internal pool - for _, envelope := range envelopes { - if err := self.add(envelope); err != nil { - // TODO Punish peer here. Invalid envelope. - glog.V(logger.Debug).Infof("%v: failed to pool envelope: %v", peer, err) - } - whisperPeer.mark(envelope) - } - } -} - -// 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. -func (self *Whisper) add(envelope *Envelope) error { - self.poolMu.Lock() - defer self.poolMu.Unlock() - - // short circuit when a received envelope has already expired - if envelope.Expiry < uint32(time.Now().Unix()) { - return nil - } - - // Insert the message into the tracked pool - hash := envelope.Hash() - if _, ok := self.messages[hash]; ok { - glog.V(logger.Detail).Infof("whisper envelope already cached: %x\n", envelope) - return nil - } - self.messages[hash] = envelope - - // Insert the message into the expiration pool for later removal - if self.expirations[envelope.Expiry] == nil { - self.expirations[envelope.Expiry] = set.NewNonTS() - } - if !self.expirations[envelope.Expiry].Has(hash) { - self.expirations[envelope.Expiry].Add(hash) - - // Notify the local node of a message arrival - go self.postEvent(envelope) - } - glog.V(logger.Detail).Infof("cached whisper envelope %x\n", envelope) - return nil -} - -// postEvent opens an envelope with the configured identities and delivers the -// message upstream from application processing. -func (self *Whisper) postEvent(envelope *Envelope) { - if message := self.open(envelope); message != nil { - self.filters.Notify(createFilter(message, envelope.Topics), message) - } -} - -// open tries to decrypt a whisper envelope with all the configured identities, -// returning the decrypted message and the key used to achieve it. If not keys -// are configured, open will return the payload as if non encrypted. -func (self *Whisper) open(envelope *Envelope) *Message { - // Short circuit if no identity is set, and assume clear-text - if len(self.keys) == 0 { - if message, err := envelope.Open(nil); err == nil { - return message - } - } - // Iterate over the keys and try to decrypt the message - for _, key := range self.keys { - message, err := envelope.Open(key) - if err == nil { - message.To = &key.PublicKey - return message - } else if err == ecies.ErrInvalidPublicKey { - return message - } - } - // Failed to decrypt, don't return anything - return nil -} - -// createFilter creates a message filter to check against installed handlers. -func createFilter(message *Message, topics []Topic) filter.Filter { - matcher := make([][]Topic, len(topics)) - for i, topic := range topics { - matcher[i] = []Topic{topic} - } - return filterer{ - to: string(crypto.FromECDSAPub(message.To)), - from: string(crypto.FromECDSAPub(message.Recover())), - matcher: newTopicMatcher(matcher...), - } -} - -// update loops until the lifetime of the whisper node, updating its internal -// state by expiring stale messages from the pool. -func (self *Whisper) update() { - // Start a ticker to check for expirations - expire := time.NewTicker(expirationCycle) - - // Repeat updates until termination is requested - for { - select { - case <-expire.C: - self.expire() - - case <-self.quit: - return - } - } -} - -// expire iterates over all the expiration timestamps, removing all stale -// messages from the pools. -func (self *Whisper) expire() { - self.poolMu.Lock() - defer self.poolMu.Unlock() - - now := uint32(time.Now().Unix()) - for then, hashSet := range self.expirations { - // Short circuit if a future time - if then > now { - continue - } - // Dump all expired messages and remove timestamp - hashSet.Each(func(v interface{}) bool { - delete(self.messages, v.(common.Hash)) - return true - }) - self.expirations[then].Clear() - } -} - -// envelopes retrieves all the messages currently pooled by the node. -func (self *Whisper) envelopes() []*Envelope { - self.poolMu.RLock() - defer self.poolMu.RUnlock() - - envelopes := make([]*Envelope, 0, len(self.messages)) - for _, envelope := range self.messages { - envelopes = append(envelopes, envelope) - } - return envelopes -} diff --git a/whisper/whisper_test.go b/whisper/whisper_test.go deleted file mode 100644 index 094df373c..000000000 --- a/whisper/whisper_test.go +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright 2014 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 - -import ( - "testing" - "time" - - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/p2p/discover" -) - -func startTestCluster(n int) []*Whisper { - // Create the batch of simulated peers - nodes := make([]*p2p.Peer, n) - for i := 0; i < n; i++ { - nodes[i] = p2p.NewPeer(discover.NodeID{}, "", nil) - } - whispers := make([]*Whisper, n) - for i := 0; i < n; i++ { - whispers[i] = New() - whispers[i].Start(nil) - } - // Wire all the peers to the root one - for i := 1; i < n; i++ { - src, dst := p2p.MsgPipe() - - go whispers[0].handlePeer(nodes[i], src) - go whispers[i].handlePeer(nodes[0], dst) - } - return whispers -} - -func TestSelfMessage(t *testing.T) { - // Start the single node cluster - client := startTestCluster(1)[0] - - // Start watching for self messages, signal any arrivals - self := client.NewIdentity() - done := make(chan struct{}) - - client.Watch(Filter{ - To: &self.PublicKey, - Fn: func(msg *Message) { - close(done) - }, - }) - // Send a dummy message to oneself - msg := NewMessage([]byte("self whisper")) - envelope, err := msg.Wrap(DefaultPoW, Options{ - From: self, - To: &self.PublicKey, - TTL: DefaultTTL, - }) - if err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - // Dump the message into the system and wait for it to pop back out - if err := client.Send(envelope); err != nil { - t.Fatalf("failed to send self-message: %v", err) - } - select { - case <-done: - case <-time.After(time.Second): - t.Fatalf("self-message receive timeout") - } -} - -func TestDirectMessage(t *testing.T) { - // Start the sender-recipient cluster - cluster := startTestCluster(2) - - sender := cluster[0] - senderId := sender.NewIdentity() - - recipient := cluster[1] - recipientId := recipient.NewIdentity() - - // Watch for arriving messages on the recipient - done := make(chan struct{}) - recipient.Watch(Filter{ - To: &recipientId.PublicKey, - Fn: func(msg *Message) { - close(done) - }, - }) - // Send a dummy message from the sender - msg := NewMessage([]byte("direct whisper")) - envelope, err := msg.Wrap(DefaultPoW, Options{ - From: senderId, - To: &recipientId.PublicKey, - TTL: DefaultTTL, - }) - if err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - if err := sender.Send(envelope); err != nil { - t.Fatalf("failed to send direct message: %v", err) - } - // Wait for an arrival or a timeout - select { - case <-done: - case <-time.After(time.Second): - t.Fatalf("direct message receive timeout") - } -} - -func TestAnonymousBroadcast(t *testing.T) { - testBroadcast(true, t) -} - -func TestIdentifiedBroadcast(t *testing.T) { - testBroadcast(false, t) -} - -func testBroadcast(anonymous bool, t *testing.T) { - // Start the single sender multi recipient cluster - cluster := startTestCluster(3) - - sender := cluster[1] - targets := cluster[1:] - for _, target := range targets { - if !anonymous { - target.NewIdentity() - } - } - // Watch for arriving messages on the recipients - dones := make([]chan struct{}, len(targets)) - for i := 0; i < len(targets); i++ { - done := make(chan struct{}) // need for the closure - dones[i] = done - - targets[i].Watch(Filter{ - Topics: NewFilterTopicsFromStringsFlat("broadcast topic"), - Fn: func(msg *Message) { - close(done) - }, - }) - } - // Send a dummy message from the sender - msg := NewMessage([]byte("broadcast whisper")) - envelope, err := msg.Wrap(DefaultPoW, Options{ - Topics: NewTopicsFromStrings("broadcast topic"), - TTL: DefaultTTL, - }) - if err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - if err := sender.Send(envelope); err != nil { - t.Fatalf("failed to send broadcast message: %v", err) - } - // Wait for an arrival on each recipient, or timeouts - timeout := time.After(time.Second) - for _, done := range dones { - select { - case <-done: - case <-timeout: - t.Fatalf("broadcast message receive timeout") - } - } -} - -func TestMessageExpiration(t *testing.T) { - // Start the single node cluster and inject a dummy message - node := startTestCluster(1)[0] - - message := NewMessage([]byte("expiring message")) - envelope, err := message.Wrap(DefaultPoW, Options{TTL: time.Second}) - if err != nil { - t.Fatalf("failed to wrap message: %v", err) - } - if err := node.Send(envelope); err != nil { - t.Fatalf("failed to inject message: %v", err) - } - // Check that the message is inside the cache - node.poolMu.RLock() - _, found := node.messages[envelope.Hash()] - node.poolMu.RUnlock() - - if !found { - t.Fatalf("message not found in cache") - } - // Wait for expiration and check cache again - time.Sleep(time.Second) // wait for expiration - time.Sleep(2 * expirationCycle) // wait for cleanup cycle - - node.poolMu.RLock() - _, found = node.messages[envelope.Hash()] - node.poolMu.RUnlock() - if found { - t.Fatalf("message not expired from cache") - } - - // Check that adding an expired envelope doesn't do anything. - node.add(envelope) - node.poolMu.RLock() - _, found = node.messages[envelope.Hash()] - node.poolMu.RUnlock() - if found { - t.Fatalf("message was added to cache") - } -} diff --git a/whisper/whisperv2/api.go b/whisper/whisperv2/api.go new file mode 100644 index 000000000..9c9c6a84c --- /dev/null +++ b/whisper/whisperv2/api.go @@ -0,0 +1,413 @@ +// Copyright 2015 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 whisperv2 + +import ( + "encoding/json" + "fmt" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rpc" +) + +// PublicWhisperAPI provides the whisper RPC service. +type PublicWhisperAPI struct { + w *Whisper + + messagesMu sync.RWMutex + messages map[int]*whisperFilter +} + +type whisperOfflineError struct{} + +func (e *whisperOfflineError) Error() string { + return "whisper is offline" +} + +// whisperOffLineErr is returned when the node doesn't offer the shh service. +var whisperOffLineErr = new(whisperOfflineError) + +// NewPublicWhisperAPI create a new RPC whisper service. +func NewPublicWhisperAPI(w *Whisper) *PublicWhisperAPI { + return &PublicWhisperAPI{w: w, messages: make(map[int]*whisperFilter)} +} + +// Version returns the Whisper version this node offers. +func (s *PublicWhisperAPI) Version() (*rpc.HexNumber, error) { + if s.w == nil { + return rpc.NewHexNumber(0), whisperOffLineErr + } + return rpc.NewHexNumber(s.w.Version()), nil +} + +// HasIdentity checks if the the whisper node is configured with the private key +// of the specified public pair. +func (s *PublicWhisperAPI) HasIdentity(identity string) (bool, error) { + if s.w == nil { + return false, whisperOffLineErr + } + return s.w.HasIdentity(crypto.ToECDSAPub(common.FromHex(identity))), nil +} + +// NewIdentity generates a new cryptographic identity for the client, and injects +// it into the known identities for message decryption. +func (s *PublicWhisperAPI) NewIdentity() (string, error) { + if s.w == nil { + return "", whisperOffLineErr + } + + identity := s.w.NewIdentity() + return common.ToHex(crypto.FromECDSAPub(&identity.PublicKey)), nil +} + +type NewFilterArgs struct { + To string + From string + Topics [][][]byte +} + +// NewWhisperFilter creates and registers a new message filter to watch for inbound whisper messages. +func (s *PublicWhisperAPI) NewFilter(args NewFilterArgs) (*rpc.HexNumber, error) { + if s.w == nil { + return nil, whisperOffLineErr + } + + var id int + filter := Filter{ + To: crypto.ToECDSAPub(common.FromHex(args.To)), + From: crypto.ToECDSAPub(common.FromHex(args.From)), + Topics: NewFilterTopics(args.Topics...), + Fn: func(message *Message) { + wmsg := NewWhisperMessage(message) + s.messagesMu.RLock() // Only read lock to the filter pool + defer s.messagesMu.RUnlock() + if s.messages[id] != nil { + s.messages[id].insert(wmsg) + } + }, + } + + id = s.w.Watch(filter) + + s.messagesMu.Lock() + s.messages[id] = newWhisperFilter(id, s.w) + s.messagesMu.Unlock() + + return rpc.NewHexNumber(id), nil +} + +// GetFilterChanges retrieves all the new messages matched by a filter since the last retrieval. +func (s *PublicWhisperAPI) GetFilterChanges(filterId rpc.HexNumber) []WhisperMessage { + s.messagesMu.RLock() + defer s.messagesMu.RUnlock() + + if s.messages[filterId.Int()] != nil { + if changes := s.messages[filterId.Int()].retrieve(); changes != nil { + return changes + } + } + return returnWhisperMessages(nil) +} + +// UninstallFilter disables and removes an existing filter. +func (s *PublicWhisperAPI) UninstallFilter(filterId rpc.HexNumber) bool { + s.messagesMu.Lock() + defer s.messagesMu.Unlock() + + if _, ok := s.messages[filterId.Int()]; ok { + delete(s.messages, filterId.Int()) + return true + } + return false +} + +// GetMessages retrieves all the known messages that match a specific filter. +func (s *PublicWhisperAPI) GetMessages(filterId rpc.HexNumber) []WhisperMessage { + // Retrieve all the cached messages matching a specific, existing filter + s.messagesMu.RLock() + defer s.messagesMu.RUnlock() + + var messages []*Message + if s.messages[filterId.Int()] != nil { + messages = s.messages[filterId.Int()].messages() + } + + return returnWhisperMessages(messages) +} + +// returnWhisperMessages converts aNhisper message to a RPC whisper message. +func returnWhisperMessages(messages []*Message) []WhisperMessage { + msgs := make([]WhisperMessage, len(messages)) + for i, msg := range messages { + msgs[i] = NewWhisperMessage(msg) + } + return msgs +} + +type PostArgs struct { + From string `json:"from"` + To string `json:"to"` + Topics [][]byte `json:"topics"` + Payload string `json:"payload"` + Priority int64 `json:"priority"` + TTL int64 `json:"ttl"` +} + +// Post injects a message into the whisper network for distribution. +func (s *PublicWhisperAPI) Post(args PostArgs) (bool, error) { + if s.w == nil { + return false, whisperOffLineErr + } + + // construct whisper message with transmission options + message := NewMessage(common.FromHex(args.Payload)) + options := Options{ + To: crypto.ToECDSAPub(common.FromHex(args.To)), + TTL: time.Duration(args.TTL) * time.Second, + Topics: NewTopics(args.Topics...), + } + + // set sender identity + if len(args.From) > 0 { + if key := s.w.GetIdentity(crypto.ToECDSAPub(common.FromHex(args.From))); key != nil { + options.From = key + } else { + return false, fmt.Errorf("unknown identity to send from: %s", args.From) + } + } + + // Wrap and send the message + pow := time.Duration(args.Priority) * time.Millisecond + envelope, err := message.Wrap(pow, options) + if err != nil { + return false, err + } + + return true, s.w.Send(envelope) +} + +// WhisperMessage is the RPC representation of a whisper message. +type WhisperMessage struct { + ref *Message + + Payload string `json:"payload"` + To string `json:"to"` + From string `json:"from"` + Sent int64 `json:"sent"` + TTL int64 `json:"ttl"` + Hash string `json:"hash"` +} + +func (args *PostArgs) UnmarshalJSON(data []byte) (err error) { + var obj struct { + From string `json:"from"` + To string `json:"to"` + Topics []string `json:"topics"` + Payload string `json:"payload"` + Priority rpc.HexNumber `json:"priority"` + TTL rpc.HexNumber `json:"ttl"` + } + + if err := json.Unmarshal(data, &obj); err != nil { + return err + } + + args.From = obj.From + args.To = obj.To + args.Payload = obj.Payload + args.Priority = obj.Priority.Int64() + args.TTL = obj.TTL.Int64() + + // decode topic strings + args.Topics = make([][]byte, len(obj.Topics)) + for i, topic := range obj.Topics { + args.Topics[i] = common.FromHex(topic) + } + + return nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface, invoked to convert a +// JSON message blob into a WhisperFilterArgs structure. +func (args *NewFilterArgs) UnmarshalJSON(b []byte) (err error) { + // Unmarshal the JSON message and sanity check + var obj struct { + To interface{} `json:"to"` + From interface{} `json:"from"` + Topics interface{} `json:"topics"` + } + if err := json.Unmarshal(b, &obj); err != nil { + return err + } + + // Retrieve the simple data contents of the filter arguments + if obj.To == nil { + args.To = "" + } else { + argstr, ok := obj.To.(string) + if !ok { + return fmt.Errorf("to is not a string") + } + args.To = argstr + } + if obj.From == nil { + args.From = "" + } else { + argstr, ok := obj.From.(string) + if !ok { + return fmt.Errorf("from is not a string") + } + args.From = argstr + } + // Construct the nested topic array + if obj.Topics != nil { + // Make sure we have an actual topic array + list, ok := obj.Topics.([]interface{}) + if !ok { + return fmt.Errorf("topics is not an array") + } + // Iterate over each topic and handle nil, string or array + topics := make([][]string, len(list)) + for idx, field := range list { + switch value := field.(type) { + case nil: + topics[idx] = []string{} + + case string: + topics[idx] = []string{value} + + case []interface{}: + topics[idx] = make([]string, len(value)) + for i, nested := range value { + switch value := nested.(type) { + case nil: + topics[idx][i] = "" + + case string: + topics[idx][i] = value + + default: + return fmt.Errorf("topic[%d][%d] is not a string", idx, i) + } + } + default: + return fmt.Errorf("topic[%d] not a string or array", idx) + } + } + + topicsDecoded := make([][][]byte, len(topics)) + for i, condition := range topics { + topicsDecoded[i] = make([][]byte, len(condition)) + for j, topic := range condition { + topicsDecoded[i][j] = common.FromHex(topic) + } + } + + args.Topics = topicsDecoded + } + return nil +} + +// whisperFilter is the message cache matching a specific filter, accumulating +// inbound messages until the are requested by the client. +type whisperFilter struct { + id int // Filter identifier for old message retrieval + ref *Whisper // Whisper reference for old message retrieval + + cache []WhisperMessage // Cache of messages not yet polled + skip map[common.Hash]struct{} // List of retrieved messages to avoid duplication + update time.Time // Time of the last message query + + lock sync.RWMutex // Lock protecting the filter internals +} + +// messages retrieves all the cached messages from the entire pool matching the +// filter, resetting the filter's change buffer. +func (w *whisperFilter) messages() []*Message { + w.lock.Lock() + defer w.lock.Unlock() + + w.cache = nil + w.update = time.Now() + + w.skip = make(map[common.Hash]struct{}) + messages := w.ref.Messages(w.id) + for _, message := range messages { + w.skip[message.Hash] = struct{}{} + } + return messages +} + +// insert injects a new batch of messages into the filter cache. +func (w *whisperFilter) insert(messages ...WhisperMessage) { + w.lock.Lock() + defer w.lock.Unlock() + + for _, message := range messages { + if _, ok := w.skip[message.ref.Hash]; !ok { + w.cache = append(w.cache, messages...) + } + } +} + +// retrieve fetches all the cached messages from the filter. +func (w *whisperFilter) retrieve() (messages []WhisperMessage) { + w.lock.Lock() + defer w.lock.Unlock() + + messages, w.cache = w.cache, nil + w.update = time.Now() + + return +} + +// activity returns the last time instance when client requests were executed on +// the filter. +func (w *whisperFilter) activity() time.Time { + w.lock.RLock() + defer w.lock.RUnlock() + + return w.update +} + +// newWhisperFilter creates a new serialized, poll based whisper topic filter. +func newWhisperFilter(id int, ref *Whisper) *whisperFilter { + return &whisperFilter{ + id: id, + ref: ref, + + update: time.Now(), + skip: make(map[common.Hash]struct{}), + } +} + +// NewWhisperMessage converts an internal message into an API version. +func NewWhisperMessage(message *Message) WhisperMessage { + return WhisperMessage{ + ref: message, + + Payload: common.ToHex(message.Payload), + From: common.ToHex(crypto.FromECDSAPub(message.Recover())), + To: common.ToHex(crypto.FromECDSAPub(message.To)), + Sent: message.Sent.Unix(), + TTL: int64(message.TTL / time.Second), + Hash: common.ToHex(message.Hash.Bytes()), + } +} diff --git a/whisper/whisperv2/doc.go b/whisper/whisperv2/doc.go new file mode 100644 index 000000000..7252f44b1 --- /dev/null +++ b/whisper/whisperv2/doc.go @@ -0,0 +1,32 @@ +// Copyright 2014 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 PoC-1. + +(https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec) + +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. +*/ +package whisperv2 diff --git a/whisper/whisperv2/envelope.go b/whisper/whisperv2/envelope.go new file mode 100644 index 000000000..7110ab457 --- /dev/null +++ b/whisper/whisperv2/envelope.go @@ -0,0 +1,147 @@ +// Copyright 2014 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. For formal details please see +// the specs at https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec#envelopes. + +package whisperv2 + +import ( + "crypto/ecdsa" + "encoding/binary" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/ethereum/go-ethereum/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 // Whisper protocol specifies int32, really should be int64 + TTL uint32 // ^^^^^^ + Topics []Topic + Data []byte + Nonce uint32 + + hash common.Hash // Cached hash of the envelope to avoid rehashing every time +} + +// NewEnvelope wraps a Whisper message with expiration and destination data +// included into an envelope for network forwarding. +func NewEnvelope(ttl time.Duration, topics []Topic, msg *Message) *Envelope { + return &Envelope{ + Expiry: uint32(time.Now().Add(ttl).Unix()), + TTL: uint32(ttl.Seconds()), + Topics: topics, + Data: msg.bytes(), + Nonce: 0, + } +} + +// Seal closes the envelope by spending the requested amount of time as a proof +// of work on hashing the data. +func (self *Envelope) Seal(pow time.Duration) { + d := make([]byte, 64) + copy(d[:32], self.rlpWithoutNonce()) + + finish, bestBit := time.Now().Add(pow).UnixNano(), 0 + for nonce := uint32(0); time.Now().UnixNano() < finish; { + for i := 0; i < 1024; i++ { + binary.BigEndian.PutUint32(d[60:], nonce) + + firstBit := common.FirstBitSet(common.BigD(crypto.Keccak256(d))) + if firstBit > bestBit { + self.Nonce, bestBit = nonce, firstBit + } + nonce++ + } + } +} + +// rlpWithoutNonce returns the RLP encoded envelope contents, except the nonce. +func (self *Envelope) rlpWithoutNonce() []byte { + enc, _ := rlp.EncodeToBytes([]interface{}{self.Expiry, self.TTL, self.Topics, self.Data}) + return enc +} + +// Open extracts the message contained within a potentially encrypted envelope. +func (self *Envelope) Open(key *ecdsa.PrivateKey) (msg *Message, err error) { + // Split open the payload into a message construct + data := self.Data + + message := &Message{ + Flags: data[0], + Sent: time.Unix(int64(self.Expiry-self.TTL), 0), + TTL: time.Duration(self.TTL) * time.Second, + Hash: self.Hash(), + } + data = data[1:] + + if message.Flags&signatureFlag == signatureFlag { + if len(data) < signatureLength { + return nil, fmt.Errorf("unable to open envelope. First bit set but len(data) < len(signature)") + } + message.Signature, data = data[:signatureLength], data[signatureLength:] + } + message.Payload = data + + // Decrypt the message, if requested + if key == nil { + return message, nil + } + err = message.decrypt(key) + switch err { + case nil: + return message, nil + + case ecies.ErrInvalidPublicKey: // Payload isn't encrypted + return message, err + + default: + return nil, fmt.Errorf("unable to open envelope, decrypt failed: %v", err) + } +} + +// Hash returns the SHA3 hash of the envelope, calculating it if not yet done. +func (self *Envelope) Hash() common.Hash { + if (self.hash == common.Hash{}) { + enc, _ := rlp.EncodeToBytes(self) + self.hash = crypto.Keccak256Hash(enc) + } + return self.hash +} + +// DecodeRLP decodes an Envelope from an RLP data stream. +func (self *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 so we can reuse the Envelope struct definition. + type rlpenv Envelope + if err := rlp.DecodeBytes(raw, (*rlpenv)(self)); err != nil { + return err + } + self.hash = crypto.Keccak256Hash(raw) + return nil +} diff --git a/whisper/whisperv2/envelope_test.go b/whisper/whisperv2/envelope_test.go new file mode 100644 index 000000000..75e2fbe8a --- /dev/null +++ b/whisper/whisperv2/envelope_test.go @@ -0,0 +1,158 @@ +// Copyright 2015 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 whisperv2 + +import ( + "bytes" + "testing" + "time" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" +) + +func TestEnvelopeOpen(t *testing.T) { + payload := []byte("hello world") + message := NewMessage(payload) + + envelope, err := message.Wrap(DefaultPoW, Options{}) + if err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + opened, err := envelope.Open(nil) + if err != nil { + t.Fatalf("failed to open envelope: %v", err) + } + if opened.Flags != message.Flags { + t.Fatalf("flags mismatch: have %d, want %d", opened.Flags, message.Flags) + } + if bytes.Compare(opened.Signature, message.Signature) != 0 { + t.Fatalf("signature mismatch: have 0x%x, want 0x%x", opened.Signature, message.Signature) + } + if bytes.Compare(opened.Payload, message.Payload) != 0 { + t.Fatalf("payload mismatch: have 0x%x, want 0x%x", opened.Payload, message.Payload) + } + if opened.Sent.Unix() != message.Sent.Unix() { + t.Fatalf("send time mismatch: have %d, want %d", opened.Sent, message.Sent) + } + if opened.TTL/time.Second != DefaultTTL/time.Second { + t.Fatalf("message TTL mismatch: have %v, want %v", opened.TTL, DefaultTTL) + } + + if opened.Hash != envelope.Hash() { + t.Fatalf("message hash mismatch: have 0x%x, want 0x%x", opened.Hash, envelope.Hash()) + } +} + +func TestEnvelopeAnonymousOpenUntargeted(t *testing.T) { + payload := []byte("hello envelope") + envelope, err := NewMessage(payload).Wrap(DefaultPoW, Options{}) + if err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + opened, err := envelope.Open(nil) + if err != nil { + t.Fatalf("failed to open envelope: %v", err) + } + if opened.To != nil { + t.Fatalf("recipient mismatch: have 0x%x, want nil", opened.To) + } + if bytes.Compare(opened.Payload, payload) != 0 { + t.Fatalf("payload mismatch: have 0x%x, want 0x%x", opened.Payload, payload) + } +} + +func TestEnvelopeAnonymousOpenTargeted(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to generate test identity: %v", err) + } + + payload := []byte("hello envelope") + envelope, err := NewMessage(payload).Wrap(DefaultPoW, Options{ + To: &key.PublicKey, + }) + if err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + opened, err := envelope.Open(nil) + if err != nil { + t.Fatalf("failed to open envelope: %v", err) + } + if opened.To != nil { + t.Fatalf("recipient mismatch: have 0x%x, want nil", opened.To) + } + if bytes.Compare(opened.Payload, payload) == 0 { + t.Fatalf("payload match, should have been encrypted: 0x%x", opened.Payload) + } +} + +func TestEnvelopeIdentifiedOpenUntargeted(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to generate test identity: %v", err) + } + + payload := []byte("hello envelope") + envelope, err := NewMessage(payload).Wrap(DefaultPoW, Options{}) + if err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + opened, err := envelope.Open(key) + switch err { + case nil: + t.Fatalf("envelope opened with bad key: %v", opened) + + case ecies.ErrInvalidPublicKey: + // Ok, key mismatch but opened + + default: + t.Fatalf("failed to open envelope: %v", err) + } + + if opened.To != nil { + t.Fatalf("recipient mismatch: have 0x%x, want nil", opened.To) + } + if bytes.Compare(opened.Payload, payload) != 0 { + t.Fatalf("payload mismatch: have 0x%x, want 0x%x", opened.Payload, payload) + } +} + +func TestEnvelopeIdentifiedOpenTargeted(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to generate test identity: %v", err) + } + + payload := []byte("hello envelope") + envelope, err := NewMessage(payload).Wrap(DefaultPoW, Options{ + To: &key.PublicKey, + }) + if err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + opened, err := envelope.Open(key) + if err != nil { + t.Fatalf("failed to open envelope: %v", err) + } + if opened.To != nil { + t.Fatalf("recipient mismatch: have 0x%x, want nil", opened.To) + } + if bytes.Compare(opened.Payload, payload) != 0 { + t.Fatalf("payload mismatch: have 0x%x, want 0x%x", opened.Payload, payload) + } +} diff --git a/whisper/whisperv2/filter.go b/whisper/whisperv2/filter.go new file mode 100644 index 000000000..8ce4a54fb --- /dev/null +++ b/whisper/whisperv2/filter.go @@ -0,0 +1,132 @@ +// Copyright 2014 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 message filter for fine grained subscriptions. + +package whisperv2 + +import ( + "crypto/ecdsa" + + "github.com/ethereum/go-ethereum/event/filter" +) + +// Filter is used to subscribe to specific types of whisper messages. +type Filter struct { + To *ecdsa.PublicKey // Recipient of the message + From *ecdsa.PublicKey // Sender of the message + Topics [][]Topic // Topics to filter messages with + Fn func(msg *Message) // Handler in case of a match +} + +// NewFilterTopics creates a 2D topic array used by whisper.Filter from binary +// data elements. +func NewFilterTopics(data ...[][]byte) [][]Topic { + filter := make([][]Topic, len(data)) + for i, condition := range data { + // Handle the special case of condition == [[]byte{}] + if len(condition) == 1 && len(condition[0]) == 0 { + filter[i] = []Topic{} + continue + } + // Otherwise flatten normally + filter[i] = NewTopics(condition...) + } + return filter +} + +// NewFilterTopicsFlat creates a 2D topic array used by whisper.Filter from flat +// binary data elements. +func NewFilterTopicsFlat(data ...[]byte) [][]Topic { + filter := make([][]Topic, len(data)) + for i, element := range data { + // Only add non-wildcard topics + filter[i] = make([]Topic, 0, 1) + if len(element) > 0 { + filter[i] = append(filter[i], NewTopic(element)) + } + } + return filter +} + +// NewFilterTopicsFromStrings creates a 2D topic array used by whisper.Filter +// from textual data elements. +func NewFilterTopicsFromStrings(data ...[]string) [][]Topic { + filter := make([][]Topic, len(data)) + for i, condition := range data { + // Handle the special case of condition == [""] + if len(condition) == 1 && condition[0] == "" { + filter[i] = []Topic{} + continue + } + // Otherwise flatten normally + filter[i] = NewTopicsFromStrings(condition...) + } + return filter +} + +// NewFilterTopicsFromStringsFlat creates a 2D topic array used by whisper.Filter from flat +// binary data elements. +func NewFilterTopicsFromStringsFlat(data ...string) [][]Topic { + filter := make([][]Topic, len(data)) + for i, element := range data { + // Only add non-wildcard topics + filter[i] = make([]Topic, 0, 1) + if element != "" { + filter[i] = append(filter[i], NewTopicFromString(element)) + } + } + return filter +} + +// filterer is the internal, fully initialized filter ready to match inbound +// messages to a variety of criteria. +type filterer struct { + to string // Recipient of the message + from string // Sender of the message + matcher *topicMatcher // Topics to filter messages with + fn func(data interface{}) // Handler in case of a match +} + +// Compare checks if the specified filter matches the current one. +func (self filterer) Compare(f filter.Filter) bool { + filter := f.(filterer) + + // Check the message sender and recipient + if len(self.to) > 0 && self.to != filter.to { + return false + } + if len(self.from) > 0 && self.from != filter.from { + return false + } + // Check the topic filtering + topics := make([]Topic, len(filter.matcher.conditions)) + for i, group := range filter.matcher.conditions { + // Message should contain a single topic entry, extract + for topics[i], _ = range group { + break + } + } + if !self.matcher.Matches(topics) { + return false + } + return true +} + +// Trigger is called when a filter successfully matches an inbound message. +func (self filterer) Trigger(data interface{}) { + self.fn(data) +} diff --git a/whisper/whisperv2/filter_test.go b/whisper/whisperv2/filter_test.go new file mode 100644 index 000000000..5a14a84bb --- /dev/null +++ b/whisper/whisperv2/filter_test.go @@ -0,0 +1,215 @@ +// Copyright 2015 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 whisperv2 + +import ( + "bytes" + + "testing" +) + +var filterTopicsCreationTests = []struct { + topics [][]string + filter [][][4]byte +}{ + { // Simple topic filter + topics: [][]string{ + {"abc", "def", "ghi"}, + {"def"}, + {"ghi", "abc"}, + }, + filter: [][][4]byte{ + {{0x4e, 0x03, 0x65, 0x7a}, {0x34, 0x60, 0x7c, 0x9b}, {0x21, 0x41, 0x7d, 0xf9}}, + {{0x34, 0x60, 0x7c, 0x9b}}, + {{0x21, 0x41, 0x7d, 0xf9}, {0x4e, 0x03, 0x65, 0x7a}}, + }, + }, + { // Wild-carded topic filter + topics: [][]string{ + {"abc", "def", "ghi"}, + {}, + {""}, + {"def"}, + }, + filter: [][][4]byte{ + {{0x4e, 0x03, 0x65, 0x7a}, {0x34, 0x60, 0x7c, 0x9b}, {0x21, 0x41, 0x7d, 0xf9}}, + {}, + {}, + {{0x34, 0x60, 0x7c, 0x9b}}, + }, + }, +} + +var filterTopicsCreationFlatTests = []struct { + topics []string + filter [][][4]byte +}{ + { // Simple topic list + topics: []string{"abc", "def", "ghi"}, + filter: [][][4]byte{ + {{0x4e, 0x03, 0x65, 0x7a}}, + {{0x34, 0x60, 0x7c, 0x9b}}, + {{0x21, 0x41, 0x7d, 0xf9}}, + }, + }, + { // Wild-carded topic list + topics: []string{"abc", "", "ghi"}, + filter: [][][4]byte{ + {{0x4e, 0x03, 0x65, 0x7a}}, + {}, + {{0x21, 0x41, 0x7d, 0xf9}}, + }, + }, +} + +func TestFilterTopicsCreation(t *testing.T) { + // Check full filter creation + for i, tt := range filterTopicsCreationTests { + // Check the textual creation + filter := NewFilterTopicsFromStrings(tt.topics...) + if len(filter) != len(tt.topics) { + t.Errorf("test %d: condition count mismatch: have %v, want %v", i, len(filter), len(tt.topics)) + continue + } + for j, condition := range filter { + if len(condition) != len(tt.filter[j]) { + t.Errorf("test %d, condition %d: size mismatch: have %v, want %v", i, j, len(condition), len(tt.filter[j])) + continue + } + for k := 0; k < len(condition); k++ { + if bytes.Compare(condition[k][:], tt.filter[j][k][:]) != 0 { + t.Errorf("test %d, condition %d, segment %d: filter mismatch: have 0x%x, want 0x%x", i, j, k, condition[k], tt.filter[j][k]) + } + } + } + // Check the binary creation + binary := make([][][]byte, len(tt.topics)) + for j, condition := range tt.topics { + binary[j] = make([][]byte, len(condition)) + for k, segment := range condition { + binary[j][k] = []byte(segment) + } + } + filter = NewFilterTopics(binary...) + if len(filter) != len(tt.topics) { + t.Errorf("test %d: condition count mismatch: have %v, want %v", i, len(filter), len(tt.topics)) + continue + } + for j, condition := range filter { + if len(condition) != len(tt.filter[j]) { + t.Errorf("test %d, condition %d: size mismatch: have %v, want %v", i, j, len(condition), len(tt.filter[j])) + continue + } + for k := 0; k < len(condition); k++ { + if bytes.Compare(condition[k][:], tt.filter[j][k][:]) != 0 { + t.Errorf("test %d, condition %d, segment %d: filter mismatch: have 0x%x, want 0x%x", i, j, k, condition[k], tt.filter[j][k]) + } + } + } + } + // Check flat filter creation + for i, tt := range filterTopicsCreationFlatTests { + // Check the textual creation + filter := NewFilterTopicsFromStringsFlat(tt.topics...) + if len(filter) != len(tt.topics) { + t.Errorf("test %d: condition count mismatch: have %v, want %v", i, len(filter), len(tt.topics)) + continue + } + for j, condition := range filter { + if len(condition) != len(tt.filter[j]) { + t.Errorf("test %d, condition %d: size mismatch: have %v, want %v", i, j, len(condition), len(tt.filter[j])) + continue + } + for k := 0; k < len(condition); k++ { + if bytes.Compare(condition[k][:], tt.filter[j][k][:]) != 0 { + t.Errorf("test %d, condition %d, segment %d: filter mismatch: have 0x%x, want 0x%x", i, j, k, condition[k], tt.filter[j][k]) + } + } + } + // Check the binary creation + binary := make([][]byte, len(tt.topics)) + for j, topic := range tt.topics { + binary[j] = []byte(topic) + } + filter = NewFilterTopicsFlat(binary...) + if len(filter) != len(tt.topics) { + t.Errorf("test %d: condition count mismatch: have %v, want %v", i, len(filter), len(tt.topics)) + continue + } + for j, condition := range filter { + if len(condition) != len(tt.filter[j]) { + t.Errorf("test %d, condition %d: size mismatch: have %v, want %v", i, j, len(condition), len(tt.filter[j])) + continue + } + for k := 0; k < len(condition); k++ { + if bytes.Compare(condition[k][:], tt.filter[j][k][:]) != 0 { + t.Errorf("test %d, condition %d, segment %d: filter mismatch: have 0x%x, want 0x%x", i, j, k, condition[k], tt.filter[j][k]) + } + } + } + } +} + +var filterCompareTests = []struct { + matcher filterer + message filterer + match bool +}{ + { // Wild-card filter matching anything + matcher: filterer{to: "", from: "", matcher: newTopicMatcher()}, + message: filterer{to: "to", from: "from", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, + match: true, + }, + { // Filter matching the to field + matcher: filterer{to: "to", from: "", matcher: newTopicMatcher()}, + message: filterer{to: "to", from: "from", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, + match: true, + }, + { // Filter rejecting the to field + matcher: filterer{to: "to", from: "", matcher: newTopicMatcher()}, + message: filterer{to: "", from: "from", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, + match: false, + }, + { // Filter matching the from field + matcher: filterer{to: "", from: "from", matcher: newTopicMatcher()}, + message: filterer{to: "to", from: "from", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, + match: true, + }, + { // Filter rejecting the from field + matcher: filterer{to: "", from: "from", matcher: newTopicMatcher()}, + message: filterer{to: "to", from: "", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, + match: false, + }, + { // Filter matching the topic field + matcher: filterer{to: "", from: "from", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, + message: filterer{to: "to", from: "from", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, + match: true, + }, + { // Filter rejecting the topic field + matcher: filterer{to: "", from: "", matcher: newTopicMatcher(NewFilterTopicsFromStringsFlat("topic")...)}, + message: filterer{to: "to", from: "from", matcher: newTopicMatcher()}, + match: false, + }, +} + +func TestFilterCompare(t *testing.T) { + for i, tt := range filterCompareTests { + if match := tt.matcher.Compare(tt.message); match != tt.match { + t.Errorf("test %d: match mismatch: have %v, want %v", i, match, tt.match) + } + } +} diff --git a/whisper/whisperv2/main.go b/whisper/whisperv2/main.go new file mode 100644 index 000000000..be4160489 --- /dev/null +++ b/whisper/whisperv2/main.go @@ -0,0 +1,106 @@ +// Copyright 2014 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 . + +// +build none + +// Contains a simple whisper peer setup and self messaging to allow playing +// around with the protocol and API without a fancy client implementation. + +package main + +import ( + "fmt" + "log" + "os" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/nat" + "github.com/ethereum/go-ethereum/whisper" +) + +func main() { + logger.AddLogSystem(logger.NewStdLogSystem(os.Stdout, log.LstdFlags, logger.InfoLevel)) + + // Generate the peer identity + key, err := crypto.GenerateKey() + if err != nil { + fmt.Printf("Failed to generate peer key: %v.\n", err) + os.Exit(-1) + } + name := common.MakeName("whisper-go", "1.0") + shh := whisper.New() + + // Create an Ethereum peer to communicate through + server := p2p.Server{ + PrivateKey: key, + MaxPeers: 10, + Name: name, + Protocols: []p2p.Protocol{shh.Protocol()}, + ListenAddr: ":30300", + NAT: nat.Any(), + } + fmt.Println("Starting Ethereum peer...") + if err := server.Start(); err != nil { + fmt.Printf("Failed to start Ethereum peer: %v.\n", err) + os.Exit(1) + } + + // Send a message to self to check that something works + payload := fmt.Sprintf("Hello world, this is %v. In case you're wondering, the time is %v", name, time.Now()) + if err := selfSend(shh, []byte(payload)); err != nil { + fmt.Printf("Failed to self message: %v.\n", err) + os.Exit(-1) + } +} + +// SendSelf wraps a payload into a Whisper envelope and forwards it to itself. +func selfSend(shh *whisper.Whisper, payload []byte) error { + ok := make(chan struct{}) + + // Start watching for self messages, output any arrivals + id := shh.NewIdentity() + shh.Watch(whisper.Filter{ + To: &id.PublicKey, + Fn: func(msg *whisper.Message) { + fmt.Printf("Message received: %s, signed with 0x%x.\n", string(msg.Payload), msg.Signature) + close(ok) + }, + }) + // Wrap the payload and encrypt it + msg := whisper.NewMessage(payload) + envelope, err := msg.Wrap(whisper.DefaultPoW, whisper.Options{ + From: id, + To: &id.PublicKey, + TTL: whisper.DefaultTTL, + }) + if err != nil { + return fmt.Errorf("failed to seal message: %v", err) + } + // Dump the message into the system and wait for it to pop back out + if err := shh.Send(envelope); err != nil { + return fmt.Errorf("failed to send self-message: %v", err) + } + select { + case <-ok: + case <-time.After(time.Second): + return fmt.Errorf("failed to receive message in time") + } + return nil +} diff --git a/whisper/whisperv2/message.go b/whisper/whisperv2/message.go new file mode 100644 index 000000000..7ef9d0912 --- /dev/null +++ b/whisper/whisperv2/message.go @@ -0,0 +1,156 @@ +// Copyright 2014 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. For formal details please see +// the specs at https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec#messages. + +package whisperv2 + +import ( + "crypto/ecdsa" + "math/rand" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" +) + +// Message 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 Message struct { + Flags byte // First bit is signature presence, rest reserved and should be random + Signature []byte + Payload []byte + + Sent time.Time // Time when the message was posted into the network + TTL time.Duration // Maximum time to live allowed for the message + + To *ecdsa.PublicKey // Message recipient (identity used to decode the message) + Hash common.Hash // Message envelope hash to act as a unique id +} + +// Options specifies the exact way a message should be wrapped into an Envelope. +type Options struct { + From *ecdsa.PrivateKey + To *ecdsa.PublicKey + TTL time.Duration + Topics []Topic +} + +// NewMessage creates and initializes a non-signed, non-encrypted Whisper message. +func NewMessage(payload []byte) *Message { + // Construct an initial flag set: no signature, rest random + flags := byte(rand.Intn(256)) + flags &= ^signatureFlag + + // Assemble and return the message + return &Message{ + Flags: flags, + Payload: payload, + Sent: time.Now(), + } +} + +// Wrap bundles the message into an Envelope to transmit over the network. +// +// pow (Proof Of Work) controls how much time to spend on hashing the message, +// inherently controlling its priority through the network (smaller hash, bigger +// priority). +// +// The user can control the amount of identity, privacy and encryption through +// the options parameter as follows: +// - options.From == nil && options.To == nil: anonymous broadcast +// - options.From != nil && options.To == nil: signed broadcast (known sender) +// - options.From == nil && options.To != nil: encrypted anonymous message +// - options.From != nil && options.To != nil: encrypted signed message +func (self *Message) Wrap(pow time.Duration, options Options) (*Envelope, error) { + // Use the default TTL if non was specified + if options.TTL == 0 { + options.TTL = DefaultTTL + } + self.TTL = options.TTL + + // Sign and encrypt the message if requested + if options.From != nil { + if err := self.sign(options.From); err != nil { + return nil, err + } + } + if options.To != nil { + if err := self.encrypt(options.To); err != nil { + return nil, err + } + } + // Wrap the processed message, seal it and return + envelope := NewEnvelope(options.TTL, options.Topics, self) + envelope.Seal(pow) + + return envelope, nil +} + +// sign calculates and sets the cryptographic signature for the message , also +// setting the sign flag. +func (self *Message) sign(key *ecdsa.PrivateKey) (err error) { + self.Flags |= signatureFlag + self.Signature, err = crypto.Sign(self.hash(), key) + return +} + +// Recover retrieves the public key of the message signer. +func (self *Message) Recover() *ecdsa.PublicKey { + defer func() { recover() }() // in case of invalid signature + + // Short circuit if no signature is present + if self.Signature == nil { + return nil + } + // Otherwise try and recover the signature + pub, err := crypto.SigToPub(self.hash(), self.Signature) + if err != nil { + glog.V(logger.Error).Infof("Could not get public key from signature: %v", err) + return nil + } + return pub +} + +// encrypt encrypts a message payload with a public key. +func (self *Message) encrypt(key *ecdsa.PublicKey) (err error) { + self.Payload, err = crypto.Encrypt(key, self.Payload) + return +} + +// decrypt decrypts an encrypted payload with a private key. +func (self *Message) decrypt(key *ecdsa.PrivateKey) error { + cleartext, err := crypto.Decrypt(key, self.Payload) + if err == nil { + self.Payload = cleartext + } + return err +} + +// hash calculates the SHA3 checksum of the message flags and payload. +func (self *Message) hash() []byte { + return crypto.Keccak256(append([]byte{self.Flags}, self.Payload...)) +} + +// bytes flattens the message contents (flags, signature and payload) into a +// single binary blob. +func (self *Message) bytes() []byte { + return append([]byte{self.Flags}, append(self.Signature, self.Payload...)...) +} diff --git a/whisper/whisperv2/message_test.go b/whisper/whisperv2/message_test.go new file mode 100644 index 000000000..efa64e431 --- /dev/null +++ b/whisper/whisperv2/message_test.go @@ -0,0 +1,159 @@ +// Copyright 2014 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 whisperv2 + +import ( + "bytes" + "crypto/elliptic" + "testing" + "time" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/secp256k1" +) + +// Tests whether a message can be wrapped without any identity or encryption. +func TestMessageSimpleWrap(t *testing.T) { + payload := []byte("hello world") + + msg := NewMessage(payload) + if _, err := msg.Wrap(DefaultPoW, Options{}); err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + if msg.Flags&signatureFlag != 0 { + t.Fatalf("signature flag mismatch: have %d, want %d", msg.Flags&signatureFlag, 0) + } + if len(msg.Signature) != 0 { + t.Fatalf("signature found for simple wrapping: 0x%x", msg.Signature) + } + if bytes.Compare(msg.Payload, payload) != 0 { + t.Fatalf("payload mismatch after wrapping: have 0x%x, want 0x%x", msg.Payload, payload) + } + if msg.TTL/time.Second != DefaultTTL/time.Second { + t.Fatalf("message TTL mismatch: have %v, want %v", msg.TTL, DefaultTTL) + } +} + +// Tests whether a message can be signed, and wrapped in plain-text. +func TestMessageCleartextSignRecover(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to create crypto key: %v", err) + } + payload := []byte("hello world") + + msg := NewMessage(payload) + if _, err := msg.Wrap(DefaultPoW, Options{ + From: key, + }); err != nil { + t.Fatalf("failed to sign message: %v", err) + } + if msg.Flags&signatureFlag != signatureFlag { + t.Fatalf("signature flag mismatch: have %d, want %d", msg.Flags&signatureFlag, signatureFlag) + } + if bytes.Compare(msg.Payload, payload) != 0 { + t.Fatalf("payload mismatch after signing: have 0x%x, want 0x%x", msg.Payload, payload) + } + + pubKey := msg.Recover() + if pubKey == nil { + t.Fatalf("failed to recover public key") + } + p1 := elliptic.Marshal(secp256k1.S256(), key.PublicKey.X, key.PublicKey.Y) + p2 := elliptic.Marshal(secp256k1.S256(), pubKey.X, pubKey.Y) + if !bytes.Equal(p1, p2) { + t.Fatalf("public key mismatch: have 0x%x, want 0x%x", p2, p1) + } +} + +// Tests whether a message can be encrypted and decrypted using an anonymous +// sender (i.e. no signature). +func TestMessageAnonymousEncryptDecrypt(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to create recipient crypto key: %v", err) + } + payload := []byte("hello world") + + msg := NewMessage(payload) + envelope, err := msg.Wrap(DefaultPoW, Options{ + To: &key.PublicKey, + }) + if err != nil { + t.Fatalf("failed to encrypt message: %v", err) + } + if msg.Flags&signatureFlag != 0 { + t.Fatalf("signature flag mismatch: have %d, want %d", msg.Flags&signatureFlag, 0) + } + if len(msg.Signature) != 0 { + t.Fatalf("signature found for anonymous message: 0x%x", msg.Signature) + } + + out, err := envelope.Open(key) + if err != nil { + t.Fatalf("failed to open encrypted message: %v", err) + } + if !bytes.Equal(out.Payload, payload) { + t.Errorf("payload mismatch: have 0x%x, want 0x%x", out.Payload, payload) + } +} + +// Tests whether a message can be properly signed and encrypted. +func TestMessageFullCrypto(t *testing.T) { + fromKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to create sender crypto key: %v", err) + } + toKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to create recipient crypto key: %v", err) + } + + payload := []byte("hello world") + msg := NewMessage(payload) + envelope, err := msg.Wrap(DefaultPoW, Options{ + From: fromKey, + To: &toKey.PublicKey, + }) + if err != nil { + t.Fatalf("failed to encrypt message: %v", err) + } + if msg.Flags&signatureFlag != signatureFlag { + t.Fatalf("signature flag mismatch: have %d, want %d", msg.Flags&signatureFlag, signatureFlag) + } + if len(msg.Signature) == 0 { + t.Fatalf("no signature found for signed message") + } + + out, err := envelope.Open(toKey) + if err != nil { + t.Fatalf("failed to open encrypted message: %v", err) + } + if !bytes.Equal(out.Payload, payload) { + t.Errorf("payload mismatch: have 0x%x, want 0x%x", out.Payload, payload) + } + + pubKey := out.Recover() + if pubKey == nil { + t.Fatalf("failed to recover public key") + } + p1 := elliptic.Marshal(secp256k1.S256(), fromKey.PublicKey.X, fromKey.PublicKey.Y) + p2 := elliptic.Marshal(secp256k1.S256(), pubKey.X, pubKey.Y) + if !bytes.Equal(p1, p2) { + t.Fatalf("public key mismatch: have 0x%x, want 0x%x", p2, p1) + } +} diff --git a/whisper/whisperv2/peer.go b/whisper/whisperv2/peer.go new file mode 100644 index 000000000..404ebd513 --- /dev/null +++ b/whisper/whisperv2/peer.go @@ -0,0 +1,175 @@ +// Copyright 2014 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 whisperv2 + +import ( + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rlp" + "gopkg.in/fatih/set.v0" +) + +// peer represents a whisper protocol peer connection. +type peer struct { + host *Whisper + peer *p2p.Peer + ws p2p.MsgReadWriter + + known *set.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, + known: set.New(), + quit: make(chan struct{}), + } +} + +// start initiates the peer updater, periodically broadcasting the whisper packets +// into the network. +func (self *peer) start() { + go self.update() + glog.V(logger.Debug).Infof("%v: whisper started", self.peer) +} + +// stop terminates the peer updater, stopping message forwarding to it. +func (self *peer) stop() { + close(self.quit) + glog.V(logger.Debug).Infof("%v: whisper stopped", self.peer) +} + +// handshake sends the protocol initiation status message to the remote peer and +// verifies the remote status too. +func (self *peer) handshake() error { + // Send the handshake status message asynchronously + errc := make(chan error, 1) + go func() { + errc <- p2p.SendItems(self.ws, statusCode, protocolVersion) + }() + // Fetch the remote status packet and verify protocol match + packet, err := self.ws.ReadMsg() + if err != nil { + return err + } + if packet.Code != statusCode { + return fmt.Errorf("peer sent %x before status packet", packet.Code) + } + s := rlp.NewStream(packet.Payload, uint64(packet.Size)) + if _, err := s.List(); err != nil { + return fmt.Errorf("bad status message: %v", err) + } + peerVersion, err := s.Uint() + if err != nil { + return fmt.Errorf("bad status message: %v", err) + } + if peerVersion != protocolVersion { + return fmt.Errorf("protocol version mismatch %d != %d", peerVersion, protocolVersion) + } + // Wait until out own status is consumed too + if err := <-errc; err != nil { + return fmt.Errorf("failed to send status packet: %v", err) + } + return nil +} + +// update executes periodic operations on the peer, including message transmission +// and expiration. +func (self *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: + self.expire() + + case <-transmit.C: + if err := self.broadcast(); err != nil { + glog.V(logger.Info).Infof("%v: broadcast failed: %v", self.peer, err) + return + } + + case <-self.quit: + return + } + } +} + +// mark marks an envelope known to the peer so that it won't be sent back. +func (self *peer) mark(envelope *Envelope) { + self.known.Add(envelope.Hash()) +} + +// marked checks if an envelope is already known to the remote peer. +func (self *peer) marked(envelope *Envelope) bool { + return self.known.Has(envelope.Hash()) +} + +// expire iterates over all the known envelopes in the host and removes all +// expired (unknown) ones from the known list. +func (self *peer) expire() { + // Assemble the list of available envelopes + available := set.NewNonTS() + for _, envelope := range self.host.envelopes() { + available.Add(envelope.Hash()) + } + // Cross reference availability with known status + unmark := make(map[common.Hash]struct{}) + self.known.Each(func(v interface{}) bool { + if !available.Has(v.(common.Hash)) { + unmark[v.(common.Hash)] = struct{}{} + } + return true + }) + // Dump all known but unavailable + for hash, _ := range unmark { + self.known.Remove(hash) + } +} + +// broadcast iterates over the collection of envelopes and transmits yet unknown +// ones over the network. +func (self *peer) broadcast() error { + // Fetch the envelopes and collect the unknown ones + envelopes := self.host.envelopes() + transmit := make([]*Envelope, 0, len(envelopes)) + for _, envelope := range envelopes { + if !self.marked(envelope) { + transmit = append(transmit, envelope) + self.mark(envelope) + } + } + // Transmit the unknown batch (potentially empty) + if err := p2p.Send(self.ws, messagesCode, transmit); err != nil { + return err + } + glog.V(logger.Detail).Infoln(self.peer, "broadcasted", len(transmit), "message(s)") + return nil +} diff --git a/whisper/whisperv2/peer_test.go b/whisper/whisperv2/peer_test.go new file mode 100644 index 000000000..9755e134c --- /dev/null +++ b/whisper/whisperv2/peer_test.go @@ -0,0 +1,261 @@ +// Copyright 2015 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 whisperv2 + +import ( + "testing" + "time" + + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/discover" +) + +type testPeer struct { + client *Whisper + stream *p2p.MsgPipeRW + termed chan struct{} +} + +func startTestPeer() *testPeer { + // Create a simulated P2P remote peer and data streams to it + remote := p2p.NewPeer(discover.NodeID{}, "", nil) + tester, tested := p2p.MsgPipe() + + // Create a whisper client and connect with it to the tester peer + client := New() + client.Start(nil) + + termed := make(chan struct{}) + go func() { + defer client.Stop() + defer close(termed) + defer tested.Close() + + client.handlePeer(remote, tested) + }() + + return &testPeer{ + client: client, + stream: tester, + termed: termed, + } +} + +func startTestPeerInited() (*testPeer, error) { + peer := startTestPeer() + + if err := p2p.ExpectMsg(peer.stream, statusCode, []uint64{protocolVersion}); err != nil { + peer.stream.Close() + return nil, err + } + if err := p2p.SendItems(peer.stream, statusCode, protocolVersion); err != nil { + peer.stream.Close() + return nil, err + } + return peer, nil +} + +func TestPeerStatusMessage(t *testing.T) { + tester := startTestPeer() + + // Wait for the handshake status message and check it + if err := p2p.ExpectMsg(tester.stream, statusCode, []uint64{protocolVersion}); err != nil { + t.Fatalf("status message mismatch: %v", err) + } + // Terminate the node + tester.stream.Close() + + select { + case <-tester.termed: + case <-time.After(time.Second): + t.Fatalf("local close timed out") + } +} + +func TestPeerHandshakeFail(t *testing.T) { + tester := startTestPeer() + + // Wait for and check the handshake + if err := p2p.ExpectMsg(tester.stream, statusCode, []uint64{protocolVersion}); err != nil { + t.Fatalf("status message mismatch: %v", err) + } + // Send an invalid handshake status and verify disconnect + if err := p2p.SendItems(tester.stream, messagesCode); err != nil { + t.Fatalf("failed to send malformed status: %v", err) + } + select { + case <-tester.termed: + case <-time.After(time.Second): + t.Fatalf("remote close timed out") + } +} + +func TestPeerHandshakeSuccess(t *testing.T) { + tester := startTestPeer() + + // Wait for and check the handshake + if err := p2p.ExpectMsg(tester.stream, statusCode, []uint64{protocolVersion}); err != nil { + t.Fatalf("status message mismatch: %v", err) + } + // Send a valid handshake status and make sure connection stays live + if err := p2p.SendItems(tester.stream, statusCode, protocolVersion); err != nil { + t.Fatalf("failed to send status: %v", err) + } + select { + case <-tester.termed: + t.Fatalf("valid handshake disconnected") + + case <-time.After(100 * time.Millisecond): + } + // Clean up the test + tester.stream.Close() + + select { + case <-tester.termed: + case <-time.After(time.Second): + t.Fatalf("local close timed out") + } +} + +func TestPeerSend(t *testing.T) { + // Start a tester and execute the handshake + tester, err := startTestPeerInited() + if err != nil { + t.Fatalf("failed to start initialized peer: %v", err) + } + defer tester.stream.Close() + + // Construct a message and inject into the tester + message := NewMessage([]byte("peer broadcast test message")) + envelope, err := message.Wrap(DefaultPoW, Options{ + TTL: DefaultTTL, + }) + if err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + if err := tester.client.Send(envelope); err != nil { + t.Fatalf("failed to send message: %v", err) + } + // Check that the message is eventually forwarded + payload := []interface{}{envelope} + if err := p2p.ExpectMsg(tester.stream, messagesCode, payload); err != nil { + t.Fatalf("message mismatch: %v", err) + } + // Make sure that even with a re-insert, an empty batch is received + if err := tester.client.Send(envelope); err != nil { + t.Fatalf("failed to send message: %v", err) + } + if err := p2p.ExpectMsg(tester.stream, messagesCode, []interface{}{}); err != nil { + t.Fatalf("message mismatch: %v", err) + } +} + +func TestPeerDeliver(t *testing.T) { + // Start a tester and execute the handshake + tester, err := startTestPeerInited() + if err != nil { + t.Fatalf("failed to start initialized peer: %v", err) + } + defer tester.stream.Close() + + // Watch for all inbound messages + arrived := make(chan struct{}, 1) + tester.client.Watch(Filter{ + Fn: func(message *Message) { + arrived <- struct{}{} + }, + }) + // Construct a message and deliver it to the tester peer + message := NewMessage([]byte("peer broadcast test message")) + envelope, err := message.Wrap(DefaultPoW, Options{ + TTL: DefaultTTL, + }) + if err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + if err := p2p.Send(tester.stream, messagesCode, []*Envelope{envelope}); err != nil { + t.Fatalf("failed to transfer message: %v", err) + } + // Check that the message is delivered upstream + select { + case <-arrived: + case <-time.After(time.Second): + t.Fatalf("message delivery timeout") + } + // Check that a resend is not delivered + if err := p2p.Send(tester.stream, messagesCode, []*Envelope{envelope}); err != nil { + t.Fatalf("failed to transfer message: %v", err) + } + select { + case <-time.After(2 * transmissionCycle): + case <-arrived: + t.Fatalf("repeating message arrived") + } +} + +func TestPeerMessageExpiration(t *testing.T) { + // Start a tester and execute the handshake + tester, err := startTestPeerInited() + if err != nil { + t.Fatalf("failed to start initialized peer: %v", err) + } + defer tester.stream.Close() + + // Fetch the peer instance for later inspection + tester.client.peerMu.RLock() + if peers := len(tester.client.peers); peers != 1 { + t.Fatalf("peer pool size mismatch: have %v, want %v", peers, 1) + } + var peer *peer + for peer, _ = range tester.client.peers { + break + } + tester.client.peerMu.RUnlock() + + // Construct a message and pass it through the tester + message := NewMessage([]byte("peer test message")) + envelope, err := message.Wrap(DefaultPoW, Options{ + TTL: time.Second, + }) + if err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + if err := tester.client.Send(envelope); err != nil { + t.Fatalf("failed to send message: %v", err) + } + payload := []interface{}{envelope} + if err := p2p.ExpectMsg(tester.stream, messagesCode, payload); err != nil { + // A premature empty message may have been broadcast, check the next too + if err := p2p.ExpectMsg(tester.stream, messagesCode, payload); err != nil { + t.Fatalf("message mismatch: %v", err) + } + } + // Check that the message is inside the cache + if !peer.known.Has(envelope.Hash()) { + t.Fatalf("message not found in cache") + } + // Discard messages until expiration and check cache again + exp := time.Now().Add(time.Second + 2*expirationCycle + 100*time.Millisecond) + for time.Now().Before(exp) { + if err := p2p.ExpectMsg(tester.stream, messagesCode, []interface{}{}); err != nil { + t.Fatalf("message mismatch: %v", err) + } + } + if peer.known.Has(envelope.Hash()) { + t.Fatalf("message not expired from cache") + } +} diff --git a/whisper/whisperv2/topic.go b/whisper/whisperv2/topic.go new file mode 100644 index 000000000..3e2b47bd3 --- /dev/null +++ b/whisper/whisperv2/topic.go @@ -0,0 +1,140 @@ +// Copyright 2015 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. For formal details please see +// the specs at https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec#topics. + +package whisperv2 + +import "github.com/ethereum/go-ethereum/crypto" + +// Topic 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 Topic [4]byte + +// NewTopic creates a topic from the 4 byte prefix of the SHA3 hash of the data. +// +// Note, empty topics are considered the wildcard, and cannot be used in messages. +func NewTopic(data []byte) Topic { + prefix := [4]byte{} + copy(prefix[:], crypto.Keccak256(data)[:4]) + return Topic(prefix) +} + +// NewTopics creates a list of topics from a list of binary data elements, by +// iteratively calling NewTopic on each of them. +func NewTopics(data ...[]byte) []Topic { + topics := make([]Topic, len(data)) + for i, element := range data { + topics[i] = NewTopic(element) + } + return topics +} + +// NewTopicFromString creates a topic using the binary data contents of the +// specified string. +func NewTopicFromString(data string) Topic { + return NewTopic([]byte(data)) +} + +// NewTopicsFromStrings creates a list of topics from a list of textual data +// elements, by iteratively calling NewTopicFromString on each of them. +func NewTopicsFromStrings(data ...string) []Topic { + topics := make([]Topic, len(data)) + for i, element := range data { + topics[i] = NewTopicFromString(element) + } + return topics +} + +// String converts a topic byte array to a string representation. +func (self *Topic) String() string { + return string(self[:]) +} + +// topicMatcher is a filter expression to verify if a list of topics contained +// in an arriving message matches some topic conditions. The topic matcher is +// built up of a list of conditions, each of which must be satisfied by the +// corresponding topic in the message. Each condition may require: a) an exact +// topic match; b) a match from a set of topics; or c) a wild-card matching all. +// +// If a message contains more topics than required by the matcher, those beyond +// the condition count are ignored and assumed to match. +// +// Consider the following sample topic matcher: +// sample := { +// {TopicA1, TopicA2, TopicA3}, +// {TopicB}, +// nil, +// {TopicD1, TopicD2} +// } +// In order for a message to pass this filter, it should enumerate at least 4 +// topics, the first any of [TopicA1, TopicA2, TopicA3], the second mandatory +// "TopicB", the third is ignored by the filter and the fourth either "TopicD1" +// or "TopicD2". If the message contains further topics, the filter will match +// them too. +type topicMatcher struct { + conditions []map[Topic]struct{} +} + +// newTopicMatcher create a topic matcher from a list of topic conditions. +func newTopicMatcher(topics ...[]Topic) *topicMatcher { + matcher := make([]map[Topic]struct{}, len(topics)) + for i, condition := range topics { + matcher[i] = make(map[Topic]struct{}) + for _, topic := range condition { + matcher[i][topic] = struct{}{} + } + } + return &topicMatcher{conditions: matcher} +} + +// newTopicMatcherFromBinary create a topic matcher from a list of binary conditions. +func newTopicMatcherFromBinary(data ...[][]byte) *topicMatcher { + topics := make([][]Topic, len(data)) + for i, condition := range data { + topics[i] = NewTopics(condition...) + } + return newTopicMatcher(topics...) +} + +// newTopicMatcherFromStrings creates a topic matcher from a list of textual +// conditions. +func newTopicMatcherFromStrings(data ...[]string) *topicMatcher { + topics := make([][]Topic, len(data)) + for i, condition := range data { + topics[i] = NewTopicsFromStrings(condition...) + } + return newTopicMatcher(topics...) +} + +// Matches checks if a list of topics matches this particular condition set. +func (self *topicMatcher) Matches(topics []Topic) bool { + // Mismatch if there aren't enough topics + if len(self.conditions) > len(topics) { + return false + } + // Check each topic condition for existence (skip wild-cards) + for i := 0; i < len(topics) && i < len(self.conditions); i++ { + if len(self.conditions[i]) > 0 { + if _, ok := self.conditions[i][topics[i]]; !ok { + return false + } + } + } + return true +} diff --git a/whisper/whisperv2/topic_test.go b/whisper/whisperv2/topic_test.go new file mode 100644 index 000000000..efd4a2c61 --- /dev/null +++ b/whisper/whisperv2/topic_test.go @@ -0,0 +1,215 @@ +// Copyright 2015 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 whisperv2 + +import ( + "bytes" + "testing" +) + +var topicCreationTests = []struct { + data []byte + hash [4]byte +}{ + {hash: [4]byte{0x8f, 0x9a, 0x2b, 0x7d}, data: []byte("test name")}, + {hash: [4]byte{0xf2, 0x6e, 0x77, 0x79}, data: []byte("some other test")}, +} + +func TestTopicCreation(t *testing.T) { + // Create the topics individually + for i, tt := range topicCreationTests { + topic := NewTopic(tt.data) + if bytes.Compare(topic[:], tt.hash[:]) != 0 { + t.Errorf("binary test %d: hash mismatch: have %v, want %v.", i, topic, tt.hash) + } + } + for i, tt := range topicCreationTests { + topic := NewTopicFromString(string(tt.data)) + if bytes.Compare(topic[:], tt.hash[:]) != 0 { + t.Errorf("textual test %d: hash mismatch: have %v, want %v.", i, topic, tt.hash) + } + } + // Create the topics in batches + binaryData := make([][]byte, len(topicCreationTests)) + for i, tt := range topicCreationTests { + binaryData[i] = tt.data + } + textualData := make([]string, len(topicCreationTests)) + for i, tt := range topicCreationTests { + textualData[i] = string(tt.data) + } + + topics := NewTopics(binaryData...) + for i, tt := range topicCreationTests { + if bytes.Compare(topics[i][:], tt.hash[:]) != 0 { + t.Errorf("binary batch test %d: hash mismatch: have %v, want %v.", i, topics[i], tt.hash) + } + } + topics = NewTopicsFromStrings(textualData...) + for i, tt := range topicCreationTests { + if bytes.Compare(topics[i][:], tt.hash[:]) != 0 { + t.Errorf("textual batch test %d: hash mismatch: have %v, want %v.", i, topics[i], tt.hash) + } + } +} + +var topicMatcherCreationTest = struct { + binary [][][]byte + textual [][]string + matcher []map[[4]byte]struct{} +}{ + binary: [][][]byte{ + [][]byte{}, + [][]byte{ + []byte("Topic A"), + }, + [][]byte{ + []byte("Topic B1"), + []byte("Topic B2"), + []byte("Topic B3"), + }, + }, + textual: [][]string{ + []string{}, + []string{"Topic A"}, + []string{"Topic B1", "Topic B2", "Topic B3"}, + }, + matcher: []map[[4]byte]struct{}{ + map[[4]byte]struct{}{}, + map[[4]byte]struct{}{ + [4]byte{0x25, 0xfc, 0x95, 0x66}: struct{}{}, + }, + map[[4]byte]struct{}{ + [4]byte{0x93, 0x6d, 0xec, 0x09}: struct{}{}, + [4]byte{0x25, 0x23, 0x34, 0xd3}: struct{}{}, + [4]byte{0x6b, 0xc2, 0x73, 0xd1}: struct{}{}, + }, + }, +} + +func TestTopicMatcherCreation(t *testing.T) { + test := topicMatcherCreationTest + + matcher := newTopicMatcherFromBinary(test.binary...) + for i, cond := range matcher.conditions { + for topic, _ := range cond { + if _, ok := test.matcher[i][topic]; !ok { + t.Errorf("condition %d; extra topic found: 0x%x", i, topic[:]) + } + } + } + for i, cond := range test.matcher { + for topic, _ := range cond { + if _, ok := matcher.conditions[i][topic]; !ok { + t.Errorf("condition %d; topic not found: 0x%x", i, topic[:]) + } + } + } + + matcher = newTopicMatcherFromStrings(test.textual...) + for i, cond := range matcher.conditions { + for topic, _ := range cond { + if _, ok := test.matcher[i][topic]; !ok { + t.Errorf("condition %d; extra topic found: 0x%x", i, topic[:]) + } + } + } + for i, cond := range test.matcher { + for topic, _ := range cond { + if _, ok := matcher.conditions[i][topic]; !ok { + t.Errorf("condition %d; topic not found: 0x%x", i, topic[:]) + } + } + } +} + +var topicMatcherTests = []struct { + filter [][]string + topics []string + match bool +}{ + // Empty topic matcher should match everything + { + filter: [][]string{}, + topics: []string{}, + match: true, + }, + { + filter: [][]string{}, + topics: []string{"a", "b", "c"}, + match: true, + }, + // Fixed topic matcher should match strictly, but only prefix + { + filter: [][]string{[]string{"a"}, []string{"b"}}, + topics: []string{"a"}, + match: false, + }, + { + filter: [][]string{[]string{"a"}, []string{"b"}}, + topics: []string{"a", "b"}, + match: true, + }, + { + filter: [][]string{[]string{"a"}, []string{"b"}}, + topics: []string{"a", "b", "c"}, + match: true, + }, + // Multi-matcher should match any from a sub-group + { + filter: [][]string{[]string{"a1", "a2"}}, + topics: []string{"a"}, + match: false, + }, + { + filter: [][]string{[]string{"a1", "a2"}}, + topics: []string{"a1"}, + match: true, + }, + { + filter: [][]string{[]string{"a1", "a2"}}, + topics: []string{"a2"}, + match: true, + }, + // Wild-card condition should match anything + { + filter: [][]string{[]string{}, []string{"b"}}, + topics: []string{"a"}, + match: false, + }, + { + filter: [][]string{[]string{}, []string{"b"}}, + topics: []string{"a", "b"}, + match: true, + }, + { + filter: [][]string{[]string{}, []string{"b"}}, + topics: []string{"b", "b"}, + match: true, + }, +} + +func TestTopicMatcher(t *testing.T) { + for i, tt := range topicMatcherTests { + topics := NewTopicsFromStrings(tt.topics...) + + matcher := newTopicMatcherFromStrings(tt.filter...) + if match := matcher.Matches(topics); match != tt.match { + t.Errorf("test %d: match mismatch: have %v, want %v", i, match, tt.match) + } + } +} diff --git a/whisper/whisperv2/whisper.go b/whisper/whisperv2/whisper.go new file mode 100644 index 000000000..d9054959e --- /dev/null +++ b/whisper/whisperv2/whisper.go @@ -0,0 +1,378 @@ +// Copyright 2014 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 whisperv2 + +import ( + "crypto/ecdsa" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/ethereum/go-ethereum/event/filter" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rpc" + + "gopkg.in/fatih/set.v0" +) + +const ( + statusCode = 0x00 + messagesCode = 0x01 + + protocolVersion uint64 = 0x02 + protocolName = "shh" + + signatureFlag = byte(1 << 7) + signatureLength = 65 + + expirationCycle = 800 * time.Millisecond + transmissionCycle = 300 * time.Millisecond +) + +const ( + DefaultTTL = 50 * time.Second + DefaultPoW = 50 * time.Millisecond +) + +type MessageEvent struct { + To *ecdsa.PrivateKey + From *ecdsa.PublicKey + Message *Message +} + +// Whisper represents a dark communication interface through the Ethereum +// network, using its very own P2P communication layer. +type Whisper struct { + protocol p2p.Protocol + filters *filter.Filters + + keys map[string]*ecdsa.PrivateKey + + messages map[common.Hash]*Envelope // Pool of messages currently tracked by this node + expirations map[uint32]*set.SetNonTS // Message expiration pool (TODO: something lighter) + poolMu sync.RWMutex // Mutex to sync the message and expiration pools + + peers map[*peer]struct{} // Set of currently active peers + peerMu sync.RWMutex // Mutex to sync the active peer set + + quit chan struct{} +} + +// New creates a Whisper client ready to communicate through the Ethereum P2P +// network. +func New() *Whisper { + whisper := &Whisper{ + filters: filter.New(), + keys: make(map[string]*ecdsa.PrivateKey), + messages: make(map[common.Hash]*Envelope), + expirations: make(map[uint32]*set.SetNonTS), + peers: make(map[*peer]struct{}), + quit: make(chan struct{}), + } + whisper.filters.Start() + + // p2p whisper sub protocol handler + whisper.protocol = p2p.Protocol{ + Name: protocolName, + Version: uint(protocolVersion), + Length: 2, + Run: whisper.handlePeer, + } + + return whisper +} + +// APIs returns the RPC descriptors the Whisper implementation offers +func (s *Whisper) APIs() []rpc.API { + return []rpc.API{ + { + Namespace: "shh", + Version: "1.0", + Service: NewPublicWhisperAPI(s), + Public: true, + }, + } +} + +// Protocols returns the whisper sub-protocols ran by this particular client. +func (self *Whisper) Protocols() []p2p.Protocol { + return []p2p.Protocol{self.protocol} +} + +// Version returns the whisper sub-protocols version number. +func (self *Whisper) Version() uint { + return self.protocol.Version +} + +// NewIdentity generates a new cryptographic identity for the client, and injects +// it into the known identities for message decryption. +func (self *Whisper) NewIdentity() *ecdsa.PrivateKey { + key, err := crypto.GenerateKey() + if err != nil { + panic(err) + } + self.keys[string(crypto.FromECDSAPub(&key.PublicKey))] = key + + return key +} + +// HasIdentity checks if the the whisper node is configured with the private key +// of the specified public pair. +func (self *Whisper) HasIdentity(key *ecdsa.PublicKey) bool { + return self.keys[string(crypto.FromECDSAPub(key))] != nil +} + +// GetIdentity retrieves the private key of the specified public identity. +func (self *Whisper) GetIdentity(key *ecdsa.PublicKey) *ecdsa.PrivateKey { + return self.keys[string(crypto.FromECDSAPub(key))] +} + +// Watch installs a new message handler to run in case a matching packet arrives +// from the whisper network. +func (self *Whisper) Watch(options Filter) int { + filter := filterer{ + to: string(crypto.FromECDSAPub(options.To)), + from: string(crypto.FromECDSAPub(options.From)), + matcher: newTopicMatcher(options.Topics...), + fn: func(data interface{}) { + options.Fn(data.(*Message)) + }, + } + return self.filters.Install(filter) +} + +// Unwatch removes an installed message handler. +func (self *Whisper) Unwatch(id int) { + self.filters.Uninstall(id) +} + +// Send injects a message into the whisper send queue, to be distributed in the +// network in the coming cycles. +func (self *Whisper) Send(envelope *Envelope) error { + return self.add(envelope) +} + +// Start implements node.Service, starting the background data propagation thread +// of the Whisper protocol. +func (self *Whisper) Start(*p2p.Server) error { + glog.V(logger.Info).Infoln("Whisper started") + go self.update() + return nil +} + +// Stop implements node.Service, stopping the background data propagation thread +// of the Whisper protocol. +func (self *Whisper) Stop() error { + close(self.quit) + glog.V(logger.Info).Infoln("Whisper stopped") + return nil +} + +// Messages retrieves all the currently pooled messages matching a filter id. +func (self *Whisper) Messages(id int) []*Message { + messages := make([]*Message, 0) + if filter := self.filters.Get(id); filter != nil { + for _, envelope := range self.messages { + if message := self.open(envelope); message != nil { + if self.filters.Match(filter, createFilter(message, envelope.Topics)) { + messages = append(messages, message) + } + } + } + } + return messages +} + +// handlePeer is called by the underlying P2P layer when the whisper sub-protocol +// connection is negotiated. +func (self *Whisper) handlePeer(peer *p2p.Peer, rw p2p.MsgReadWriter) error { + // Create the new peer and start tracking it + whisperPeer := newPeer(self, peer, rw) + + self.peerMu.Lock() + self.peers[whisperPeer] = struct{}{} + self.peerMu.Unlock() + + defer func() { + self.peerMu.Lock() + delete(self.peers, whisperPeer) + self.peerMu.Unlock() + }() + + // Run the peer handshake and state updates + if err := whisperPeer.handshake(); err != nil { + return err + } + whisperPeer.start() + defer whisperPeer.stop() + + // Read and process inbound messages directly to merge into client-global state + for { + // Fetch the next packet and decode the contained envelopes + packet, err := rw.ReadMsg() + if err != nil { + return err + } + var envelopes []*Envelope + if err := packet.Decode(&envelopes); err != nil { + glog.V(logger.Info).Infof("%v: failed to decode envelope: %v", peer, err) + continue + } + // Inject all envelopes into the internal pool + for _, envelope := range envelopes { + if err := self.add(envelope); err != nil { + // TODO Punish peer here. Invalid envelope. + glog.V(logger.Debug).Infof("%v: failed to pool envelope: %v", peer, err) + } + whisperPeer.mark(envelope) + } + } +} + +// 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. +func (self *Whisper) add(envelope *Envelope) error { + self.poolMu.Lock() + defer self.poolMu.Unlock() + + // short circuit when a received envelope has already expired + if envelope.Expiry < uint32(time.Now().Unix()) { + return nil + } + + // Insert the message into the tracked pool + hash := envelope.Hash() + if _, ok := self.messages[hash]; ok { + glog.V(logger.Detail).Infof("whisper envelope already cached: %x\n", envelope) + return nil + } + self.messages[hash] = envelope + + // Insert the message into the expiration pool for later removal + if self.expirations[envelope.Expiry] == nil { + self.expirations[envelope.Expiry] = set.NewNonTS() + } + if !self.expirations[envelope.Expiry].Has(hash) { + self.expirations[envelope.Expiry].Add(hash) + + // Notify the local node of a message arrival + go self.postEvent(envelope) + } + glog.V(logger.Detail).Infof("cached whisper envelope %x\n", envelope) + return nil +} + +// postEvent opens an envelope with the configured identities and delivers the +// message upstream from application processing. +func (self *Whisper) postEvent(envelope *Envelope) { + if message := self.open(envelope); message != nil { + self.filters.Notify(createFilter(message, envelope.Topics), message) + } +} + +// open tries to decrypt a whisper envelope with all the configured identities, +// returning the decrypted message and the key used to achieve it. If not keys +// are configured, open will return the payload as if non encrypted. +func (self *Whisper) open(envelope *Envelope) *Message { + // Short circuit if no identity is set, and assume clear-text + if len(self.keys) == 0 { + if message, err := envelope.Open(nil); err == nil { + return message + } + } + // Iterate over the keys and try to decrypt the message + for _, key := range self.keys { + message, err := envelope.Open(key) + if err == nil { + message.To = &key.PublicKey + return message + } else if err == ecies.ErrInvalidPublicKey { + return message + } + } + // Failed to decrypt, don't return anything + return nil +} + +// createFilter creates a message filter to check against installed handlers. +func createFilter(message *Message, topics []Topic) filter.Filter { + matcher := make([][]Topic, len(topics)) + for i, topic := range topics { + matcher[i] = []Topic{topic} + } + return filterer{ + to: string(crypto.FromECDSAPub(message.To)), + from: string(crypto.FromECDSAPub(message.Recover())), + matcher: newTopicMatcher(matcher...), + } +} + +// update loops until the lifetime of the whisper node, updating its internal +// state by expiring stale messages from the pool. +func (self *Whisper) update() { + // Start a ticker to check for expirations + expire := time.NewTicker(expirationCycle) + + // Repeat updates until termination is requested + for { + select { + case <-expire.C: + self.expire() + + case <-self.quit: + return + } + } +} + +// expire iterates over all the expiration timestamps, removing all stale +// messages from the pools. +func (self *Whisper) expire() { + self.poolMu.Lock() + defer self.poolMu.Unlock() + + now := uint32(time.Now().Unix()) + for then, hashSet := range self.expirations { + // Short circuit if a future time + if then > now { + continue + } + // Dump all expired messages and remove timestamp + hashSet.Each(func(v interface{}) bool { + delete(self.messages, v.(common.Hash)) + return true + }) + self.expirations[then].Clear() + } +} + +// envelopes retrieves all the messages currently pooled by the node. +func (self *Whisper) envelopes() []*Envelope { + self.poolMu.RLock() + defer self.poolMu.RUnlock() + + envelopes := make([]*Envelope, 0, len(self.messages)) + for _, envelope := range self.messages { + envelopes = append(envelopes, envelope) + } + return envelopes +} diff --git a/whisper/whisperv2/whisper_test.go b/whisper/whisperv2/whisper_test.go new file mode 100644 index 000000000..1e0d3f85d --- /dev/null +++ b/whisper/whisperv2/whisper_test.go @@ -0,0 +1,216 @@ +// Copyright 2014 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 whisperv2 + +import ( + "testing" + "time" + + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/discover" +) + +func startTestCluster(n int) []*Whisper { + // Create the batch of simulated peers + nodes := make([]*p2p.Peer, n) + for i := 0; i < n; i++ { + nodes[i] = p2p.NewPeer(discover.NodeID{}, "", nil) + } + whispers := make([]*Whisper, n) + for i := 0; i < n; i++ { + whispers[i] = New() + whispers[i].Start(nil) + } + // Wire all the peers to the root one + for i := 1; i < n; i++ { + src, dst := p2p.MsgPipe() + + go whispers[0].handlePeer(nodes[i], src) + go whispers[i].handlePeer(nodes[0], dst) + } + return whispers +} + +func TestSelfMessage(t *testing.T) { + // Start the single node cluster + client := startTestCluster(1)[0] + + // Start watching for self messages, signal any arrivals + self := client.NewIdentity() + done := make(chan struct{}) + + client.Watch(Filter{ + To: &self.PublicKey, + Fn: func(msg *Message) { + close(done) + }, + }) + // Send a dummy message to oneself + msg := NewMessage([]byte("self whisper")) + envelope, err := msg.Wrap(DefaultPoW, Options{ + From: self, + To: &self.PublicKey, + TTL: DefaultTTL, + }) + if err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + // Dump the message into the system and wait for it to pop back out + if err := client.Send(envelope); err != nil { + t.Fatalf("failed to send self-message: %v", err) + } + select { + case <-done: + case <-time.After(time.Second): + t.Fatalf("self-message receive timeout") + } +} + +func TestDirectMessage(t *testing.T) { + // Start the sender-recipient cluster + cluster := startTestCluster(2) + + sender := cluster[0] + senderId := sender.NewIdentity() + + recipient := cluster[1] + recipientId := recipient.NewIdentity() + + // Watch for arriving messages on the recipient + done := make(chan struct{}) + recipient.Watch(Filter{ + To: &recipientId.PublicKey, + Fn: func(msg *Message) { + close(done) + }, + }) + // Send a dummy message from the sender + msg := NewMessage([]byte("direct whisper")) + envelope, err := msg.Wrap(DefaultPoW, Options{ + From: senderId, + To: &recipientId.PublicKey, + TTL: DefaultTTL, + }) + if err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + if err := sender.Send(envelope); err != nil { + t.Fatalf("failed to send direct message: %v", err) + } + // Wait for an arrival or a timeout + select { + case <-done: + case <-time.After(time.Second): + t.Fatalf("direct message receive timeout") + } +} + +func TestAnonymousBroadcast(t *testing.T) { + testBroadcast(true, t) +} + +func TestIdentifiedBroadcast(t *testing.T) { + testBroadcast(false, t) +} + +func testBroadcast(anonymous bool, t *testing.T) { + // Start the single sender multi recipient cluster + cluster := startTestCluster(3) + + sender := cluster[1] + targets := cluster[1:] + for _, target := range targets { + if !anonymous { + target.NewIdentity() + } + } + // Watch for arriving messages on the recipients + dones := make([]chan struct{}, len(targets)) + for i := 0; i < len(targets); i++ { + done := make(chan struct{}) // need for the closure + dones[i] = done + + targets[i].Watch(Filter{ + Topics: NewFilterTopicsFromStringsFlat("broadcast topic"), + Fn: func(msg *Message) { + close(done) + }, + }) + } + // Send a dummy message from the sender + msg := NewMessage([]byte("broadcast whisper")) + envelope, err := msg.Wrap(DefaultPoW, Options{ + Topics: NewTopicsFromStrings("broadcast topic"), + TTL: DefaultTTL, + }) + if err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + if err := sender.Send(envelope); err != nil { + t.Fatalf("failed to send broadcast message: %v", err) + } + // Wait for an arrival on each recipient, or timeouts + timeout := time.After(time.Second) + for _, done := range dones { + select { + case <-done: + case <-timeout: + t.Fatalf("broadcast message receive timeout") + } + } +} + +func TestMessageExpiration(t *testing.T) { + // Start the single node cluster and inject a dummy message + node := startTestCluster(1)[0] + + message := NewMessage([]byte("expiring message")) + envelope, err := message.Wrap(DefaultPoW, Options{TTL: time.Second}) + if err != nil { + t.Fatalf("failed to wrap message: %v", err) + } + if err := node.Send(envelope); err != nil { + t.Fatalf("failed to inject message: %v", err) + } + // Check that the message is inside the cache + node.poolMu.RLock() + _, found := node.messages[envelope.Hash()] + node.poolMu.RUnlock() + + if !found { + t.Fatalf("message not found in cache") + } + // Wait for expiration and check cache again + time.Sleep(time.Second) // wait for expiration + time.Sleep(2 * expirationCycle) // wait for cleanup cycle + + node.poolMu.RLock() + _, found = node.messages[envelope.Hash()] + node.poolMu.RUnlock() + if found { + t.Fatalf("message not expired from cache") + } + + // Check that adding an expired envelope doesn't do anything. + node.add(envelope) + node.poolMu.RLock() + _, found = node.messages[envelope.Hash()] + node.poolMu.RUnlock() + if found { + t.Fatalf("message was added to cache") + } +} diff --git a/whisper/whisperv5/benchmarks_test.go b/whisper/whisperv5/benchmarks_test.go new file mode 100644 index 000000000..8bb6c0574 --- /dev/null +++ b/whisper/whisperv5/benchmarks_test.go @@ -0,0 +1,202 @@ +// 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 whisperv5 + +import ( + "testing" + + "github.com/ethereum/go-ethereum/crypto" +) + +func BenchmarkDeriveKeyMaterial(b *testing.B) { + for i := 0; i < b.N; i++ { + deriveKeyMaterial([]byte("test"), 0) + } +} + +func BenchmarkDeriveOneTimeKey(b *testing.B) { + for i := 0; i < b.N; i++ { + DeriveOneTimeKey([]byte("test value 1"), []byte("test value 2"), 0) + } +} + +//func TestEncryptionSym(b *testing.T) { +func BenchmarkEncryptionSym(b *testing.B) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + b.Errorf("failed generateMessageParams with seed %d: %s.", seed, err) + return + } + + 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.Errorf("failed generateMessageParams with seed %d: %s.", seed, err) + return + } + key, err := crypto.GenerateKey() + if err != nil { + b.Errorf("failed GenerateKey with seed %d: %s.", seed, err) + return + } + 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.Errorf("failed Wrap with seed %d: %s.", seed, err) + return + } + } +} + +func BenchmarkDecryptionSymValid(b *testing.B) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + b.Errorf("failed generateMessageParams with seed %d: %s.", seed, err) + return + } + msg := NewSentMessage(params) + env, err := msg.Wrap(params) + if err != nil { + b.Errorf("failed Wrap with seed %d: %s.", seed, err) + return + } + f := Filter{KeySym: params.KeySym} + + for i := 0; i < b.N; i++ { + msg := env.Open(&f) + if msg == nil { + b.Errorf("failed to open with seed %d.", seed) + return + } + } +} + +func BenchmarkDecryptionSymInvalid(b *testing.B) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + b.Errorf("failed generateMessageParams with seed %d: %s.", seed, err) + return + } + msg := NewSentMessage(params) + env, err := msg.Wrap(params) + if err != nil { + b.Errorf("failed Wrap with seed %d: %s.", seed, err) + return + } + f := Filter{KeySym: []byte("arbitrary stuff here")} + + for i := 0; i < b.N; i++ { + msg := env.Open(&f) + if msg != nil { + b.Errorf("opened envelope with invalid key, seed: %d.", seed) + return + } + } +} + +func BenchmarkDecryptionAsymValid(b *testing.B) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + b.Errorf("failed generateMessageParams with seed %d: %s.", seed, err) + return + } + key, err := crypto.GenerateKey() + if err != nil { + b.Errorf("failed GenerateKey with seed %d: %s.", seed, err) + return + } + f := Filter{KeyAsym: key} + params.KeySym = nil + params.Dst = &key.PublicKey + msg := NewSentMessage(params) + env, err := msg.Wrap(params) + if err != nil { + b.Errorf("failed Wrap with seed %d: %s.", seed, err) + return + } + + for i := 0; i < b.N; i++ { + msg := env.Open(&f) + if msg == nil { + b.Errorf("fail to open, seed: %d.", seed) + return + } + } +} + +func BenchmarkDecryptionAsymInvalid(b *testing.B) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + b.Errorf("failed generateMessageParams with seed %d: %s.", seed, err) + return + } + key, err := crypto.GenerateKey() + if err != nil { + b.Errorf("failed GenerateKey with seed %d: %s.", seed, err) + return + } + params.KeySym = nil + params.Dst = &key.PublicKey + msg := NewSentMessage(params) + env, err := msg.Wrap(params) + if err != nil { + b.Errorf("failed Wrap with seed %d: %s.", seed, err) + return + } + + key, err = crypto.GenerateKey() + if err != nil { + b.Errorf("failed GenerateKey with seed %d: %s.", seed, err) + return + } + f := Filter{KeyAsym: key} + + for i := 0; i < b.N; i++ { + msg := env.Open(&f) + if msg != nil { + b.Errorf("opened envelope with invalid key, seed: %d.", seed) + return + } + } +} diff --git a/whisper/whisperv5/doc.go b/whisper/whisperv5/doc.go new file mode 100644 index 000000000..ef3b93d12 --- /dev/null +++ b/whisper/whisperv5/doc.go @@ -0,0 +1,87 @@ +// 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 PoC-1. + +(https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec) + +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. +*/ +package whisperv5 + +import ( + "fmt" + "time" +) + +const ( + EnvelopeVersion = uint64(0) + ProtocolVersion = uint64(5) + ProtocolVersionStr = "5.0" + ProtocolName = "shh" + + statusCode = 0 + messagesCode = 1 + p2pCode = 2 + mailRequestCode = 3 + NumberOfMessageCodes = 4 + + paddingMask = byte(3) + signatureFlag = byte(4) + + TopicLength = 4 + signatureLength = 65 + aesKeyLength = 32 + saltLength = 12 + + MaxMessageLength = 0xFFFF // todo: remove this restriction after testing in morden and analizing stats. this should be regulated by MinimumPoW. + MinimumPoW = 10.0 // todo: review + + padSizeLimitLower = 128 // it can not be less - we don't want to reveal the absence of signature + padSizeLimitUpper = 256 // just an arbitrary number, could be changed without losing compatibility + + expirationCycle = time.Second + transmissionCycle = 300 * time.Millisecond + + DefaultTTL = 50 // seconds + SynchAllowance = 10 // seconds +) + +type unknownVersionError uint64 + +func (e unknownVersionError) Error() string { + return fmt.Sprintf("invalid envelope version %d", uint64(e)) +} + +// 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, data []byte) +} diff --git a/whisper/whisperv5/envelope.go b/whisper/whisperv5/envelope.go new file mode 100644 index 000000000..57d454a08 --- /dev/null +++ b/whisper/whisperv5/envelope.go @@ -0,0 +1,233 @@ +// 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. For formal details please see +// the specs at https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec#envelopes. + +package whisperv5 + +import ( + "crypto/ecdsa" + "encoding/binary" + "fmt" + "math" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/ethereum/go-ethereum/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 { + Version []byte + Expiry uint32 + TTL uint32 + Topic TopicType + Salt []byte + AESNonce []byte + Data []byte + EnvNonce uint64 + + pow float64 // Message-specific PoW as described in the Whisper specification. + hash common.Hash // Cached hash of the envelope to avoid rehashing every time. + // Don't access hash directly, use Hash() function instead. +} + +// NewEnvelope wraps a Whisper message with expiration and destination data +// included into an envelope for network forwarding. +func NewEnvelope(ttl uint32, topic TopicType, salt []byte, aesNonce []byte, msg *SentMessage) *Envelope { + env := Envelope{ + Version: make([]byte, 1), + Expiry: uint32(time.Now().Add(time.Second * time.Duration(ttl)).Unix()), + TTL: ttl, + Topic: topic, + Salt: salt, + AESNonce: aesNonce, + Data: msg.Raw, + EnvNonce: 0, + } + + if EnvelopeVersion < 256 { + env.Version[0] = byte(EnvelopeVersion) + } else { + panic("please increase the size of Envelope.Version before releasing this version") + } + + return &env +} + +func (e *Envelope) IsSymmetric() bool { + return e.AESNonce != nil +} + +func (e *Envelope) isAsymmetric() bool { + return !e.IsSymmetric() +} + +func (e *Envelope) Ver() uint64 { + return bytesToIntLittleEndian(e.Version) +} + +// 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) { + var target int + if options.PoW == 0 { + // adjust for the duration of Seal() execution only if execution time is predefined unconditionally + e.Expiry += options.WorkTime + } else { + target = e.powToFirstBit(options.PoW) + } + + buf := make([]byte, 64) + h := crypto.Keccak256(e.rlpWithoutNonce()) + copy(buf[:32], h) + + finish, bestBit := time.Now().Add(time.Duration(options.WorkTime)*time.Second).UnixNano(), 0 + for nonce := uint64(0); time.Now().UnixNano() < finish; { + for i := 0; i < 1024; i++ { + binary.BigEndian.PutUint64(buf[56:], nonce) + h = crypto.Keccak256(buf) + firstBit := common.FirstBitSet(common.BigD(h)) + if firstBit > bestBit { + e.EnvNonce, bestBit = nonce, firstBit + if target > 0 && bestBit >= target { + return + } + } + nonce++ + } + } +} + +func (e *Envelope) PoW() float64 { + if e.pow == 0 { + e.calculatePoW(0) + } + return e.pow +} + +func (e *Envelope) calculatePoW(diff uint32) { + buf := make([]byte, 64) + h := crypto.Keccak256(e.rlpWithoutNonce()) + copy(buf[:32], h) + binary.BigEndian.PutUint64(buf[56:], e.EnvNonce) + h = crypto.Keccak256(buf) + firstBit := common.FirstBitSet(common.BigD(h)) + x := math.Pow(2, float64(firstBit)) + x /= float64(len(e.Data)) + x /= float64(e.TTL + diff) + e.pow = x +} + +func (e *Envelope) powToFirstBit(pow float64) int { + x := pow + x *= float64(len(e.Data)) + x *= float64(e.TTL) + bits := math.Log2(x) + bits = math.Ceil(bits) + return int(bits) +} + +// 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.Salt, e.AESNonce, e.Data}) + 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, e.Salt, e.AESNonce) + 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 e.isAsymmetric() { + msg, _ = e.OpenAsymmetric(watcher.KeyAsym) + if msg != nil { + msg.Dst = &watcher.KeyAsym.PublicKey + } + } else if e.IsSymmetric() { + msg, _ = e.OpenSymmetric(watcher.KeySym) + if msg != nil { + msg.SymKeyHash = crypto.Keccak256Hash(watcher.KeySym) + } + } + + if msg != nil { + ok := msg.Validate() + 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() + msg.EnvelopeVersion = e.Ver() + } + return msg +} diff --git a/whisper/whisperv5/filter.go b/whisper/whisperv5/filter.go new file mode 100644 index 000000000..5cc7be587 --- /dev/null +++ b/whisper/whisperv5/filter.go @@ -0,0 +1,197 @@ +// 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 whisperv5 + +import ( + "crypto/ecdsa" + "sync" + + "github.com/ethereum/go-ethereum/common" +) + +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 []TopicType // Topics to filter messages with + PoW float64 // Proof of work as described in the Whisper spec + AcceptP2P 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 + + Messages map[common.Hash]*ReceivedMessage + mutex sync.RWMutex +} + +type Filters struct { + id int + watchers map[int]*Filter + whisper *Whisper + mutex sync.RWMutex +} + +func NewFilters(w *Whisper) *Filters { + return &Filters{ + watchers: make(map[int]*Filter), + whisper: w, + } +} + +func (fs *Filters) Install(watcher *Filter) int { + if watcher.Messages == nil { + watcher.Messages = make(map[common.Hash]*ReceivedMessage) + } + + fs.mutex.Lock() + defer fs.mutex.Unlock() + + fs.watchers[fs.id] = watcher + ret := fs.id + fs.id++ + return ret +} + +func (fs *Filters) Uninstall(id int) { + fs.mutex.Lock() + defer fs.mutex.Unlock() + delete(fs.watchers, id) +} + +func (fs *Filters) Get(i int) *Filter { + fs.mutex.RLock() + defer fs.mutex.RUnlock() + return fs.watchers[i] +} + +func (fs *Filters) NotifyWatchers(env *Envelope, messageCode uint64) { + fs.mutex.RLock() + var msg *ReceivedMessage + for _, watcher := range fs.watchers { + if messageCode == p2pCode && !watcher.AcceptP2P { + continue + } + + match := false + if msg != nil { + match = watcher.MatchMessage(msg) + } else { + match = watcher.MatchEnvelope(env) + if match { + msg = env.Open(watcher) + } + } + + if match && msg != nil { + watcher.Trigger(msg) + } + } + fs.mutex.RUnlock() // we need to unlock before calling addDecryptedMessage + + if msg != nil { + fs.whisper.addDecryptedMessage(msg) + } +} + +func (f *Filter) expectsAsymmetricEncryption() bool { + return f.KeyAsym != nil +} + +func (f *Filter) expectsSymmetricEncryption() bool { + return f.KeySym != nil +} + +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 + } +} + +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 +} + +func (f *Filter) MatchMessage(msg *ReceivedMessage) bool { + if f.PoW > 0 && msg.PoW < f.PoW { + return false + } + if f.Src != nil && !isPubKeyEqual(msg.Src, f.Src) { + return false + } + + if f.expectsAsymmetricEncryption() && msg.isAsymmetricEncryption() { + // if Dst match, ignore the topic + return isPubKeyEqual(&f.KeyAsym.PublicKey, msg.Dst) + } else if f.expectsSymmetricEncryption() && msg.isSymmetricEncryption() { + // check if that both the key and the topic match + if f.SymKeyHash == msg.SymKeyHash { + for _, t := range f.Topics { + if t == msg.Topic { + return true + } + } + return false + } + } + return false +} + +func (f *Filter) MatchEnvelope(envelope *Envelope) bool { + if f.PoW > 0 && envelope.pow < f.PoW { + return false + } + + encryptionMethodMatch := false + if f.expectsAsymmetricEncryption() && envelope.isAsymmetric() { + encryptionMethodMatch = true + if f.Topics == nil { + // wildcard + return true + } + } else if f.expectsSymmetricEncryption() && envelope.IsSymmetric() { + encryptionMethodMatch = true + } + + if encryptionMethodMatch { + for _, t := range f.Topics { + if t == envelope.Topic { + return true + } + } + } + + return false +} + +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/whisperv5/filter_test.go b/whisper/whisperv5/filter_test.go new file mode 100644 index 000000000..8c25b0519 --- /dev/null +++ b/whisper/whisperv5/filter_test.go @@ -0,0 +1,707 @@ +// 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 whisperv5 + +import ( + "math/big" + "math/rand" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/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() + rand.Seed(seed) +} + +func InitDebugTest(i int64) { + seed = i + rand.Seed(seed) +} + +type FilterTestCase struct { + f *Filter + id int + alive bool + msgCnt int +} + +func generateFilter(x *testing.T, symmetric bool) (*Filter, error) { + var f Filter + f.Messages = make(map[common.Hash]*ReceivedMessage) + + const topicNum = 8 + f.Topics = make([]TopicType, topicNum) + for i := 0; i < topicNum; i++ { + randomize(f.Topics[i][:]) + f.Topics[i][0] = 0x01 + } + + key, err := crypto.GenerateKey() + if err != nil { + x.Errorf("generateFilter failed 1 with seed %d.", seed) + return nil, err + } + f.Src = &key.PublicKey + + if symmetric { + f.KeySym = make([]byte, 12) + randomize(f.KeySym) + f.SymKeyHash = crypto.Keccak256Hash(f.KeySym) + } else { + f.KeyAsym, err = crypto.GenerateKey() + if err != nil { + x.Errorf("generateFilter failed 2 with seed %d.", seed) + return nil, err + } + } + + // AcceptP2P & PoW are not set + return &f, nil +} + +func generateTestCases(x *testing.T, SizeTestFilters int) []FilterTestCase { + cases := make([]FilterTestCase, SizeTestFilters) + for i := 0; i < SizeTestFilters; i++ { + f, _ := generateFilter(x, true) + cases[i].f = f + cases[i].alive = (rand.Int()&int(1) == 0) + } + return cases +} + +func TestInstallFilters(x *testing.T) { + InitSingleTest() + + const SizeTestFilters = 256 + w := NewWhisper(nil) + filters := NewFilters(w) + tst := generateTestCases(x, SizeTestFilters) + + var j int + for i := 0; i < SizeTestFilters; i++ { + j = filters.Install(tst[i].f) + tst[i].id = j + } + + if j < SizeTestFilters-1 { + x.Errorf("seed %d: wrong index %d", seed, j) + return + } + + for _, t := range tst { + if !t.alive { + filters.Uninstall(t.id) + } + } + + for i, t := range tst { + fil := filters.Get(t.id) + exist := (fil != nil) + if exist != t.alive { + x.Errorf("seed %d: failed alive: %d, %v, %v", seed, i, exist, t.alive) + return + } + if exist && fil.PoW != t.f.PoW { + x.Errorf("seed %d: failed Get: %d, %v, %v", seed, i, exist, t.alive) + return + } + } +} + +func TestComparePubKey(x *testing.T) { + InitSingleTest() + + key1, err := crypto.GenerateKey() + if err != nil { + x.Errorf("failed GenerateKey 1 with seed %d: %s.", seed, err) + return + } + key2, err := crypto.GenerateKey() + if err != nil { + x.Errorf("failed GenerateKey 2 with seed %d: %s.", seed, err) + return + } + if isPubKeyEqual(&key1.PublicKey, &key2.PublicKey) { + x.Errorf("failed !equal with seed %d.", seed) + return + } + + // generate key3 == key1 + rand.Seed(seed) + key3, err := crypto.GenerateKey() + if err != nil { + x.Errorf("failed GenerateKey 3 with seed %d: %s.", seed, err) + return + } + if isPubKeyEqual(&key1.PublicKey, &key3.PublicKey) { + x.Errorf("failed equal with seed %d.", seed) + return + } +} + +func TestMatchEnvelope(x *testing.T) { + InitSingleTest() + + fsym, err := generateFilter(x, true) + if err != nil { + x.Errorf("failed generateFilter 1 with seed %d: %s.", seed, err) + return + } + + fasym, err := generateFilter(x, false) + if err != nil { + x.Errorf("failed generateFilter 2 with seed %d: %s.", seed, err) + return + } + + params, err := generateMessageParams() + if err != nil { + x.Errorf("failed generateMessageParams 3 with seed %d: %s.", seed, err) + return + } + + params.Topic[0] = 0xFF // ensure mismatch + + // mismatch with pseudo-random data + msg := NewSentMessage(params) + env, err := msg.Wrap(params) + if err != nil { + x.Errorf("failed Wrap 4 with seed %d: %s.", seed, err) + return + } + match := fsym.MatchEnvelope(env) + if match { + x.Errorf("failed test case 5 with seed %d.", seed) + return + } + match = fasym.MatchEnvelope(env) + if match { + x.Errorf("failed test case 6 with seed %d.", seed) + return + } + + // encrypt symmetrically + i := rand.Int() % 4 + fsym.Topics[i] = params.Topic + fasym.Topics[i] = params.Topic + msg = NewSentMessage(params) + env, err = msg.Wrap(params) + if err != nil { + x.Errorf("failed test case 7 with seed %d, test case 3: %s.", seed, err) + return + } + + // symmetric + matching topic: match + match = fsym.MatchEnvelope(env) + if !match { + x.Errorf("failed test case 8 with seed %d.", seed) + return + } + + // asymmetric + matching topic: mismatch + match = fasym.MatchEnvelope(env) + if match { + x.Errorf("failed test case 9 with seed %d.", seed) + return + } + + // symmetric + matching topic + insufficient PoW: mismatch + fsym.PoW = env.PoW() + 1.0 + match = fsym.MatchEnvelope(env) + if match { + x.Errorf("failed test case 10 with seed %d.", seed) + return + } + + // symmetric + matching topic + sufficient PoW: match + fsym.PoW = env.PoW() / 2 + match = fsym.MatchEnvelope(env) + if !match { + x.Errorf("failed test case 11 with seed %d.", seed) + return + } + + // symmetric + topics are nil: mismatch + prevTopics := fsym.Topics + fsym.Topics = nil + match = fasym.MatchEnvelope(env) + if match { + x.Errorf("failed test case 12 with seed %d.", seed) + return + } + fsym.Topics = prevTopics + + // encrypt asymmetrically + key, err := crypto.GenerateKey() + if err != nil { + x.Errorf("failed GenerateKey 13 with seed %d: %s.", seed, err) + return + } + params.KeySym = nil + params.Dst = &key.PublicKey + msg = NewSentMessage(params) + env, err = msg.Wrap(params) + if err != nil { + x.Errorf("failed test case 14 with seed %d, test case 3: %s.", seed, err) + return + } + + // encryption method mismatch + match = fsym.MatchEnvelope(env) + if match { + x.Errorf("failed test case 15 with seed %d.", seed) + return + } + + // asymmetric + mismatching topic: mismatch + match = fasym.MatchEnvelope(env) + if !match { + x.Errorf("failed test case 16 with seed %d.", seed) + return + } + + // asymmetric + matching topic: match + fasym.Topics[i] = fasym.Topics[i+1] + match = fasym.MatchEnvelope(env) + if match { + x.Errorf("failed test case 17 with seed %d.", seed) + return + } + + // asymmetric + topic is nil (wildcard): match + fasym.Topics = nil + match = fasym.MatchEnvelope(env) + if !match { + x.Errorf("failed test case 18 with seed %d.", seed) + return + } + + // asymmetric + insufficient PoW: mismatch + fasym.PoW = env.PoW() + 1.0 + match = fasym.MatchEnvelope(env) + if match { + x.Errorf("failed test case 19 with seed %d.", seed) + return + } + + // asymmetric + sufficient PoW: match + fasym.PoW = env.PoW() / 2 + match = fasym.MatchEnvelope(env) + if !match { + x.Errorf("failed test case 20 with seed %d.", seed) + return + } +} + +func TestMatchMessageSym(x *testing.T) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + x.Errorf("failed generateMessageParams with seed %d: %s.", seed, err) + return + } + + f, err := generateFilter(x, true) + if err != nil { + x.Errorf("failed generateFilter 1 with seed %d: %s.", seed, err) + return + } + + const index = 1 + params.KeySym = f.KeySym + params.Topic = f.Topics[index] + + sentMessage := NewSentMessage(params) + env, err := sentMessage.Wrap(params) + if err != nil { + x.Errorf("failed Wrap 2 with seed %d: %s.", seed, err) + return + } + + msg := env.Open(f) + if msg == nil { + x.Errorf("failed to open 3 with seed %d.", seed) + return + } + + // Src mismatch + if f.MatchMessage(msg) { + x.Errorf("failed test case 4 with seed %d.", seed) + return + } + + // Src: match + *f.Src.X = *params.Src.PublicKey.X + *f.Src.Y = *params.Src.PublicKey.Y + if !f.MatchMessage(msg) { + x.Errorf("failed test case 5 with seed %d.", seed) + return + } + + // insufficient PoW: mismatch + f.PoW = msg.PoW + 1.0 + if f.MatchMessage(msg) { + x.Errorf("failed test case 6 with seed %d.", seed) + return + } + + // sufficient PoW: match + f.PoW = msg.PoW / 2 + if !f.MatchMessage(msg) { + x.Errorf("failed test case 7 with seed %d.", seed) + return + } + + // topic mismatch + f.Topics[index][0]++ + if f.MatchMessage(msg) { + x.Errorf("failed test case 8 with seed %d.", seed) + return + } + f.Topics[index][0]-- + + // key mismatch + f.SymKeyHash[0]++ + if f.MatchMessage(msg) { + x.Errorf("failed test case 9 with seed %d.", seed) + return + } + f.SymKeyHash[0]-- + + // Src absent: match + f.Src = nil + if !f.MatchMessage(msg) { + x.Errorf("failed test case 10 with seed %d.", seed) + return + } + + // key hash mismatch mismatch + h := f.SymKeyHash + f.SymKeyHash = common.Hash{} + if f.MatchMessage(msg) { + x.Errorf("failed test case 11 with seed %d.", seed) + return + } + f.SymKeyHash = h + if !f.MatchMessage(msg) { + x.Errorf("failed test case 12 with seed %d.", seed) + return + } + + // encryption method mismatch + f.KeySym = nil + f.KeyAsym, err = crypto.GenerateKey() + if err != nil { + x.Errorf("failed GenerateKey 13 with seed %d: %s.", seed, err) + return + } + if f.MatchMessage(msg) { + x.Errorf("failed test case 14 with seed %d.", seed) + return + } +} + +func TestMatchMessageAsym(x *testing.T) { + InitSingleTest() + + f, err := generateFilter(x, false) + if err != nil { + x.Errorf("failed generateFilter with seed %d: %s.", seed, err) + return + } + + params, err := generateMessageParams() + if err != nil { + x.Errorf("failed generateMessageParams with seed %d: %s.", seed, err) + return + } + + const index = 1 + params.Topic = f.Topics[index] + params.Dst = &f.KeyAsym.PublicKey + keySymOrig := params.KeySym + params.KeySym = nil + + sentMessage := NewSentMessage(params) + env, err := sentMessage.Wrap(params) + if err != nil { + x.Errorf("failed Wrap with seed %d: %s.", seed, err) + return + } + + msg := env.Open(f) + if msg == nil { + x.Errorf("failed to open with seed %d.", seed) + return + } + + // Src mismatch + if f.MatchMessage(msg) { + x.Errorf("failed test case 4 with seed %d.", seed) + return + } + + // Src: match + *f.Src.X = *params.Src.PublicKey.X + *f.Src.Y = *params.Src.PublicKey.Y + if !f.MatchMessage(msg) { + x.Errorf("failed test case 5 with seed %d.", seed) + return + } + + // insufficient PoW: mismatch + f.PoW = msg.PoW + 1.0 + if f.MatchMessage(msg) { + x.Errorf("failed test case 6 with seed %d.", seed) + return + } + + // sufficient PoW: match + f.PoW = msg.PoW / 2 + if !f.MatchMessage(msg) { + x.Errorf("failed test case 7 with seed %d.", seed) + return + } + + // topic mismatch, but still match, because for asymmetric encryption + // only private key matters (in case the message is already decrypted) + f.Topics[index][0]++ + if !f.MatchMessage(msg) { + x.Errorf("failed test case 8 with seed %d.", seed) + return + } + f.Topics[index][0]-- + + // key mismatch + prev := *f.KeyAsym.PublicKey.X + zero := *big.NewInt(0) + *f.KeyAsym.PublicKey.X = zero + if f.MatchMessage(msg) { + x.Errorf("failed test case 9 with seed %d.", seed) + return + } + *f.KeyAsym.PublicKey.X = prev + + // Src absent: match + f.Src = nil + if !f.MatchMessage(msg) { + x.Errorf("failed test case 10 with seed %d.", seed) + return + } + + // encryption method mismatch + f.KeySym = keySymOrig + f.KeyAsym = nil + if f.MatchMessage(msg) { + x.Errorf("failed test case 11 with seed %d.", seed) + return + } +} + +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.AcceptP2P = orig.AcceptP2P + clone.SymKeyHash = orig.SymKeyHash + return &clone +} + +func generateCompatibeEnvelope(x *testing.T, f *Filter) *Envelope { + params, err := generateMessageParams() + if err != nil { + x.Errorf("failed generateMessageParams 77 with seed %d: %s.", seed, err) + return nil + } + + params.KeySym = f.KeySym + params.Topic = f.Topics[2] + sentMessage := NewSentMessage(params) + env, err := sentMessage.Wrap(params) + if err != nil { + x.Errorf("failed Wrap 78 with seed %d: %s.", seed, err) + return nil + } + return env +} + +func TestWatchers(x *testing.T) { + InitSingleTest() + + const NumFilters = 16 + const NumMessages = 256 + var i, j int + var e *Envelope + + w := NewWhisper(nil) + filters := NewFilters(w) + tst := generateTestCases(x, NumFilters) + for i = 0; i < NumFilters; i++ { + tst[i].f.Src = nil + j = filters.Install(tst[i].f) + tst[i].id = j + } + + last := j + + var envelopes [NumMessages]*Envelope + for i = 0; i < NumMessages; i++ { + j = rand.Int() % NumFilters + e = generateCompatibeEnvelope(x, tst[j].f) + envelopes[i] = e + tst[j].msgCnt++ + } + + for i = 0; i < NumMessages; i++ { + filters.NotifyWatchers(envelopes[i], messagesCode) + } + + 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 { + x.Errorf("failed test case 1 with seed %d: total = %d, want: %d.", seed, total, NumMessages) + return + } + + for i = 0; i < NumFilters; i++ { + mail = tst[i].f.Retrieve() + if len(mail) != 0 { + x.Errorf("failed test case 2 with seed %d: i = %d.", seed, i) + return + } + + if tst[i].msgCnt != count[i] { + x.Errorf("failed test case 3 with seed %d: i = %d, get %d, want %d.", seed, i, tst[i].msgCnt, count[i]) + return + } + } + + // another round with a cloned filter + + clone := cloneFilter(tst[0].f) + filters.Uninstall(last) + 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(x, tst[0].f) + envelopes[0] = e + tst[0].msgCnt++ + for i = 1; i < NumMessages; i++ { + j = rand.Int() % NumFilters + e = generateCompatibeEnvelope(x, tst[j].f) + envelopes[i] = e + tst[j].msgCnt++ + } + + for i = 0; i < NumMessages; i++ { + filters.NotifyWatchers(envelopes[i], messagesCode) + } + + 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] { + x.Errorf("failed test case 4 with seed %d: total = %d, count[0] = %d.", seed, total, count[0]) + return + } + + if combined != count[0] { + x.Errorf("failed test case 5 with seed %d: combined = %d, count[0] = %d.", seed, combined, count[0]) + return + } + + if combined != count[last] { + x.Errorf("failed test case 6 with seed %d: combined = %d, count[last] = %d.", seed, combined, count[last]) + return + } + + for i = 1; i < NumFilters-1; i++ { + mail = tst[i].f.Retrieve() + if len(mail) != 0 { + x.Errorf("failed test case 7 with seed %d: i = %d.", seed, i) + return + } + + if tst[i].msgCnt != count[i] { + x.Errorf("failed test case 8 with seed %d: i = %d, get %d, want %d.", seed, i, tst[i].msgCnt, count[i]) + return + } + } + + // test AcceptP2P + + total = 0 + filters.NotifyWatchers(envelopes[0], p2pCode) + + for i = 0; i < NumFilters; i++ { + mail = tst[i].f.Retrieve() + total += len(mail) + } + + if total != 0 { + x.Errorf("failed test case 9 with seed %d.", seed) + return + } + + f := filters.Get(0) + f.AcceptP2P = true + total = 0 + filters.NotifyWatchers(envelopes[0], p2pCode) + + for i = 0; i < NumFilters; i++ { + mail = tst[i].f.Retrieve() + total += len(mail) + } + + if total != 1 { + x.Errorf("failed test case 10 with seed %d: total = %d.", seed, total) + return + } +} diff --git a/whisper/whisperv5/message.go b/whisper/whisperv5/message.go new file mode 100644 index 000000000..680d1f8a2 --- /dev/null +++ b/whisper/whisperv5/message.go @@ -0,0 +1,378 @@ +// 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. For formal details please see +// the specs at https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec#messages. +// todo: fix the spec link, and move it to doc.go + +package whisperv5 + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + crand "crypto/rand" + "crypto/sha256" + "errors" + "fmt" + mrand "math/rand" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "golang.org/x/crypto/pbkdf2" +) + +// Options 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. +type ReceivedMessage struct { + Raw []byte + + Payload []byte + Padding []byte + Signature []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, associated with the Topic + EnvelopeHash common.Hash // Message envelope hash to act as a unique id + EnvelopeVersion uint64 +} + +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 +} + +func DeriveOneTimeKey(key []byte, salt []byte, version uint64) ([]byte, error) { + if version == 0 { + derivedKey := pbkdf2.Key(key, salt, 8, aesKeyLength, sha256.New) + return derivedKey, nil + } else { + return nil, unknownVersionError(version) + } +} + +// NewMessage creates and initializes a non-signed, non-encrypted Whisper message. +func NewSentMessage(params *MessageParams) *SentMessage { + msg := SentMessage{} + msg.Raw = make([]byte, 1, len(params.Payload)+len(params.Payload)+signatureLength+padSizeLimitUpper) + msg.Raw[0] = 0 // set all the flags to zero + msg.appendPadding(params) + msg.Raw = append(msg.Raw, params.Payload...) + return &msg +} + +// appendPadding appends the pseudorandom padding bytes and sets the padding flag. +// The last byte contains the size of padding (thus, its size must not exceed 256). +func (msg *SentMessage) appendPadding(params *MessageParams) { + total := len(params.Payload) + 1 + if params.Src != nil { + total += signatureLength + } + padChunk := padSizeLimitUpper + if total <= padSizeLimitLower { + padChunk = padSizeLimitLower + } + odd := total % padChunk + if odd > 0 { + padSize := padChunk - odd + if padSize > 255 { + // this algorithm is only valid if padSizeLimitUpper <= 256. + // if padSizeLimitUpper will ever change, please fix the algorithm + // (for more information see ReceivedMessage.extractPadding() function). + panic("please fix the padding algorithm before releasing new version") + } + buf := make([]byte, padSize) + randomize(buf[1:]) // change to: err = mrand.Read(buf[1:]) + buf[0] = byte(padSize) + if params.Padding != nil { + copy(buf[1:], params.Padding) + } + msg.Raw = append(msg.Raw, buf...) + msg.Raw[0] |= byte(0x1) // number of bytes indicating the padding size + } +} + +// 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 + glog.V(logger.Error).Infof("Trying to sign a message which was already signed") + return nil + } + + msg.Raw[0] |= signatureFlag + hash := crypto.Keccak256(msg.Raw) + signature, err := crypto.Sign(hash, key) + if err != nil { + msg.Raw[0] &= ^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 fmt.Errorf("Invalid public key provided for asymmetric encryption") + } + encrypted, err := crypto.Encrypt(key, msg.Raw) + 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) (salt []byte, nonce []byte, err error) { + if !validateSymmetricKey(key) { + return nil, nil, errors.New("invalid key provided for symmetric encryption") + } + + salt = make([]byte, saltLength) + _, err = crand.Read(salt) + if err != nil { + return nil, nil, err + } else if !validateSymmetricKey(salt) { + return nil, nil, errors.New("crypto/rand failed to generate salt") + } + + derivedKey, err := DeriveOneTimeKey(key, salt, EnvelopeVersion) + if err != nil { + return nil, nil, err + } + if !validateSymmetricKey(derivedKey) { + return nil, nil, errors.New("failed to derive one-time key") + } + block, err := aes.NewCipher(derivedKey) + if err != nil { + return nil, nil, err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, nil, err + } + + // never use more than 2^32 random nonces with a given key + nonce = make([]byte, aesgcm.NonceSize()) + _, err = crand.Read(nonce) + if err != nil { + return nil, nil, err + } + msg.Raw = aesgcm.Seal(nil, nonce, msg.Raw, nil) + return salt, nonce, nil +} + +// Wrap bundles the message into an Envelope to transmit over the network. +// +// pow (Proof Of Work) controls how much time to spend on hashing the message, +// inherently controlling its priority through the network (smaller hash, bigger +// priority). +// +// The user can control the amount of identity, privacy and encryption through +// the options parameter as follows: +// - options.From == nil && options.To == nil: anonymous broadcast +// - options.From != nil && options.To == nil: signed broadcast (known sender) +// - options.From == nil && options.To != nil: encrypted anonymous message +// - options.From != nil && options.To != nil: encrypted signed message +func (msg *SentMessage) Wrap(options *MessageParams) (envelope *Envelope, err error) { + if options.TTL == 0 { + options.TTL = DefaultTTL + } + if options.Src != nil { + err = msg.sign(options.Src) + if err != nil { + return nil, err + } + } + if len(msg.Raw) > MaxMessageLength { + glog.V(logger.Error).Infof("Message size must not exceed %d bytes", MaxMessageLength) + return nil, errors.New("Oversized message") + } + var salt, nonce []byte + if options.Dst != nil { + err = msg.encryptAsymmetric(options.Dst) + } else if options.KeySym != nil { + salt, nonce, err = msg.encryptSymmetric(options.KeySym) + } else { + err = errors.New("Unable to encrypt the message: neither Dst nor Key") + } + + if err != nil { + return nil, err + } + + envelope = NewEnvelope(options.TTL, options.Topic, salt, nonce, msg) + envelope.Seal(options) + 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, salt []byte, nonce []byte) error { + derivedKey, err := DeriveOneTimeKey(key, salt, msg.EnvelopeVersion) + if err != nil { + return err + } + + block, err := aes.NewCipher(derivedKey) + if err != nil { + return err + } + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return err + } + if len(nonce) != aesgcm.NonceSize() { + info := fmt.Sprintf("Wrong AES nonce size - want: %d, got: %d", len(nonce), aesgcm.NonceSize()) + glog.V(logger.Error).Infof(info) + return errors.New(info) + } + decrypted, err := aesgcm.Open(nil, nonce, msg.Raw, nil) + if err != nil { + return err + } + msg.Raw = decrypted + return nil +} + +// decryptAsymmetric decrypts an encrypted payload with a private key. +func (msg *ReceivedMessage) decryptAsymmetric(key *ecdsa.PrivateKey) error { + decrypted, err := crypto.Decrypt(key, msg.Raw) + if err == nil { + msg.Raw = decrypted + } + return err +} + +// Validate checks the validity and extracts the fields in case of success +func (msg *ReceivedMessage) Validate() 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:] + msg.Src = msg.SigToPubKey() + if msg.Src == nil { + return false + } + } + + padSize, ok := msg.extractPadding(end) + if !ok { + return false + } + + msg.Payload = msg.Raw[1+padSize : end] + return true +} + +// extractPadding extracts the padding from raw message. +// although we don't support sending messages with padding size +// exceeding 255 bytes, such messages are perfectly valid, and +// can be successfully decrypted. +func (msg *ReceivedMessage) extractPadding(end int) (int, bool) { + paddingSize := 0 + sz := int(msg.Raw[0] & paddingMask) // number of bytes containing the entire size of padding, could be zero + if sz != 0 { + paddingSize = int(bytesToIntLittleEndian(msg.Raw[1 : 1+sz])) + if paddingSize < sz || paddingSize+1 > end { + return 0, false + } + msg.Padding = msg.Raw[1+sz : 1+paddingSize] + } + return paddingSize, true +} + +// Recover retrieves the public key of the message signer. +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 { + glog.V(logger.Error).Infof("Could not get public key from signature: %v", err) + return nil + } + return pub +} + +// hash calculates the SHA3 checksum of the message flags, 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) +} + +// rand.Rand provides a Read method in Go 1.7 and later, +// but we can't use it yet. +func randomize(b []byte) { + cnt := 0 + val := mrand.Int63() + for n := 0; n < len(b); n++ { + b[n] = byte(val) + val >>= 8 + cnt++ + if cnt >= 7 { + cnt = 0 + val = mrand.Int63() + } + } +} diff --git a/whisper/whisperv5/message_test.go b/whisper/whisperv5/message_test.go new file mode 100644 index 000000000..78041fc1a --- /dev/null +++ b/whisper/whisperv5/message_test.go @@ -0,0 +1,306 @@ +// 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 whisperv5 + +import ( + "bytes" + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/crypto" +) + +func copyFromBuf(dst []byte, src []byte, beg int) int { + copy(dst, src[beg:]) + return beg + len(dst) +} + +func generateMessageParams() (*MessageParams, error) { + buf := make([]byte, 1024) + randomize(buf) + sz := rand.Intn(400) + + var p MessageParams + p.TTL = uint32(rand.Intn(1024)) + p.Payload = make([]byte, sz) + p.Padding = make([]byte, padSizeLimitUpper) + p.KeySym = make([]byte, aesKeyLength) + + var b int + b = copyFromBuf(p.Payload, buf, b) + b = copyFromBuf(p.Padding, buf, b) + b = copyFromBuf(p.KeySym, buf, b) + p.Topic = BytesToTopic(buf[b:]) + + var err error + p.Src, err = crypto.GenerateKey() + if err != nil { + return nil, err + } + + // p.Dst, p.PoW, p.WorkTime are not set + p.PoW = 0.01 + return &p, nil +} + +func singleMessageTest(x *testing.T, symmetric bool) { + params, err := generateMessageParams() + if err != nil { + x.Errorf("failed generateMessageParams with seed %d: %s.", seed, err) + return + } + + key, err := crypto.GenerateKey() + if err != nil { + x.Errorf("failed GenerateKey with seed %d: %s.", seed, err) + return + } + + if !symmetric { + params.KeySym = nil + params.Dst = &key.PublicKey + } + + text := make([]byte, 0, 512) + steg := make([]byte, 0, 512) + raw := make([]byte, 0, 1024) + text = append(text, params.Payload...) + steg = append(steg, params.Padding...) + raw = append(raw, params.Padding...) + + msg := NewSentMessage(params) + env, err := msg.Wrap(params) + if err != nil { + x.Errorf("failed Wrap with seed %d: %s.", seed, err) + return + } + + var decrypted *ReceivedMessage + if symmetric { + decrypted, err = env.OpenSymmetric(params.KeySym) + } else { + decrypted, err = env.OpenAsymmetric(key) + } + + if err != nil { + x.Errorf("failed to encrypt with seed %d: %s.", seed, err) + return + } + + if !decrypted.Validate() { + x.Errorf("failed to validate with seed %d.", seed) + return + } + + padsz := len(decrypted.Padding) + if bytes.Compare(steg[:padsz], decrypted.Padding) != 0 { + x.Errorf("failed with seed %d: compare padding.", seed) + return + } + if bytes.Compare(text, decrypted.Payload) != 0 { + x.Errorf("failed with seed %d: compare payload.", seed) + return + } + if !isMessageSigned(decrypted.Raw[0]) { + x.Errorf("failed with seed %d: unsigned.", seed) + return + } + if len(decrypted.Signature) != signatureLength { + x.Errorf("failed with seed %d: signature len %d.", seed, len(decrypted.Signature)) + return + } + if !isPubKeyEqual(decrypted.Src, ¶ms.Src.PublicKey) { + x.Errorf("failed with seed %d: signature mismatch.", seed) + return + } +} + +func TestMessageEncryption(x *testing.T) { + InitSingleTest() + + var symmetric bool + for i := 0; i < 256; i++ { + singleMessageTest(x, symmetric) + symmetric = !symmetric + } +} + +func TestMessageWrap(x *testing.T) { + seed = int64(1777444222) + rand.Seed(seed) + target := 128.0 + + params, err := generateMessageParams() + if err != nil { + x.Errorf("failed generateMessageParams with seed %d: %s.", seed, err) + return + } + + msg := NewSentMessage(params) + params.TTL = 1 + params.WorkTime = 12 + params.PoW = target + env, err := msg.Wrap(params) + if err != nil { + x.Errorf("failed Wrap with seed %d: %s.", seed, err) + return + } + + pow := env.PoW() + if pow < target { + x.Errorf("failed Wrap with seed %d: pow < target (%f vs. %f).", seed, pow, target) + return + } +} + +func TestMessageSeal(x *testing.T) { + // this test depends on deterministic choice of seed (1976726903) + seed = int64(1976726903) + rand.Seed(seed) + + params, err := generateMessageParams() + if err != nil { + x.Errorf("failed generateMessageParams with seed %d: %s.", seed, err) + return + } + + msg := NewSentMessage(params) + params.TTL = 1 + aesnonce := make([]byte, 12) + salt := make([]byte, 12) + randomize(aesnonce) + randomize(salt) + + env := NewEnvelope(params.TTL, params.Topic, salt, aesnonce, msg) + if err != nil { + x.Errorf("failed Wrap with seed %d: %s.", seed, err) + return + } + + 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 { + x.Errorf("failed Wrap with seed %d: pow < target (%f vs. %f).", seed, pow, target) + return + } + + params.WorkTime = 1 + params.PoW = 1000000000.0 + env.Seal(params) + env.calculatePoW(0) + pow = env.PoW() + if pow < 2*target { + x.Errorf("failed Wrap with seed %d: pow too small %f.", seed, pow) + return + } +} + +func TestEnvelopeOpen(x *testing.T) { + InitSingleTest() + + var symmetric bool + for i := 0; i < 256; i++ { + singleEnvelopeOpenTest(x, symmetric) + symmetric = !symmetric + } +} + +func singleEnvelopeOpenTest(x *testing.T, symmetric bool) { + params, err := generateMessageParams() + if err != nil { + x.Errorf("failed generateMessageParams with seed %d: %s.", seed, err) + return + } + + key, err := crypto.GenerateKey() + if err != nil { + x.Errorf("failed GenerateKey with seed %d: %s.", seed, err) + return + } + + if !symmetric { + params.KeySym = nil + params.Dst = &key.PublicKey + } + + text := make([]byte, 0, 512) + steg := make([]byte, 0, 512) + raw := make([]byte, 0, 1024) + text = append(text, params.Payload...) + steg = append(steg, params.Padding...) + raw = append(raw, params.Padding...) + + msg := NewSentMessage(params) + env, err := msg.Wrap(params) + if err != nil { + x.Errorf("failed Wrap with seed %d: %s.", seed, err) + return + } + + f := Filter{KeyAsym: key, KeySym: params.KeySym} + decrypted := env.Open(&f) + if decrypted == nil { + x.Errorf("failed to open with seed %d.", seed) + return + } + + padsz := len(decrypted.Padding) + if bytes.Compare(steg[:padsz], decrypted.Padding) != 0 { + x.Errorf("failed with seed %d: compare padding.", seed) + return + } + if bytes.Compare(text, decrypted.Payload) != 0 { + x.Errorf("failed with seed %d: compare payload.", seed) + return + } + if !isMessageSigned(decrypted.Raw[0]) { + x.Errorf("failed with seed %d: unsigned.", seed) + return + } + if len(decrypted.Signature) != signatureLength { + x.Errorf("failed with seed %d: signature len %d.", seed, len(decrypted.Signature)) + return + } + if !isPubKeyEqual(decrypted.Src, ¶ms.Src.PublicKey) { + x.Errorf("failed with seed %d: signature mismatch.", seed) + return + } + if decrypted.isAsymmetricEncryption() == symmetric { + x.Errorf("failed with seed %d: asymmetric %v vs. %v.", seed, decrypted.isAsymmetricEncryption(), symmetric) + return + } + if decrypted.isSymmetricEncryption() != symmetric { + x.Errorf("failed with seed %d: symmetric %v vs. %v.", seed, decrypted.isSymmetricEncryption(), symmetric) + return + } + if !symmetric { + if decrypted.Dst == nil { + x.Errorf("failed with seed %d: dst is nil.", seed) + return + } + if !isPubKeyEqual(decrypted.Dst, &key.PublicKey) { + x.Errorf("failed with seed %d: Dst.", seed) + return + } + } +} diff --git a/whisper/whisperv5/peer.go b/whisper/whisperv5/peer.go new file mode 100644 index 000000000..fc1afb6d6 --- /dev/null +++ b/whisper/whisperv5/peer.go @@ -0,0 +1,174 @@ +// 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 whisperv5 + +import ( + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rlp" + set "gopkg.in/fatih/set.v0" +) + +// peer represents a whisper protocol peer connection. +type Peer struct { + host *Whisper + peer *p2p.Peer + ws p2p.MsgReadWriter + trusted bool + + known *set.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, + known: set.New(), + quit: make(chan struct{}), + } +} + +// start initiates the peer updater, periodically broadcasting the whisper packets +// into the network. +func (p *Peer) start() { + go p.update() + glog.V(logger.Debug).Infof("%v: whisper started", p.peer) +} + +// stop terminates the peer updater, stopping message forwarding to it. +func (p *Peer) stop() { + close(p.quit) + glog.V(logger.Debug).Infof("%v: whisper stopped", p.peer) +} + +// handshake sends the protocol initiation status message to the remote peer and +// verifies the remote status too. +func (p *Peer) handshake() error { + // Send the handshake status message asynchronously + errc := make(chan error, 1) + go func() { + errc <- p2p.Send(p.ws, statusCode, ProtocolVersion) + }() + // Fetch the remote status packet and verify protocol match + packet, err := p.ws.ReadMsg() + if err != nil { + return err + } + if packet.Code != statusCode { + return fmt.Errorf("peer sent %x before status packet", packet.Code) + } + s := rlp.NewStream(packet.Payload, uint64(packet.Size)) + peerVersion, err := s.Uint() + if err != nil { + return fmt.Errorf("bad status message: %v", err) + } + if peerVersion != ProtocolVersion { + return fmt.Errorf("protocol version mismatch %d != %d", peerVersion, ProtocolVersion) + } + // Wait until out own status is consumed too + if err := <-errc; err != nil { + return fmt.Errorf("failed to send status packet: %v", err) + } + return nil +} + +// update executes periodic operations on the peer, including message transmission +// and expiration. +func (p *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: + p.expire() + + case <-transmit.C: + if err := p.broadcast(); err != nil { + glog.V(logger.Info).Infof("%v: broadcast failed: %v", p.peer, err) + return + } + + case <-p.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.Has(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() { + // Assemble the list of available envelopes + available := set.NewNonTS() + for _, envelope := range peer.host.Envelopes() { + available.Add(envelope.Hash()) + } + // Cross reference availability with known status + unmark := make(map[common.Hash]struct{}) + peer.known.Each(func(v interface{}) bool { + if !available.Has(v.(common.Hash)) { + unmark[v.(common.Hash)] = struct{}{} + } + return true + }) + // Dump all known but unavailable + for hash, _ := range unmark { + peer.known.Remove(hash) + } +} + +// broadcast iterates over the collection of envelopes and transmits yet unknown +// ones over the network. +func (p *Peer) broadcast() error { + // Fetch the envelopes and collect the unknown ones + envelopes := p.host.Envelopes() + transmit := make([]*Envelope, 0, len(envelopes)) + for _, envelope := range envelopes { + if !p.marked(envelope) { + transmit = append(transmit, envelope) + p.mark(envelope) + } + } + // Transmit the unknown batch (potentially empty) + if err := p2p.Send(p.ws, messagesCode, transmit); err != nil { + return err + } + glog.V(logger.Detail).Infoln(p.peer, "broadcasted", len(transmit), "message(s)") + return nil +} diff --git a/whisper/whisperv5/peer_test.go b/whisper/whisperv5/peer_test.go new file mode 100644 index 000000000..03c4725d8 --- /dev/null +++ b/whisper/whisperv5/peer_test.go @@ -0,0 +1,307 @@ +// 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 whisperv5 + +import ( + "bytes" + "crypto/ecdsa" + "fmt" + "net" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/discover" + "github.com/ethereum/go-ethereum/p2p/nat" +) + +var keys []string = []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", +} + +const NumNodes = 16 // must not exceed the number of keys (32) + +type TestData struct { + counter [NumNodes]int + mutex sync.RWMutex +} + +type TestNode struct { + shh *Whisper + id *ecdsa.PrivateKey + server *p2p.Server + filerId int +} + +var result TestData +var nodes [NumNodes]*TestNode +var sharedKey []byte = []byte("some arbitrary data here") +var sharedTopic TopicType = TopicType{0xF, 0x1, 0x2, 0} +var expectedMessage []byte = []byte("per rectum ad astra") + +// This test does the following: +// 1. creates a chain of whisper nodes, +// 2. installs the filters with shared (predefined) parameters, +// 3. each node sends a number of random (undecryptable) messages, +// 4. first node sends one expected (decryptable) message, +// 5. checks if each node have received and decrypted exactly one message. +func TestSimulation(x *testing.T) { + initialize(x) + + for i := 0; i < NumNodes; i++ { + sendMsg(x, false, i) + } + + sendMsg(x, true, 0) + checkPropagation(x) + stopServers() +} + +func initialize(x *testing.T) { + //glog.SetV(6) + //glog.SetToStderr(true) + + var err error + ip := net.IPv4(127, 0, 0, 1) + port0 := 30303 + + for i := 0; i < NumNodes; i++ { + var node TestNode + node.shh = NewWhisper(nil) + node.shh.test = true + tt := make([]TopicType, 0) + tt = append(tt, sharedTopic) + f := Filter{KeySym: sharedKey, Topics: tt} + node.filerId = node.shh.Watch(&f) + node.id, err = crypto.HexToECDSA(keys[i]) + if err != nil { + x.Errorf("failed convert the key: %s.", keys[i]) + return + } + port := port0 + i + addr := fmt.Sprintf(":%d", port) // e.g. ":30303" + name := common.MakeName("whisper-go", "2.0") + var peers []*discover.Node + if i > 0 { + peerNodeId := nodes[i-1].id + peerPort := uint16(port - 1) + peerNode := discover.PubkeyID(&peerNodeId.PublicKey) + peer := discover.NewNode(peerNode, ip, peerPort, peerPort) + peers = append(peers, peer) + } + + node.server = &p2p.Server{ + Config: p2p.Config{ + PrivateKey: node.id, + MaxPeers: NumNodes/2 + 1, + Name: name, + Protocols: node.shh.Protocols(), + ListenAddr: addr, + NAT: nat.Any(), + BootstrapNodes: peers, + StaticNodes: peers, + TrustedNodes: peers, + }, + } + + err = node.server.Start() + if err != nil { + x.Errorf("failed to start server %d.", i) + return + } + + nodes[i] = &node + } +} + +func stopServers() { + for i := 0; i < NumNodes; i++ { + n := nodes[i] + if n != nil { + n.shh.Unwatch(n.filerId) + n.server.Stop() + } + } +} + +func checkPropagation(x *testing.T) { + if x.Failed() { + return + } + + const cycle = 100 + const iterations = 100 + + for j := 0; j < iterations; j++ { + time.Sleep(cycle * time.Millisecond) + + for i := 0; i < NumNodes; i++ { + f := nodes[i].shh.GetFilter(nodes[i].filerId) + if f == nil { + x.Errorf("failed to get filterId %d from node %d.", nodes[i].filerId, i) + return + } + + mail := f.Retrieve() + if !validateMail(x, i, mail) { + return + } + + if isTestComplete() { + + return + } + } + } + + x.Errorf("Test was not complete: timeout %d seconds.", iterations*cycle/1000) +} + +func validateMail(x *testing.T, index int, mail []*ReceivedMessage) bool { + var cnt int + for _, m := range mail { + if bytes.Compare(m.Payload, expectedMessage) == 0 { + cnt++ + } + } + + if cnt == 0 { + // no messages received yet: nothing is wrong + return true + } + if cnt > 1 { + x.Errorf("node %d received %d.", index, cnt) + return false + } + + if cnt > 0 { + result.mutex.Lock() + defer result.mutex.Unlock() + result.counter[index] += cnt + if result.counter[index] > 1 { + x.Errorf("node %d accumulated %d.", index, result.counter[index]) + return false + } + } + return true +} + +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) < 2 { + return false + } + } + + return true +} + +func sendMsg(x *testing.T, expected bool, id int) { + if x.Failed() { + return + } + + opt := MessageParams{KeySym: sharedKey, Topic: sharedTopic, Payload: expectedMessage, PoW: 0.00000001} + if !expected { + opt.KeySym[0]++ + opt.Topic[0]++ + opt.Payload = opt.Payload[1:] + } + + msg := NewSentMessage(&opt) + envelope, err := msg.Wrap(&opt) + if err != nil { + x.Errorf("failed to seal message.") + return + } + + err = nodes[id].shh.Send(envelope) + if err != nil { + x.Errorf("failed to send message.") + return + } +} + +func TestPeerBasic(x *testing.T) { + InitSingleTest() + + params, err := generateMessageParams() + if err != nil { + x.Errorf("failed 1 with seed %d.", seed) + return + } + + params.PoW = 0.001 + msg := NewSentMessage(params) + env, err := msg.Wrap(params) + if err != nil { + x.Errorf("failed 2 with seed %d.", seed) + return + } + + p := newPeer(nil, nil, nil) + p.mark(env) + if !p.marked(env) { + x.Errorf("failed 3 with seed %d.", seed) + return + } +} diff --git a/whisper/whisperv5/topic.go b/whisper/whisperv5/topic.go new file mode 100644 index 000000000..c29c344be --- /dev/null +++ b/whisper/whisperv5/topic.go @@ -0,0 +1,70 @@ +// 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. For formal details please see +// the specs at https://github.com/ethereum/wiki/wiki/Whisper-PoC-1-Protocol-Spec#topics. + +package whisperv5 + +import ( + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/common" +) + +// Topic 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 + +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 (topic *TopicType) String() string { + return string(common.ToHex(topic[:])) +} + +// UnmarshalJSON parses a hex representation to a topic. +func (t *TopicType) UnmarshalJSON(input []byte) error { + length := len(input) + if length >= 2 && input[0] == '"' && input[length-1] == '"' { + input = input[1 : length-1] + } + // strip "0x" for length check + if len(input) > 1 && strings.ToLower(string(input[:2])) == "0x" { + input = input[2:] + } + // validate the length of the input + if len(input) != TopicLength*2 { + return fmt.Errorf("unmarshalJSON failed: topic must be exactly %d bytes", TopicLength) + } + b := common.FromHex(string(input)) + if b == nil { + return fmt.Errorf("unmarshalJSON failed: wrong topic format") + } + *t = BytesToTopic(b) + return nil +} diff --git a/whisper/whisperv5/topic_test.go b/whisper/whisperv5/topic_test.go new file mode 100644 index 000000000..c2a940b79 --- /dev/null +++ b/whisper/whisperv5/topic_test.go @@ -0,0 +1,136 @@ +// 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 whisperv5 + +import "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(x *testing.T) { + for i, tst := range topicStringTests { + s := tst.topic.String() + if s != tst.str { + x.Errorf("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}, +} + +func TestBytesToTopic(x *testing.T) { + for i, tst := range bytesToTopicTests { + t := BytesToTopic(tst.data) + if t != tst.topic { + x.Errorf("failed test %d: have %v, want %v.", i, t, tst.topic) + } + } +} + +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")}, + {topic: TopicType{0x00, 0x00, 0x00, 0x00}, data: []byte("00000000")}, + {topic: TopicType{0x00, 0x80, 0x01, 0x00}, data: []byte("00800100")}, + {topic: TopicType{0x00, 0x7f, 0x80, 0xff}, data: []byte("007f80ff")}, + {topic: TopicType{0xff, 0x80, 0x7f, 0x00}, data: []byte("ff807f00")}, + {topic: TopicType{0xf2, 0x6e, 0x77, 0x79}, data: []byte("f26e7779")}, +} + +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("00000001")}, +} + +func TestUnmarshalTestsGood(x *testing.T) { + for i, tst := range unmarshalTestsGood { + var t TopicType + err := t.UnmarshalJSON(tst.data) + if err != nil { + x.Errorf("failed test %d. input: %v.", i, tst.data) + } else if t != tst.topic { + x.Errorf("failed test %d: have %v, want %v.", i, t, tst.topic) + } + } +} + +func TestUnmarshalTestsBad(x *testing.T) { + // in this test UnmarshalJSON() is supposed to fail + for i, tst := range unmarshalTestsBad { + var t TopicType + err := t.UnmarshalJSON(tst.data) + if err == nil { + x.Errorf("failed test %d. input: %v.", i, tst.data) + } + } +} + +func TestUnmarshalTestsUgly(x *testing.T) { + // in this test UnmarshalJSON() is NOT supposed to fail, but result should be wrong + for i, tst := range unmarshalTestsUgly { + var t TopicType + err := t.UnmarshalJSON(tst.data) + if err != nil { + x.Errorf("failed test %d. input: %v.", i, tst.data) + } else if t == tst.topic { + x.Errorf("failed test %d: have %v, want %v.", i, t, tst.topic) + } + } +} diff --git a/whisper/whisperv5/whisper.go b/whisper/whisperv5/whisper.go new file mode 100644 index 000000000..836810824 --- /dev/null +++ b/whisper/whisperv5/whisper.go @@ -0,0 +1,585 @@ +// 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 whisperv5 + +import ( + "bytes" + "crypto/ecdsa" + crand "crypto/rand" + "crypto/sha256" + "fmt" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rlp" + "golang.org/x/crypto/pbkdf2" + set "gopkg.in/fatih/set.v0" +) + +// Whisper represents a dark communication interface through the Ethereum +// network, using its very own P2P communication layer. +type Whisper struct { + protocol p2p.Protocol + filters *Filters + + privateKeys map[string]*ecdsa.PrivateKey + symKeys map[string][]byte + keyMu sync.RWMutex + + envelopes map[common.Hash]*Envelope // Pool of messages currently tracked by this node + messages map[common.Hash]*ReceivedMessage // Pool of successfully decrypted messages, which are not expired yet + expirations map[uint32]*set.SetNonTS // Message expiration pool + poolMu sync.RWMutex // Mutex to sync the message and expiration pools + + peers map[*Peer]struct{} // Set of currently active peers + peerMu sync.RWMutex // Mutex to sync the active peer set + + mailServer MailServer + + quit chan struct{} + test bool +} + +// New creates a Whisper client ready to communicate through the Ethereum P2P network. +// Param s should be passed if you want to implement mail server, otherwise nil. +func NewWhisper(server MailServer) *Whisper { + whisper := &Whisper{ + privateKeys: make(map[string]*ecdsa.PrivateKey), + symKeys: make(map[string][]byte), + envelopes: make(map[common.Hash]*Envelope), + messages: make(map[common.Hash]*ReceivedMessage), + expirations: make(map[uint32]*set.SetNonTS), + peers: make(map[*Peer]struct{}), + mailServer: server, + quit: make(chan struct{}), + } + whisper.filters = NewFilters(whisper) + + // p2p whisper sub protocol handler + whisper.protocol = p2p.Protocol{ + Name: ProtocolName, + Version: uint(ProtocolVersion), + Length: NumberOfMessageCodes, + Run: whisper.HandlePeer, + } + + return whisper +} + +// Protocols returns the whisper sub-protocols ran by this particular client. +func (w *Whisper) Protocols() []p2p.Protocol { + return []p2p.Protocol{w.protocol} +} + +// Version returns the whisper sub-protocols version number. +func (w *Whisper) Version() uint { + return w.protocol.Version +} + +func (w *Whisper) getPeer(peerID []byte) (*Peer, error) { + w.peerMu.Lock() + defer w.peerMu.Unlock() + for p, _ := range w.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) +} + +// MarkPeerTrusted marks specific peer trusted, which will allow it +// to send historic (expired) messages. +func (w *Whisper) MarkPeerTrusted(peerID []byte) error { + p, err := w.getPeer(peerID) + if err != nil { + return err + } + p.trusted = true + return nil +} + +func (w *Whisper) RequestHistoricMessages(peerID []byte, data []byte) error { + p, err := w.getPeer(peerID) + if err != nil { + return err + } + p.trusted = true + return p2p.Send(p.ws, mailRequestCode, data) +} + +func (w *Whisper) SendP2PMessage(peerID []byte, envelope *Envelope) error { + p, err := w.getPeer(peerID) + if err != nil { + return err + } + return p2p.Send(p.ws, p2pCode, envelope) +} + +// NewIdentity generates a new cryptographic identity for the client, and injects +// it into the known identities for message decryption. +func (w *Whisper) NewIdentity() *ecdsa.PrivateKey { + key, err := crypto.GenerateKey() + if err != nil || !validatePrivateKey(key) { + key, err = crypto.GenerateKey() // retry once + } + if err != nil { + panic(err) + } + if !validatePrivateKey(key) { + panic("Failed to generate valid key") + } + w.keyMu.Lock() + defer w.keyMu.Unlock() + w.privateKeys[common.ToHex(crypto.FromECDSAPub(&key.PublicKey))] = key + return key +} + +// DeleteIdentity deletes the specified key if it exists. +func (w *Whisper) DeleteIdentity(key string) { + w.keyMu.Lock() + defer w.keyMu.Unlock() + delete(w.privateKeys, key) +} + +// HasIdentity checks if the the whisper node is configured with the private key +// of the specified public pair. +func (w *Whisper) HasIdentity(pubKey string) bool { + w.keyMu.RLock() + defer w.keyMu.RUnlock() + return w.privateKeys[pubKey] != nil +} + +// GetIdentity retrieves the private key of the specified public identity. +func (w *Whisper) GetIdentity(pubKey string) *ecdsa.PrivateKey { + w.keyMu.RLock() + defer w.keyMu.RUnlock() + return w.privateKeys[pubKey] +} + +func (w *Whisper) GenerateSymKey(name string) error { + buf := make([]byte, aesKeyLength*2) + _, err := crand.Read(buf) // todo: check how safe is this function + if err != nil { + return err + } else if !validateSymmetricKey(buf) { + return fmt.Errorf("crypto/rand failed to generate random data") + } + + key := buf[:aesKeyLength] + salt := buf[aesKeyLength:] + derived, err := DeriveOneTimeKey(key, salt, EnvelopeVersion) + if err != nil { + return err + } else if !validateSymmetricKey(derived) { + return fmt.Errorf("failed to derive valid key") + } + + w.keyMu.Lock() + defer w.keyMu.Unlock() + + if w.symKeys[name] != nil { + return fmt.Errorf("Key with name [%s] already exists", name) + } + w.symKeys[name] = derived + return nil +} + +func (w *Whisper) AddSymKey(name string, key []byte) error { + if w.HasSymKey(name) { + return fmt.Errorf("Key with name [%s] already exists", name) + } + + derived, err := deriveKeyMaterial(key, EnvelopeVersion) + if err != nil { + return err + } + + w.keyMu.Lock() + defer w.keyMu.Unlock() + + // double check is necessary, because deriveKeyMaterial() is slow + if w.symKeys[name] != nil { + return fmt.Errorf("Key with name [%s] already exists", name) + } + w.symKeys[name] = derived + return nil +} + +func (w *Whisper) HasSymKey(name string) bool { + w.keyMu.RLock() + defer w.keyMu.RUnlock() + return w.symKeys[name] != nil +} + +func (w *Whisper) DeleteSymKey(name string) { + w.keyMu.Lock() + defer w.keyMu.Unlock() + delete(w.symKeys, name) +} + +func (w *Whisper) GetSymKey(name string) []byte { + w.keyMu.RLock() + defer w.keyMu.RUnlock() + return w.symKeys[name] +} + +// Watch installs a new message handler to run in case a matching packet arrives +// from the whisper network. +func (w *Whisper) Watch(f *Filter) int { + return w.filters.Install(f) +} + +func (w *Whisper) GetFilter(id int) *Filter { + return w.filters.Get(id) +} + +// Unwatch removes an installed message handler. +func (w *Whisper) Unwatch(id int) { + w.filters.Uninstall(id) +} + +// Send injects a message into the whisper send queue, to be distributed in the +// network in the coming cycles. +func (w *Whisper) Send(envelope *Envelope) error { + return w.add(envelope) +} + +// Start implements node.Service, starting the background data propagation thread +// of the Whisper protocol. +func (w *Whisper) Start(*p2p.Server) error { + glog.V(logger.Info).Infoln("Whisper started") + go w.update() + return nil +} + +// Stop implements node.Service, stopping the background data propagation thread +// of the Whisper protocol. +func (w *Whisper) Stop() error { + close(w.quit) + glog.V(logger.Info).Infoln("Whisper stopped") + return nil +} + +// handlePeer is called by the underlying P2P layer when the whisper sub-protocol +// connection is negotiated. +func (wh *Whisper) HandlePeer(peer *p2p.Peer, rw p2p.MsgReadWriter) error { + // Create the new peer and start tracking it + whisperPeer := newPeer(wh, peer, rw) + + wh.peerMu.Lock() + wh.peers[whisperPeer] = struct{}{} + wh.peerMu.Unlock() + + defer func() { + wh.peerMu.Lock() + delete(wh.peers, whisperPeer) + wh.peerMu.Unlock() + }() + + // Run the peer handshake and state updates + if err := whisperPeer.handshake(); err != nil { + return err + } + whisperPeer.start() + defer whisperPeer.stop() + + return wh.runMessageLoop(whisperPeer, rw) +} + +// runMessageLoop reads and processes inbound messages directly to merge into client-global state. +func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error { + for { + // fetch the next packet + packet, err := rw.ReadMsg() + if err != nil { + return err + } + + switch packet.Code { + case statusCode: + // this should not happen, but no need to panic; just ignore this message. + glog.V(logger.Warn).Infof("%v: unxepected status message received", p.peer) + case messagesCode: + // decode the contained envelopes + var envelopes []*Envelope + if err := packet.Decode(&envelopes); err != nil { + glog.V(logger.Warn).Infof("%v: failed to decode envelope: [%v], peer will be disconnected", p.peer, err) + return fmt.Errorf("garbage received") + } + // inject all envelopes into the internal pool + for _, envelope := range envelopes { + if err := wh.add(envelope); err != nil { + glog.V(logger.Warn).Infof("%v: bad envelope received: [%v], peer will be disconnected", p.peer, err) + return fmt.Errorf("invalid envelope") + } + p.mark(envelope) + if wh.mailServer != nil { + wh.mailServer.Archive(envelope) + } + } + case p2pCode: + // 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 envelopes []*Envelope + if err := packet.Decode(&envelopes); err != nil { + glog.V(logger.Warn).Infof("%v: failed to decode direct message: [%v], peer will be disconnected", p.peer, err) + return fmt.Errorf("garbage received (directMessage)") + } + for _, envelope := range envelopes { + wh.postEvent(envelope, p2pCode) + } + } + case mailRequestCode: + // Must be processed if mail server is implemented. Otherwise ignore. + if wh.mailServer != nil { + s := rlp.NewStream(packet.Payload, uint64(packet.Size)) + data, err := s.Bytes() + if err == nil { + wh.mailServer.DeliverMail(p, data) + } else { + glog.V(logger.Error).Infof("%v: bad requestHistoricMessages received: [%v]", p.peer, err) + } + } + 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. +func (wh *Whisper) add(envelope *Envelope) error { + now := uint32(time.Now().Unix()) + sent := envelope.Expiry - envelope.TTL + + if sent > now { + if sent-SynchAllowance > now { + return fmt.Errorf("message created in the future") + } else { + // recalculate PoW, adjusted for the time difference, plus one second for latency + envelope.calculatePoW(sent - now + 1) + } + } + + if envelope.Expiry < now { + if envelope.Expiry+SynchAllowance*2 < now { + return fmt.Errorf("very old message") + } else { + return nil // drop envelope without error + } + } + + if len(envelope.Data) > MaxMessageLength { + return fmt.Errorf("huge messages are not allowed") + } + + if len(envelope.Version) > 4 { + return fmt.Errorf("oversized Version") + } + + if len(envelope.AESNonce) > 12 { + // the standard AES GSM nonce size is 12, + // but const gcmStandardNonceSize cannot be accessed directly + return fmt.Errorf("oversized AESNonce") + } + + if len(envelope.Salt) > saltLength { + return fmt.Errorf("oversized Salt") + } + + if envelope.PoW() < MinimumPoW && !wh.test { + glog.V(logger.Debug).Infof("envelope with low PoW dropped: %f", envelope.PoW()) + return nil // drop envelope without error + } + + hash := envelope.Hash() + + wh.poolMu.Lock() + _, alreadyCached := wh.envelopes[hash] + if !alreadyCached { + wh.envelopes[hash] = envelope + if wh.expirations[envelope.Expiry] == nil { + wh.expirations[envelope.Expiry] = set.NewNonTS() + } + if !wh.expirations[envelope.Expiry].Has(hash) { + wh.expirations[envelope.Expiry].Add(hash) + } + } + wh.poolMu.Unlock() + + if alreadyCached { + glog.V(logger.Detail).Infof("whisper envelope already cached: %x\n", envelope) + } else { + wh.postEvent(envelope, messagesCode) // notify the local node about the new message + glog.V(logger.Detail).Infof("cached whisper envelope %v\n", envelope) + } + return nil +} + +// postEvent delivers the message to the watchers. +func (w *Whisper) postEvent(envelope *Envelope, messageCode uint64) { + // if the version of incoming message is higher than + // currently supported version, we can not decrypt it, + // and therefore just ignore this message + if envelope.Ver() <= EnvelopeVersion { + // todo: review if you need an additional thread here + go w.filters.NotifyWatchers(envelope, messageCode) + } +} + +// update loops until the lifetime of the whisper node, updating its internal +// state by expiring stale messages from the pool. +func (w *Whisper) update() { + // Start a ticker to check for expirations + expire := time.NewTicker(expirationCycle) + + // Repeat updates until termination is requested + for { + select { + case <-expire.C: + w.expire() + + case <-w.quit: + return + } + } +} + +// expire iterates over all the expiration timestamps, removing all stale +// messages from the pools. +func (w *Whisper) expire() { + w.poolMu.Lock() + defer w.poolMu.Unlock() + + now := uint32(time.Now().Unix()) + for then, hashSet := range w.expirations { + // Short circuit if a future time + if then > now { + continue + } + // Dump all expired messages and remove timestamp + hashSet.Each(func(v interface{}) bool { + delete(w.envelopes, v.(common.Hash)) + delete(w.messages, v.(common.Hash)) + return true + }) + w.expirations[then].Clear() + } +} + +// envelopes retrieves all the messages currently pooled by the node. +func (w *Whisper) Envelopes() []*Envelope { + w.poolMu.RLock() + defer w.poolMu.RUnlock() + + all := make([]*Envelope, 0, len(w.envelopes)) + for _, envelope := range w.envelopes { + all = append(all, envelope) + } + return all +} + +// Messages retrieves all the decrypted messages matching a filter id. +func (w *Whisper) Messages(id int) []*ReceivedMessage { + result := make([]*ReceivedMessage, 0) + w.poolMu.RLock() + defer w.poolMu.RUnlock() + + if filter := w.filters.Get(id); filter != nil { + for _, msg := range w.messages { + if filter.MatchMessage(msg) { + result = append(result, msg) + } + } + } + return result +} + +func (w *Whisper) addDecryptedMessage(msg *ReceivedMessage) { + w.poolMu.Lock() + defer w.poolMu.Unlock() + + w.messages[msg.EnvelopeHash] = msg +} + +func ValidatePublicKey(k *ecdsa.PublicKey) bool { + return k != nil && k.X != nil && k.Y != nil && k.X.Sign() != 0 && k.Y.Sign() != 0 +} + +func validatePrivateKey(k *ecdsa.PrivateKey) bool { + if k == nil || k.D == nil || k.D.Sign() == 0 { + return false + } + return ValidatePublicKey(&k.PublicKey) +} + +// validateSymmetricKey returns false if the key contains all zeros +func validateSymmetricKey(k []byte) bool { + return len(k) > 0 && !containsOnlyZeros(k) +} + +func containsOnlyZeros(data []byte) bool { + for _, b := range data { + if b != 0 { + return false + } + } + return true +} + +func bytesToIntLittleEndian(b []byte) (res uint64) { + mul := uint64(1) + for i := 0; i < len(b); i++ { + res += uint64(b[i]) * mul + mul *= 256 + } + return res +} + +func BytesToIntBigEndian(b []byte) (res uint64) { + for i := 0; i < len(b); i++ { + res *= 256 + res += uint64(b[i]) + } + return res +} + +// DeriveSymmetricKey derives symmetric key material from the key or password. +// pbkdf2 is used for security, in case people use password instead of randomly generated keys. +func deriveKeyMaterial(key []byte, version uint64) (derivedKey []byte, err error) { + if version == 0 { + // kdf should run no less than 0.1 seconds on average compute, + // because it's a once in a session experience + derivedKey := pbkdf2.Key(key, nil, 65356, aesKeyLength, sha256.New) + return derivedKey, nil + } else { + return nil, unknownVersionError(version) + } +} diff --git a/whisper/whisperv5/whisper_test.go b/whisper/whisperv5/whisper_test.go new file mode 100644 index 000000000..1db26265a --- /dev/null +++ b/whisper/whisperv5/whisper_test.go @@ -0,0 +1,377 @@ +// 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 whisperv5 + +import ( + "bytes" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +func TestWhisperBasic(x *testing.T) { + w := NewWhisper(nil) + p := w.Protocols() + shh := p[0] + if shh.Name != ProtocolName { + x.Errorf("failed Protocol Name: %v.", shh.Name) + return + } + if uint64(shh.Version) != ProtocolVersion { + x.Errorf("failed Protocol Version: %v.", shh.Version) + return + } + if shh.Length != NumberOfMessageCodes { + x.Errorf("failed Protocol Length: %v.", shh.Length) + return + } + if shh.Run == nil { + x.Errorf("failed shh.Run.") + return + } + if uint64(w.Version()) != ProtocolVersion { + x.Errorf("failed whisper Version: %v.", shh.Version) + return + } + if w.GetFilter(0) != nil { + x.Errorf("failed GetFilter.") + return + } + + peerID := make([]byte, 64) + randomize(peerID) + peer, err := w.getPeer(peerID) + if peer != nil { + x.Errorf("failed GetPeer.") + return + } + err = w.MarkPeerTrusted(peerID) + if err == nil { + x.Errorf("failed MarkPeerTrusted.") + return + } + err = w.RequestHistoricMessages(peerID, peerID) + if err == nil { + x.Errorf("failed RequestHistoricMessages.") + return + } + err = w.SendP2PMessage(peerID, nil) + if err == nil { + x.Errorf("failed SendP2PMessage.") + return + } + exist := w.HasSymKey("non-existing") + if exist { + x.Errorf("failed HasSymKey.") + return + } + key := w.GetSymKey("non-existing") + if key != nil { + x.Errorf("failed GetSymKey.") + return + } + mail := w.Envelopes() + if len(mail) != 0 { + x.Errorf("failed w.Envelopes().") + return + } + m := w.Messages(0) + if len(m) != 0 { + x.Errorf("failed w.Messages.") + return + } + + var derived []byte + ver := uint64(0xDEADBEEF) + derived, err = deriveKeyMaterial(peerID, ver) + if err != unknownVersionError(ver) { + x.Errorf("failed deriveKeyMaterial 1 with param = %v: %s.", peerID, err) + return + } + derived, err = deriveKeyMaterial(peerID, 0) + if err != nil { + x.Errorf("failed deriveKeyMaterial 2 with param = %v: %s.", peerID, err) + return + } + if !validateSymmetricKey(derived) { + x.Errorf("failed validateSymmetricKey with param = %v.", derived) + return + } + if containsOnlyZeros(derived) { + x.Errorf("failed containsOnlyZeros with param = %v.", derived) + return + } + + buf := []byte{0xFF, 0xE5, 0x80, 0x2, 0} + le := bytesToIntLittleEndian(buf) + be := BytesToIntBigEndian(buf) + if le != uint64(0x280e5ff) { + x.Errorf("failed bytesToIntLittleEndian: %d.", le) + return + } + if be != uint64(0xffe5800200) { + x.Errorf("failed BytesToIntBigEndian: %d.", be) + return + } + + pk := w.NewIdentity() + if !validatePrivateKey(pk) { + x.Errorf("failed validatePrivateKey: %v.", pk) + return + } + if !ValidatePublicKey(&pk.PublicKey) { + x.Errorf("failed ValidatePublicKey: %v.", pk) + return + } +} + +func TestWhisperIdentityManagement(x *testing.T) { + w := NewWhisper(nil) + id1 := w.NewIdentity() + id2 := w.NewIdentity() + pub1 := common.ToHex(crypto.FromECDSAPub(&id1.PublicKey)) + pub2 := common.ToHex(crypto.FromECDSAPub(&id2.PublicKey)) + pk1 := w.GetIdentity(pub1) + pk2 := w.GetIdentity(pub2) + if !w.HasIdentity(pub1) { + x.Errorf("failed HasIdentity 1.") + return + } + if !w.HasIdentity(pub2) { + x.Errorf("failed HasIdentity 2.") + return + } + if pk1 != id1 { + x.Errorf("failed GetIdentity 3.") + return + } + if pk2 != id2 { + x.Errorf("failed GetIdentity 4.") + return + } + + // Delete one identity + w.DeleteIdentity(pub1) + pk1 = w.GetIdentity(pub1) + pk2 = w.GetIdentity(pub2) + if w.HasIdentity(pub1) { + x.Errorf("failed HasIdentity 11.") + return + } + if !w.HasIdentity(pub2) { + x.Errorf("failed HasIdentity 12.") + return + } + if pk1 != nil { + x.Errorf("failed GetIdentity 13.") + return + } + if pk2 != id2 { + x.Errorf("failed GetIdentity 14.") + return + } + + // Delete again non-existing identity + w.DeleteIdentity(pub1) + pk1 = w.GetIdentity(pub1) + pk2 = w.GetIdentity(pub2) + if w.HasIdentity(pub1) { + x.Errorf("failed HasIdentity 21.") + return + } + if !w.HasIdentity(pub2) { + x.Errorf("failed HasIdentity 22.") + return + } + if pk1 != nil { + x.Errorf("failed GetIdentity 23.") + return + } + if pk2 != id2 { + x.Errorf("failed GetIdentity 24.") + return + } + + // Delete second identity + w.DeleteIdentity(pub2) + pk1 = w.GetIdentity(pub1) + pk2 = w.GetIdentity(pub2) + if w.HasIdentity(pub1) { + x.Errorf("failed HasIdentity 31.") + return + } + if w.HasIdentity(pub2) { + x.Errorf("failed HasIdentity 32.") + return + } + if pk1 != nil { + x.Errorf("failed GetIdentity 33.") + return + } + if pk2 != nil { + x.Errorf("failed GetIdentity 34.") + return + } +} + +func TestWhisperSymKeyManagement(x *testing.T) { + InitSingleTest() + + var k1, k2 []byte + w := NewWhisper(nil) + id1 := string("arbitrary-string-1") + id2 := string("arbitrary-string-2") + + err := w.GenerateSymKey(id1) + if err != nil { + x.Errorf("failed test case 1 with seed %d: %s.", seed, err) + return + } + + k1 = w.GetSymKey(id1) + k2 = w.GetSymKey(id2) + if !w.HasSymKey(id1) { + x.Errorf("failed HasIdentity 2.") + return + } + if w.HasSymKey(id2) { + x.Errorf("failed HasIdentity 3.") + return + } + if k1 == nil { + x.Errorf("failed GetIdentity 4.") + return + } + if k2 != nil { + x.Errorf("failed GetIdentity 5.") + return + } + + // add existing id, nothing should change + randomKey := make([]byte, 16) + randomize(randomKey) + err = w.AddSymKey(id1, randomKey) + if err == nil { + x.Errorf("failed test case 10 with seed %d.", seed) + return + } + + k1 = w.GetSymKey(id1) + k2 = w.GetSymKey(id2) + if !w.HasSymKey(id1) { + x.Errorf("failed HasIdentity 12.") + return + } + if w.HasSymKey(id2) { + x.Errorf("failed HasIdentity 13.") + return + } + if k1 == nil { + x.Errorf("failed GetIdentity 14.") + return + } + if bytes.Compare(k1, randomKey) == 0 { + x.Errorf("failed GetIdentity 15: k1 == randomKey.") + return + } + if k2 != nil { + x.Errorf("failed GetIdentity 16.") + return + } + + err = w.AddSymKey(id2, randomKey) // add non-existing (yet) + if err != nil { + x.Errorf("failed test case 21 with seed %d: %s.", seed, err) + return + } + k1 = w.GetSymKey(id1) + k2 = w.GetSymKey(id2) + if !w.HasSymKey(id1) { + x.Errorf("failed HasIdentity 22.") + return + } + if !w.HasSymKey(id2) { + x.Errorf("failed HasIdentity 23.") + return + } + if k1 == nil { + x.Errorf("failed GetIdentity 24.") + return + } + if k2 == nil { + x.Errorf("failed GetIdentity 25.") + return + } + if bytes.Compare(k1, k2) == 0 { + x.Errorf("failed GetIdentity 26.") + return + } + if bytes.Compare(k1, randomKey) == 0 { + x.Errorf("failed GetIdentity 27.") + return + } + if len(k1) != aesKeyLength { + x.Errorf("failed GetIdentity 28.") + return + } + if len(k2) != aesKeyLength { + x.Errorf("failed GetIdentity 29.") + return + } + + w.DeleteSymKey(id1) + k1 = w.GetSymKey(id1) + k2 = w.GetSymKey(id2) + if w.HasSymKey(id1) { + x.Errorf("failed HasIdentity 31.") + return + } + if !w.HasSymKey(id2) { + x.Errorf("failed HasIdentity 32.") + return + } + if k1 != nil { + x.Errorf("failed GetIdentity 33.") + return + } + if k2 == nil { + x.Errorf("failed GetIdentity 34.") + return + } + + w.DeleteSymKey(id1) + w.DeleteSymKey(id2) + k1 = w.GetSymKey(id1) + k2 = w.GetSymKey(id2) + if w.HasSymKey(id1) { + x.Errorf("failed HasIdentity 41.") + return + } + if w.HasSymKey(id2) { + x.Errorf("failed HasIdentity 42.") + return + } + if k1 != nil { + x.Errorf("failed GetIdentity 43.") + return + } + if k2 != nil { + x.Errorf("failed GetIdentity 44.") + return + } +} -- cgit v1.2.3