Skip to content

Commit

Permalink
whisper/whisperv6: remove aesnonce (ethereum#15578)
Browse files Browse the repository at this point in the history
As per EIP-627, the salt for symmetric encryption is now
part of the payload. This commit does that.
  • Loading branch information
gballet authored and fjl committed Dec 8, 2017
1 parent b587427 commit d95962c
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 73 deletions.
8 changes: 5 additions & 3 deletions whisper/whisperv6/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ import (

const (
EnvelopeVersion = uint64(0)
ProtocolVersion = uint64(5)
ProtocolVersionStr = "5.0"
ProtocolVersion = uint64(6)
ProtocolVersionStr = "6.0"
ProtocolName = "shh"

statusCode = 0 // used by whisper protocol
messagesCode = 1 // normal whisper message
p2pCode = 2 // peer-to-peer message (to be consumed by the peer, but not forwarded any further)
p2pRequestCode = 3 // peer-to-peer message, used by Dapp protocol
NumberOfMessageCodes = 64
NumberOfMessageCodes = 128

paddingMask = byte(3)
signatureFlag = byte(4)
Expand All @@ -67,6 +67,8 @@ const (

DefaultTTL = 50 // seconds
SynchAllowance = 10 // seconds

EnvelopeHeaderLength = 20
)

type unknownVersionError uint64
Expand Down
52 changes: 24 additions & 28 deletions whisper/whisperv6/envelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,12 @@ import (
// 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
AESNonce []byte
Data []byte
Nonce uint64
Version []byte
Expiry uint32
TTL uint32
Topic TopicType
Data []byte
Nonce 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.
Expand All @@ -51,26 +50,25 @@ type Envelope struct {

// 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)
return EnvelopeHeaderLength + len(e.Version) + 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})
res, _ := rlp.EncodeToBytes([]interface{}{e.Version, e.Expiry, e.TTL, e.Topic, e.Data})
return res
}

// NewEnvelope wraps a Whisper message with expiration and destination data
// included into an envelope for network forwarding.
func NewEnvelope(ttl uint32, topic TopicType, aesNonce []byte, msg *sentMessage) *Envelope {
func NewEnvelope(ttl uint32, topic TopicType, msg *sentMessage) *Envelope {
env := Envelope{
Version: make([]byte, 1),
Expiry: uint32(time.Now().Add(time.Second * time.Duration(ttl)).Unix()),
TTL: ttl,
Topic: topic,
AESNonce: aesNonce,
Data: msg.Raw,
Nonce: 0,
Version: make([]byte, 1),
Expiry: uint32(time.Now().Add(time.Second * time.Duration(ttl)).Unix()),
TTL: ttl,
Topic: topic,
Data: msg.Raw,
Nonce: 0,
}

if EnvelopeVersion < 256 {
Expand All @@ -82,14 +80,6 @@ func NewEnvelope(ttl uint32, topic TopicType, aesNonce []byte, msg *sentMessage)
return &env
}

func (e *Envelope) IsSymmetric() bool {
return len(e.AESNonce) > 0
}

func (e *Envelope) isAsymmetric() bool {
return !e.IsSymmetric()
}

func (e *Envelope) Ver() uint64 {
return bytesToUintLittleEndian(e.Version)
}
Expand Down Expand Up @@ -209,7 +199,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.AESNonce)
err = msg.decryptSymmetric(key)
if err != nil {
msg = nil
}
Expand All @@ -218,12 +208,18 @@ func (e *Envelope) OpenSymmetric(key []byte) (msg *ReceivedMessage, err error) {

// 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() {
// The API interface forbids filters doing both symmetric and
// asymmetric encryption.
if watcher.expectsAsymmetricEncryption() && watcher.expectsSymmetricEncryption() {
return nil
}

if watcher.expectsAsymmetricEncryption() {
msg, _ = e.OpenAsymmetric(watcher.KeyAsym)
if msg != nil {
msg.Dst = &watcher.KeyAsym.PublicKey
}
} else if e.IsSymmetric() {
} else if watcher.expectsSymmetricEncryption() {
msg, _ = e.OpenSymmetric(watcher.KeySym)
if msg != nil {
msg.SymKeyHash = crypto.Keccak256Hash(watcher.KeySym)
Expand Down
64 changes: 64 additions & 0 deletions whisper/whisperv6/envelope_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

// Contains the tests associated with the Whisper protocol Envelope object.

package whisperv6

import (
mrand "math/rand"
"testing"

"github.com/ethereum/go-ethereum/crypto"
)

func TestEnvelopeOpenAcceptsOnlyOneKeyTypeInFilter(t *testing.T) {
symKey := make([]byte, aesKeyLength)
mrand.Read(symKey)

asymKey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf("failed GenerateKey with seed %d: %s.", seed, err)
}

params := MessageParams{
PoW: 0.01,
WorkTime: 1,
TTL: uint32(mrand.Intn(1024)),
Payload: make([]byte, 50),
KeySym: symKey,
Dst: nil,
}

mrand.Read(params.Payload)

msg, err := NewSentMessage(&params)
if err != nil {
t.Fatalf("failed to create new message with seed %d: %s.", seed, err)
}

e, err := msg.Wrap(&params)
if err != nil {
t.Fatalf("Failed to Wrap the message in an envelope with seed %d: %s", seed, err)
}

f := Filter{KeySym: symKey, KeyAsym: asymKey}

decrypted := e.Open(&f)
if decrypted != nil {
t.Fatalf("Managed to decrypt a message with an invalid filter, seed %d", seed)
}
}
17 changes: 11 additions & 6 deletions whisper/whisperv6/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ func NewFilters(w *Whisper) *Filters {
}

func (fs *Filters) Install(watcher *Filter) (string, error) {
if watcher.KeySym != nil && watcher.KeyAsym != nil {
return "", fmt.Errorf("filters must choose between symmetric and asymmetric keys")
}

if watcher.Messages == nil {
watcher.Messages = make(map[common.Hash]*ReceivedMessage)
}
Expand Down Expand Up @@ -175,6 +179,9 @@ func (f *Filter) Retrieve() (all []*ReceivedMessage) {
return all
}

// MatchMessage checks if the filter matches an already decrypted
// message (i.e. a Message that has already been handled by
// MatchEnvelope when checked by a previous filter)
func (f *Filter) MatchMessage(msg *ReceivedMessage) bool {
if f.PoW > 0 && msg.PoW < f.PoW {
return false
Expand All @@ -188,17 +195,15 @@ func (f *Filter) MatchMessage(msg *ReceivedMessage) bool {
return false
}

// MatchEvelope checks if it's worth decrypting the message. If
// it returns `true`, client code is expected to attempt decrypting
// the message and subsequently call MatchMessage.
func (f *Filter) MatchEnvelope(envelope *Envelope) bool {
if f.PoW > 0 && envelope.pow < f.PoW {
return false
}

if f.expectsAsymmetricEncryption() && envelope.isAsymmetric() {
return f.MatchTopic(envelope.Topic)
} else if f.expectsSymmetricEncryption() && envelope.IsSymmetric() {
return f.MatchTopic(envelope.Topic)
}
return false
return f.MatchTopic(envelope.Topic)
}

func (f *Filter) MatchTopic(topic TopicType) bool {
Expand Down
36 changes: 30 additions & 6 deletions whisper/whisperv6/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,36 @@ func TestInstallIdenticalFilters(t *testing.T) {
}
}

func TestInstallFilterWithSymAndAsymKeys(t *testing.T) {
InitSingleTest()

w := New(&Config{})
filters := NewFilters(w)
filter1, _ := generateFilter(t, true)

asymKey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf("Unable to create asymetric keys: %v", err)
}

// Copy the first filter since some of its fields
// are randomly gnerated.
filter := &Filter{
KeySym: filter1.KeySym,
KeyAsym: asymKey,
Topics: filter1.Topics,
PoW: filter1.PoW,
AllowP2P: filter1.AllowP2P,
Messages: make(map[common.Hash]*ReceivedMessage),
}

_, err = filters.Install(filter)

if err == nil {
t.Fatalf("Error detecting that a filter had both an asymmetric and symmetric key, with seed %d", seed)
}
}

func TestComparePubKey(t *testing.T) {
InitSingleTest()

Expand Down Expand Up @@ -312,12 +342,6 @@ func TestMatchEnvelope(t *testing.T) {
t.Fatalf("failed MatchEnvelope() symmetric with seed %d.", seed)
}

// asymmetric + matching topic: mismatch
match = fasym.MatchEnvelope(env)
if match {
t.Fatalf("failed MatchEnvelope() asymmetric with seed %d.", seed)
}

// symmetric + matching topic + insufficient PoW: mismatch
fsym.PoW = env.PoW() + 1.0
match = fsym.MatchEnvelope(env)
Expand Down
46 changes: 27 additions & 19 deletions whisper/whisperv6/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type ReceivedMessage struct {
Payload []byte
Padding []byte
Signature []byte
Salt []byte

PoW float64 // Proof of work as described in the Whisper spec
Sent uint32 // Time when the message was posted into the network
Expand Down Expand Up @@ -196,31 +197,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) (nonce []byte, err error) {
func (msg *sentMessage) encryptSymmetric(key []byte) (err error) {
if !validateSymmetricKey(key) {
return nil, errors.New("invalid key provided for symmetric encryption")
return errors.New("invalid key provided for symmetric encryption")
}

block, err := aes.NewCipher(key)
if err != nil {
return nil, err
return err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
return err
}

// never use more than 2^32 random nonces with a given key
nonce = make([]byte, aesgcm.NonceSize())
_, err = crand.Read(nonce)
salt := make([]byte, aesgcm.NonceSize())
_, err = crand.Read(salt)
if err != nil {
return nil, err
} else if !validateSymmetricKey(nonce) {
return nil, errors.New("crypto/rand failed to generate nonce")
return err
} else if !validateSymmetricKey(salt) {
return errors.New("crypto/rand failed to generate salt")
}

msg.Raw = aesgcm.Seal(nil, nonce, msg.Raw, nil)
return nonce, nil
msg.Raw = append(aesgcm.Seal(nil, salt, msg.Raw, nil), salt...)
return nil
}

// Wrap bundles the message into an Envelope to transmit over the network.
Expand All @@ -233,19 +234,18 @@ func (msg *sentMessage) Wrap(options *MessageParams) (envelope *Envelope, err er
return nil, err
}
}
var nonce []byte
if options.Dst != nil {
err = msg.encryptAsymmetric(options.Dst)
} else if options.KeySym != nil {
nonce, err = msg.encryptSymmetric(options.KeySym)
err = msg.encryptSymmetric(options.KeySym)
} else {
err = errors.New("unable to encrypt the message: neither symmetric nor assymmetric key provided")
}
if err != nil {
return nil, err
}

envelope = NewEnvelope(options.TTL, options.Topic, nonce, msg)
envelope = NewEnvelope(options.TTL, options.Topic, msg)
if err = envelope.Seal(options); err != nil {
return nil, err
}
Expand All @@ -254,7 +254,14 @@ 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, nonce []byte) error {
func (msg *ReceivedMessage) decryptSymmetric(key []byte) error {
// In v6, symmetric messages are expected to contain the 12-byte
// "salt" at the end of the payload.
if len(msg.Raw) < AESNonceLength {
return errors.New("missing salt or invalid payload in symmetric message")
}
salt := msg.Raw[len(msg.Raw)-AESNonceLength:]

block, err := aes.NewCipher(key)
if err != nil {
return err
Expand All @@ -263,15 +270,16 @@ func (msg *ReceivedMessage) decryptSymmetric(key []byte, nonce []byte) error {
if err != nil {
return err
}
if len(nonce) != aesgcm.NonceSize() {
log.Error("decrypting the message", "AES nonce size", len(nonce))
return errors.New("wrong AES nonce size")
if len(salt) != aesgcm.NonceSize() {
log.Error("decrypting the message", "AES salt size", len(salt))
return errors.New("wrong AES salt size")
}
decrypted, err := aesgcm.Open(nil, nonce, msg.Raw, nil)
decrypted, err := aesgcm.Open(nil, salt, msg.Raw[:len(msg.Raw)-AESNonceLength], nil)
if err != nil {
return err
}
msg.Raw = decrypted
msg.Salt = salt
return nil
}

Expand Down
Loading

0 comments on commit d95962c

Please sign in to comment.