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 }