From 79789af2e7fce8807d21a8eedbf42d41a7c55848 Mon Sep 17 00:00:00 2001 From: gluk256 Date: Sat, 29 Oct 2016 14:11:37 +0200 Subject: [PATCH] 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/shhapi/api.go | 504 ++++++++++++++++ whisper/shhapi/api_test.go | 170 ++++++ whisper/{ => whisperv2}/api.go | 2 +- whisper/{ => whisperv2}/doc.go | 2 +- whisper/{ => whisperv2}/envelope.go | 2 +- whisper/{ => whisperv2}/envelope_test.go | 2 +- whisper/{ => whisperv2}/filter.go | 2 +- whisper/{ => whisperv2}/filter_test.go | 2 +- whisper/{ => whisperv2}/main.go | 0 whisper/{ => whisperv2}/message.go | 2 +- whisper/{ => whisperv2}/message_test.go | 2 +- whisper/{ => whisperv2}/peer.go | 2 +- whisper/{ => whisperv2}/peer_test.go | 2 +- whisper/{ => whisperv2}/topic.go | 2 +- whisper/{ => whisperv2}/topic_test.go | 2 +- whisper/{ => whisperv2}/whisper.go | 2 +- whisper/{ => whisperv2}/whisper_test.go | 2 +- 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 ++++++++++++ 32 files changed, 4449 insertions(+), 16 deletions(-) create mode 100644 whisper/shhapi/api.go create mode 100644 whisper/shhapi/api_test.go rename whisper/{ => whisperv2}/api.go (99%) rename whisper/{ => whisperv2}/doc.go (98%) rename whisper/{ => whisperv2}/envelope.go (99%) rename whisper/{ => whisperv2}/envelope_test.go (99%) rename whisper/{ => whisperv2}/filter.go (99%) rename whisper/{ => whisperv2}/filter_test.go (99%) rename whisper/{ => whisperv2}/main.go (100%) rename whisper/{ => whisperv2}/message.go (99%) rename whisper/{ => whisperv2}/message_test.go (99%) rename whisper/{ => whisperv2}/peer.go (99%) rename whisper/{ => whisperv2}/peer_test.go (99%) rename whisper/{ => whisperv2}/topic.go (99%) rename whisper/{ => whisperv2}/topic_test.go (99%) rename whisper/{ => whisperv2}/whisper.go (99%) rename whisper/{ => whisperv2}/whisper_test.go (99%) 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/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/api.go b/whisper/whisperv2/api.go similarity index 99% rename from whisper/api.go rename to whisper/whisperv2/api.go index d1f454a1f..9c9c6a84c 100644 --- a/whisper/api.go +++ b/whisper/whisperv2/api.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package whisper +package whisperv2 import ( "encoding/json" diff --git a/whisper/doc.go b/whisper/whisperv2/doc.go similarity index 98% rename from whisper/doc.go rename to whisper/whisperv2/doc.go index cfb0b5117..7252f44b1 100644 --- a/whisper/doc.go +++ b/whisper/whisperv2/doc.go @@ -29,4 +29,4 @@ Whisper is a pure identity-based messaging system. Whisper provides a low-level or prejudiced by the low-level hardware attributes and characteristics, particularly the notion of singular endpoints. */ -package whisper +package whisperv2 diff --git a/whisper/envelope.go b/whisper/whisperv2/envelope.go similarity index 99% rename from whisper/envelope.go rename to whisper/whisperv2/envelope.go index 97d489b96..7110ab457 100644 --- a/whisper/envelope.go +++ b/whisper/whisperv2/envelope.go @@ -17,7 +17,7 @@ // 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 +package whisperv2 import ( "crypto/ecdsa" diff --git a/whisper/envelope_test.go b/whisper/whisperv2/envelope_test.go similarity index 99% rename from whisper/envelope_test.go rename to whisper/whisperv2/envelope_test.go index 3bfe52737..75e2fbe8a 100644 --- a/whisper/envelope_test.go +++ b/whisper/whisperv2/envelope_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package whisper +package whisperv2 import ( "bytes" diff --git a/whisper/filter.go b/whisper/whisperv2/filter.go similarity index 99% rename from whisper/filter.go rename to whisper/whisperv2/filter.go index 9f6d6b781..8ce4a54fb 100644 --- a/whisper/filter.go +++ b/whisper/whisperv2/filter.go @@ -16,7 +16,7 @@ // Contains the message filter for fine grained subscriptions. -package whisper +package whisperv2 import ( "crypto/ecdsa" diff --git a/whisper/filter_test.go b/whisper/whisperv2/filter_test.go similarity index 99% rename from whisper/filter_test.go rename to whisper/whisperv2/filter_test.go index b805b2baf..5a14a84bb 100644 --- a/whisper/filter_test.go +++ b/whisper/whisperv2/filter_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package whisper +package whisperv2 import ( "bytes" diff --git a/whisper/main.go b/whisper/whisperv2/main.go similarity index 100% rename from whisper/main.go rename to whisper/whisperv2/main.go diff --git a/whisper/message.go b/whisper/whisperv2/message.go similarity index 99% rename from whisper/message.go rename to whisper/whisperv2/message.go index f05b5d8b5..7ef9d0912 100644 --- a/whisper/message.go +++ b/whisper/whisperv2/message.go @@ -17,7 +17,7 @@ // 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 +package whisperv2 import ( "crypto/ecdsa" diff --git a/whisper/message_test.go b/whisper/whisperv2/message_test.go similarity index 99% rename from whisper/message_test.go rename to whisper/whisperv2/message_test.go index 921c967a9..efa64e431 100644 --- a/whisper/message_test.go +++ b/whisper/whisperv2/message_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package whisper +package whisperv2 import ( "bytes" diff --git a/whisper/peer.go b/whisper/whisperv2/peer.go similarity index 99% rename from whisper/peer.go rename to whisper/whisperv2/peer.go index ee10e66e7..404ebd513 100644 --- a/whisper/peer.go +++ b/whisper/whisperv2/peer.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package whisper +package whisperv2 import ( "fmt" diff --git a/whisper/peer_test.go b/whisper/whisperv2/peer_test.go similarity index 99% rename from whisper/peer_test.go rename to whisper/whisperv2/peer_test.go index 636bd8ca1..9755e134c 100644 --- a/whisper/peer_test.go +++ b/whisper/whisperv2/peer_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package whisper +package whisperv2 import ( "testing" diff --git a/whisper/topic.go b/whisper/whisperv2/topic.go similarity index 99% rename from whisper/topic.go rename to whisper/whisperv2/topic.go index d37eb25ee..3e2b47bd3 100644 --- a/whisper/topic.go +++ b/whisper/whisperv2/topic.go @@ -17,7 +17,7 @@ // 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 +package whisperv2 import "github.com/ethereum/go-ethereum/crypto" diff --git a/whisper/topic_test.go b/whisper/whisperv2/topic_test.go similarity index 99% rename from whisper/topic_test.go rename to whisper/whisperv2/topic_test.go index 9c45f6740..efd4a2c61 100644 --- a/whisper/topic_test.go +++ b/whisper/whisperv2/topic_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package whisper +package whisperv2 import ( "bytes" diff --git a/whisper/whisper.go b/whisper/whisperv2/whisper.go similarity index 99% rename from whisper/whisper.go rename to whisper/whisperv2/whisper.go index 0a49c1000..d9054959e 100644 --- a/whisper/whisper.go +++ b/whisper/whisperv2/whisper.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package whisper +package whisperv2 import ( "crypto/ecdsa" diff --git a/whisper/whisper_test.go b/whisper/whisperv2/whisper_test.go similarity index 99% rename from whisper/whisper_test.go rename to whisper/whisperv2/whisper_test.go index 094df373c..1e0d3f85d 100644 --- a/whisper/whisper_test.go +++ b/whisper/whisperv2/whisper_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package whisper +package whisperv2 import ( "testing" 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 + } +}