From 95f0bd0acf301bf8415747c4ff050e8a4dfdc864 Mon Sep 17 00:00:00 2001 From: gluk256 Date: Wed, 26 Apr 2017 21:05:48 +0200 Subject: [PATCH] whisper: message format refactoring (#14335) * whisper: salt removed from AES encryption * whisper: padding format updated * whisper: padding test added * whisper: padding refactored, tests fixed * whisper: padding test updated * whisper: wnode bugfix * whisper: send/receive protocol updated * whisper: minor update * whisper: bugfix in test * whisper: updated parameter names and comments * whisper: functions renamed * whisper: minor refactoring --- cmd/wnode/main.go | 31 +++--- whisper/mailserver/server_test.go | 13 ++- whisper/whisperv5/api.go | 47 ++++++--- whisper/whisperv5/api_test.go | 8 +- whisper/whisperv5/benchmarks_test.go | 20 ++-- whisper/whisperv5/doc.go | 16 ++- whisper/whisperv5/envelope.go | 27 +++--- whisper/whisperv5/filter_test.go | 39 ++++++-- whisper/whisperv5/message.go | 133 +++++++++++++------------ whisper/whisperv5/message_test.go | 140 ++++++++++++++++++++------- whisper/whisperv5/peer.go | 21 ++-- whisper/whisperv5/peer_test.go | 10 +- whisper/whisperv5/whisper.go | 55 ++++------- whisper/whisperv5/whisper_test.go | 15 ++- 14 files changed, 343 insertions(+), 232 deletions(-) diff --git a/cmd/wnode/main.go b/cmd/wnode/main.go index 23b1804872..f9b689b65f 100644 --- a/cmd/wnode/main.go +++ b/cmd/wnode/main.go @@ -65,7 +65,7 @@ var ( pub *ecdsa.PublicKey asymKey *ecdsa.PrivateKey nodeid *ecdsa.PrivateKey - topic []byte + topic whisper.TopicType asymKeyID string filterID string symPass string @@ -84,7 +84,7 @@ var ( testMode = flag.Bool("test", false, "use of predefined parameters for diagnostics") echoMode = flag.Bool("echo", false, "echo mode: prints some arguments for diagnostics") - argVerbosity = flag.Int("verbosity", int(log.LvlWarn), "log verbosity level") + argVerbosity = flag.Int("verbosity", int(log.LvlError), "log verbosity level") argTTL = flag.Uint("ttl", 30, "time-to-live for messages in seconds") argWorkTime = flag.Uint("work", 5, "work time in seconds") argMaxSize = flag.Int("maxsize", whisper.DefaultMaxMessageLength, "max size of message") @@ -129,7 +129,7 @@ func processArgs() { if err != nil { utils.Fatalf("Failed to parse the topic: %s", err) } - topic = x + topic = whisper.BytesToTopic(x) } if *asymmetricMode && len(*argPub) > 0 { @@ -307,7 +307,11 @@ func configureNode() { if *asymmetricMode { if len(*argPub) == 0 { s := scanLine("Please enter the peer's public key: ") - pub = crypto.ToECDSAPub(common.FromHex(s)) + b := common.FromHex(s) + if b == nil { + utils.Fatalf("Error: can not convert hexadecimal string") + } + pub = crypto.ToECDSAPub(b) if !isKeyValid(pub) { utils.Fatalf("Error: invalid public key") } @@ -354,7 +358,7 @@ func configureNode() { filter := whisper.Filter{ KeySym: symKey, KeyAsym: asymKey, - Topics: [][]byte{topic}, + Topics: [][]byte{topic[:]}, AllowP2P: p2pAccept, } filterID, err = shh.Subscribe(&filter) @@ -365,7 +369,7 @@ func configureNode() { } func generateTopic(password []byte) { - x := pbkdf2.Key(password, password, 8196, 128, sha512.New) + x := pbkdf2.Key(password, password, 4096, 128, sha512.New) for i := 0; i < len(x); i++ { topic[i%whisper.TopicLength] ^= x[i] } @@ -485,16 +489,15 @@ func sendMsg(payload []byte) common.Hash { Dst: pub, KeySym: symKey, Payload: payload, - Topic: whisper.BytesToTopic(topic), + Topic: topic, TTL: uint32(*argTTL), PoW: *argPoW, WorkTime: uint32(*argWorkTime), } - msg := whisper.NewSentMessage(¶ms) - if msg == nil { - fmt.Printf("failed to create new message (OS level error)") - os.Exit(0) + msg, err := whisper.NewSentMessage(¶ms) + if err != nil { + utils.Fatalf("failed to create new message: %s", err) } envelope, err := msg.Wrap(¶ms) if err != nil { @@ -624,9 +627,9 @@ func requestExpiredMessagesLoop() { params.Src = nodeid params.WorkTime = 5 - msg := whisper.NewSentMessage(¶ms) - if msg == nil { - utils.Fatalf("failed to create new message (OS level error)") + msg, err := whisper.NewSentMessage(¶ms) + if err != nil { + utils.Fatalf("failed to create new message: %s", err) } env, err := msg.Wrap(¶ms) if err != nil { diff --git a/whisper/mailserver/server_test.go b/whisper/mailserver/server_test.go index 15652ea4ad..64dbcd7838 100644 --- a/whisper/mailserver/server_test.go +++ b/whisper/mailserver/server_test.go @@ -58,15 +58,19 @@ func TestDBKey(t *testing.T) { } func generateEnvelope(t *testing.T) *whisper.Envelope { + h := crypto.Keccak256Hash([]byte("test sample data")) params := &whisper.MessageParams{ - KeySym: []byte("test key"), + KeySym: h[:], Topic: whisper.TopicType{}, Payload: []byte("test payload"), PoW: powRequirement, WorkTime: 2, } - msg := whisper.NewSentMessage(params) + msg, err := whisper.NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := msg.Wrap(params) if err != nil { t.Fatalf("failed to wrap with seed %d: %s.", seed, err) @@ -188,7 +192,10 @@ func createRequest(t *testing.T, p *ServerTestParams) *whisper.Envelope { Src: p.key, } - msg := whisper.NewSentMessage(params) + msg, err := whisper.NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := msg.Wrap(params) if err != nil { t.Fatalf("failed to wrap with seed %d: %s.", seed, err) diff --git a/whisper/whisperv5/api.go b/whisper/whisperv5/api.go index 579efba9e3..841bbc2bab 100644 --- a/whisper/whisperv5/api.go +++ b/whisper/whisperv5/api.go @@ -214,7 +214,6 @@ func (api *PublicWhisperAPI) Subscribe(args WhisperFilterArgs) (string, error) { } filter := Filter{ - Src: crypto.ToECDSAPub(common.FromHex(args.SignedWith)), PoW: args.MinPoW, Messages: make(map[common.Hash]*ReceivedMessage), AllowP2P: args.AllowP2P, @@ -233,6 +232,11 @@ func (api *PublicWhisperAPI) Subscribe(args WhisperFilterArgs) (string, error) { } if len(args.SignedWith) > 0 { + sb := common.FromHex(args.SignedWith) + if sb == nil { + return "", errors.New("subscribe: SignedWith parameter is invalid") + } + filter.Src = crypto.ToECDSAPub(sb) if !ValidatePublicKey(filter.Src) { return "", errors.New("subscribe: invalid 'SignedWith' field") } @@ -269,9 +273,10 @@ func (api *PublicWhisperAPI) Unsubscribe(id string) { api.whisper.Unsubscribe(id) } -// GetSubscriptionMessages retrieves all the new messages matched by a filter since the last retrieval. -func (api *PublicWhisperAPI) GetSubscriptionMessages(filterId string) []*WhisperMessage { - f := api.whisper.GetFilter(filterId) +// GetSubscriptionMessages retrieves all the new messages matched by the corresponding +// subscription filter since the last retrieval. +func (api *PublicWhisperAPI) GetNewSubscriptionMessages(id string) []*WhisperMessage { + f := api.whisper.GetFilter(id) if f != nil { newMail := f.Retrieve() return toWhisperMessages(newMail) @@ -279,10 +284,10 @@ func (api *PublicWhisperAPI) GetSubscriptionMessages(filterId string) []*Whisper return toWhisperMessages(nil) } -// GetMessages retrieves all the floating messages that match a specific filter. +// GetMessages retrieves all the floating messages that match a specific subscription filter. // It is likely to be called once per session, right after Subscribe call. -func (api *PublicWhisperAPI) GetMessages(filterId string) []*WhisperMessage { - all := api.whisper.Messages(filterId) +func (api *PublicWhisperAPI) GetFloatingMessages(id string) []*WhisperMessage { + all := api.whisper.Messages(id) return toWhisperMessages(all) } @@ -345,7 +350,11 @@ func (api *PublicWhisperAPI) Post(args PostArgs) error { return errors.New("post: topic is missing for symmetric encryption") } } else if args.Type == "asym" { - params.Dst = crypto.ToECDSAPub(common.FromHex(args.Key)) + kb := common.FromHex(args.Key) + if kb == nil { + return errors.New("post: public key for asymmetric encryption is invalid") + } + params.Dst = crypto.ToECDSAPub(kb) if !ValidatePublicKey(params.Dst) { return errors.New("post: public key for asymmetric encryption is invalid") } @@ -354,9 +363,9 @@ func (api *PublicWhisperAPI) Post(args PostArgs) error { } // encrypt and send - message := NewSentMessage(¶ms) - if message == nil { - return errors.New("post: failed create new message, probably due to failed rand function (OS level)") + message, err := NewSentMessage(¶ms) + if err != nil { + return err } envelope, err := message.Wrap(¶ms) if err != nil { @@ -383,7 +392,7 @@ type PostArgs struct { Type string `json:"type"` // "sym"/"asym" (symmetric or asymmetric) TTL uint32 `json:"ttl"` // time-to-live in seconds SignWith string `json:"signWith"` // id of the signing key - Key string `json:"key"` // id of encryption key + Key string `json:"key"` // key id (in case of sym) or public key (in case of asym) Topic hexutil.Bytes `json:"topic"` // topic (4 bytes) Padding hexutil.Bytes `json:"padding"` // optional padding bytes Payload hexutil.Bytes `json:"payload"` // payload to be encrypted @@ -474,7 +483,6 @@ type WhisperMessage struct { // NewWhisperMessage converts an internal message into an API version. func NewWhisperMessage(message *ReceivedMessage) *WhisperMessage { msg := WhisperMessage{ - Topic: common.ToHex(message.Topic[:]), Payload: common.ToHex(message.Payload), Padding: common.ToHex(message.Padding), Timestamp: message.Sent, @@ -483,11 +491,20 @@ func NewWhisperMessage(message *ReceivedMessage) *WhisperMessage { Hash: common.ToHex(message.EnvelopeHash.Bytes()), } + if len(message.Topic) == TopicLength { + msg.Topic = common.ToHex(message.Topic[:]) + } if message.Dst != nil { - msg.Dst = common.ToHex(crypto.FromECDSAPub(message.Dst)) + b := crypto.FromECDSAPub(message.Dst) + if b != nil { + msg.Dst = common.ToHex(b) + } } if isMessageSigned(message.Raw[0]) { - msg.Src = common.ToHex(crypto.FromECDSAPub(message.SigToPubKey())) + b := crypto.FromECDSAPub(message.SigToPubKey()) + if b != nil { + msg.Src = common.ToHex(b) + } } return &msg } diff --git a/whisper/whisperv5/api_test.go b/whisper/whisperv5/api_test.go index 9207c6f109..c837b0a145 100644 --- a/whisper/whisperv5/api_test.go +++ b/whisper/whisperv5/api_test.go @@ -43,7 +43,7 @@ func TestBasic(t *testing.T) { t.Fatalf("wrong version: %d.", ver) } - mail := api.GetSubscriptionMessages("non-existent-id") + mail := api.GetNewSubscriptionMessages("non-existent-id") if len(mail) != 0 { t.Fatalf("failed GetFilterChanges: premature result") } @@ -282,7 +282,7 @@ func waitForMessages(api *PublicWhisperAPI, id string, target int) []*WhisperMes // timeout: 2 seconds result := make([]*WhisperMessage, 0, target) for i := 0; i < 100; i++ { - mail := api.GetSubscriptionMessages(id) + mail := api.GetNewSubscriptionMessages(id) if len(mail) > 0 { for _, m := range mail { result = append(result, m) @@ -448,7 +448,7 @@ func TestIntegrationSym(t *testing.T) { f.Topics = make([][]byte, 2) f.Topics[0] = topics[0][:] f.Topics[1] = topics[1][:] - f.MinPoW = 0.324 + f.MinPoW = DefaultMinimumPoW / 2 f.SignedWith = sigPubKey.String() f.AllowP2P = false @@ -546,7 +546,7 @@ func TestIntegrationSymWithFilter(t *testing.T) { f.Topics = make([][]byte, 2) f.Topics[0] = topics[0][:] f.Topics[1] = topics[1][:] - f.MinPoW = 0.324 + f.MinPoW = DefaultMinimumPoW / 2 f.SignedWith = sigPubKey.String() f.AllowP2P = false diff --git a/whisper/whisperv5/benchmarks_test.go b/whisper/whisperv5/benchmarks_test.go index 417b2881b8..dcfbcb56d8 100644 --- a/whisper/whisperv5/benchmarks_test.go +++ b/whisper/whisperv5/benchmarks_test.go @@ -28,12 +28,6 @@ func BenchmarkDeriveKeyMaterial(b *testing.B) { } } -func BenchmarkDeriveOneTimeKey(b *testing.B) { - for i := 0; i < b.N; i++ { - DeriveOneTimeKey([]byte("test value 1"), []byte("test value 2"), 0) - } -} - func BenchmarkEncryptionSym(b *testing.B) { InitSingleTest() @@ -43,7 +37,7 @@ func BenchmarkEncryptionSym(b *testing.B) { } for i := 0; i < b.N; i++ { - msg := NewSentMessage(params) + msg, _ := NewSentMessage(params) _, err := msg.Wrap(params) if err != nil { b.Errorf("failed Wrap with seed %d: %s.", seed, err) @@ -68,7 +62,7 @@ func BenchmarkEncryptionAsym(b *testing.B) { params.Dst = &key.PublicKey for i := 0; i < b.N; i++ { - msg := NewSentMessage(params) + msg, _ := NewSentMessage(params) _, err := msg.Wrap(params) if err != nil { b.Fatalf("failed Wrap with seed %d: %s.", seed, err) @@ -83,7 +77,7 @@ func BenchmarkDecryptionSymValid(b *testing.B) { if err != nil { b.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) } - msg := NewSentMessage(params) + msg, _ := NewSentMessage(params) env, err := msg.Wrap(params) if err != nil { b.Fatalf("failed Wrap with seed %d: %s.", seed, err) @@ -105,7 +99,7 @@ func BenchmarkDecryptionSymInvalid(b *testing.B) { if err != nil { b.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) } - msg := NewSentMessage(params) + msg, _ := NewSentMessage(params) env, err := msg.Wrap(params) if err != nil { b.Fatalf("failed Wrap with seed %d: %s.", seed, err) @@ -134,7 +128,7 @@ func BenchmarkDecryptionAsymValid(b *testing.B) { f := Filter{KeyAsym: key} params.KeySym = nil params.Dst = &key.PublicKey - msg := NewSentMessage(params) + msg, _ := NewSentMessage(params) env, err := msg.Wrap(params) if err != nil { b.Fatalf("failed Wrap with seed %d: %s.", seed, err) @@ -161,7 +155,7 @@ func BenchmarkDecryptionAsymInvalid(b *testing.B) { } params.KeySym = nil params.Dst = &key.PublicKey - msg := NewSentMessage(params) + msg, _ := NewSentMessage(params) env, err := msg.Wrap(params) if err != nil { b.Fatalf("failed Wrap with seed %d: %s.", seed, err) @@ -203,7 +197,7 @@ func BenchmarkPoW(b *testing.B) { for i := 0; i < b.N; i++ { increment(params.Payload) - msg := NewSentMessage(params) + msg, _ := NewSentMessage(params) _, err := msg.Wrap(params) if err != nil { b.Fatalf("failed Wrap with seed %d: %s.", seed, err) diff --git a/whisper/whisperv5/doc.go b/whisper/whisperv5/doc.go index d60868f670..768291a161 100644 --- a/whisper/whisperv5/doc.go +++ b/whisper/whisperv5/doc.go @@ -49,18 +49,16 @@ const ( paddingMask = byte(3) signatureFlag = byte(4) - TopicLength = 4 - signatureLength = 65 - aesKeyLength = 32 - saltLength = 12 - AESNonceMaxLength = 12 - keyIdSize = 32 + TopicLength = 4 + signatureLength = 65 + aesKeyLength = 32 + AESNonceLength = 12 + keyIdSize = 32 DefaultMaxMessageLength = 1024 * 1024 - DefaultMinimumPoW = 1.0 // todo: review after testing. + DefaultMinimumPoW = 0.2 - 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 + padSizeLimit = 256 // just an arbitrary number, could be changed without breaking the protocol (must not exceed 2^24) messageQueueLimit = 1024 expirationCycle = time.Second diff --git a/whisper/whisperv5/envelope.go b/whisper/whisperv5/envelope.go index dffa7b2862..d95fcab750 100644 --- a/whisper/whisperv5/envelope.go +++ b/whisper/whisperv5/envelope.go @@ -40,7 +40,6 @@ type Envelope struct { Expiry uint32 TTL uint32 Topic TopicType - Salt []byte AESNonce []byte Data []byte EnvNonce uint64 @@ -50,15 +49,25 @@ type Envelope struct { // Don't access hash directly, use Hash() function instead. } +// size returns the size of envelope as it is sent (i.e. public fields only) +func (e *Envelope) size() int { + return 20 + len(e.Version) + len(e.AESNonce) + len(e.Data) +} + +// rlpWithoutNonce returns the RLP encoded envelope contents, except the nonce. +func (e *Envelope) rlpWithoutNonce() []byte { + res, _ := rlp.EncodeToBytes([]interface{}{e.Version, e.Expiry, e.TTL, e.Topic, e.AESNonce, e.Data}) + return res +} + // NewEnvelope wraps a Whisper message with expiration and destination data // included into an envelope for network forwarding. -func NewEnvelope(ttl uint32, topic TopicType, salt []byte, aesNonce []byte, msg *SentMessage) *Envelope { +func NewEnvelope(ttl uint32, topic TopicType, 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, @@ -126,10 +135,6 @@ func (e *Envelope) Seal(options *MessageParams) error { return nil } -func (e *Envelope) size() int { - return len(e.Data) + len(e.Version) + len(e.AESNonce) + len(e.Salt) + 20 -} - func (e *Envelope) PoW() float64 { if e.pow == 0 { e.calculatePoW(0) @@ -159,12 +164,6 @@ func (e *Envelope) powToFirstBit(pow float64) int { 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{}) { @@ -210,7 +209,7 @@ func (e *Envelope) OpenAsymmetric(key *ecdsa.PrivateKey) (*ReceivedMessage, erro // 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) + err = msg.decryptSymmetric(key, e.AESNonce) if err != nil { msg = nil } diff --git a/whisper/whisperv5/filter_test.go b/whisper/whisperv5/filter_test.go index ae21d17396..dd4ab9e8db 100644 --- a/whisper/whisperv5/filter_test.go +++ b/whisper/whisperv5/filter_test.go @@ -68,7 +68,7 @@ func generateFilter(t *testing.T, symmetric bool) (*Filter, error) { f.Src = &key.PublicKey if symmetric { - f.KeySym = make([]byte, 12) + f.KeySym = make([]byte, aesKeyLength) mrand.Read(f.KeySym) f.SymKeyHash = crypto.Keccak256Hash(f.KeySym) } else { @@ -179,7 +179,10 @@ func TestMatchEnvelope(t *testing.T) { params.Topic[0] = 0xFF // ensure mismatch // mismatch with pseudo-random data - msg := NewSentMessage(params) + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := msg.Wrap(params) if err != nil { t.Fatalf("failed Wrap with seed %d: %s.", seed, err) @@ -197,7 +200,10 @@ func TestMatchEnvelope(t *testing.T) { i := mrand.Int() % 4 fsym.Topics[i] = params.Topic[:] fasym.Topics[i] = params.Topic[:] - msg = NewSentMessage(params) + msg, err = NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err = msg.Wrap(params) if err != nil { t.Fatalf("failed Wrap() with seed %d: %s.", seed, err) @@ -245,7 +251,10 @@ func TestMatchEnvelope(t *testing.T) { } params.KeySym = nil params.Dst = &key.PublicKey - msg = NewSentMessage(params) + msg, err = NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err = msg.Wrap(params) if err != nil { t.Fatalf("failed Wrap() with seed %d: %s.", seed, err) @@ -323,12 +332,14 @@ func TestMatchMessageSym(t *testing.T) { params.KeySym = f.KeySym params.Topic = BytesToTopic(f.Topics[index]) - sentMessage := NewSentMessage(params) + sentMessage, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := sentMessage.Wrap(params) if err != nil { t.Fatalf("failed Wrap with seed %d: %s.", seed, err) } - msg := env.Open(f) if msg == nil { t.Fatalf("failed Open with seed %d.", seed) @@ -419,12 +430,14 @@ func TestMatchMessageAsym(t *testing.T) { keySymOrig := params.KeySym params.KeySym = nil - sentMessage := NewSentMessage(params) + sentMessage, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := sentMessage.Wrap(params) if err != nil { t.Fatalf("failed Wrap with seed %d: %s.", seed, err) } - msg := env.Open(f) if msg == nil { t.Fatalf("failed to open with seed %d.", seed) @@ -506,7 +519,10 @@ func generateCompatibeEnvelope(t *testing.T, f *Filter) *Envelope { params.KeySym = f.KeySym params.Topic = BytesToTopic(f.Topics[2]) - sentMessage := NewSentMessage(params) + sentMessage, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := sentMessage.Wrap(params) if err != nil { t.Fatalf("failed Wrap with seed %d: %s.", seed, err) @@ -678,7 +694,10 @@ func TestVariableTopics(t *testing.T) { if err != nil { t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) } - msg := NewSentMessage(params) + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := msg.Wrap(params) if err != nil { t.Fatalf("failed Wrap with seed %d: %s.", seed, err) diff --git a/whisper/whisperv5/message.go b/whisper/whisperv5/message.go index 9b9c389a6a..4ef469b515 100644 --- a/whisper/whisperv5/message.go +++ b/whisper/whisperv5/message.go @@ -23,14 +23,14 @@ import ( "crypto/cipher" "crypto/ecdsa" crand "crypto/rand" - "crypto/sha256" + "encoding/binary" "errors" + "strconv" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/ecies" "github.com/ethereum/go-ethereum/log" - "golang.org/x/crypto/pbkdf2" ) // Options specifies the exact way a message should be wrapped into an Envelope. @@ -86,58 +86,76 @@ 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 { +func NewSentMessage(params *MessageParams) (*SentMessage, error) { msg := SentMessage{} - msg.Raw = make([]byte, 1, len(params.Payload)+len(params.Payload)+signatureLength+padSizeLimitUpper) + msg.Raw = make([]byte, 1, len(params.Payload)+len(params.Padding)+signatureLength+padSizeLimit) msg.Raw[0] = 0 // set all the flags to zero err := msg.appendPadding(params) if err != nil { - log.Error("failed to create NewSentMessage", "err", err) - return nil + return nil, err } msg.Raw = append(msg.Raw, params.Payload...) - return &msg + return &msg, nil +} + +// getSizeOfLength returns the number of bytes necessary to encode the entire size padding (including these bytes) +func getSizeOfLength(b []byte) (sz int, err error) { + sz = intSize(len(b)) // first iteration + sz = intSize(len(b) + sz) // second iteration + if sz > 3 { + err = errors.New("oversized padding parameter") + } + return sz, err +} + +// sizeOfIntSize returns minimal number of bytes necessary to encode an integer value +func intSize(i int) (s int) { + for s = 1; i >= 256; s++ { + i /= 256 + } + return s } // 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) error { - total := len(params.Payload) + 1 + rawSize := len(params.Payload) + 1 if params.Src != nil { - total += signatureLength + rawSize += 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). + odd := rawSize % padSizeLimit + + if len(params.Padding) != 0 { + padSize := len(params.Padding) + padLengthSize, err := getSizeOfLength(params.Padding) + if err != nil { + return err + } + totalPadSize := padSize + padLengthSize + buf := make([]byte, 8) + binary.LittleEndian.PutUint32(buf, uint32(totalPadSize)) + buf = buf[:padLengthSize] + msg.Raw = append(msg.Raw, buf...) + msg.Raw = append(msg.Raw, params.Padding...) + msg.Raw[0] |= byte(padLengthSize) // number of bytes indicating the padding size + } else if odd != 0 { + totalPadSize := padSizeLimit - odd + if totalPadSize > 255 { + // this algorithm is only valid if padSizeLimit < 256. + // if padSizeLimit will ever change, please fix the algorithm + // (please see also ReceivedMessage.extractPadding() function). panic("please fix the padding algorithm before releasing new version") } - buf := make([]byte, padSize) + buf := make([]byte, totalPadSize) _, err := crand.Read(buf[1:]) if err != nil { return err } - buf[0] = byte(padSize) - if params.Padding != nil { - copy(buf[1:], params.Padding) + if totalPadSize > 6 && !validateSymmetricKey(buf) { + return errors.New("failed to generate random padding of size " + strconv.Itoa(totalPadSize)) } + buf[0] = byte(totalPadSize) msg.Raw = append(msg.Raw, buf...) msg.Raw[0] |= byte(0x1) // number of bytes indicating the padding size } @@ -178,46 +196,31 @@ func (msg *SentMessage) encryptAsymmetric(key *ecdsa.PublicKey) error { // 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) { +func (msg *SentMessage) encryptSymmetric(key []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") + return nil, errors.New("invalid key provided for symmetric encryption") } - 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) + block, err := aes.NewCipher(key) if err != nil { - return nil, nil, err + return nil, err } aesgcm, err := cipher.NewGCM(block) if err != nil { - return nil, nil, err + return 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 + return nil, err } else if !validateSymmetricKey(nonce) { - return nil, nil, errors.New("crypto/rand failed to generate nonce") + return nil, errors.New("crypto/rand failed to generate nonce") } msg.Raw = aesgcm.Seal(nil, nonce, msg.Raw, nil) - return salt, nonce, nil + return nonce, nil } // Wrap bundles the message into an Envelope to transmit over the network. @@ -231,11 +234,11 @@ func (msg *SentMessage) Wrap(options *MessageParams) (envelope *Envelope, err er return nil, err } } - var salt, nonce []byte + var nonce []byte if options.Dst != nil { err = msg.encryptAsymmetric(options.Dst) } else if options.KeySym != nil { - salt, nonce, err = msg.encryptSymmetric(options.KeySym) + nonce, err = msg.encryptSymmetric(options.KeySym) } else { err = errors.New("unable to encrypt the message: neither symmetric nor assymmetric key provided") } @@ -244,7 +247,7 @@ func (msg *SentMessage) Wrap(options *MessageParams) (envelope *Envelope, err er return nil, err } - envelope = NewEnvelope(options.TTL, options.Topic, salt, nonce, msg) + envelope = NewEnvelope(options.TTL, options.Topic, nonce, msg) err = envelope.Seal(options) if err != nil { return nil, err @@ -254,13 +257,8 @@ func (msg *SentMessage) Wrap(options *MessageParams) (envelope *Envelope, err er // 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) +func (msg *ReceivedMessage) decryptSymmetric(key []byte, nonce []byte) error { + block, err := aes.NewCipher(key) if err != nil { return err } @@ -323,7 +321,8 @@ func (msg *ReceivedMessage) Validate() bool { // 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 + sz := int(msg.Raw[0] & paddingMask) // number of bytes indicating the entire size of padding (including these bytes) + // could be zero -- it means no padding if sz != 0 { paddingSize = int(bytesToUintLittleEndian(msg.Raw[1 : 1+sz])) if paddingSize < sz || paddingSize+1 > end { diff --git a/whisper/whisperv5/message_test.go b/whisper/whisperv5/message_test.go index 1ed7250d3a..aa82a02f36 100644 --- a/whisper/whisperv5/message_test.go +++ b/whisper/whisperv5/message_test.go @@ -31,9 +31,9 @@ func copyFromBuf(dst []byte, src []byte, beg int) int { } func generateMessageParams() (*MessageParams, error) { - // set all the parameters except p.Dst + // set all the parameters except p.Dst and p.Padding - buf := make([]byte, 1024) + buf := make([]byte, 4) mrand.Read(buf) sz := mrand.Intn(400) @@ -42,14 +42,10 @@ func generateMessageParams() (*MessageParams, error) { p.WorkTime = 1 p.TTL = uint32(mrand.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:]) + mrand.Read(p.Payload) + mrand.Read(p.KeySym) + p.Topic = BytesToTopic(buf) var err error p.Src, err = crypto.GenerateKey() @@ -77,11 +73,12 @@ func singleMessageTest(t *testing.T, symmetric bool) { } text := make([]byte, 0, 512) - steg := make([]byte, 0, 512) text = append(text, params.Payload...) - steg = append(steg, params.Padding...) - msg := NewSentMessage(params) + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := msg.Wrap(params) if err != nil { t.Fatalf("failed Wrap with seed %d: %s.", seed, err) @@ -102,10 +99,6 @@ func singleMessageTest(t *testing.T, symmetric bool) { t.Fatalf("failed to validate with seed %d.", seed) } - padsz := len(decrypted.Padding) - if !bytes.Equal(steg[:padsz], decrypted.Padding) { - t.Fatalf("failed with seed %d: compare padding.", seed) - } if !bytes.Equal(text, decrypted.Payload) { t.Fatalf("failed with seed %d: compare payload.", seed) } @@ -140,7 +133,10 @@ func TestMessageWrap(t *testing.T) { t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) } - msg := NewSentMessage(params) + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } params.TTL = 1 params.WorkTime = 12 params.PoW = target @@ -155,7 +151,10 @@ func TestMessageWrap(t *testing.T) { } // set PoW target too high, expect error - msg2 := NewSentMessage(params) + msg2, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } params.TTL = 1000000 params.WorkTime = 1 params.PoW = 10000000.0 @@ -175,14 +174,15 @@ func TestMessageSeal(t *testing.T) { t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) } - msg := NewSentMessage(params) + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } params.TTL = 1 aesnonce := make([]byte, 12) - salt := make([]byte, 12) mrand.Read(aesnonce) - mrand.Read(salt) - env := NewEnvelope(params.TTL, params.Topic, salt, aesnonce, msg) + env := NewEnvelope(params.TTL, params.Topic, aesnonce, msg) if err != nil { t.Fatalf("failed Wrap with seed %d: %s.", seed, err) } @@ -236,11 +236,12 @@ func singleEnvelopeOpenTest(t *testing.T, symmetric bool) { } text := make([]byte, 0, 512) - steg := make([]byte, 0, 512) text = append(text, params.Payload...) - steg = append(steg, params.Padding...) - msg := NewSentMessage(params) + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := msg.Wrap(params) if err != nil { t.Fatalf("failed Wrap with seed %d: %s.", seed, err) @@ -252,10 +253,6 @@ func singleEnvelopeOpenTest(t *testing.T, symmetric bool) { t.Fatalf("failed to open with seed %d.", seed) } - padsz := len(decrypted.Padding) - if !bytes.Equal(steg[:padsz], decrypted.Padding) { - t.Fatalf("failed with seed %d: compare padding.", seed) - } if !bytes.Equal(text, decrypted.Payload) { t.Fatalf("failed with seed %d: compare payload.", seed) } @@ -291,21 +288,38 @@ func TestEncryptWithZeroKey(t *testing.T) { if err != nil { t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) } - - msg := NewSentMessage(params) - + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } params.KeySym = make([]byte, aesKeyLength) _, err = msg.Wrap(params) if err == nil { t.Fatalf("wrapped with zero key, seed: %d.", seed) } + params, err = generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + msg, err = NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } params.KeySym = make([]byte, 0) _, err = msg.Wrap(params) if err == nil { t.Fatalf("wrapped with empty key, seed: %d.", seed) } + params, err = generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) + } + msg, err = NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } params.KeySym = nil _, err = msg.Wrap(params) if err == nil { @@ -320,7 +334,10 @@ func TestRlpEncode(t *testing.T) { if err != nil { t.Fatalf("failed generateMessageParams with seed %d: %s.", seed, err) } - msg := NewSentMessage(params) + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := msg.Wrap(params) if err != nil { t.Fatalf("wrapped with zero key, seed: %d.", seed) @@ -344,3 +361,60 @@ func TestRlpEncode(t *testing.T) { t.Fatalf("Hashes are not equal: %x vs. %x", he, hd) } } + +func singlePaddingTest(t *testing.T, padSize int) { + params, err := generateMessageParams() + if err != nil { + t.Fatalf("failed generateMessageParams with seed %d and sz=%d: %s.", seed, padSize, err) + } + params.Padding = make([]byte, padSize) + params.PoW = 0.0000000001 + pad := make([]byte, padSize) + _, err = mrand.Read(pad) + if err != nil { + t.Fatalf("padding is not generated (seed %d): %s", seed, err) + } + n := copy(params.Padding, pad) + if n != padSize { + t.Fatalf("padding is not copied (seed %d): %s", seed, err) + } + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } + env, err := msg.Wrap(params) + if err != nil { + t.Fatalf("failed to wrap, seed: %d and sz=%d.", seed, padSize) + } + f := Filter{KeySym: params.KeySym} + decrypted := env.Open(&f) + if decrypted == nil { + t.Fatalf("failed to open, seed and sz=%d: %d.", seed, padSize) + } + if !bytes.Equal(pad, decrypted.Padding) { + t.Fatalf("padding is not retireved as expected with seed %d and sz=%d:\n[%x]\n[%x].", seed, padSize, pad, decrypted.Padding) + } +} + +func TestPadding(t *testing.T) { + InitSingleTest() + + for i := 1; i < 260; i++ { + singlePaddingTest(t, i) + } + + lim := 256 * 256 + for i := lim - 5; i < lim+2; i++ { + singlePaddingTest(t, i) + } + + for i := 0; i < 256; i++ { + n := mrand.Intn(256*254) + 256 + singlePaddingTest(t, n) + } + + for i := 0; i < 256; i++ { + n := mrand.Intn(256*1024) + 256*256 + singlePaddingTest(t, n) + } +} diff --git a/whisper/whisperv5/peer.go b/whisper/whisperv5/peer.go index 184c4ebf8f..179c931795 100644 --- a/whisper/whisperv5/peer.go +++ b/whisper/whisperv5/peer.go @@ -149,23 +149,22 @@ func (peer *Peer) expire() { // 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 + var cnt int 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) + err := p2p.Send(p.ws, messagesCode, envelope) + if err != nil { + return err + } else { + p.mark(envelope) + cnt++ + } } } - if len(transmit) == 0 { - return nil - } - // Transmit the unknown batch (potentially empty) - if err := p2p.Send(p.ws, messagesCode, transmit); err != nil { - return err + if cnt > 0 { + log.Trace("broadcast", "num. messages", cnt) } - log.Trace("broadcast", "num. messages", len(transmit)) return nil } diff --git a/whisper/whisperv5/peer_test.go b/whisper/whisperv5/peer_test.go index a79b6ad144..d3cd63b0b2 100644 --- a/whisper/whisperv5/peer_test.go +++ b/whisper/whisperv5/peer_test.go @@ -265,7 +265,10 @@ func sendMsg(t *testing.T, expected bool, id int) { opt.Payload = opt.Payload[1:] } - msg := NewSentMessage(&opt) + msg, err := NewSentMessage(&opt) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } envelope, err := msg.Wrap(&opt) if err != nil { t.Fatalf("failed to seal message: %s", err) @@ -286,7 +289,10 @@ func TestPeerBasic(t *testing.T) { } params.PoW = 0.001 - msg := NewSentMessage(params) + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := msg.Wrap(params) if err != nil { t.Fatalf("failed Wrap with seed %d.", seed) diff --git a/whisper/whisperv5/whisper.go b/whisper/whisperv5/whisper.go index c4d5d04a74..f2aad08efb 100644 --- a/whisper/whisperv5/whisper.go +++ b/whisper/whisperv5/whisper.go @@ -262,24 +262,14 @@ func (w *Whisper) GetPrivateKey(id string) (*ecdsa.PrivateKey, error) { // GenerateSymKey generates a random symmetric key and stores it under id, // which is then returned. Will be used in the future for session key exchange. func (w *Whisper) GenerateSymKey() (string, error) { - const size = aesKeyLength * 2 - buf := make([]byte, size) - _, err := crand.Read(buf) + key := make([]byte, aesKeyLength) + _, err := crand.Read(key) if err != nil { return "", err - } else if !validateSymmetricKey(buf) { + } else if !validateSymmetricKey(key) { return "", fmt.Errorf("error in GenerateSymKey: 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") - } - id, err := GenerateRandomID() if err != nil { return "", fmt.Errorf("failed to generate ID: %s", err) @@ -291,7 +281,7 @@ func (w *Whisper) GenerateSymKey() (string, error) { if w.symKeys[id] != nil { return "", fmt.Errorf("failed to generate unique ID") } - w.symKeys[id] = derived + w.symKeys[id] = key return id, nil } @@ -395,6 +385,9 @@ func (w *Whisper) Unsubscribe(id string) error { // network in the coming cycles. func (w *Whisper) Send(envelope *Envelope) error { ok, err := w.add(envelope) + if err != nil { + return err + } if !ok { return fmt.Errorf("failed to add envelope") } @@ -469,21 +462,18 @@ func (wh *Whisper) runMessageLoop(p *Peer, rw p2p.MsgReadWriter) error { log.Warn("unxepected status message received", "peer", p.peer.ID()) case messagesCode: // decode the contained envelopes - var envelopes []*Envelope - if err := packet.Decode(&envelopes); err != nil { + var envelope Envelope + if err := packet.Decode(&envelope); err != nil { log.Warn("failed to decode envelope, peer will be disconnected", "peer", p.peer.ID(), "err", err) return errors.New("invalid envelope") } - // inject all envelopes into the internal pool - for _, envelope := range envelopes { - cached, err := wh.add(envelope) - if err != nil { - log.Warn("bad envelope received, peer will be disconnected", "peer", p.peer.ID(), "err", err) - return errors.New("invalid envelope") - } - if cached { - p.mark(envelope) - } + cached, err := wh.add(&envelope) + if err != nil { + log.Warn("bad envelope received, peer will be disconnected", "peer", p.peer.ID(), "err", err) + return errors.New("invalid envelope") + } + if cached { + p.mark(&envelope) } case p2pCode: // peer-to-peer message, sent directly to peer bypassing PoW checks, etc. @@ -550,14 +540,11 @@ func (wh *Whisper) add(envelope *Envelope) (bool, error) { return false, fmt.Errorf("oversized version [%x]", envelope.Hash()) } - if len(envelope.AESNonce) > AESNonceMaxLength { - // the standard AES GSM nonce size is 12, - // but const gcmStandardNonceSize cannot be accessed directly - return false, fmt.Errorf("oversized AESNonce [%x]", envelope.Hash()) - } - - if len(envelope.Salt) > saltLength { - return false, fmt.Errorf("oversized salt [%x]", envelope.Hash()) + aesNonceSize := len(envelope.AESNonce) + if aesNonceSize != 0 && aesNonceSize != AESNonceLength { + // the standard AES GCM nonce size is 12 bytes, + // but constant gcmStandardNonceSize cannot be accessed (not exported) + return false, fmt.Errorf("wrong size of AESNonce: %d bytes [env: %x]", aesNonceSize, envelope.Hash()) } if envelope.PoW() < wh.minPoW { diff --git a/whisper/whisperv5/whisper_test.go b/whisper/whisperv5/whisper_test.go index d5668259e5..225728c42e 100644 --- a/whisper/whisperv5/whisper_test.go +++ b/whisper/whisperv5/whisper_test.go @@ -455,7 +455,10 @@ func TestExpiry(t *testing.T) { } params.TTL = 1 - msg := NewSentMessage(params) + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := msg.Wrap(params) if err != nil { t.Fatalf("failed Wrap with seed %d: %s.", seed, err) @@ -515,7 +518,10 @@ func TestCustomization(t *testing.T) { params.Topic = BytesToTopic(f.Topics[2]) params.PoW = smallPoW params.TTL = 3600 * 24 // one day - msg := NewSentMessage(params) + msg, err := NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err := msg.Wrap(params) if err != nil { t.Fatalf("failed Wrap with seed %d: %s.", seed, err) @@ -533,7 +539,10 @@ func TestCustomization(t *testing.T) { } params.TTL++ - msg = NewSentMessage(params) + msg, err = NewSentMessage(params) + if err != nil { + t.Fatalf("failed to create new message with seed %d: %s.", seed, err) + } env, err = msg.Wrap(params) if err != nil { t.Fatalf("failed Wrap with seed %d: %s.", seed, err)