github.com/status-im/status-go@v1.1.0/wakuv2/api.go (about) 1 // Copyright 2019 The Waku Library Authors. 2 // 3 // The Waku library is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Lesser General Public License as published by 5 // the Free Software Foundation, either version 3 of the License, or 6 // (at your option) any later version. 7 // 8 // The Waku library is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty off 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Lesser General Public License for more details. 12 // 13 // You should have received a copy of the GNU Lesser General Public License 14 // along with the Waku library. If not, see <http://www.gnu.org/licenses/>. 15 // 16 // This software uses the go-ethereum library, which is licensed 17 // under the GNU Lesser General Public Library, version 3 or any later. 18 19 package wakuv2 20 21 import ( 22 "context" 23 "crypto/ecdsa" 24 "errors" 25 "fmt" 26 "sync" 27 "time" 28 29 "github.com/waku-org/go-waku/waku/v2/payload" 30 "github.com/waku-org/go-waku/waku/v2/protocol/pb" 31 32 "github.com/status-im/status-go/wakuv2/common" 33 34 "github.com/ethereum/go-ethereum/common/hexutil" 35 "github.com/ethereum/go-ethereum/crypto" 36 "github.com/ethereum/go-ethereum/log" 37 "github.com/ethereum/go-ethereum/rpc" 38 39 "google.golang.org/protobuf/proto" 40 ) 41 42 // List of errors 43 var ( 44 ErrSymAsym = errors.New("specify either a symmetric or an asymmetric key") 45 ErrInvalidSymmetricKey = errors.New("invalid symmetric key") 46 ErrInvalidPublicKey = errors.New("invalid public key") 47 ErrInvalidSigningPubKey = errors.New("invalid signing public key") 48 ErrTooLowPoW = errors.New("message rejected, PoW too low") 49 ErrNoTopics = errors.New("missing topic(s)") 50 ) 51 52 // PublicWakuAPI provides the waku RPC service that can be 53 // use publicly without security implications. 54 type PublicWakuAPI struct { 55 w *Waku 56 57 mu sync.Mutex 58 lastUsed map[string]time.Time // keeps track when a filter was polled for the last time. 59 } 60 61 // NewPublicWakuAPI create a new RPC waku service. 62 func NewPublicWakuAPI(w *Waku) *PublicWakuAPI { 63 api := &PublicWakuAPI{ 64 w: w, 65 lastUsed: make(map[string]time.Time), 66 } 67 return api 68 } 69 70 // Info contains diagnostic information. 71 type Info struct { 72 Messages int `json:"messages"` // Number of floating messages. 73 MaxMessageSize uint32 `json:"maxMessageSize"` // Maximum accepted message size 74 } 75 76 // Context is used higher up the food-chain and without significant refactoring is not a simple thing to remove / change 77 78 // Info returns diagnostic information about the waku node. 79 func (api *PublicWakuAPI) Info(ctx context.Context) Info { 80 return Info{ 81 Messages: len(api.w.msgQueue), 82 MaxMessageSize: api.w.MaxMessageSize(), 83 } 84 } 85 86 // NewKeyPair generates a new public and private key pair for message decryption and encryption. 87 // It returns an ID that can be used to refer to the keypair. 88 func (api *PublicWakuAPI) NewKeyPair(ctx context.Context) (string, error) { 89 return api.w.NewKeyPair() 90 } 91 92 // AddPrivateKey imports the given private key. 93 func (api *PublicWakuAPI) AddPrivateKey(ctx context.Context, privateKey hexutil.Bytes) (string, error) { 94 key, err := crypto.ToECDSA(privateKey) 95 if err != nil { 96 return "", err 97 } 98 return api.w.AddKeyPair(key) 99 } 100 101 // DeleteKeyPair removes the key with the given key if it exists. 102 func (api *PublicWakuAPI) DeleteKeyPair(ctx context.Context, key string) (bool, error) { 103 if ok := api.w.DeleteKeyPair(key); ok { 104 return true, nil 105 } 106 return false, fmt.Errorf("key pair %s not found", key) 107 } 108 109 // HasKeyPair returns an indication if the node has a key pair that is associated with the given id. 110 func (api *PublicWakuAPI) HasKeyPair(ctx context.Context, id string) bool { 111 return api.w.HasKeyPair(id) 112 } 113 114 // GetPublicKey returns the public key associated with the given key. The key is the hex 115 // encoded representation of a key in the form specified in section 4.3.6 of ANSI X9.62. 116 func (api *PublicWakuAPI) GetPublicKey(ctx context.Context, id string) (hexutil.Bytes, error) { 117 key, err := api.w.GetPrivateKey(id) 118 if err != nil { 119 return hexutil.Bytes{}, err 120 } 121 return crypto.FromECDSAPub(&key.PublicKey), nil 122 } 123 124 // GetPrivateKey returns the private key associated with the given key. The key is the hex 125 // encoded representation of a key in the form specified in section 4.3.6 of ANSI X9.62. 126 func (api *PublicWakuAPI) GetPrivateKey(ctx context.Context, id string) (hexutil.Bytes, error) { 127 key, err := api.w.GetPrivateKey(id) 128 if err != nil { 129 return hexutil.Bytes{}, err 130 } 131 return crypto.FromECDSA(key), nil 132 } 133 134 // NewSymKey generate a random symmetric key. 135 // It returns an ID that can be used to refer to the key. 136 // Can be used encrypting and decrypting messages where the key is known to both parties. 137 func (api *PublicWakuAPI) NewSymKey(ctx context.Context) (string, error) { 138 return api.w.GenerateSymKey() 139 } 140 141 // AddSymKey import a symmetric key. 142 // It returns an ID that can be used to refer to the key. 143 // Can be used encrypting and decrypting messages where the key is known to both parties. 144 func (api *PublicWakuAPI) AddSymKey(ctx context.Context, key hexutil.Bytes) (string, error) { 145 return api.w.AddSymKeyDirect([]byte(key)) 146 } 147 148 // GenerateSymKeyFromPassword derive a key from the given password, stores it, and returns its ID. 149 func (api *PublicWakuAPI) GenerateSymKeyFromPassword(ctx context.Context, passwd string) (string, error) { 150 return api.w.AddSymKeyFromPassword(passwd) 151 } 152 153 // HasSymKey returns an indication if the node has a symmetric key associated with the given key. 154 func (api *PublicWakuAPI) HasSymKey(ctx context.Context, id string) bool { 155 return api.w.HasSymKey(id) 156 } 157 158 // GetSymKey returns the symmetric key associated with the given id. 159 func (api *PublicWakuAPI) GetSymKey(ctx context.Context, id string) (hexutil.Bytes, error) { 160 return api.w.GetSymKey(id) 161 } 162 163 // DeleteSymKey deletes the symmetric key that is associated with the given id. 164 func (api *PublicWakuAPI) DeleteSymKey(ctx context.Context, id string) bool { 165 return api.w.DeleteSymKey(id) 166 } 167 168 func (api *PublicWakuAPI) BloomFilter() []byte { 169 return nil 170 } 171 172 //go:generate gencodec -type NewMessage -field-override newMessageOverride -out gen_newmessage_json.go 173 174 // NewMessage represents a new waku message that is posted through the RPC. 175 type NewMessage struct { 176 SymKeyID string `json:"symKeyID"` 177 PublicKey []byte `json:"pubKey"` 178 Sig string `json:"sig"` 179 PubsubTopic string `json:"pubsubTopic"` 180 ContentTopic common.TopicType `json:"topic"` 181 Payload []byte `json:"payload"` 182 Padding []byte `json:"padding"` 183 TargetPeer string `json:"targetPeer"` 184 Ephemeral bool `json:"ephemeral"` 185 Priority *int `json:"priority"` 186 } 187 188 // Post posts a message on the Waku network. 189 // returns the hash of the message in case of success. 190 func (api *PublicWakuAPI) Post(ctx context.Context, req NewMessage) (hexutil.Bytes, error) { 191 var ( 192 symKeyGiven = len(req.SymKeyID) > 0 193 pubKeyGiven = len(req.PublicKey) > 0 194 err error 195 ) 196 197 // user must specify either a symmetric or an asymmetric key 198 if (symKeyGiven && pubKeyGiven) || (!symKeyGiven && !pubKeyGiven) { 199 return nil, ErrSymAsym 200 } 201 202 var keyInfo *payload.KeyInfo = new(payload.KeyInfo) 203 204 // Set key that is used to sign the message 205 if len(req.Sig) > 0 { 206 privKey, err := api.w.GetPrivateKey(req.Sig) 207 if err != nil { 208 return nil, err 209 } 210 keyInfo.PrivKey = privKey 211 } 212 213 // Set symmetric key that is used to encrypt the message 214 if symKeyGiven { 215 keyInfo.Kind = payload.Symmetric 216 217 if req.ContentTopic == (common.TopicType{}) { // topics are mandatory with symmetric encryption 218 return nil, ErrNoTopics 219 } 220 if keyInfo.SymKey, err = api.w.GetSymKey(req.SymKeyID); err != nil { 221 return nil, err 222 } 223 if !common.ValidateDataIntegrity(keyInfo.SymKey, common.AESKeyLength) { 224 return nil, ErrInvalidSymmetricKey 225 } 226 } 227 228 // Set asymmetric key that is used to encrypt the message 229 if pubKeyGiven { 230 keyInfo.Kind = payload.Asymmetric 231 232 var pubK *ecdsa.PublicKey 233 if pubK, err = crypto.UnmarshalPubkey(req.PublicKey); err != nil { 234 return nil, ErrInvalidPublicKey 235 } 236 keyInfo.PubKey = *pubK 237 } 238 239 var version uint32 = 1 // Use wakuv1 encryption 240 241 p := new(payload.Payload) 242 p.Data = req.Payload 243 p.Key = keyInfo 244 245 payload, err := p.Encode(version) 246 if err != nil { 247 return nil, err 248 } 249 250 wakuMsg := &pb.WakuMessage{ 251 Payload: payload, 252 Version: &version, 253 ContentTopic: req.ContentTopic.ContentTopic(), 254 Timestamp: proto.Int64(api.w.timestamp()), 255 Meta: []byte{}, // TODO: empty for now. Once we use Waku Archive v2, we should deprecate the timestamp and use an ULID here 256 Ephemeral: &req.Ephemeral, 257 } 258 259 hash, err := api.w.Send(req.PubsubTopic, wakuMsg, req.Priority) 260 261 if err != nil { 262 return nil, err 263 } 264 265 return hash, nil 266 } 267 268 // UninstallFilter is alias for Unsubscribe 269 func (api *PublicWakuAPI) UninstallFilter(ctx context.Context, id string) { 270 api.w.Unsubscribe(ctx, id) // nolint: errcheck 271 } 272 273 // Unsubscribe disables and removes an existing filter. 274 func (api *PublicWakuAPI) Unsubscribe(ctx context.Context, id string) { 275 api.w.Unsubscribe(ctx, id) // nolint: errcheck 276 } 277 278 // Criteria holds various filter options for inbound messages. 279 type Criteria struct { 280 SymKeyID string `json:"symKeyID"` 281 PrivateKeyID string `json:"privateKeyID"` 282 Sig []byte `json:"sig"` 283 PubsubTopic string `json:"pubsubTopic"` 284 ContentTopics []common.TopicType `json:"topics"` 285 } 286 287 // Messages set up a subscription that fires events when messages arrive that match 288 // the given set of criteria. 289 func (api *PublicWakuAPI) Messages(ctx context.Context, crit Criteria) (*rpc.Subscription, error) { 290 var ( 291 symKeyGiven = len(crit.SymKeyID) > 0 292 pubKeyGiven = len(crit.PrivateKeyID) > 0 293 err error 294 ) 295 296 // ensure that the RPC connection supports subscriptions 297 notifier, supported := rpc.NotifierFromContext(ctx) 298 if !supported { 299 return nil, rpc.ErrNotificationsUnsupported 300 } 301 302 // user must specify either a symmetric or an asymmetric key 303 if (symKeyGiven && pubKeyGiven) || (!symKeyGiven && !pubKeyGiven) { 304 return nil, ErrSymAsym 305 } 306 307 filter := common.Filter{ 308 Messages: common.NewMemoryMessageStore(), 309 } 310 311 if len(crit.Sig) > 0 { 312 if filter.Src, err = crypto.UnmarshalPubkey(crit.Sig); err != nil { 313 return nil, ErrInvalidSigningPubKey 314 } 315 } 316 317 filter.PubsubTopic = crit.PubsubTopic 318 filter.ContentTopics = common.NewTopicSet(crit.ContentTopics) 319 320 // listen for message that are encrypted with the given symmetric key 321 if symKeyGiven { 322 if len(filter.ContentTopics) == 0 { 323 return nil, ErrNoTopics 324 } 325 key, err := api.w.GetSymKey(crit.SymKeyID) 326 if err != nil { 327 return nil, err 328 } 329 if !common.ValidateDataIntegrity(key, common.AESKeyLength) { 330 return nil, ErrInvalidSymmetricKey 331 } 332 filter.KeySym = key 333 filter.SymKeyHash = crypto.Keccak256Hash(filter.KeySym) 334 } 335 336 // listen for messages that are encrypted with the given public key 337 if pubKeyGiven { 338 filter.KeyAsym, err = api.w.GetPrivateKey(crit.PrivateKeyID) 339 if err != nil || filter.KeyAsym == nil { 340 return nil, ErrInvalidPublicKey 341 } 342 } 343 344 id, err := api.w.Subscribe(&filter) 345 if err != nil { 346 return nil, err 347 } 348 349 // create subscription and start waiting for message events 350 rpcSub := notifier.CreateSubscription() 351 go func() { 352 // for now poll internally, refactor waku internal for channel support 353 ticker := time.NewTicker(250 * time.Millisecond) 354 defer ticker.Stop() 355 356 for { 357 select { 358 case <-ticker.C: 359 if filter := api.w.GetFilter(id); filter != nil { 360 for _, rpcMessage := range toMessage(filter.Retrieve()) { 361 if err := notifier.Notify(rpcSub.ID, rpcMessage); err != nil { 362 log.Error("Failed to send notification", "err", err) 363 } 364 } 365 } 366 case <-rpcSub.Err(): 367 _ = api.w.Unsubscribe(context.Background(), id) 368 return 369 } 370 } 371 }() 372 373 return rpcSub, nil 374 } 375 376 //go:generate gencodec -type Message -field-override messageOverride -out gen_message_json.go 377 378 // Message is the RPC representation of a waku message. 379 type Message struct { 380 Sig []byte `json:"sig,omitempty"` 381 Timestamp uint32 `json:"timestamp"` 382 PubsubTopic string `json:"pubsubTopic"` 383 ContentTopic common.TopicType `json:"topic"` 384 Payload []byte `json:"payload"` 385 Padding []byte `json:"padding"` 386 Hash []byte `json:"hash"` 387 Dst []byte `json:"recipientPublicKey,omitempty"` 388 } 389 390 // ToWakuMessage converts an internal message into an API version. 391 func ToWakuMessage(message *common.ReceivedMessage) *Message { 392 msg := Message{ 393 Payload: message.Data, 394 Padding: message.Padding, 395 Timestamp: message.Sent, 396 Hash: message.Hash().Bytes(), 397 PubsubTopic: message.PubsubTopic, 398 ContentTopic: message.ContentTopic, 399 } 400 401 if message.Dst != nil { 402 b := crypto.FromECDSAPub(message.Dst) 403 if b != nil { 404 msg.Dst = b 405 } 406 } 407 408 if message.Src != nil { 409 b := crypto.FromECDSAPub(message.Src) 410 if b != nil { 411 msg.Sig = b 412 } 413 } 414 415 return &msg 416 } 417 418 // toMessage converts a set of messages to its RPC representation. 419 func toMessage(messages []*common.ReceivedMessage) []*Message { 420 msgs := make([]*Message, len(messages)) 421 for i, msg := range messages { 422 msgs[i] = ToWakuMessage(msg) 423 } 424 return msgs 425 } 426 427 // GetFilterMessages returns the messages that match the filter criteria and 428 // are received between the last poll and now. 429 func (api *PublicWakuAPI) GetFilterMessages(id string) ([]*Message, error) { 430 api.mu.Lock() 431 f := api.w.GetFilter(id) 432 if f == nil { 433 api.mu.Unlock() 434 return nil, fmt.Errorf("filter not found") 435 } 436 api.lastUsed[id] = time.Now() 437 api.mu.Unlock() 438 439 receivedMessages := f.Retrieve() 440 messages := make([]*Message, 0, len(receivedMessages)) 441 for _, msg := range receivedMessages { 442 messages = append(messages, ToWakuMessage(msg)) 443 } 444 445 return messages, nil 446 } 447 448 // DeleteMessageFilter deletes a filter. 449 func (api *PublicWakuAPI) DeleteMessageFilter(id string) (bool, error) { 450 api.mu.Lock() 451 defer api.mu.Unlock() 452 453 delete(api.lastUsed, id) 454 return true, api.w.Unsubscribe(context.Background(), id) 455 } 456 457 // NewMessageFilter creates a new filter that can be used to poll for 458 // (new) messages that satisfy the given criteria. 459 func (api *PublicWakuAPI) NewMessageFilter(req Criteria) (string, error) { 460 var ( 461 src *ecdsa.PublicKey 462 keySym []byte 463 keyAsym *ecdsa.PrivateKey 464 465 symKeyGiven = len(req.SymKeyID) > 0 466 asymKeyGiven = len(req.PrivateKeyID) > 0 467 468 err error 469 ) 470 471 // user must specify either a symmetric or an asymmetric key 472 if (symKeyGiven && asymKeyGiven) || (!symKeyGiven && !asymKeyGiven) { 473 return "", ErrSymAsym 474 } 475 476 if len(req.Sig) > 0 { 477 if src, err = crypto.UnmarshalPubkey(req.Sig); err != nil { 478 return "", ErrInvalidSigningPubKey 479 } 480 } 481 482 if symKeyGiven { 483 if keySym, err = api.w.GetSymKey(req.SymKeyID); err != nil { 484 return "", err 485 } 486 if !common.ValidateDataIntegrity(keySym, common.AESKeyLength) { 487 return "", ErrInvalidSymmetricKey 488 } 489 } 490 491 if asymKeyGiven { 492 if keyAsym, err = api.w.GetPrivateKey(req.PrivateKeyID); err != nil { 493 return "", err 494 } 495 } 496 497 f := &common.Filter{ 498 Src: src, 499 KeySym: keySym, 500 KeyAsym: keyAsym, 501 PubsubTopic: req.PubsubTopic, 502 ContentTopics: common.NewTopicSet(req.ContentTopics), 503 Messages: common.NewMemoryMessageStore(), 504 } 505 506 id, err := api.w.Subscribe(f) 507 if err != nil { 508 return "", err 509 } 510 511 api.mu.Lock() 512 api.lastUsed[id] = time.Now() 513 api.mu.Unlock() 514 515 return id, nil 516 }