-
Notifications
You must be signed in to change notification settings - Fork 244
/
service.go
455 lines (373 loc) · 10.6 KB
/
service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
package filter
import (
"crypto/ecdsa"
"encoding/hex"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/status-im/status-go/services/shhext/chat/sharedsecret"
whisper "github.com/status-im/whisper/whisperv6"
"math/big"
"sync"
)
const (
discoveryTopic = "contact-discovery"
)
// The number of partitions
var nPartitions = big.NewInt(5000)
var minPow = 0.0
type Filter struct {
FilterID string
Topic whisper.TopicType
SymKeyID string
}
type Chat struct {
// ChatID is the identifier of the chat
ChatID string `json:"chatId"`
// SymKeyID is the symmetric key id used for symmetric chats
SymKeyID string `json:"symKeyId"`
// OneToOne tells us if we need to use asymmetric encryption for this chat
OneToOne bool `json:"oneToOne"`
// Listen is whether we are actually listening for messages on this chat, or the filter is only created in order to be able to post on the topic
Listen bool `json:"listen"`
// FilterID the whisper filter id generated
FilterID string `json:"filterId"`
// Identity is the public key of the other recipient for non-public chats
Identity string `json:"identity"`
// Topic is the whisper topic
Topic whisper.TopicType `json:"topic"`
}
type Service struct {
whisper *whisper.Whisper
secret *sharedsecret.Service
chats map[string]*Chat
mutex sync.Mutex
}
// New returns a new filter service
func New(w *whisper.Whisper, s *sharedsecret.Service) *Service {
return &Service{
whisper: w,
secret: s,
mutex: sync.Mutex{},
chats: make(map[string]*Chat),
}
}
// LoadChat should return a list of newly chats loaded
func (s *Service) Init(chats []*Chat) ([]*Chat, error) {
log.Debug("Initializing filter service", "chats", chats)
keyID := s.whisper.SelectedKeyPairID()
if keyID == "" {
return nil, errors.New("no key selected")
}
myKey, err := s.whisper.GetPrivateKey(keyID)
if err != nil {
return nil, err
}
// Add our own topic
log.Debug("Loading one to one chats")
identityStr := fmt.Sprintf("%x", crypto.FromECDSAPub(&myKey.PublicKey))
_, err = s.loadOneToOne(myKey, identityStr, true)
if err != nil {
log.Error("Error loading one to one chats", "err", err)
return nil, err
}
// Add discovery topic
log.Debug("Loading discovery topics")
err = s.loadDiscovery(myKey)
if err != nil {
return nil, err
}
// Add the various one to one and public chats
log.Debug("Loading chats")
for _, chat := range chats {
_, err = s.load(myKey, chat)
if err != nil {
return nil, err
}
}
// Add the negotiated secrets
log.Debug("Loading negotiated topics")
secrets, err := s.secret.All()
if err != nil {
return nil, err
}
for _, secret := range secrets {
if _, err := s.ProcessNegotiatedSecret(secret); err != nil {
return nil, err
}
}
s.mutex.Lock()
defer s.mutex.Unlock()
var allChats []*Chat
for _, chat := range s.chats {
allChats = append(allChats, chat)
}
return allChats, nil
}
// Stop removes all the filters
func (s *Service) Stop() error {
for _, chat := range s.chats {
if err := s.Remove(chat); err != nil {
return err
}
}
return nil
}
// Remove remove all the filters associated with a chat/identity
func (s *Service) Remove(chat *Chat) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if err := s.whisper.Unsubscribe(chat.FilterID); err != nil {
return err
}
if chat.SymKeyID != "" {
s.whisper.DeleteSymKey(chat.SymKeyID)
}
delete(s.chats, chat.ChatID)
return nil
}
// LoadPartitioned creates a filter for a partitioned topic
func (s *Service) LoadPartitioned(myKey *ecdsa.PrivateKey, theirPublicKey *ecdsa.PublicKey, listen bool) (*Chat, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
chatID := PublicKeyToPartitionedTopic(theirPublicKey)
if _, ok := s.chats[chatID]; ok {
return s.chats[chatID], nil
}
// We set up a filter so we can publish, but we discard envelopes if listen is false
filter, err := s.addAsymmetricFilter(myKey, chatID, listen)
if err != nil {
return nil, err
}
chat := &Chat{
ChatID: chatID,
FilterID: filter.FilterID,
Topic: filter.Topic,
Listen: listen,
}
s.chats[chatID] = chat
return chat, nil
}
// Load creates filters for a given chat, and returns all the created filters
func (s *Service) Load(chat *Chat) ([]*Chat, error) {
keyID := s.whisper.SelectedKeyPairID()
if keyID == "" {
return nil, errors.New("no key selected")
}
myKey, err := s.whisper.GetPrivateKey(keyID)
if err != nil {
return nil, err
}
return s.load(myKey, chat)
}
func ContactCodeTopic(identity string) string {
return "0x" + identity + "-contact-code"
}
// Get returns a negotiated chat given an identity
func (s *Service) GetNegotiated(identity *ecdsa.PublicKey) *Chat {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.chats[negotiatedID(identity)]
}
// GetByID returns a chat by chatID
func (s *Service) GetByID(chatID string) *Chat {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.chats[chatID]
}
// ProcessNegotiatedSecret adds a filter based on the agreed secret
func (s *Service) ProcessNegotiatedSecret(secret *sharedsecret.Secret) (*Chat, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
chatID := negotiatedID(secret.Identity)
// If we already have a chat do nothing
if _, ok := s.chats[chatID]; ok {
return s.chats[chatID], nil
}
keyString := fmt.Sprintf("%x", secret.Key)
filter, err := s.addSymmetric(keyString)
if err != nil {
return nil, err
}
identityStr := fmt.Sprintf("%x", crypto.FromECDSAPub(secret.Identity))
chat := &Chat{
ChatID: chatID,
Topic: filter.Topic,
SymKeyID: filter.SymKeyID,
FilterID: filter.FilterID,
Identity: identityStr,
Listen: true,
}
log.Info("PROCESSING SECRET", "chat-id", chatID, "topic", filter.Topic, "symKey", keyString)
s.chats[chat.ChatID] = chat
return chat, nil
}
// ToTopic converts a string to a whisper topic
func ToTopic(s string) []byte {
return crypto.Keccak256([]byte(s))[:whisper.TopicLength]
}
// PublicKeyToPartitionedTopic returns the associated partitioned topic string
// with the given public key
func PublicKeyToPartitionedTopic(publicKey *ecdsa.PublicKey) string {
partition := big.NewInt(0)
partition.Mod(publicKey.X, nPartitions)
return fmt.Sprintf("contact-discovery-%d", partition.Int64())
}
// PublicKeyToPartitionedTopicBytes returns the bytes of the partitioned topic
// associated with the given public key
func PublicKeyToPartitionedTopicBytes(publicKey *ecdsa.PublicKey) []byte {
return ToTopic(PublicKeyToPartitionedTopic(publicKey))
}
// loadDiscovery adds the discovery filter
func (s *Service) loadDiscovery(myKey *ecdsa.PrivateKey) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if _, ok := s.chats[discoveryTopic]; ok {
return nil
}
discoveryChat := &Chat{
ChatID: discoveryTopic,
Listen: true,
}
discoveryResponse, err := s.addAsymmetricFilter(myKey, discoveryChat.ChatID, true)
if err != nil {
return err
}
discoveryChat.Topic = discoveryResponse.Topic
discoveryChat.FilterID = discoveryResponse.FilterID
s.chats[discoveryChat.ChatID] = discoveryChat
return nil
}
// loadPublic adds a filter for a public chat
func (s *Service) loadPublic(chat *Chat) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if _, ok := s.chats[chat.ChatID]; ok {
return nil
}
filterAndTopic, err := s.addSymmetric(chat.ChatID)
if err != nil {
return err
}
chat.FilterID = filterAndTopic.FilterID
chat.SymKeyID = filterAndTopic.SymKeyID
chat.Topic = filterAndTopic.Topic
chat.Listen = true
s.chats[chat.ChatID] = chat
return nil
}
// loadOneToOne creates two filters for a given chat, one listening to the contact codes
// and another on the partitioned topic, if listen is specified.
func (s *Service) loadOneToOne(myKey *ecdsa.PrivateKey, identity string, listen bool) ([]*Chat, error) {
var chats []*Chat
contactCodeChat, err := s.loadContactCode(identity)
if err != nil {
return nil, err
}
chats = append(chats, contactCodeChat)
if listen {
publicKeyBytes, err := hex.DecodeString(identity)
if err != nil {
return nil, err
}
publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes)
if err != nil {
return nil, err
}
partitionedChat, err := s.LoadPartitioned(myKey, publicKey, listen)
if err != nil {
return nil, err
}
chats = append(chats, partitionedChat)
}
return chats, nil
}
// loadContactCode creates a filter for the topic are advertised for a given identity
func (s *Service) loadContactCode(identity string) (*Chat, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
chatID := ContactCodeTopic(identity)
if _, ok := s.chats[chatID]; ok {
return s.chats[chatID], nil
}
contactCodeFilter, err := s.addSymmetric(chatID)
if err != nil {
return nil, err
}
chat := &Chat{
ChatID: chatID,
FilterID: contactCodeFilter.FilterID,
Topic: contactCodeFilter.Topic,
SymKeyID: contactCodeFilter.SymKeyID,
Identity: identity,
Listen: true,
}
s.chats[chatID] = chat
return chat, nil
}
// addSymmetric adds a symmetric key filter
func (s *Service) addSymmetric(chatID string) (*Filter, error) {
var symKey []byte
topic := ToTopic(chatID)
topics := [][]byte{topic}
symKeyID, err := s.whisper.AddSymKeyFromPassword(chatID)
if err != nil {
log.Error("SYM KEYN FAILED", "err", err)
return nil, err
}
if symKey, err = s.whisper.GetSymKey(symKeyID); err != nil {
return nil, err
}
f := &whisper.Filter{
KeySym: symKey,
PoW: minPow,
AllowP2P: true,
Topics: topics,
Messages: s.whisper.NewMessageStore(),
}
id, err := s.whisper.Subscribe(f)
if err != nil {
return nil, err
}
return &Filter{
FilterID: id,
SymKeyID: symKeyID,
Topic: whisper.BytesToTopic(topic),
}, nil
}
// addAsymmetricFilter adds a filter with our privatekey, and set minPow according to the listen parameter
func (s *Service) addAsymmetricFilter(keyAsym *ecdsa.PrivateKey, chatID string, listen bool) (*Filter, error) {
var err error
var pow float64
if listen {
pow = minPow
} else {
// Set high pow so we discard messages
pow = 1
}
topic := ToTopic(chatID)
topics := [][]byte{topic}
f := &whisper.Filter{
KeyAsym: keyAsym,
PoW: pow,
AllowP2P: true,
Topics: topics,
Messages: s.whisper.NewMessageStore(),
}
id, err := s.whisper.Subscribe(f)
if err != nil {
return nil, err
}
return &Filter{FilterID: id, Topic: whisper.BytesToTopic(topic)}, nil
}
func negotiatedID(identity *ecdsa.PublicKey) string {
return fmt.Sprintf("0x%x-negotiated", crypto.FromECDSAPub(identity))
}
func (s *Service) load(myKey *ecdsa.PrivateKey, chat *Chat) ([]*Chat, error) {
log.Debug("Loading chat", "chatID", chat.ChatID)
if chat.OneToOne {
return s.loadOneToOne(myKey, chat.Identity, false)
}
return []*Chat{chat}, s.loadPublic(chat)
}