mirror of
https://github.com/status-im/status-console-client.git
synced 2025-02-25 17:15:14 +00:00
450 lines
10 KiB
Go
450 lines
10 KiB
Go
package adapters
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"fmt"
|
|
"log"
|
|
"path/filepath"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/p2p"
|
|
"github.com/pkg/errors"
|
|
"github.com/status-im/status-console-client/protocol/v1"
|
|
"github.com/status-im/status-go/node"
|
|
"github.com/status-im/status-go/services/shhext"
|
|
"github.com/status-im/status-go/services/shhext/chat"
|
|
whisper "github.com/status-im/whisper/whisperv6"
|
|
)
|
|
|
|
type whisperServiceKeysManager struct {
|
|
shh *whisper.Whisper
|
|
|
|
// Identity of the current user.
|
|
// It must be the same private key
|
|
// that is used in the PFS service.
|
|
privateKey *ecdsa.PrivateKey
|
|
|
|
passToSymKeyMutex sync.RWMutex
|
|
passToSymKeyCache map[string]string
|
|
}
|
|
|
|
func (m *whisperServiceKeysManager) PrivateKey() *ecdsa.PrivateKey {
|
|
return m.privateKey
|
|
}
|
|
|
|
func (m *whisperServiceKeysManager) AddOrGetKeyPair(priv *ecdsa.PrivateKey) (string, error) {
|
|
// caching is handled in Whisper
|
|
return m.shh.AddKeyPair(priv)
|
|
}
|
|
|
|
func (m *whisperServiceKeysManager) AddOrGetSymKeyFromPassword(password string) (string, error) {
|
|
m.passToSymKeyMutex.Lock()
|
|
defer m.passToSymKeyMutex.Unlock()
|
|
|
|
if val, ok := m.passToSymKeyCache[password]; ok {
|
|
return val, nil
|
|
}
|
|
|
|
id, err := m.shh.AddSymKeyFromPassword(password)
|
|
if err != nil {
|
|
return id, err
|
|
}
|
|
|
|
m.passToSymKeyCache[password] = id
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func (m *whisperServiceKeysManager) GetRawSymKey(id string) ([]byte, error) {
|
|
return m.shh.GetSymKey(id)
|
|
}
|
|
|
|
// WhisperServiceAdapter is an adapter for Whisper service
|
|
// the implements Protocol interface.
|
|
type WhisperServiceAdapter struct {
|
|
node *node.StatusNode // TODO: replace with an interface
|
|
shh *whisper.Whisper
|
|
keysManager *whisperServiceKeysManager
|
|
|
|
pfs *chat.ProtocolService
|
|
|
|
selectedMailServerEnode string
|
|
}
|
|
|
|
// WhisperServiceAdapter must implement Protocol interface.
|
|
var _ protocol.Protocol = (*WhisperServiceAdapter)(nil)
|
|
|
|
// NewWhisperServiceAdapter returns a new WhisperServiceAdapter.
|
|
func NewWhisperServiceAdapter(node *node.StatusNode, shh *whisper.Whisper, privateKey *ecdsa.PrivateKey) *WhisperServiceAdapter {
|
|
return &WhisperServiceAdapter{
|
|
node: node,
|
|
shh: shh,
|
|
keysManager: &whisperServiceKeysManager{
|
|
shh: shh,
|
|
privateKey: privateKey,
|
|
passToSymKeyCache: make(map[string]string),
|
|
},
|
|
}
|
|
}
|
|
|
|
// InitPFS adds support for PFS messages.
|
|
func (a *WhisperServiceAdapter) InitPFS(baseDir string) error {
|
|
addBundlesHandler := func(addedBundles []chat.IdentityAndIDPair) {
|
|
log.Printf("added bundles: %v", addedBundles)
|
|
}
|
|
|
|
const (
|
|
// TODO: manage these values properly
|
|
dbPath = "pfs_v1.db"
|
|
sqlSecretKey = "enc-key-abc"
|
|
instalationID = "instalation-1"
|
|
)
|
|
|
|
dir := filepath.Join(baseDir, dbPath)
|
|
persistence, err := chat.NewSQLLitePersistence(dir, sqlSecretKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pfs := chat.NewProtocolService(
|
|
chat.NewEncryptionService(
|
|
persistence,
|
|
chat.DefaultEncryptionServiceConfig(instalationID),
|
|
),
|
|
addBundlesHandler,
|
|
)
|
|
|
|
a.SetPFS(pfs)
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetPFS sets the PFS service and a private key.
|
|
func (a *WhisperServiceAdapter) SetPFS(pfs *chat.ProtocolService) {
|
|
a.pfs = pfs
|
|
}
|
|
|
|
// Subscribe subscribes to a public chat using the Whisper service.
|
|
func (a *WhisperServiceAdapter) Subscribe(
|
|
ctx context.Context,
|
|
in chan<- *protocol.Message,
|
|
options protocol.SubscribeOptions,
|
|
) (*protocol.Subscription, error) {
|
|
if err := options.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filter := newFilter(a.keysManager)
|
|
if err := updateFilterFromSubscribeOptions(filter, options); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filterID, err := a.shh.Subscribe(filter.ToWhisper())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
subWhisper := newWhisperSubscription(a.shh, filterID)
|
|
sub := protocol.NewSubscription()
|
|
|
|
go func() {
|
|
defer subWhisper.Unsubscribe() // nolint: errcheck
|
|
|
|
t := time.NewTicker(time.Second)
|
|
defer t.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-t.C:
|
|
received, err := subWhisper.Messages()
|
|
if err != nil {
|
|
sub.Cancel(err)
|
|
return
|
|
}
|
|
|
|
messages := a.handleMessages(received)
|
|
for _, m := range messages {
|
|
in <- m
|
|
}
|
|
case <-sub.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return sub, nil
|
|
}
|
|
|
|
func (a *WhisperServiceAdapter) handleMessages(received []*whisper.ReceivedMessage) []*protocol.Message {
|
|
var messages []*protocol.Message
|
|
|
|
for _, item := range received {
|
|
message, err := a.decodeMessage(item)
|
|
if err != nil {
|
|
log.Printf("failed to decode message %#+x: %v", item.EnvelopeHash.Bytes(), err)
|
|
continue
|
|
}
|
|
messages = append(messages, message)
|
|
}
|
|
|
|
sort.Slice(messages, func(i, j int) bool {
|
|
return messages[i].Clock < messages[j].Clock
|
|
})
|
|
|
|
return messages
|
|
}
|
|
|
|
func (a *WhisperServiceAdapter) decodeMessage(message *whisper.ReceivedMessage) (*protocol.Message, error) {
|
|
payload := message.Payload
|
|
publicKey := message.SigToPubKey()
|
|
hash := message.EnvelopeHash.Bytes()
|
|
|
|
if a.pfs != nil {
|
|
decryptedPayload, err := a.pfs.HandleMessage(
|
|
a.keysManager.PrivateKey(),
|
|
publicKey,
|
|
payload,
|
|
hash,
|
|
)
|
|
if err != nil {
|
|
log.Printf("failed to handle message %#+x by PFS: %v", hash, err)
|
|
} else {
|
|
payload = decryptedPayload
|
|
}
|
|
}
|
|
|
|
decoded, err := protocol.DecodeMessage(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
decoded.ID = hash
|
|
decoded.SigPubKey = publicKey
|
|
|
|
return &decoded, nil
|
|
}
|
|
|
|
// Send sends a new message using the Whisper service.
|
|
func (a *WhisperServiceAdapter) Send(
|
|
ctx context.Context,
|
|
data []byte,
|
|
options protocol.SendOptions,
|
|
) ([]byte, error) {
|
|
if err := options.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if a.pfs != nil {
|
|
encryptedPayload, err := a.pfs.BuildDirectMessage(
|
|
a.keysManager.PrivateKey(),
|
|
options.Recipient,
|
|
data,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data = encryptedPayload
|
|
}
|
|
|
|
newMessage, err := newNewMessage(a.keysManager, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := updateNewMessageFromSendOptions(newMessage, options); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Only public Whisper API implements logic to send messages.
|
|
shhAPI := whisper.NewPublicWhisperAPI(a.shh)
|
|
return shhAPI.Post(ctx, newMessage.ToWhisper())
|
|
}
|
|
|
|
// Request requests messages from mail servers.
|
|
func (a *WhisperServiceAdapter) Request(ctx context.Context, options protocol.RequestOptions) error {
|
|
if err := options.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: remove from here. MailServerEnode must be provided in the params.
|
|
enode, err := a.selectAndAddMailServer()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keyID, err := a.keysManager.AddOrGetSymKeyFromPassword(MailServerPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := createShhextRequestMessagesParam(enode, keyID, options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
now := time.Now()
|
|
_, err = a.requestMessages(ctx, req, true)
|
|
|
|
log.Printf("[WhisperServiceAdapter::Request] took %s", time.Since(now))
|
|
|
|
return err
|
|
}
|
|
|
|
func (a *WhisperServiceAdapter) selectAndAddMailServer() (string, error) {
|
|
if a.selectedMailServerEnode != "" {
|
|
return a.selectedMailServerEnode, nil
|
|
}
|
|
|
|
config := a.node.Config()
|
|
enode := randomItem(config.ClusterConfig.TrustedMailServers)
|
|
errCh := waitForPeerAsync(
|
|
a.node.GethNode().Server(),
|
|
enode,
|
|
p2p.PeerEventTypeAdd,
|
|
time.Second*5,
|
|
)
|
|
|
|
log.Printf("[WhisperServiceAdapter::selectAndAddMailServer] randomly selected %s node", enode)
|
|
|
|
if err := a.node.AddPeer(enode); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
err := <-errCh
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to add mail server %s: %v", enode, err)
|
|
} else {
|
|
a.selectedMailServerEnode = enode
|
|
}
|
|
|
|
return enode, err
|
|
}
|
|
|
|
func (a *WhisperServiceAdapter) requestMessages(ctx context.Context, req shhext.MessagesRequest, followCursor bool) (resp shhext.MessagesResponse, err error) {
|
|
shhextService, err := a.node.ShhExtService()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
shhextAPI := shhext.NewPublicAPI(shhextService)
|
|
|
|
log.Printf("[WhisperServiceAdapter::requestMessages] request for a chunk with %d messages\n", req.Limit)
|
|
start := time.Now()
|
|
resp, err = shhextAPI.RequestMessagesSync(shhext.RetryConfig{
|
|
BaseTimeout: time.Second * 10,
|
|
StepTimeout: time.Second,
|
|
MaxRetries: 3,
|
|
}, req)
|
|
if err != nil {
|
|
log.Printf("[WhisperServiceAdapter::requestMessages] timed out. err %v\n", err)
|
|
return
|
|
}
|
|
log.Printf("[WhisperServiceAdapter::requestMessages] delivery for %d message took %v\n", req.Limit, time.Since(start))
|
|
|
|
log.Printf("[WhisperServiceAdapter::requestMessages] response = %+v, err = %v\n", resp, err)
|
|
|
|
if resp.Error != nil {
|
|
err = resp.Error
|
|
return
|
|
}
|
|
if !followCursor || resp.Cursor == "" {
|
|
return
|
|
}
|
|
|
|
req.Cursor = resp.Cursor
|
|
log.Printf("[WhisperServiceAdapter::requestMessages] request messages with cursor %v\n", req.Cursor)
|
|
return a.requestMessages(ctx, req, true)
|
|
}
|
|
|
|
// whisperSubscription encapsulates a Whisper filter.
|
|
type whisperSubscription struct {
|
|
shh *whisper.Whisper
|
|
filterID string
|
|
}
|
|
|
|
// newWhisperSubscription returns a new whisperSubscription.
|
|
func newWhisperSubscription(shh *whisper.Whisper, filterID string) *whisperSubscription {
|
|
return &whisperSubscription{
|
|
shh: shh,
|
|
filterID: filterID,
|
|
}
|
|
}
|
|
|
|
// Messages retrieves a list of messages for a given filter.
|
|
func (s whisperSubscription) Messages() ([]*whisper.ReceivedMessage, error) {
|
|
f := s.shh.GetFilter(s.filterID)
|
|
if f == nil {
|
|
return nil, errors.New("filter does not exist")
|
|
}
|
|
messages := f.Retrieve()
|
|
return messages, nil
|
|
}
|
|
|
|
// Unsubscribe removes the subscription.
|
|
func (s whisperSubscription) Unsubscribe() error {
|
|
return s.shh.Unsubscribe(s.filterID)
|
|
}
|
|
|
|
type filter struct {
|
|
*whisper.Filter
|
|
keys keysManager
|
|
}
|
|
|
|
func newFilter(keys keysManager) *filter {
|
|
return &filter{
|
|
Filter: &whisper.Filter{
|
|
PoW: 0,
|
|
AllowP2P: true,
|
|
Messages: whisper.NewMemoryMessageStore(),
|
|
},
|
|
keys: keys,
|
|
}
|
|
}
|
|
|
|
func (f *filter) ToWhisper() *whisper.Filter {
|
|
return f.Filter
|
|
}
|
|
|
|
func (f *filter) updateForPublicGroup(name string) error {
|
|
topic, err := ToTopic(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.Topics = append(f.Topics, topic[:])
|
|
|
|
symKeyID, err := f.keys.AddOrGetSymKeyFromPassword(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
symKey, err := f.keys.GetRawSymKey(symKeyID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.KeySym = symKey
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *filter) updateForPrivate(name string, recipient *ecdsa.PublicKey) error {
|
|
topic, err := ToTopic(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.Topics = append(f.Topics, topic[:])
|
|
|
|
f.KeyAsym = f.keys.PrivateKey()
|
|
|
|
return nil
|
|
}
|
|
|
|
func updateFilterFromSubscribeOptions(f *filter, options protocol.SubscribeOptions) error {
|
|
if options.Recipient != nil && options.ChatName != "" {
|
|
return f.updateForPrivate(options.ChatName, options.Recipient)
|
|
} else if options.ChatName != "" {
|
|
return f.updateForPublicGroup(options.ChatName)
|
|
} else {
|
|
return errors.New("unrecognized options")
|
|
}
|
|
}
|