github.com/ethersphere/bee/v2@v2.2.0/pkg/pss/trojan.go (about)

     1  // Copyright 2020 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package pss
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"crypto/ecdsa"
    11  	random "crypto/rand"
    12  	"encoding/binary"
    13  	"encoding/hex"
    14  	"errors"
    15  	"fmt"
    16  	"io"
    17  
    18  	"github.com/btcsuite/btcd/btcec/v2"
    19  	"github.com/ethersphere/bee/v2/pkg/bmtpool"
    20  	"github.com/ethersphere/bee/v2/pkg/crypto"
    21  	"github.com/ethersphere/bee/v2/pkg/encryption"
    22  	"github.com/ethersphere/bee/v2/pkg/encryption/elgamal"
    23  	"github.com/ethersphere/bee/v2/pkg/swarm"
    24  	"golang.org/x/sync/errgroup"
    25  )
    26  
    27  var (
    28  	// ErrPayloadTooBig is returned when a given payload for a Message type is longer than the maximum amount allowed
    29  	ErrPayloadTooBig = fmt.Errorf("message payload size cannot be greater than %d bytes", MaxPayloadSize)
    30  
    31  	// ErrEmptyTargets is returned when the given target list for a trojan chunk is empty
    32  	ErrEmptyTargets = errors.New("target list cannot be empty")
    33  
    34  	// ErrVarLenTargets is returned when the given target list for a trojan chunk has addresses of different lengths
    35  	ErrVarLenTargets = errors.New("target list cannot have targets of different length")
    36  )
    37  
    38  // Topic is the type that classifies messages, allows client applications to subscribe to
    39  type Topic [32]byte
    40  
    41  // NewTopic creates a new Topic from an input string by taking its hash
    42  func NewTopic(text string) Topic {
    43  	bytes, _ := crypto.LegacyKeccak256([]byte(text))
    44  	var topic Topic
    45  	copy(topic[:], bytes[:32])
    46  	return topic
    47  }
    48  
    49  // Target is an alias for a partial address (overlay prefix) serving as potential destination
    50  type Target []byte
    51  
    52  // Targets is an alias for a collection of targets
    53  type Targets []Target
    54  
    55  const (
    56  	// MaxPayloadSize is the maximum allowed payload size for the Message type, in bytes
    57  	MaxPayloadSize = swarm.ChunkSize - 3*swarm.HashSize
    58  )
    59  
    60  // Wrap creates a new serialised message with the given topic, payload and recipient public key used
    61  // for encryption
    62  // - span as topic hint  (H(key|topic)[0:8]) to match topic
    63  // chunk payload:
    64  // - nonce is chosen so that the chunk address will have one of the targets as its prefix and thus will be forwarded to the neighbourhood of the recipient overlay address the target is derived from
    65  // trojan payload:
    66  // - ephemeral  public key for el-Gamal encryption
    67  // ciphertext - plaintext:
    68  // - plaintext length encoding
    69  // - integrity protection
    70  // message:
    71  func Wrap(ctx context.Context, topic Topic, msg []byte, recipient *ecdsa.PublicKey, targets Targets) (swarm.Chunk, error) {
    72  	if len(msg) > MaxPayloadSize {
    73  		return nil, ErrPayloadTooBig
    74  	}
    75  
    76  	// integrity protection and plaintext msg length encoding
    77  	integrity, err := crypto.LegacyKeccak256(msg)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	binary.BigEndian.PutUint16(integrity[:2], uint16(len(msg)))
    82  
    83  	// integrity segment prepended to msg
    84  	plaintext := append(integrity, msg...)
    85  	// use el-Gamal with ECDH on an ephemeral key, recipient public key and topic as salt
    86  	enc, ephpub, err := elgamal.NewEncryptor(recipient, topic[:], 4032, swarm.NewHasher)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	ciphertext, err := enc.Encrypt(plaintext)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	// prepend serialised ephemeral public key to the ciphertext
    96  	// NOTE: only the random bytes of the compressed public key are used
    97  	// in order not to leak anything, the one bit parity info of the magic byte
    98  	// is encoded in the parity of the 28th byte of the mined nonce
    99  	ephpubBytes := crypto.EncodeSecp256k1PublicKey(ephpub)
   100  	payload := append(ephpubBytes[1:], ciphertext...)
   101  	odd := ephpubBytes[0]&0x1 != 0
   102  
   103  	if err := checkTargets(targets); err != nil {
   104  		return nil, err
   105  	}
   106  	targetsLen := len(targets[0])
   107  
   108  	// topic hash, the first 8 bytes is used as the span of the chunk
   109  	hash, err := crypto.LegacyKeccak256(append(enc.Key(), topic[:]...))
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	hint := hash[:8]
   114  	h := hasher(hint, payload)
   115  
   116  	// f is evaluating the mined nonce
   117  	// it accepts the nonce if it has the parity required by the ephemeral public key  AND
   118  	// the chunk hashes to an address matching one of the targets
   119  	f := func(nonce []byte) (swarm.Chunk, error) {
   120  		hash, err := h(nonce)
   121  		if err != nil {
   122  			return nil, err
   123  		}
   124  		if !contains(targets, hash[:targetsLen]) {
   125  			return nil, nil
   126  		}
   127  		chunk := swarm.NewChunk(swarm.NewAddress(hash), append(hint, append(nonce, payload...)...))
   128  		return chunk, nil
   129  	}
   130  	return mine(ctx, odd, f)
   131  }
   132  
   133  // Unwrap takes a chunk, a topic and a private key, and tries to decrypt the payload
   134  // using the private key, the prepended ephemeral public key for el-Gamal using the topic as salt
   135  func Unwrap(ctx context.Context, key *ecdsa.PrivateKey, chunk swarm.Chunk, topics []Topic) (topic Topic, msg []byte, err error) {
   136  	chunkData := chunk.Data()
   137  	pubkey, err := extractPublicKey(chunkData)
   138  	if err != nil {
   139  		return Topic{}, nil, err
   140  	}
   141  	hint := chunkData[:8]
   142  	for _, topic = range topics {
   143  		select {
   144  		case <-ctx.Done():
   145  			return Topic{}, nil, ctx.Err()
   146  		default:
   147  		}
   148  		dec, err := matchTopic(key, pubkey, hint, topic[:])
   149  		if err != nil {
   150  			privk := crypto.Secp256k1PrivateKeyFromBytes(topic[:])
   151  			dec, err = matchTopic(privk, pubkey, hint, topic[:])
   152  			if err != nil {
   153  				continue
   154  			}
   155  		}
   156  		ciphertext := chunkData[72:]
   157  		msg, err = decryptAndCheck(dec, ciphertext)
   158  		if err != nil {
   159  			continue
   160  		}
   161  		break
   162  	}
   163  	return topic, msg, nil
   164  }
   165  
   166  // checkTargets verifies that the list of given targets is non empty and with elements of matching size
   167  func checkTargets(targets Targets) error {
   168  	if len(targets) == 0 {
   169  		return ErrEmptyTargets
   170  	}
   171  	validLen := len(targets[0]) // take first element as allowed length
   172  	for i := 1; i < len(targets); i++ {
   173  		if len(targets[i]) != validLen {
   174  			return ErrVarLenTargets
   175  		}
   176  	}
   177  	return nil
   178  }
   179  
   180  func hasher(span, b []byte) func([]byte) ([]byte, error) {
   181  	return func(nonce []byte) ([]byte, error) {
   182  		s := append(nonce, b...)
   183  		hasher := bmtpool.Get()
   184  		defer bmtpool.Put(hasher)
   185  		hasher.SetHeader(span)
   186  		if _, err := hasher.Write(s); err != nil {
   187  			return nil, err
   188  		}
   189  		return hasher.Hash(nil)
   190  	}
   191  }
   192  
   193  // contains returns whether the given collection contains the given element
   194  func contains(col Targets, elem []byte) bool {
   195  	for i := range col {
   196  		if bytes.Equal(elem, col[i]) {
   197  			return true
   198  		}
   199  	}
   200  	return false
   201  }
   202  
   203  // mine iteratively enumerates different nonces until the address (BMT hash) of the chunkhas one of the targets as its prefix
   204  func mine(ctx context.Context, odd bool, f func(nonce []byte) (swarm.Chunk, error)) (swarm.Chunk, error) {
   205  	initnonce := make([]byte, 32)
   206  	if _, err := io.ReadFull(random.Reader, initnonce); err != nil {
   207  		return nil, err
   208  	}
   209  	if odd {
   210  		initnonce[28] |= 0x01
   211  	} else {
   212  		initnonce[28] &= 0xfe
   213  	}
   214  	ctx, cancel := context.WithCancel(ctx)
   215  	defer cancel()
   216  	eg, ctx := errgroup.WithContext(ctx)
   217  	result := make(chan swarm.Chunk, 8)
   218  	for i := 0; i < 8; i++ {
   219  		eg.Go(func() error {
   220  			nonce := make([]byte, 32)
   221  			copy(nonce, initnonce)
   222  			for {
   223  				select {
   224  				case <-ctx.Done():
   225  					return ctx.Err()
   226  				default:
   227  				}
   228  				if _, err := io.ReadFull(random.Reader, nonce[:4]); err != nil {
   229  					return err
   230  				}
   231  				res, err := f(nonce)
   232  				if err != nil {
   233  					return err
   234  				}
   235  				if res != nil {
   236  					result <- res
   237  					return nil
   238  				}
   239  			}
   240  		})
   241  	}
   242  	var err error
   243  	go func() {
   244  		err = eg.Wait()
   245  		result <- nil
   246  	}()
   247  	r := <-result
   248  	if r == nil {
   249  		return nil, err
   250  	}
   251  	return r, nil
   252  }
   253  
   254  // extracts ephemeral public key from the chunk data to use with el-Gamal
   255  func extractPublicKey(chunkData []byte) (*ecdsa.PublicKey, error) {
   256  	pubkeyBytes := make([]byte, 33)
   257  	pubkeyBytes[0] |= 0x2
   258  	copy(pubkeyBytes[1:], chunkData[40:72])
   259  	if chunkData[36]|0x1 != 0 {
   260  		pubkeyBytes[0] |= 0x1
   261  	}
   262  	pubkey, err := btcec.ParsePubKey(pubkeyBytes)
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  	return pubkey.ToECDSA(), err
   267  }
   268  
   269  // topic is needed to decrypt the trojan payload, but no need to perform decryption with each
   270  // instead the hash of the secret key and the topic is matched against a hint (64 bit meta info)q
   271  // proper integrity check will disambiguate any potential collisions (false positives)
   272  // if the topic matches the hint, it returns the el-Gamal decryptor, otherwise an error
   273  func matchTopic(key *ecdsa.PrivateKey, pubkey *ecdsa.PublicKey, hint, topic []byte) (encryption.Decrypter, error) {
   274  	dec, err := elgamal.NewDecrypter(key, pubkey, topic, swarm.NewHasher)
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	match, err := crypto.LegacyKeccak256(append(dec.Key(), topic...))
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  	if !bytes.Equal(hint, match[:8]) {
   283  		return nil, errors.New("topic does not match hint")
   284  	}
   285  	return dec, nil
   286  }
   287  
   288  // decrypts the ciphertext with an el-Gamal decryptor using a topic that matched the hint
   289  // the msg is extracted from the plaintext and its integrity is checked
   290  func decryptAndCheck(dec encryption.Decrypter, ciphertext []byte) ([]byte, error) {
   291  	plaintext, err := dec.Decrypt(ciphertext)
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  	length := int(binary.BigEndian.Uint16(plaintext[:2]))
   296  	if length > MaxPayloadSize {
   297  		return nil, errors.New("invalid length")
   298  	}
   299  	msg := plaintext[32 : 32+length]
   300  	integrity := plaintext[2:32]
   301  	hash, err := crypto.LegacyKeccak256(msg)
   302  	if err != nil {
   303  		return nil, err
   304  	}
   305  	if !bytes.Equal(integrity, hash[2:]) {
   306  		return nil, errors.New("invalid message")
   307  	}
   308  	// bingo
   309  	return msg, nil
   310  }
   311  
   312  // ParseRecipient extract ephemeral public key from the hexadecimal string to use with el-Gamal.
   313  func ParseRecipient(recipientHexString string) (*ecdsa.PublicKey, error) {
   314  	publicKeyBytes, err := hex.DecodeString(recipientHexString)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  	pubkey, err := btcec.ParsePubKey(publicKeyBytes)
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  	return pubkey.ToECDSA(), err
   323  }