From ec7c0e9c7d34f73d89d8fb4be0e116aa29c3a29d Mon Sep 17 00:00:00 2001 From: frank Date: Fri, 6 Jan 2023 20:21:14 +0800 Subject: [PATCH] Sync all devices after initial pairing (#3047) --- VERSION | 2 +- api/geth_backend.go | 12 + mobile/status.go | 25 +- multiaccounts/accounts/database.go | 18 +- node/status_node_services.go | 4 + protocol/messenger.go | 167 +++++--- protocol/messenger_bookmarks.go | 6 +- protocol/messenger_chats.go | 8 +- protocol/messenger_communities.go | 8 +- protocol/messenger_contact_verification.go | 38 +- protocol/messenger_contacts.go | 30 +- protocol/messenger_group_chat.go | 2 +- protocol/messenger_installations_test.go | 2 +- protocol/messenger_saved_address.go | 26 +- protocol/messenger_sync_bookmark_test.go | 4 +- protocol/messenger_sync_raw_messages.go | 210 ++++++++++ .../messenger_sync_saved_addresses_test.go | 2 +- protocol/messenger_sync_settings.go | 4 +- protocol/messenger_sync_verification_test.go | 4 +- protocol/messenger_sync_wallets_test.go | 2 +- protocol/protobuf/pairing.pb.go | 396 ++++++++++++------ protocol/protobuf/pairing.proto | 15 + server/certs.go | 111 +---- server/certs_test.go | 8 +- server/client.go | 205 --------- server/components_test.go | 69 --- server/encryption.go | 11 - server/handlers.go | 141 ------- server/ips.go | 4 +- server/ips_test.go | 87 ---- server/pairing/certs.go | 168 ++++++++ server/pairing/client.go | 300 +++++++++++++ server/pairing/components_test.go | 165 ++++++++ server/{ => pairing}/connection.go | 2 +- server/{ => pairing}/connection_test.go | 14 +- server/{ => pairing}/events.go | 15 +- server/pairing/handlers.go | 205 +++++++++ server/{ => pairing}/payload_manager.go | 259 ++++++++---- server/{ => pairing}/payload_manager_test.go | 48 +-- server/pairing/raw_message_collector.go | 20 + server/pairing/raw_message_handler.go | 142 +++++++ .../{server_pairing.go => pairing/server.go} | 97 +++-- server/{ => pairing}/server_pairing_test.go | 84 +++- server/pairing/sync_device_test.go | 316 ++++++++++++++ server/server.go | 12 + server/server_media.go | 2 +- server/server_test.go | 4 +- services/accounts/service.go | 10 + services/ext/api.go | 2 +- 49 files changed, 2407 insertions(+), 1079 deletions(-) create mode 100644 protocol/messenger_sync_raw_messages.go delete mode 100644 server/client.go delete mode 100644 server/encryption.go delete mode 100644 server/ips_test.go create mode 100644 server/pairing/certs.go create mode 100644 server/pairing/client.go create mode 100644 server/pairing/components_test.go rename server/{ => pairing}/connection.go (99%) rename server/{ => pairing}/connection_test.go (79%) rename server/{ => pairing}/events.go (67%) create mode 100644 server/pairing/handlers.go rename server/{ => pairing}/payload_manager.go (52%) rename server/{ => pairing}/payload_manager_test.go (91%) create mode 100644 server/pairing/raw_message_collector.go create mode 100644 server/pairing/raw_message_handler.go rename server/{server_pairing.go => pairing/server.go} (50%) rename server/{ => pairing}/server_pairing_test.go (72%) create mode 100644 server/pairing/sync_device_test.go diff --git a/VERSION b/VERSION index 8387e21a2..12f9c914f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.117.3 +0.118.0 diff --git a/api/geth_backend.go b/api/geth_backend.go index 58dc1b3ed..1a831f72e 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -31,6 +31,7 @@ import ( "github.com/status-im/status-go/node" "github.com/status-im/status-go/nodecfg" "github.com/status-im/status-go/params" + "github.com/status-im/status-go/protocol" "github.com/status-im/status-go/rpc" "github.com/status-im/status-go/services/ext" "github.com/status-im/status-go/services/personal" @@ -1349,6 +1350,17 @@ func (b *GethStatusBackend) SignGroupMembership(content string) (string, error) return crypto.SignStringAsHex(content, selectedChatAccount.AccountKey.PrivateKey) } +func (b *GethStatusBackend) Messenger() *protocol.Messenger { + node := b.StatusNode() + if node != nil { + wakuExtService := node.WakuExtService() + if wakuExtService != nil { + return wakuExtService.Messenger() + } + } + return nil +} + // SignHash exposes vanilla ECDSA signing for signing a message for Swarm func (b *GethStatusBackend) SignHash(hexEncodedHash string) (string, error) { hash, err := hexutil.Decode(hexEncodedHash) diff --git a/mobile/status.go b/mobile/status.go index b1829da5a..a45847df1 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -36,6 +36,7 @@ import ( "github.com/status-im/status-go/protocol/identity/colorhash" "github.com/status-im/status-go/protocol/identity/emojihash" "github.com/status-im/status-go/server" + "github.com/status-im/status-go/server/pairing" "github.com/status-im/status-go/services/personal" "github.com/status-im/status-go/services/typeddata" "github.com/status-im/status-go/signal" @@ -938,42 +939,40 @@ func GenerateImages(filepath string, aX, aY, bX, bY int) string { return string(data) } -// GetConnectionStringForBeingBootstrapped starts a server.Receiving server.PairingServer -// then generates a server.ConnectionParams. Used when the device is Logged out or has no Account keys +// GetConnectionStringForBeingBootstrapped starts a pairing.Receiving pairing.PairingServer +// then generates a pairing.ConnectionParams. Used when the device is Logged out or has no Account keys // and the device has no camera to read a QR code with // // Example: A desktop device (device without camera) receiving account data from mobile (device with camera) func GetConnectionStringForBeingBootstrapped(configJSON string) string { if configJSON == "" { - return makeJSONResponse(fmt.Errorf("no config given, PairingPayloadSourceConfig is expected")) + return makeJSONResponse(fmt.Errorf("no config given, PayloadSourceConfig is expected")) } - - cs, err := server.StartUpPairingServer(statusBackend.GetMultiaccountDB(), server.Receiving, configJSON) + cs, err := pairing.StartUpPairingServer(statusBackend, pairing.Receiving, configJSON) if err != nil { return makeJSONResponse(err) } return cs } -// GetConnectionStringForBootstrappingAnotherDevice starts a server.Sending server.PairingServer -// then generates a server.ConnectionParams. Used when the device is Logged in and therefore has Account keys +// GetConnectionStringForBootstrappingAnotherDevice starts a pairing.Sending pairing.Server +// then generates a pairing.ConnectionParams. Used when the device is Logged in and therefore has Account keys // and the device might not have a camera // // Example: A mobile or desktop device (devices that MAY have a camera but MUST have a screen) // sending account data to a mobile (device with camera) func GetConnectionStringForBootstrappingAnotherDevice(configJSON string) string { if configJSON == "" { - return makeJSONResponse(fmt.Errorf("no config given, PairingPayloadSourceConfig is expected")) + return makeJSONResponse(fmt.Errorf("no config given, PayloadSourceConfig is expected")) } - - cs, err := server.StartUpPairingServer(statusBackend.GetMultiaccountDB(), server.Sending, configJSON) + cs, err := pairing.StartUpPairingServer(statusBackend, pairing.Sending, configJSON) if err != nil { return makeJSONResponse(err) } return cs } -// InputConnectionStringForBootstrapping starts a server.PairingClient +// InputConnectionStringForBootstrapping starts a pairing.Client // The given server.ConnectionParams string will determine the server.Mode // // server.Mode = server.Sending @@ -988,10 +987,10 @@ func GetConnectionStringForBootstrappingAnotherDevice(configJSON string) string // a device with a screen (mobile or desktop devices) func InputConnectionStringForBootstrapping(cs, configJSON string) string { if configJSON == "" { - return makeJSONResponse(fmt.Errorf("no config given, PairingPayloadSourceConfig is expected")) + return makeJSONResponse(fmt.Errorf("no config given, PayloadSourceConfig is expected")) } - err := server.StartUpPairingClient(statusBackend.GetMultiaccountDB(), cs, configJSON) + err := pairing.StartUpPairingClient(statusBackend, cs, configJSON) return makeJSONResponse(err) } diff --git a/multiaccounts/accounts/database.go b/multiaccounts/accounts/database.go index 52764fe4b..f8d135bd1 100644 --- a/multiaccounts/accounts/database.go +++ b/multiaccounts/accounts/database.go @@ -120,15 +120,29 @@ func NewDB(db *sql.DB) (*Database, error) { } // DB Gets db sql.DB -func (db Database) DB() *sql.DB { +func (db *Database) DB() *sql.DB { return db.db } // Close closes database. -func (db Database) Close() error { +func (db *Database) Close() error { return db.db.Close() } +func (db *Database) GetAccountsByKeyUID(keyUID string) ([]*Account, error) { + accounts, err := db.GetAccounts() + if err != nil { + return nil, err + } + filteredAccounts := make([]*Account, 0) + for _, account := range accounts { + if account.KeyUID == keyUID { + filteredAccounts = append(filteredAccounts, account) + } + } + return filteredAccounts, nil +} + func (db *Database) GetAccounts() ([]*Account, error) { rows, err := db.db.Query(`SELECT address, wallet, chat, type, storage, pubkey, path, name, emoji, color, hidden, derived_from, clock, key_uid FROM accounts ORDER BY created_at`) diff --git a/node/status_node_services.go b/node/status_node_services.go index 5e4ca9225..b91acce53 100644 --- a/node/status_node_services.go +++ b/node/status_node_services.go @@ -211,6 +211,10 @@ func (b *StatusNode) AccountService() *accountssvc.Service { return b.accountsSrvc } +func (b *StatusNode) BrowserService() *browsers.Service { + return b.browsersSrvc +} + func (b *StatusNode) WakuService() *waku.Waku { return b.wakuSrvc } diff --git a/protocol/messenger.go b/protocol/messenger.go index 54cc8818d..31e1588ed 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -157,6 +157,9 @@ type Messenger struct { mailPeersMutex sync.Mutex handleMessagesMutex sync.Mutex handleImportMessagesMutex sync.Mutex + + // flag to disable checking #hasPairedDevices + localPairing bool } type connStatus int @@ -1335,7 +1338,7 @@ func (m *Messenger) watchIdentityImageChanges() { for { select { case <-channel: - err := m.syncProfilePictures() + err := m.syncProfilePictures(m.dispatchMessage) if err != nil { m.logger.Error("failed to sync profile pictures to paired devices", zap.Error(err)) } @@ -1718,9 +1721,16 @@ func (m *Messenger) ReSendChatMessage(ctx context.Context, messageID string) err return m.reSendRawMessage(ctx, messageID) } +func (m *Messenger) SetLocalPairing(localPairing bool) { + m.localPairing = localPairing +} func (m *Messenger) hasPairedDevices() bool { logger := m.logger.Named("hasPairedDevices") + if m.localPairing { + return true + } + var count int m.allInstallations.Range(func(installationID string, installation *multidevice.Installation) (shouldContinue bool) { if installation.Enabled { @@ -2166,7 +2176,7 @@ func (m *Messenger) ShareImageMessage(request *requests.ShareImageMessage) (*Mes return response, nil } -func (m *Messenger) syncProfilePictures() error { +func (m *Messenger) syncProfilePictures(rawMessageHandler RawMessageHandler) error { if !m.hasPairedDevices() { return nil } @@ -2207,12 +2217,14 @@ func (m *Messenger) syncProfilePictures() error { return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_PROFILE_PICTURE, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return err } @@ -2223,7 +2235,11 @@ func (m *Messenger) syncProfilePictures() error { // SyncDevices sends all public chats and contacts to paired devices // TODO remove use of photoPath in contacts -func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) (err error) { +func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string, rawMessageHandler RawMessageHandler) (err error) { + if rawMessageHandler == nil { + rawMessageHandler = m.dispatchMessage + } + myID := contactIDFromPublicKey(&m.identity.PublicKey) displayName, err := m.settings.DisplayName() @@ -2231,14 +2247,14 @@ func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) return err } - if _, err = m.sendContactUpdate(ctx, myID, displayName, ensName, photoPath); err != nil { + if _, err = m.sendContactUpdate(ctx, myID, displayName, ensName, photoPath, rawMessageHandler); err != nil { return err } m.allChats.Range(func(chatID string, chat *Chat) (shouldContinue bool) { isPublicChat := !chat.Timeline() && !chat.ProfileUpdates() && chat.Public() if isPublicChat && chat.Active { - err = m.syncPublicChat(ctx, chat) + err = m.syncPublicChat(ctx, chat, rawMessageHandler) if err != nil { return false } @@ -2251,7 +2267,7 @@ func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) } if !pending { - err = m.syncChatRemoving(ctx, chatID) + err = m.syncChatRemoving(ctx, chatID, rawMessageHandler) if err != nil { return false } @@ -2259,14 +2275,14 @@ func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) } if (isPublicChat || chat.OneToOne() || chat.PrivateGroupChat() || chat.CommunityChat()) && chat.Active { - err := m.syncChatMessagesRead(ctx, chatID, chat.ReadMessagesAtClockValue) + err := m.syncChatMessagesRead(ctx, chatID, chat.ReadMessagesAtClockValue, rawMessageHandler) if err != nil { return false } } if isPublicChat && chat.Active && chat.DeletedAtClockValue > 0 { - err = m.syncClearHistory(ctx, chat) + err = m.syncClearHistory(ctx, chat, rawMessageHandler) if err != nil { return false } @@ -2281,7 +2297,7 @@ func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) { if contact.ID != myID && (contact.LocalNickname != "" || contact.Added || contact.Blocked) { - if err = m.syncContact(ctx, contact); err != nil { + if err = m.syncContact(ctx, contact, rawMessageHandler); err != nil { return false } } @@ -2293,7 +2309,7 @@ func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) return err } for _, c := range cs { - if err = m.syncCommunity(ctx, c); err != nil { + if err = m.syncCommunity(ctx, c, rawMessageHandler); err != nil { return err } } @@ -2303,7 +2319,7 @@ func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) return err } for _, b := range bookmarks { - if err = m.SyncBookmark(ctx, b); err != nil { + if err = m.SyncBookmark(ctx, b, rawMessageHandler); err != nil { return err } } @@ -2313,7 +2329,7 @@ func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) return err } for id, ts := range trustedUsers { - if err = m.SyncTrustedUser(ctx, id, ts); err != nil { + if err = m.SyncTrustedUser(ctx, id, ts, rawMessageHandler); err != nil { return err } } @@ -2323,17 +2339,17 @@ func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) return err } for i := range verificationRequests { - if err = m.SyncVerificationRequest(ctx, &verificationRequests[i]); err != nil { + if err = m.SyncVerificationRequest(ctx, &verificationRequests[i], rawMessageHandler); err != nil { return err } } - err = m.syncSettings() + err = m.syncSettings(rawMessageHandler) if err != nil { return err } - err = m.syncProfilePictures() + err = m.syncProfilePictures(rawMessageHandler) if err != nil { return err } @@ -2352,14 +2368,14 @@ func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) for id, state := range ids { if state == common.ContactRequestStateAccepted || state == common.ContactRequestStateDismissed { accepted := state == common.ContactRequestStateAccepted - err := m.syncContactRequestDecision(ctx, id, accepted) + err := m.syncContactRequestDecision(ctx, id, accepted, rawMessageHandler) if err != nil { return err } } } - err = m.syncWallets(accounts) + err = m.syncWallets(accounts, rawMessageHandler) if err != nil { return err } @@ -2372,7 +2388,7 @@ func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string) for i := range savedAddresses { sa := savedAddresses[i] - err = m.syncSavedAddress(ctx, sa) + err = m.syncSavedAddress(ctx, sa, rawMessageHandler) if err != nil { return err } @@ -2385,7 +2401,7 @@ func (m *Messenger) SaveAccounts(accs []*accounts.Account) error { if err != nil { return err } - return m.syncWallets(accs) + return m.syncWallets(accs, m.dispatchMessage) } func (m *Messenger) DeleteAccount(address types.Address) error { @@ -2402,11 +2418,11 @@ func (m *Messenger) DeleteAccount(address types.Address) error { acc.Removed = true accs := []*accounts.Account{acc} - return m.syncWallets(accs) + return m.syncWallets(accs, m.dispatchMessage) } // syncWallets syncs all wallets with paired devices -func (m *Messenger) syncWallets(accs []*accounts.Account) error { +func (m *Messenger) syncWallets(accs []*accounts.Account, rawMessageHandler RawMessageHandler) error { if !m.hasPairedDevices() { return nil } @@ -2454,12 +2470,14 @@ func (m *Messenger) syncWallets(accs []*accounts.Account) error { return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_WALLET_ACCOUNT, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return err } @@ -2468,7 +2486,7 @@ func (m *Messenger) syncWallets(accs []*accounts.Account) error { return m.saveChat(chat) } -func (m *Messenger) syncContactRequestDecision(ctx context.Context, requestID string, accepted bool) error { +func (m *Messenger) syncContactRequestDecision(ctx context.Context, requestID string, accepted bool, rawMessageHandler RawMessageHandler) error { m.logger.Info("syncContactRequestDecision", zap.Any("from", requestID)) if !m.hasPairedDevices() { return nil @@ -2494,12 +2512,14 @@ func (m *Messenger) syncContactRequestDecision(ctx context.Context, requestID st return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_CONTACT_REQUEST_DECISION, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return err } @@ -2570,7 +2590,7 @@ func (m *Messenger) SendPairInstallation(ctx context.Context) (*MessengerRespons } // syncPublicChat sync a public chat with paired devices -func (m *Messenger) syncPublicChat(ctx context.Context, publicChat *Chat) error { +func (m *Messenger) syncPublicChat(ctx context.Context, publicChat *Chat, rawMessageHandler RawMessageHandler) error { var err error if !m.hasPairedDevices() { return nil @@ -2586,12 +2606,14 @@ func (m *Messenger) syncPublicChat(ctx context.Context, publicChat *Chat) error return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_PUBLIC_CHAT, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return err } @@ -2600,7 +2622,7 @@ func (m *Messenger) syncPublicChat(ctx context.Context, publicChat *Chat) error return m.saveChat(chat) } -func (m *Messenger) syncClearHistory(ctx context.Context, publicChat *Chat) error { +func (m *Messenger) syncClearHistory(ctx context.Context, publicChat *Chat, rawMessageHandler RawMessageHandler) error { var err error if !m.hasPairedDevices() { return nil @@ -2617,12 +2639,14 @@ func (m *Messenger) syncClearHistory(ctx context.Context, publicChat *Chat) erro return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_CLEAR_HISTORY, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return err } @@ -2631,7 +2655,7 @@ func (m *Messenger) syncClearHistory(ctx context.Context, publicChat *Chat) erro return m.saveChat(chat) } -func (m *Messenger) syncChatRemoving(ctx context.Context, id string) error { +func (m *Messenger) syncChatRemoving(ctx context.Context, id string, rawMessageHandler RawMessageHandler) error { var err error if !m.hasPairedDevices() { return nil @@ -2647,12 +2671,14 @@ func (m *Messenger) syncChatRemoving(ctx context.Context, id string) error { return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_CHAT_REMOVED, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return err } @@ -2662,7 +2688,7 @@ func (m *Messenger) syncChatRemoving(ctx context.Context, id string) error { } // syncContact sync as contact with paired devices -func (m *Messenger) syncContact(ctx context.Context, contact *Contact) error { +func (m *Messenger) syncContact(ctx context.Context, contact *Contact, rawMessageHandler RawMessageHandler) error { var err error if contact.IsSyncing { return nil @@ -2702,12 +2728,14 @@ func (m *Messenger) syncContact(ctx context.Context, contact *Contact) error { return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_CONTACT, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return err } @@ -2716,7 +2744,7 @@ func (m *Messenger) syncContact(ctx context.Context, contact *Contact) error { return m.saveChat(chat) } -func (m *Messenger) syncCommunity(ctx context.Context, community *communities.Community) error { +func (m *Messenger) syncCommunity(ctx context.Context, community *communities.Community, rawMessageHandler RawMessageHandler) error { logger := m.logger.Named("syncCommunity") if !m.hasPairedDevices() { logger.Debug("device has no paired devices") @@ -2747,12 +2775,14 @@ func (m *Messenger) syncCommunity(ctx context.Context, community *communities.Co return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_COMMUNITY, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return err } @@ -2762,7 +2792,7 @@ func (m *Messenger) syncCommunity(ctx context.Context, community *communities.Co return m.saveChat(chat) } -func (m *Messenger) SyncBookmark(ctx context.Context, bookmark *browsers.Bookmark) error { +func (m *Messenger) SyncBookmark(ctx context.Context, bookmark *browsers.Bookmark, rawMessageHandler RawMessageHandler) error { if !m.hasPairedDevices() { return nil } @@ -2782,20 +2812,22 @@ func (m *Messenger) SyncBookmark(ctx context.Context, bookmark *browsers.Bookmar return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_BOOKMARK, ResendAutomatically: true, - }) + } + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return err } + chat.LastClockValue = clock return m.saveChat(chat) } -func (m *Messenger) SyncTrustedUser(ctx context.Context, publicKey string, ts verification.TrustStatus) error { +func (m *Messenger) SyncTrustedUser(ctx context.Context, publicKey string, ts verification.TrustStatus, rawMessageHandler RawMessageHandler) error { if !m.hasPairedDevices() { return nil } @@ -2812,20 +2844,23 @@ func (m *Messenger) SyncTrustedUser(ctx context.Context, publicKey string, ts ve return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_TRUSTED_USER, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return err } + chat.LastClockValue = clock return m.saveChat(chat) } -func (m *Messenger) SyncVerificationRequest(ctx context.Context, vr *verification.Request) error { +func (m *Messenger) SyncVerificationRequest(ctx context.Context, vr *verification.Request, rawMessageHandler RawMessageHandler) error { if !m.hasPairedDevices() { return nil } @@ -2848,15 +2883,18 @@ func (m *Messenger) SyncVerificationRequest(ctx context.Context, vr *verificatio return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_VERIFICATION_REQUEST, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return err } + chat.LastClockValue = clock return m.saveChat(chat) } @@ -3611,6 +3649,7 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte logger.Debug("Handling SyncChatRemoved", zap.Any("message", p)) err := m.HandleSyncChatRemoved(messageState, p) if err != nil { + allMessagesProcessed = false logger.Warn("failed to handle sync removing chat", zap.Error(err)) continue } @@ -3626,7 +3665,8 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte logger.Debug("Handling SyncChatMessagesRead", zap.Any("message", p)) err := m.HandleSyncChatMessagesRead(messageState, p) if err != nil { - logger.Warn("failed to handle sync removing chat", zap.Error(err)) + allMessagesProcessed = false + logger.Warn("failed to handle sync chat message read", zap.Error(err)) continue } @@ -4094,7 +4134,7 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte p := msg.ParsedMessage.Interface().(protobuf.SyncContactRequestDecision) err := m.HandleSyncContactRequestDecision(messageState, p) if err != nil { - logger.Warn("failed to handle SyncContactRequestDecisio", zap.Error(err)) + logger.Warn("failed to handle SyncContactRequestDecision", zap.Error(err)) continue } case protobuf.SyncSavedAddress: @@ -4186,6 +4226,11 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte } } + return m.saveDataAndPrepareResponse(messageState) +} + +func (m *Messenger) saveDataAndPrepareResponse(messageState *ReceivedMessageState) (*MessengerResponse, error) { + var err error var contactsToSave []*Contact messageState.ModifiedContacts.Range(func(id string, value bool) (shouldContinue bool) { contact, ok := messageState.AllContacts.Load(id) @@ -4519,7 +4564,7 @@ func (m *Messenger) MarkMessagesSeen(chatID string, ids []string) (uint64, uint6 return count, countWithMentions, nil } -func (m *Messenger) syncChatMessagesRead(ctx context.Context, chatID string, clock uint64) error { +func (m *Messenger) syncChatMessagesRead(ctx context.Context, chatID string, clock uint64, rawMessageHandler RawMessageHandler) error { if !m.hasPairedDevices() { return nil } @@ -4535,12 +4580,14 @@ func (m *Messenger) syncChatMessagesRead(ctx context.Context, chatID string, clo return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_CHAT_MESSAGES_READ, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) return err } @@ -4557,7 +4604,7 @@ func (m *Messenger) markAllRead(chatID string, clock uint64, shouldBeSynced bool } if shouldBeSynced { - err := m.syncChatMessagesRead(context.Background(), chatID, clock) + err := m.syncChatMessagesRead(context.Background(), chatID, clock, m.dispatchMessage) if err != nil { return err } @@ -4676,7 +4723,7 @@ func (m *Messenger) muteChat(chat *Chat, contact *Contact) error { m.allChats.Store(chat.ID, chat) if contact != nil { - err := m.syncContact(context.Background(), contact) + err := m.syncContact(context.Background(), contact, m.dispatchMessage) if err != nil { return err } @@ -4712,7 +4759,7 @@ func (m *Messenger) unmuteChat(chat *Chat, contact *Contact) error { m.allChats.Store(chat.ID, chat) if contact != nil { - err := m.syncContact(context.Background(), contact) + err := m.syncContact(context.Background(), contact, m.dispatchMessage) if err != nil { return err } diff --git a/protocol/messenger_bookmarks.go b/protocol/messenger_bookmarks.go index c19d3de9f..268582303 100644 --- a/protocol/messenger_bookmarks.go +++ b/protocol/messenger_bookmarks.go @@ -14,7 +14,7 @@ func (m *Messenger) AddBookmark(ctx context.Context, bookmark browsers.Bookmark) if err != nil { return err } - return m.SyncBookmark(ctx, &bmr) + return m.SyncBookmark(ctx, &bmr, m.dispatchMessage) } func (m *Messenger) RemoveBookmark(ctx context.Context, url string) error { @@ -28,7 +28,7 @@ func (m *Messenger) RemoveBookmark(ctx context.Context, url string) error { if err != nil { return err } - return m.SyncBookmark(ctx, bmr) + return m.SyncBookmark(ctx, bmr, m.dispatchMessage) } func (m *Messenger) UpdateBookmark(ctx context.Context, oldURL string, bookmark browsers.Bookmark) error { @@ -36,7 +36,7 @@ func (m *Messenger) UpdateBookmark(ctx context.Context, oldURL string, bookmark if err != nil { return err } - return m.SyncBookmark(ctx, &bookmark) + return m.SyncBookmark(ctx, &bookmark, m.dispatchMessage) } func (m *Messenger) GarbageCollectRemovedBookmarks() error { diff --git a/protocol/messenger_chats.go b/protocol/messenger_chats.go index 140a16994..7ed22edff 100644 --- a/protocol/messenger_chats.go +++ b/protocol/messenger_chats.go @@ -164,7 +164,7 @@ func (m *Messenger) createPublicChat(chatID string, response *MessengerResponse) // Sync if it was created if !ok || !wasActive { - if err := m.syncPublicChat(context.Background(), chat); err != nil { + if err := m.syncPublicChat(context.Background(), chat, m.dispatchMessage); err != nil { return nil, err } } @@ -407,7 +407,7 @@ func (m *Messenger) deactivateChat(chatID string, deactivationClock uint64, shou // TODO: Remove filters if shouldBeSynced { - err := m.syncChatRemoving(context.Background(), chat.ID) + err := m.syncChatRemoving(context.Background(), chat.ID, m.dispatchMessage) if err != nil { return nil, err } @@ -443,7 +443,7 @@ func (m *Messenger) saveChat(chat *Chat) error { // Sync chat if it's a new active public chat, but not a timeline chat if !ok && chat.Active && chat.Public() && !chat.ProfileUpdates() && !chat.Timeline() { - if err := m.syncPublicChat(context.Background(), chat); err != nil { + if err := m.syncPublicChat(context.Background(), chat, m.dispatchMessage); err != nil { return err } } @@ -565,7 +565,7 @@ func (m *Messenger) clearHistory(id string) (*MessengerResponse, error) { } } - err = m.syncClearHistory(context.Background(), chat) + err = m.syncClearHistory(context.Background(), chat, m.dispatchMessage) if err != nil { return nil, err } diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index 28e7686dd..b3e3add09 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -368,7 +368,7 @@ func (m *Messenger) JoinCommunity(ctx context.Context, communityID types.HexByte } if com, ok := mr.communities[communityID.String()]; ok { - err = m.syncCommunity(context.Background(), com) + err = m.syncCommunity(context.Background(), com, m.dispatchMessage) if err != nil { return nil, err } @@ -493,7 +493,7 @@ func (m *Messenger) RequestToJoinCommunity(request *requests.RequestToJoinCommun if err != nil { return nil, err } - err = m.syncCommunity(context.Background(), community) + err = m.syncCommunity(context.Background(), community, m.dispatchMessage) if err != nil { return nil, err } @@ -822,7 +822,7 @@ func (m *Messenger) LeaveCommunity(communityID types.HexBytes) (*MessengerRespon m.communitiesManager.StopHistoryArchiveTasksInterval(communityID) if com, ok := mr.communities[communityID.String()]; ok { - err = m.syncCommunity(context.Background(), com) + err = m.syncCommunity(context.Background(), com, m.dispatchMessage) if err != nil { return nil, err } @@ -1052,7 +1052,7 @@ func (m *Messenger) CreateCommunity(request *requests.CreateCommunity, createDef response.AddCommunity(community) response.AddCommunitySettings(&communitySettings) - err = m.syncCommunity(context.Background(), community) + err = m.syncCommunity(context.Background(), community, m.dispatchMessage) if err != nil { return nil, err } diff --git a/protocol/messenger_contact_verification.go b/protocol/messenger_contact_verification.go index ec7c92af8..c08e3142d 100644 --- a/protocol/messenger_contact_verification.go +++ b/protocol/messenger_contact_verification.go @@ -85,7 +85,7 @@ func (m *Messenger) SendContactVerificationRequest(ctx context.Context, contactI } // We sync the contact with the other devices - err = m.syncContact(context.Background(), contact) + err = m.syncContact(context.Background(), contact, m.dispatchMessage) if err != nil { return nil, err } @@ -100,7 +100,7 @@ func (m *Messenger) SendContactVerificationRequest(ctx context.Context, contactI return nil, err } - err = m.SyncVerificationRequest(context.Background(), verifRequest) + err = m.SyncVerificationRequest(context.Background(), verifRequest, m.dispatchMessage) if err != nil { return nil, err } @@ -184,7 +184,7 @@ func (m *Messenger) CancelVerificationRequest(ctx context.Context, id string) (* } // We sync the contact with the other devices - err = m.syncContact(context.Background(), contact) + err = m.syncContact(context.Background(), contact, m.dispatchMessage) if err != nil { return nil, err } @@ -210,7 +210,7 @@ func (m *Messenger) CancelVerificationRequest(ctx context.Context, id string) (* response.AddVerificationRequest(verifRequest) - err = m.SyncVerificationRequest(context.Background(), verifRequest) + err = m.SyncVerificationRequest(context.Background(), verifRequest, m.dispatchMessage) if err != nil { return nil, err } @@ -302,7 +302,7 @@ func (m *Messenger) AcceptContactVerificationRequest(ctx context.Context, id str return nil, err } - err = m.SyncVerificationRequest(context.Background(), verifRequest) + err = m.SyncVerificationRequest(context.Background(), verifRequest, m.dispatchMessage) if err != nil { return nil, err } @@ -396,7 +396,7 @@ func (m *Messenger) VerifiedTrusted(ctx context.Context, request *requests.Verif return nil, err } - err = m.SyncTrustedUser(context.Background(), contactID, verification.TrustStatusTRUSTED) + err = m.SyncTrustedUser(context.Background(), contactID, verification.TrustStatusTRUSTED, m.dispatchMessage) if err != nil { return nil, err } @@ -436,13 +436,13 @@ func (m *Messenger) VerifiedTrusted(ctx context.Context, request *requests.Verif return nil, err } - err = m.SyncVerificationRequest(context.Background(), verifRequest) + err = m.SyncVerificationRequest(context.Background(), verifRequest, m.dispatchMessage) if err != nil { return nil, err } // We sync the contact with the other devices - err = m.syncContact(context.Background(), contact) + err = m.syncContact(context.Background(), contact, m.dispatchMessage) if err != nil { return nil, err } @@ -501,7 +501,7 @@ func (m *Messenger) VerifiedUntrustworthy(ctx context.Context, request *requests return nil, err } - err = m.SyncTrustedUser(context.Background(), contactID, verification.TrustStatusUNTRUSTWORTHY) + err = m.SyncTrustedUser(context.Background(), contactID, verification.TrustStatusUNTRUSTWORTHY, m.dispatchMessage) if err != nil { return nil, err } @@ -541,13 +541,13 @@ func (m *Messenger) VerifiedUntrustworthy(ctx context.Context, request *requests return nil, err } - err = m.SyncVerificationRequest(context.Background(), verifRequest) + err = m.SyncVerificationRequest(context.Background(), verifRequest, m.dispatchMessage) if err != nil { return nil, err } // We sync the contact with the other devices - err = m.syncContact(context.Background(), contact) + err = m.syncContact(context.Background(), contact, m.dispatchMessage) if err != nil { return nil, err } @@ -624,7 +624,7 @@ func (m *Messenger) DeclineContactVerificationRequest(ctx context.Context, id st response.AddVerificationRequest(verifRequest) - err = m.SyncVerificationRequest(context.Background(), verifRequest) + err = m.SyncVerificationRequest(context.Background(), verifRequest, m.dispatchMessage) if err != nil { return nil, err } @@ -687,7 +687,7 @@ func (m *Messenger) MarkAsTrusted(ctx context.Context, contactID string) error { return err } - return m.SyncTrustedUser(ctx, contactID, verification.TrustStatusTRUSTED) + return m.SyncTrustedUser(ctx, contactID, verification.TrustStatusTRUSTED, m.dispatchMessage) } func (m *Messenger) MarkAsUntrustworthy(ctx context.Context, contactID string) error { @@ -696,7 +696,7 @@ func (m *Messenger) MarkAsUntrustworthy(ctx context.Context, contactID string) e return err } - return m.SyncTrustedUser(ctx, contactID, verification.TrustStatusUNTRUSTWORTHY) + return m.SyncTrustedUser(ctx, contactID, verification.TrustStatusUNTRUSTWORTHY, m.dispatchMessage) } func (m *Messenger) RemoveTrustStatus(ctx context.Context, contactID string) error { @@ -705,7 +705,7 @@ func (m *Messenger) RemoveTrustStatus(ctx context.Context, contactID string) err return err } - return m.SyncTrustedUser(ctx, contactID, verification.TrustStatusUNKNOWN) + return m.SyncTrustedUser(ctx, contactID, verification.TrustStatusUNKNOWN, m.dispatchMessage) } func (m *Messenger) GetTrustStatus(contactID string) (verification.TrustStatus, error) { @@ -775,7 +775,7 @@ func (m *Messenger) HandleRequestContactVerification(state *ReceivedMessageState } m.logger.Info("SAVED", zap.String("id", persistedVR.ID)) - err = m.SyncVerificationRequest(context.Background(), persistedVR) + err = m.SyncVerificationRequest(context.Background(), persistedVR, m.dispatchMessage) if err != nil { return err } @@ -865,7 +865,7 @@ func (m *Messenger) HandleAcceptContactVerification(state *ReceivedMessageState, return err } - err = m.SyncVerificationRequest(context.Background(), persistedVR) + err = m.SyncVerificationRequest(context.Background(), persistedVR, m.dispatchMessage) if err != nil { return err } @@ -952,7 +952,7 @@ func (m *Messenger) HandleDeclineContactVerification(state *ReceivedMessageState return err } - err = m.SyncVerificationRequest(context.Background(), persistedVR) + err = m.SyncVerificationRequest(context.Background(), persistedVR, m.dispatchMessage) if err != nil { return err } @@ -1005,7 +1005,7 @@ func (m *Messenger) HandleCancelContactVerification(state *ReceivedMessageState, return err } - err = m.SyncVerificationRequest(context.Background(), persistedVR) + err = m.SyncVerificationRequest(context.Background(), persistedVR, m.dispatchMessage) if err != nil { return err } diff --git a/protocol/messenger_contacts.go b/protocol/messenger_contacts.go index 4b372fca9..1246d3387 100644 --- a/protocol/messenger_contacts.go +++ b/protocol/messenger_contacts.go @@ -37,7 +37,7 @@ func (m *Messenger) AcceptContactRequest(ctx context.Context, request *requests. return nil, err } - err = m.syncContactRequestDecision(ctx, request.ID.String(), true) + err = m.syncContactRequestDecision(ctx, request.ID.String(), true, m.dispatchMessage) if err != nil { return nil, err } @@ -195,7 +195,7 @@ func (m *Messenger) DismissContactRequest(ctx context.Context, request *requests return nil, err } - err = m.syncContactRequestDecision(ctx, request.ID.String(), false) + err = m.syncContactRequestDecision(ctx, request.ID.String(), false, m.dispatchMessage) if err != nil { return nil, err } @@ -315,7 +315,7 @@ func (m *Messenger) addContact(pubKey, ensName, nickname, displayName, contactRe if !syncing { // We sync the contact with the other devices - err := m.syncContact(context.Background(), contact) + err := m.syncContact(context.Background(), contact, m.dispatchMessage) if err != nil { return nil, err } @@ -377,7 +377,7 @@ func (m *Messenger) addContact(pubKey, ensName, nickname, displayName, contactRe } // Finally we send a contact update so they are notified we added them - response, err := m.sendContactUpdate(context.Background(), pubKey, displayName, ensName, "") + response, err := m.sendContactUpdate(context.Background(), pubKey, displayName, ensName, "", m.dispatchMessage) if err != nil { return nil, err } @@ -452,7 +452,7 @@ func (m *Messenger) removeContact(ctx context.Context, response *MessengerRespon return err } - err = m.syncContact(context.Background(), contact) + err = m.syncContact(context.Background(), contact, m.dispatchMessage) if err != nil { return err } @@ -580,7 +580,7 @@ func (m *Messenger) SetContactLocalNickname(request *requests.SetContactLocalNic response := &MessengerResponse{} response.Contacts = []*Contact{contact} - err = m.syncContact(context.Background(), contact) + err = m.syncContact(context.Background(), contact, m.dispatchMessage) if err != nil { return nil, err } @@ -621,7 +621,7 @@ func (m *Messenger) blockContact(contactID string, isDesktopFunc bool) ([]*Chat, m.allChats.Delete(buildProfileChatID(contact.ID)) } - err = m.syncContact(context.Background(), contact) + err = m.syncContact(context.Background(), contact, m.dispatchMessage) if err != nil { return nil, err } @@ -696,7 +696,7 @@ func (m *Messenger) UnblockContact(contactID string) error { m.allContacts.Store(contact.ID, contact) - err = m.syncContact(context.Background(), contact) + err = m.syncContact(context.Background(), contact, m.dispatchMessage) if err != nil { return err } @@ -719,14 +719,14 @@ func (m *Messenger) SendContactUpdates(ctx context.Context, ensName, profileImag return err } - if _, err = m.sendContactUpdate(ctx, myID, displayName, ensName, profileImage); err != nil { + if _, err = m.sendContactUpdate(ctx, myID, displayName, ensName, profileImage, m.dispatchMessage); err != nil { return err } // TODO: This should not be sending paired messages, as we do it above m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) { if contact.Added { - if _, err = m.sendContactUpdate(ctx, contact.ID, displayName, ensName, profileImage); err != nil { + if _, err = m.sendContactUpdate(ctx, contact.ID, displayName, ensName, profileImage, m.dispatchMessage); err != nil { return false } } @@ -748,10 +748,10 @@ func (m *Messenger) SendContactUpdate(ctx context.Context, chatID, ensName, prof return nil, err } - return m.sendContactUpdate(ctx, chatID, displayName, ensName, profileImage) + return m.sendContactUpdate(ctx, chatID, displayName, ensName, profileImage, m.dispatchMessage) } -func (m *Messenger) sendContactUpdate(ctx context.Context, chatID, displayName, ensName, profileImage string) (*MessengerResponse, error) { +func (m *Messenger) sendContactUpdate(ctx context.Context, chatID, displayName, ensName, profileImage string, rawMessageHandler RawMessageHandler) (*MessengerResponse, error) { var response MessengerResponse contact, ok := m.allContacts.Load(chatID) @@ -785,12 +785,14 @@ func (m *Messenger) sendContactUpdate(ctx context.Context, chatID, displayName, return nil, err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chatID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_CONTACT_UPDATE, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return nil, err } diff --git a/protocol/messenger_group_chat.go b/protocol/messenger_group_chat.go index 596684c16..3f11fb50b 100644 --- a/protocol/messenger_group_chat.go +++ b/protocol/messenger_group_chat.go @@ -725,7 +725,7 @@ func (m *Messenger) leaveGroupChat(ctx context.Context, response *MessengerRespo } if remove && shouldBeSynced { - err := m.syncChatRemoving(ctx, chat.ID) + err := m.syncChatRemoving(ctx, chat.ID, m.dispatchMessage) if err != nil { return nil, err } diff --git a/protocol/messenger_installations_test.go b/protocol/messenger_installations_test.go index 2eedcdc41..de3eecab0 100644 --- a/protocol/messenger_installations_test.go +++ b/protocol/messenger_installations_test.go @@ -210,7 +210,7 @@ func (s *MessengerInstallationSuite) TestSyncInstallation() { s.Require().NoError(err) // sync - err = s.m.SyncDevices(context.Background(), "ens-name", "profile-image") + err = s.m.SyncDevices(context.Background(), "ens-name", "profile-image", nil) s.Require().NoError(err) var allChats []*Chat diff --git a/protocol/messenger_saved_address.go b/protocol/messenger_saved_address.go index 1008624ff..ba1f78377 100644 --- a/protocol/messenger_saved_address.go +++ b/protocol/messenger_saved_address.go @@ -17,7 +17,7 @@ func (m *Messenger) UpsertSavedAddress(ctx context.Context, sa wallet.SavedAddre if err != nil { return err } - return m.syncNewSavedAddress(ctx, &sa, updatedClock) + return m.syncNewSavedAddress(ctx, &sa, updatedClock, m.dispatchMessage) } func (m *Messenger) DeleteSavedAddress(ctx context.Context, chainID uint64, address gethcommon.Address) error { @@ -25,14 +25,14 @@ func (m *Messenger) DeleteSavedAddress(ctx context.Context, chainID uint64, addr if err != nil { return err } - return m.syncDeletedSavedAddress(ctx, chainID, address, updatedClock) + return m.syncDeletedSavedAddress(ctx, chainID, address, updatedClock, m.dispatchMessage) } func (m *Messenger) garbageCollectRemovedSavedAddresses() error { return m.savedAddressesManager.DeleteSoftRemovedSavedAddresses(uint64(time.Now().AddDate(0, 0, -30).Unix())) } -func (m *Messenger) dispatchSyncSavedAddress(ctx context.Context, syncMessage protobuf.SyncSavedAddress) error { +func (m *Messenger) dispatchSyncSavedAddress(ctx context.Context, syncMessage protobuf.SyncSavedAddress, rawMessageHandler RawMessageHandler) error { if !m.hasPairedDevices() { return nil } @@ -44,12 +44,14 @@ func (m *Messenger) dispatchSyncSavedAddress(ctx context.Context, syncMessage pr return err } - _, err = m.dispatchMessage(ctx, common.RawMessage{ + rawMessage := common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_SAVED_ADDRESS, ResendAutomatically: true, - }) + } + + _, err = rawMessageHandler(ctx, rawMessage) if err != nil { return err } @@ -58,32 +60,32 @@ func (m *Messenger) dispatchSyncSavedAddress(ctx context.Context, syncMessage pr return m.saveChat(chat) } -func (m *Messenger) syncNewSavedAddress(ctx context.Context, savedAddress *wallet.SavedAddress, updateClock uint64) error { +func (m *Messenger) syncNewSavedAddress(ctx context.Context, savedAddress *wallet.SavedAddress, updateClock uint64, rawMessageHandler RawMessageHandler) error { return m.dispatchSyncSavedAddress(ctx, protobuf.SyncSavedAddress{ Address: savedAddress.Address.Bytes(), Name: savedAddress.Name, Favourite: savedAddress.Favourite, ChainId: savedAddress.ChainID, UpdateClock: updateClock, - }) + }, rawMessageHandler) } -func (m *Messenger) syncDeletedSavedAddress(ctx context.Context, chainID uint64, address gethcommon.Address, updateClock uint64) error { +func (m *Messenger) syncDeletedSavedAddress(ctx context.Context, chainID uint64, address gethcommon.Address, updateClock uint64, rawMessageHandler RawMessageHandler) error { return m.dispatchSyncSavedAddress(ctx, protobuf.SyncSavedAddress{ Address: address.Bytes(), ChainId: chainID, UpdateClock: updateClock, Removed: true, - }) + }, rawMessageHandler) } -func (m *Messenger) syncSavedAddress(ctx context.Context, savedAddress wallet.SavedAddress) (err error) { +func (m *Messenger) syncSavedAddress(ctx context.Context, savedAddress wallet.SavedAddress, rawMessageHandler RawMessageHandler) (err error) { if savedAddress.Removed { - if err = m.syncDeletedSavedAddress(ctx, savedAddress.ChainID, savedAddress.Address, savedAddress.UpdateClock); err != nil { + if err = m.syncDeletedSavedAddress(ctx, savedAddress.ChainID, savedAddress.Address, savedAddress.UpdateClock, rawMessageHandler); err != nil { return err } } else { - if err = m.syncNewSavedAddress(ctx, &savedAddress, savedAddress.UpdateClock); err != nil { + if err = m.syncNewSavedAddress(ctx, &savedAddress, savedAddress.UpdateClock, rawMessageHandler); err != nil { return err } } diff --git a/protocol/messenger_sync_bookmark_test.go b/protocol/messenger_sync_bookmark_test.go index 3f13b602a..e7200ae6d 100644 --- a/protocol/messenger_sync_bookmark_test.go +++ b/protocol/messenger_sync_bookmark_test.go @@ -109,7 +109,7 @@ func (s *MessengerSyncBookmarkSuite) TestSyncBookmark() { s.Require().NoError(err) // sync - err = s.m.SyncBookmark(context.Background(), &bookmark) + err = s.m.SyncBookmark(context.Background(), &bookmark, s.m.dispatchMessage) s.Require().NoError(err) // Wait for the message to reach its destination @@ -133,7 +133,7 @@ func (s *MessengerSyncBookmarkSuite) TestSyncBookmark() { // sync removed state bookmark.Removed = true - err = s.m.SyncBookmark(context.Background(), &bookmark) + err = s.m.SyncBookmark(context.Background(), &bookmark, s.m.dispatchMessage) s.Require().NoError(err) // Wait for the message to reach its destination diff --git a/protocol/messenger_sync_raw_messages.go b/protocol/messenger_sync_raw_messages.go new file mode 100644 index 000000000..a9fcd9944 --- /dev/null +++ b/protocol/messenger_sync_raw_messages.go @@ -0,0 +1,210 @@ +package protocol + +import ( + "context" + + "github.com/golang/protobuf/proto" + "go.uber.org/zap" + + "github.com/status-im/status-go/protocol/common" + + localnotifications "github.com/status-im/status-go/services/local-notifications" + "github.com/status-im/status-go/signal" + + "github.com/status-im/status-go/protocol/protobuf" +) + +type RawMessageHandler func(ctx context.Context, rawMessage common.RawMessage) (common.RawMessage, error) + +func (m *Messenger) HandleSyncRawMessages(rawMessages []*protobuf.RawMessage) error { + state := m.buildMessageState() + for _, rawMessage := range rawMessages { + switch rawMessage.GetMessageType() { + case protobuf.ApplicationMetadataMessage_CONTACT_UPDATE: + var message protobuf.ContactUpdate + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.HandleContactUpdate(state, message) + if err != nil { + m.logger.Warn("failed to HandleContactUpdate when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_PUBLIC_CHAT: + var message protobuf.SyncInstallationPublicChat + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + addedChat := m.HandleSyncInstallationPublicChat(state, message) + if addedChat != nil { + _, err = m.createPublicChat(addedChat.ID, state.Response) + if err != nil { + m.logger.Error("error createPublicChat when HandleSyncRawMessages", zap.Error(err)) + continue + } + } + case protobuf.ApplicationMetadataMessage_SYNC_CHAT_REMOVED: + var message protobuf.SyncChatRemoved + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.HandleSyncChatRemoved(state, message) + if err != nil { + m.logger.Error("failed to HandleSyncChatRemoved when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_CHAT_MESSAGES_READ: + var message protobuf.SyncChatMessagesRead + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.HandleSyncChatMessagesRead(state, message) + if err != nil { + m.logger.Error("failed to HandleSyncChatMessagesRead when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_CLEAR_HISTORY: + var message protobuf.SyncClearHistory + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.handleSyncClearHistory(state, message) + if err != nil { + m.logger.Error("failed to handleSyncClearHistory when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_CONTACT: + var message protobuf.SyncInstallationContactV2 + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.HandleSyncInstallationContact(state, message) + if err != nil { + m.logger.Error("failed to HandleSyncInstallationContact when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_COMMUNITY: + var message protobuf.SyncCommunity + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.handleSyncCommunity(state, message) + if err != nil { + m.logger.Error("failed to handleSyncCommunity when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_BOOKMARK: + var message protobuf.SyncBookmark + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.handleSyncBookmark(state, message) + if err != nil { + m.logger.Error("failed to handleSyncBookmark when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_TRUSTED_USER: + var message protobuf.SyncTrustedUser + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.handleSyncTrustedUser(state, message) + if err != nil { + m.logger.Error("failed to handleSyncTrustedUser when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_VERIFICATION_REQUEST: + var message protobuf.SyncVerificationRequest + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.handleSyncVerificationRequest(state, message) + if err != nil { + m.logger.Error("failed to handleSyncVerificationRequest when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_SETTING: + var message protobuf.SyncSetting + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.handleSyncSetting(state, &message) + if err != nil { + m.logger.Error("failed to handleSyncSetting when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_PROFILE_PICTURE: + var message protobuf.SyncProfilePictures + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.HandleSyncProfilePictures(state, message) + if err != nil { + m.logger.Error("failed to HandleSyncProfilePictures when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_CONTACT_REQUEST_DECISION: + var message protobuf.SyncContactRequestDecision + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.HandleSyncContactRequestDecision(state, message) + if err != nil { + m.logger.Error("failed to HandleSyncContactRequestDecision when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_WALLET_ACCOUNT: + var message protobuf.SyncWalletAccounts + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.HandleSyncWalletAccount(state, message) + if err != nil { + m.logger.Error("failed to HandleSyncWalletAccount when HandleSyncRawMessages", zap.Error(err)) + continue + } + case protobuf.ApplicationMetadataMessage_SYNC_SAVED_ADDRESS: + var message protobuf.SyncSavedAddress + err := proto.Unmarshal(rawMessage.GetPayload(), &message) + if err != nil { + return err + } + err = m.handleSyncSavedAddress(state, message) + if err != nil { + m.logger.Error("failed to handleSyncSavedAddress when HandleSyncRawMessages", zap.Error(err)) + continue + } + } + } + response, err := m.saveDataAndPrepareResponse(state) + if err != nil { + return err + } + publishMessengerResponse(response) + return nil +} + +// this is a copy implementation of the one in ext/service.go, we should refactor this? +func publishMessengerResponse(response *MessengerResponse) { + if !response.IsEmpty() { + notifications := response.Notifications() + // Clear notifications as not used for now + response.ClearNotifications() + signal.SendNewMessages(response) + localnotifications.PushMessages(notifications) + } +} diff --git a/protocol/messenger_sync_saved_addresses_test.go b/protocol/messenger_sync_saved_addresses_test.go index 6f35f7a47..46e64c475 100644 --- a/protocol/messenger_sync_saved_addresses_test.go +++ b/protocol/messenger_sync_saved_addresses_test.go @@ -142,7 +142,7 @@ func (s *MessengerSyncSavedAddressesSuite) TestSyncExistingSavedAddresses() { s.Require().NoError(err) // Trigger's a sync between devices - err = s.main.SyncDevices(context.Background(), "ens-name", "profile-image") + err = s.main.SyncDevices(context.Background(), "ens-name", "profile-image", nil) s.Require().NoError(err) // Wait and check that saved addresses are synced diff --git a/protocol/messenger_sync_settings.go b/protocol/messenger_sync_settings.go index 6e633d8cb..bbefa771f 100644 --- a/protocol/messenger_sync_settings.go +++ b/protocol/messenger_sync_settings.go @@ -52,7 +52,7 @@ func (m *Messenger) prepareSyncSettingsMessages(currentClock uint64) (resultRaw return } -func (m *Messenger) syncSettings() error { +func (m *Messenger) syncSettings(rawMessageHandler RawMessageHandler) error { logger := m.logger.Named("syncSettings") clock, _ := m.getLastClockWithRelatedChat() @@ -64,7 +64,7 @@ func (m *Messenger) syncSettings() error { } for _, rm := range rawMessages { - _, err := m.dispatchMessage(context.Background(), *rm) + _, err := rawMessageHandler(context.Background(), *rm) if err != nil { logger.Error("dispatchMessage", zap.Error(err)) return err diff --git a/protocol/messenger_sync_verification_test.go b/protocol/messenger_sync_verification_test.go index 2ae9c22e1..79a69ed1d 100644 --- a/protocol/messenger_sync_verification_test.go +++ b/protocol/messenger_sync_verification_test.go @@ -96,7 +96,7 @@ func (s *MessengerSyncVerificationRequests) TestSyncVerificationRequests() { s.Require().NoError(err) // sync - err = s.m.SyncVerificationRequest(context.Background(), request) + err = s.m.SyncVerificationRequest(context.Background(), request, s.m.dispatchMessage) s.Require().NoError(err) // Wait for the message to reach its destination @@ -160,7 +160,7 @@ func (s *MessengerSyncVerificationRequests) TestSyncTrust() { s.Require().NoError(err) // sync - err = s.m.SyncTrustedUser(context.Background(), "0x01", verification.TrustStatusTRUSTED) + err = s.m.SyncTrustedUser(context.Background(), "0x01", verification.TrustStatusTRUSTED, s.m.dispatchMessage) s.Require().NoError(err) // Wait for the message to reach its destination diff --git a/protocol/messenger_sync_wallets_test.go b/protocol/messenger_sync_wallets_test.go index fa636fbe3..a6f1d4c98 100644 --- a/protocol/messenger_sync_wallets_test.go +++ b/protocol/messenger_sync_wallets_test.go @@ -146,7 +146,7 @@ func (s *MessengerSyncWalletSuite) TestSyncWallets() { s.Len(acc1, 2, "Must have 2 accounts") // Trigger's a sync between devices - err = s.m.SyncDevices(context.Background(), "ens-name", "profile-image") + err = s.m.SyncDevices(context.Background(), "ens-name", "profile-image", nil) s.Require().NoError(err) err = tt.RetryWithBackOff(func() error { diff --git a/protocol/protobuf/pairing.pb.go b/protocol/protobuf/pairing.pb.go index c2864fe97..ef932cc9e 100644 --- a/protocol/protobuf/pairing.pb.go +++ b/protocol/protobuf/pairing.pb.go @@ -2343,6 +2343,117 @@ func (m *BackedUpProfile) GetPictures() []*SyncProfilePicture { return nil } +type RawMessage struct { + Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"` + MessageType ApplicationMetadataMessage_Type `protobuf:"varint,2,opt,name=messageType,proto3,enum=protobuf.ApplicationMetadataMessage_Type" json:"messageType,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RawMessage) Reset() { *m = RawMessage{} } +func (m *RawMessage) String() string { return proto.CompactTextString(m) } +func (*RawMessage) ProtoMessage() {} +func (*RawMessage) Descriptor() ([]byte, []int) { + return fileDescriptor_d61ab7221f0b5518, []int{29} +} + +func (m *RawMessage) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RawMessage.Unmarshal(m, b) +} +func (m *RawMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RawMessage.Marshal(b, m, deterministic) +} +func (m *RawMessage) XXX_Merge(src proto.Message) { + xxx_messageInfo_RawMessage.Merge(m, src) +} +func (m *RawMessage) XXX_Size() int { + return xxx_messageInfo_RawMessage.Size(m) +} +func (m *RawMessage) XXX_DiscardUnknown() { + xxx_messageInfo_RawMessage.DiscardUnknown(m) +} + +var xxx_messageInfo_RawMessage proto.InternalMessageInfo + +func (m *RawMessage) GetPayload() []byte { + if m != nil { + return m.Payload + } + return nil +} + +func (m *RawMessage) GetMessageType() ApplicationMetadataMessage_Type { + if m != nil { + return m.MessageType + } + return ApplicationMetadataMessage_UNKNOWN +} + +type SyncRawMessage struct { + RawMessages []*RawMessage `protobuf:"bytes,1,rep,name=rawMessages,proto3" json:"rawMessages,omitempty"` + // we need these to be able to login + SubAccountsJsonBytes []byte `protobuf:"bytes,2,opt,name=subAccountsJsonBytes,proto3" json:"subAccountsJsonBytes,omitempty"` + SettingsJsonBytes []byte `protobuf:"bytes,3,opt,name=settingsJsonBytes,proto3" json:"settingsJsonBytes,omitempty"` + NodeConfigJsonBytes []byte `protobuf:"bytes,4,opt,name=nodeConfigJsonBytes,proto3" json:"nodeConfigJsonBytes,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SyncRawMessage) Reset() { *m = SyncRawMessage{} } +func (m *SyncRawMessage) String() string { return proto.CompactTextString(m) } +func (*SyncRawMessage) ProtoMessage() {} +func (*SyncRawMessage) Descriptor() ([]byte, []int) { + return fileDescriptor_d61ab7221f0b5518, []int{30} +} + +func (m *SyncRawMessage) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SyncRawMessage.Unmarshal(m, b) +} +func (m *SyncRawMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SyncRawMessage.Marshal(b, m, deterministic) +} +func (m *SyncRawMessage) XXX_Merge(src proto.Message) { + xxx_messageInfo_SyncRawMessage.Merge(m, src) +} +func (m *SyncRawMessage) XXX_Size() int { + return xxx_messageInfo_SyncRawMessage.Size(m) +} +func (m *SyncRawMessage) XXX_DiscardUnknown() { + xxx_messageInfo_SyncRawMessage.DiscardUnknown(m) +} + +var xxx_messageInfo_SyncRawMessage proto.InternalMessageInfo + +func (m *SyncRawMessage) GetRawMessages() []*RawMessage { + if m != nil { + return m.RawMessages + } + return nil +} + +func (m *SyncRawMessage) GetSubAccountsJsonBytes() []byte { + if m != nil { + return m.SubAccountsJsonBytes + } + return nil +} + +func (m *SyncRawMessage) GetSettingsJsonBytes() []byte { + if m != nil { + return m.SettingsJsonBytes + } + return nil +} + +func (m *SyncRawMessage) GetNodeConfigJsonBytes() []byte { + if m != nil { + return m.NodeConfigJsonBytes + } + return nil +} + func init() { proto.RegisterEnum("protobuf.SyncTrustedUser_TrustStatus", SyncTrustedUser_TrustStatus_name, SyncTrustedUser_TrustStatus_value) proto.RegisterEnum("protobuf.SyncVerificationRequest_VerificationStatus", SyncVerificationRequest_VerificationStatus_name, SyncVerificationRequest_VerificationStatus_value) @@ -2379,6 +2490,8 @@ func init() { proto.RegisterType((*SyncVerificationRequest)(nil), "protobuf.SyncVerificationRequest") proto.RegisterType((*SyncContactRequestDecision)(nil), "protobuf.SyncContactRequestDecision") proto.RegisterType((*BackedUpProfile)(nil), "protobuf.BackedUpProfile") + proto.RegisterType((*RawMessage)(nil), "protobuf.RawMessage") + proto.RegisterType((*SyncRawMessage)(nil), "protobuf.SyncRawMessage") } func init() { @@ -2386,141 +2499,150 @@ func init() { } var fileDescriptor_d61ab7221f0b5518 = []byte{ - // 2174 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x58, 0x51, 0x73, 0x1b, 0x49, - 0x11, 0xbe, 0xd5, 0x2a, 0x96, 0xd4, 0x92, 0x6d, 0xdd, 0xe4, 0x2e, 0x51, 0x9c, 0xa4, 0xe2, 0x6c, - 0x48, 0x5d, 0x1e, 0x0e, 0x1f, 0x95, 0x40, 0x1d, 0x5c, 0xee, 0x8a, 0x53, 0x64, 0x73, 0xd1, 0x25, - 0x51, 0x5c, 0x63, 0x3b, 0x01, 0x8a, 0xaa, 0xad, 0xf1, 0xee, 0xc4, 0x1a, 0xbc, 0xda, 0x5d, 0x76, - 0x46, 0x0e, 0xcb, 0x0f, 0xe0, 0x07, 0xf0, 0xc2, 0xeb, 0xbd, 0xf3, 0x46, 0xd5, 0xf1, 0xc4, 0x0f, - 0xe0, 0x8d, 0x07, 0x5e, 0xa8, 0x82, 0xe2, 0x07, 0xf0, 0x2b, 0xa8, 0xe9, 0x99, 0x95, 0x76, 0x65, - 0xc9, 0x38, 0xc5, 0x13, 0x4f, 0x9a, 0xee, 0xe9, 0xee, 0xed, 0xe9, 0xee, 0xe9, 0xfe, 0x46, 0xb0, - 0x9e, 0x32, 0x91, 0x89, 0xf8, 0x64, 0x27, 0xcd, 0x12, 0x95, 0x90, 0x26, 0xfe, 0x1c, 0x4f, 0xdf, - 0x6c, 0x5d, 0x95, 0x79, 0x1c, 0xf8, 0x92, 0x2b, 0x25, 0xe2, 0x13, 0x69, 0xb6, 0x3d, 0x06, 0x37, - 0x7f, 0xc2, 0x55, 0x30, 0x16, 0xf1, 0xc9, 0x13, 0x16, 0x9c, 0xf2, 0xf0, 0x28, 0xdd, 0x65, 0x8a, - 0xed, 0x72, 0xc5, 0x44, 0x24, 0xc9, 0x1d, 0x68, 0x87, 0x4c, 0x31, 0x3f, 0x9e, 0x4e, 0x8e, 0x79, - 0xd6, 0x73, 0xb6, 0x9d, 0x07, 0xeb, 0x14, 0x34, 0x6b, 0x84, 0x1c, 0x72, 0x17, 0x3a, 0x2a, 0x51, - 0x2c, 0x2a, 0x24, 0x6a, 0x28, 0xd1, 0x46, 0x9e, 0x11, 0xf1, 0xfe, 0x5e, 0x87, 0x35, 0x6d, 0x7b, - 0x9a, 0x92, 0x0f, 0xe0, 0x4a, 0x10, 0x25, 0xc1, 0x29, 0x1a, 0xaa, 0x53, 0x43, 0x90, 0x0d, 0xa8, - 0x89, 0x10, 0x35, 0x5b, 0xb4, 0x26, 0x42, 0xf2, 0x63, 0x68, 0x06, 0x49, 0xac, 0x58, 0xa0, 0x64, - 0xcf, 0xdd, 0x76, 0x1f, 0xb4, 0x1f, 0xde, 0xdb, 0x29, 0x4e, 0xb1, 0x73, 0x90, 0xc7, 0xc1, 0x30, - 0x96, 0x8a, 0x45, 0x11, 0x53, 0x22, 0x89, 0x07, 0x46, 0xf2, 0xd5, 0x43, 0x3a, 0x53, 0x22, 0x3f, - 0x82, 0x76, 0x90, 0x4c, 0x26, 0xd3, 0x58, 0x28, 0xc1, 0x65, 0xaf, 0x8e, 0x36, 0xae, 0x57, 0x6d, - 0x0c, 0xac, 0x40, 0x4e, 0xcb, 0xb2, 0xe4, 0x25, 0x6c, 0x16, 0x66, 0x6c, 0x0c, 0x7a, 0x57, 0xb6, - 0x9d, 0x07, 0xed, 0x87, 0xf7, 0xe7, 0xea, 0x17, 0x04, 0x8c, 0x2e, 0x6a, 0x93, 0x23, 0x20, 0x25, - 0xfb, 0x85, 0xcd, 0xb5, 0x77, 0xb1, 0xb9, 0xc4, 0x00, 0x79, 0x04, 0x8d, 0x34, 0x4b, 0xde, 0x88, - 0x88, 0xf7, 0x1a, 0x68, 0xeb, 0xc6, 0xdc, 0x56, 0x61, 0x63, 0xdf, 0x08, 0xd0, 0x42, 0x92, 0xbc, - 0x80, 0x0d, 0xbb, 0x2c, 0xfc, 0x68, 0xbe, 0x8b, 0x1f, 0x0b, 0xca, 0xe4, 0x13, 0x68, 0xd8, 0x6a, - 0xea, 0xb5, 0xd0, 0xce, 0x87, 0xd5, 0x10, 0x1f, 0x98, 0x4d, 0x5a, 0x48, 0xe9, 0xe0, 0x16, 0xe5, - 0x57, 0x38, 0x00, 0xef, 0x14, 0xdc, 0x05, 0x6d, 0xef, 0xcf, 0x75, 0xe8, 0xbc, 0x98, 0x46, 0x4a, - 0xf4, 0x83, 0x20, 0x99, 0xc6, 0x8a, 0x10, 0xa8, 0xc7, 0x6c, 0xc2, 0xb1, 0xbe, 0x5a, 0x14, 0xd7, - 0xe4, 0x16, 0xb4, 0x94, 0x98, 0x70, 0xa9, 0xd8, 0x24, 0xc5, 0x2a, 0x73, 0xe9, 0x9c, 0xa1, 0x77, - 0x45, 0xc8, 0x63, 0x25, 0x82, 0x24, 0xee, 0xb9, 0xa8, 0x36, 0x67, 0x90, 0x2f, 0x01, 0x82, 0x24, - 0x4a, 0x32, 0x7f, 0xcc, 0xe4, 0xd8, 0x16, 0xd2, 0xdd, 0xb9, 0xb3, 0xe5, 0x6f, 0xef, 0x0c, 0x92, - 0x28, 0x99, 0x66, 0x4f, 0x99, 0x1c, 0xd3, 0x16, 0x2a, 0xe9, 0x25, 0xe9, 0x41, 0x03, 0x89, 0x61, - 0x88, 0x85, 0xe4, 0xd2, 0x82, 0x24, 0x1f, 0xc1, 0xe6, 0x29, 0xcf, 0x03, 0x96, 0x85, 0xbe, 0xbd, - 0xb2, 0x58, 0x16, 0x2d, 0xba, 0x61, 0xd9, 0xfb, 0x86, 0x4b, 0xae, 0x43, 0xe3, 0x94, 0xe7, 0xfe, - 0x54, 0x84, 0x98, 0xeb, 0x16, 0x5d, 0x3b, 0xe5, 0xf9, 0x91, 0x08, 0xc9, 0xe7, 0xb0, 0x26, 0x26, - 0xec, 0x84, 0xeb, 0x3c, 0x6a, 0xcf, 0xbe, 0xb3, 0xc2, 0xb3, 0x21, 0x9e, 0x47, 0xe5, 0x43, 0x2d, - 0x4c, 0xad, 0xce, 0x96, 0x07, 0x30, 0x77, 0x59, 0x5f, 0x4d, 0x11, 0x87, 0xfc, 0xd7, 0x3d, 0x67, - 0xdb, 0x7d, 0xe0, 0x52, 0x43, 0x6c, 0xfd, 0xc3, 0x81, 0xf5, 0x8a, 0x76, 0xd9, 0x19, 0xa7, 0xe2, - 0x4c, 0x11, 0xfa, 0x5a, 0x29, 0xf4, 0x3d, 0x68, 0xa4, 0x2c, 0x8f, 0x12, 0x16, 0x62, 0x68, 0x3b, - 0xb4, 0x20, 0xf5, 0xe7, 0xde, 0x8a, 0x50, 0xe9, 0x98, 0xea, 0xa0, 0x18, 0x82, 0x5c, 0x83, 0xb5, - 0x31, 0x17, 0x27, 0x63, 0x65, 0x63, 0x65, 0x29, 0xb2, 0x05, 0x4d, 0x5d, 0x78, 0x52, 0xfc, 0x86, - 0x63, 0x8c, 0x5c, 0x3a, 0xa3, 0xc9, 0x3d, 0x58, 0xcf, 0x70, 0xe5, 0x2b, 0x96, 0x9d, 0x70, 0x85, - 0x31, 0x72, 0x69, 0xc7, 0x30, 0x0f, 0x91, 0x37, 0x6f, 0x3c, 0xcd, 0x52, 0xe3, 0xf1, 0xfe, 0xe6, - 0xc0, 0xd5, 0xe7, 0x49, 0xc0, 0x22, 0x1b, 0xe9, 0x7d, 0xeb, 0xdc, 0x0f, 0xa0, 0x7e, 0xca, 0x73, - 0x89, 0xa1, 0xa8, 0xe4, 0x7b, 0x89, 0xf0, 0xce, 0x33, 0x9e, 0x53, 0x14, 0x27, 0x9f, 0x41, 0x67, - 0xa2, 0xc3, 0xce, 0x4c, 0xd8, 0x31, 0x12, 0xed, 0x87, 0xd7, 0x96, 0x27, 0x85, 0x56, 0x64, 0xf5, - 0x09, 0x53, 0x26, 0xe5, 0xdb, 0x24, 0x0b, 0x6d, 0x15, 0xce, 0xe8, 0xad, 0xef, 0x82, 0xfb, 0x8c, - 0xe7, 0x4b, 0x6b, 0x9b, 0x40, 0x5d, 0x37, 0x63, 0xfc, 0x54, 0x87, 0xe2, 0xda, 0xfb, 0xad, 0x03, - 0x5d, 0xed, 0x63, 0xb9, 0x4b, 0xae, 0xe8, 0xbc, 0x1f, 0xc1, 0xa6, 0x28, 0x49, 0xf9, 0xb3, 0x36, - 0xbc, 0x51, 0x66, 0x0f, 0x43, 0x9c, 0x03, 0xfc, 0x4c, 0x04, 0xdc, 0x57, 0x79, 0xca, 0xad, 0x87, - 0x60, 0x58, 0x87, 0x79, 0xca, 0x67, 0xce, 0xd5, 0xe7, 0xce, 0x79, 0xff, 0x76, 0xe0, 0xfa, 0x8a, - 0x76, 0x7d, 0xc9, 0x49, 0x70, 0x0f, 0xd6, 0x6d, 0xcf, 0xf1, 0xb1, 0x68, 0xed, 0x87, 0x3b, 0x96, - 0x69, 0x2a, 0xf2, 0x06, 0x34, 0x79, 0x2c, 0xfd, 0xd2, 0xe7, 0x1b, 0x3c, 0x96, 0x23, 0x1d, 0x9e, - 0xbb, 0xd0, 0x89, 0x98, 0x54, 0xfe, 0x34, 0x0d, 0x99, 0xe2, 0xe6, 0x06, 0xd6, 0x69, 0x5b, 0xf3, - 0x8e, 0x0c, 0x4b, 0x9f, 0x4c, 0xe6, 0x52, 0xf1, 0x89, 0xaf, 0xd8, 0x89, 0x6e, 0xcc, 0xae, 0x3e, - 0x99, 0x61, 0x1d, 0xb2, 0x13, 0x49, 0xee, 0xc3, 0x46, 0xa4, 0xd3, 0xee, 0xc7, 0x22, 0x38, 0xc5, - 0x8f, 0x98, 0x4b, 0xb8, 0x8e, 0xdc, 0x91, 0x65, 0x7a, 0xff, 0x72, 0xe1, 0xc6, 0xca, 0xd9, 0x44, - 0xbe, 0x07, 0x1f, 0x94, 0x1d, 0xf1, 0x51, 0x37, 0xca, 0xed, 0xe9, 0x49, 0xc9, 0xa1, 0xe7, 0x66, - 0xe7, 0xff, 0x38, 0x14, 0x3a, 0xb7, 0x2c, 0x0c, 0x79, 0x88, 0x53, 0xa1, 0x49, 0x0d, 0xa1, 0x7b, - 0xc1, 0xb1, 0x4e, 0x32, 0x0f, 0xb1, 0xe9, 0x37, 0x69, 0x41, 0x6a, 0xf9, 0xc9, 0x54, 0xfb, 0xd4, - 0x36, 0xf2, 0x48, 0x68, 0xf9, 0x8c, 0x4f, 0x92, 0x33, 0x1e, 0xf6, 0x3a, 0x46, 0xde, 0x92, 0x64, - 0x1b, 0x3a, 0x63, 0x26, 0x7d, 0x34, 0xeb, 0x4f, 0x65, 0x6f, 0x1d, 0xb7, 0x61, 0xcc, 0x64, 0x5f, - 0xb3, 0x8e, 0xf4, 0x64, 0xba, 0x7a, 0xc6, 0x33, 0xf1, 0x46, 0x04, 0xa6, 0xae, 0xa5, 0x62, 0x6a, - 0x2a, 0x7b, 0x1b, 0xd8, 0x19, 0x48, 0x79, 0xeb, 0x00, 0x77, 0x10, 0xc6, 0x64, 0x53, 0xa9, 0x0a, - 0xc9, 0x4d, 0x94, 0x6c, 0x23, 0xcf, 0x88, 0x78, 0x6f, 0xcf, 0x17, 0x73, 0x31, 0x75, 0x96, 0x17, - 0xf3, 0xb9, 0x8c, 0xd5, 0x96, 0x64, 0x6c, 0x31, 0x2d, 0xee, 0xb9, 0xb4, 0x78, 0x4f, 0x60, 0x6b, - 0xf1, 0xc3, 0xfb, 0xd3, 0xe3, 0x48, 0x04, 0x83, 0x31, 0xbb, 0xe4, 0x45, 0xf2, 0xbe, 0x75, 0x61, - 0xbd, 0x82, 0x7a, 0xfe, 0xab, 0x5e, 0x07, 0xab, 0xee, 0x0e, 0xb4, 0xd3, 0x4c, 0x9c, 0x31, 0xc5, - 0xfd, 0x53, 0x9e, 0xdb, 0x26, 0x0e, 0x96, 0xa5, 0x9b, 0xd2, 0xb6, 0x6e, 0x0c, 0x32, 0xc8, 0x44, - 0xaa, 0xfd, 0xc2, 0xa2, 0xeb, 0xd0, 0x32, 0x4b, 0xf7, 0xf4, 0x5f, 0x26, 0x22, 0xb6, 0x25, 0xd7, - 0xa4, 0x96, 0xd2, 0x1d, 0xcf, 0x24, 0x82, 0x87, 0xd8, 0xd3, 0x9b, 0x74, 0x46, 0xcf, 0x2b, 0xa2, - 0x51, 0xae, 0x88, 0x97, 0xd0, 0xcd, 0xf8, 0xaf, 0xa6, 0x5c, 0x2a, 0xe9, 0xab, 0xc4, 0xd7, 0x76, - 0xec, 0xe0, 0xbb, 0xbf, 0x0a, 0xdb, 0x59, 0xf1, 0xc3, 0xe4, 0xeb, 0x44, 0xc4, 0x74, 0x23, 0xab, - 0xd0, 0xe4, 0x31, 0x34, 0x0b, 0x44, 0x61, 0x11, 0xcc, 0x9d, 0x15, 0x86, 0x2c, 0x94, 0x91, 0x74, - 0xa6, 0xa0, 0x81, 0x03, 0x8f, 0x83, 0x2c, 0x4f, 0xd5, 0xac, 0xa2, 0xe7, 0x0c, 0xbd, 0x2b, 0x53, - 0x1e, 0x28, 0x36, 0xaf, 0xeb, 0x39, 0x43, 0xf7, 0x5d, 0x2b, 0xaa, 0xab, 0x13, 0x67, 0x4d, 0x07, - 0x23, 0xb7, 0x31, 0x67, 0x3f, 0xe3, 0xb9, 0xf4, 0xfe, 0xea, 0xc0, 0xcd, 0x0b, 0x4e, 0x64, 0xf3, - 0xe5, 0xcc, 0xf2, 0x75, 0x1b, 0x20, 0xc5, 0xda, 0xc0, 0x74, 0x99, 0xfc, 0xb7, 0x0c, 0x47, 0x67, - 0x6b, 0x96, 0x74, 0xb7, 0x9c, 0xf4, 0x0b, 0xba, 0xc6, 0x75, 0x68, 0x04, 0x63, 0xa6, 0xf4, 0x60, - 0xb8, 0x62, 0xa6, 0xbd, 0x26, 0x87, 0xa1, 0xae, 0xdb, 0x02, 0x95, 0xe6, 0x7a, 0x77, 0xcd, 0x24, - 0x7e, 0xc6, 0x1b, 0x62, 0x12, 0xf5, 0x6d, 0x32, 0x4d, 0xa2, 0x4e, 0x0d, 0xe1, 0xfd, 0xae, 0x06, - 0xdd, 0xc5, 0x72, 0x26, 0x5f, 0x94, 0x10, 0xff, 0xb9, 0xa1, 0xbb, 0xa2, 0xab, 0x96, 0xf0, 0xfe, - 0x57, 0xd0, 0xb1, 0xa7, 0xd6, 0xde, 0xc9, 0x5e, 0x6d, 0x11, 0x0d, 0xad, 0xbe, 0x3f, 0xb4, 0x9d, - 0xce, 0xd6, 0x92, 0x3c, 0x86, 0x46, 0x31, 0xbc, 0x5d, 0xac, 0x87, 0x0b, 0xdc, 0x28, 0xe6, 0x78, - 0xa1, 0xf1, 0x3f, 0xbc, 0x3a, 0xbc, 0x4f, 0x61, 0x13, 0x77, 0xb5, 0x43, 0xb6, 0xc9, 0x5d, 0xee, - 0x5e, 0x7f, 0x0e, 0x1f, 0x14, 0x8a, 0x2f, 0xb8, 0x94, 0x1a, 0xd7, 0x51, 0xce, 0x2e, 0xab, 0xfd, - 0x25, 0x5c, 0xd3, 0xda, 0xfd, 0x40, 0x89, 0x33, 0xa1, 0xf2, 0x01, 0x8f, 0x15, 0xcf, 0x2e, 0xd0, - 0xef, 0x82, 0x2b, 0x42, 0x13, 0xde, 0x0e, 0xd5, 0x4b, 0x6f, 0xd7, 0xf4, 0xa6, 0xaa, 0x85, 0x7e, - 0x10, 0x70, 0xbc, 0x04, 0x97, 0xb5, 0xb2, 0x67, 0x8a, 0xbc, 0x6a, 0x65, 0x57, 0xc8, 0x89, 0x90, - 0xf2, 0x1d, 0xcc, 0x7c, 0xe3, 0x40, 0x47, 0xdb, 0x79, 0x92, 0x24, 0xa7, 0x13, 0x96, 0x9d, 0xae, - 0x56, 0x9c, 0x66, 0x91, 0x0d, 0x83, 0x5e, 0xce, 0xc0, 0x8b, 0x5b, 0x42, 0x56, 0x37, 0xa1, 0x85, - 0x5d, 0xdb, 0xd7, 0xb2, 0xe6, 0x56, 0x34, 0x91, 0x71, 0x94, 0x45, 0xe5, 0xd9, 0x74, 0xa5, 0x3a, - 0x9b, 0x6e, 0x03, 0x84, 0x3c, 0xe2, 0x7a, 0xc6, 0x33, 0x85, 0xb7, 0xa2, 0x4e, 0x5b, 0x96, 0xd3, - 0x57, 0xde, 0xd7, 0xa6, 0xf8, 0x07, 0x11, 0x67, 0xd9, 0x53, 0x21, 0x55, 0x92, 0xe5, 0xe5, 0x3b, - 0xe6, 0x54, 0xee, 0xd8, 0x6d, 0x80, 0x40, 0x0b, 0x1a, 0x5b, 0x35, 0x63, 0xcb, 0x72, 0xfa, 0xca, - 0xfb, 0x8b, 0x03, 0x44, 0x1b, 0xb3, 0xcf, 0xbc, 0x7d, 0x11, 0xa8, 0x69, 0xc6, 0x97, 0xc2, 0xc4, - 0x12, 0x0e, 0xaf, 0xad, 0xc0, 0xe1, 0x2e, 0x3e, 0xdc, 0xcf, 0xe1, 0xf0, 0x3a, 0xb2, 0x0b, 0x1c, - 0x7e, 0x13, 0x5a, 0x38, 0xcf, 0x10, 0x88, 0x5f, 0xc1, 0x2d, 0x04, 0xe2, 0x07, 0x4b, 0x81, 0xf8, - 0x1a, 0x0a, 0xac, 0x00, 0xe2, 0x8d, 0x32, 0x10, 0x1f, 0xc3, 0xd5, 0xf3, 0x27, 0x91, 0xab, 0xdf, - 0x1a, 0x3f, 0x84, 0x66, 0x6a, 0x85, 0xec, 0x65, 0xbf, 0x55, 0xbd, 0x67, 0x55, 0x4b, 0x74, 0x26, - 0xed, 0xfd, 0xa1, 0x06, 0xef, 0x6b, 0x81, 0xd7, 0x2c, 0x8a, 0xb8, 0xba, 0x78, 0x80, 0xf7, 0xa0, - 0xc1, 0xc2, 0x30, 0xe3, 0x52, 0x16, 0x51, 0xb3, 0xa4, 0x8e, 0xcf, 0x5b, 0x34, 0x80, 0x61, 0x6b, - 0x52, 0x4b, 0xe9, 0xd8, 0xeb, 0xdc, 0x61, 0xd4, 0x9a, 0x14, 0xd7, 0x9a, 0x87, 0x98, 0xd9, 0xf4, - 0x4f, 0x5c, 0x6b, 0xcb, 0x3a, 0xf7, 0x1a, 0x14, 0x98, 0x27, 0x5f, 0x41, 0x6a, 0xe9, 0x94, 0xa9, - 0xb1, 0x05, 0x56, 0xb8, 0xd6, 0xb3, 0x64, 0xd6, 0xc2, 0xf1, 0x01, 0xd3, 0x29, 0xf7, 0xf4, 0x22, - 0xdf, 0xad, 0x52, 0xbe, 0xf5, 0x79, 0xf4, 0x2b, 0x13, 0xe7, 0x52, 0x8b, 0x1a, 0x02, 0xb3, 0x2a, - 0xc2, 0x90, 0xc7, 0x76, 0x20, 0x59, 0x6a, 0x35, 0xd2, 0xf2, 0x5e, 0x98, 0x0a, 0xab, 0x04, 0x4b, - 0x92, 0x4f, 0xa1, 0x69, 0x7b, 0x5e, 0xd1, 0xad, 0x6f, 0x56, 0xa3, 0x5f, 0x91, 0xa7, 0x33, 0x61, - 0xef, 0x4f, 0x8e, 0x29, 0xff, 0x03, 0x76, 0xc6, 0xc3, 0xbe, 0x8d, 0x65, 0x29, 0xca, 0x4e, 0x35, - 0xca, 0xcb, 0x5e, 0x94, 0xb7, 0xa0, 0xf5, 0x86, 0x9d, 0x25, 0xd3, 0x4c, 0x28, 0x6e, 0x83, 0x3f, - 0x67, 0xe8, 0x49, 0x16, 0x8c, 0x99, 0xc0, 0x87, 0x4c, 0x1d, 0x53, 0xd9, 0x40, 0x7a, 0x18, 0x5e, - 0x70, 0x65, 0xef, 0x42, 0xc7, 0xa0, 0x2f, 0xbf, 0x5c, 0x99, 0x6d, 0xc3, 0x1b, 0x60, 0x7d, 0xfe, - 0xde, 0x81, 0x0f, 0x97, 0xe2, 0x81, 0x15, 0x95, 0xb3, 0x38, 0x1d, 0xcd, 0x09, 0x2a, 0xd3, 0x71, - 0x0f, 0xee, 0x8c, 0x4d, 0x03, 0xf0, 0x59, 0x16, 0x8c, 0xc5, 0x19, 0xf7, 0xe5, 0x34, 0x4d, 0x93, - 0x4c, 0xf9, 0x3c, 0x66, 0xc7, 0x91, 0xc5, 0x82, 0x4d, 0x7a, 0xcb, 0x8a, 0xf5, 0x8d, 0xd4, 0x81, - 0x11, 0xda, 0x33, 0x32, 0xde, 0x1f, 0x1d, 0x33, 0x3a, 0x0e, 0x35, 0x52, 0xd5, 0xd8, 0x97, 0x67, - 0x97, 0x7c, 0x5b, 0x7d, 0x01, 0x6b, 0x16, 0xec, 0xea, 0xef, 0x6c, 0x2c, 0x62, 0xa8, 0x92, 0xc1, - 0x9d, 0xc3, 0x39, 0x0c, 0xa6, 0x56, 0xc9, 0xfb, 0x0c, 0xda, 0x25, 0x36, 0x69, 0x43, 0xe3, 0x68, - 0xf4, 0x6c, 0xf4, 0xf2, 0xf5, 0xa8, 0xfb, 0x9e, 0x26, 0x0e, 0xe9, 0xd1, 0xc1, 0xe1, 0xde, 0x6e, - 0xd7, 0x21, 0xef, 0xc3, 0xfa, 0xd1, 0x08, 0xc9, 0xd7, 0x2f, 0xe9, 0xe1, 0xd3, 0x9f, 0x75, 0x6b, - 0xde, 0x37, 0xae, 0xc1, 0xd2, 0xaf, 0x4a, 0x40, 0xdc, 0x02, 0x9b, 0x15, 0xce, 0x13, 0xa8, 0xbf, - 0xc9, 0x92, 0x49, 0x51, 0x0a, 0x7a, 0xad, 0x0f, 0xa4, 0x12, 0xdb, 0xb3, 0x6b, 0x2a, 0xd1, 0xa5, - 0x11, 0x8c, 0x75, 0xe5, 0xc5, 0x27, 0x05, 0x8e, 0x99, 0x33, 0x74, 0x4a, 0x2c, 0xfa, 0x33, 0xed, - 0xd4, 0xbe, 0x7f, 0x66, 0xbc, 0x3e, 0xbe, 0xc1, 0x33, 0x2e, 0xd3, 0x24, 0x96, 0xc5, 0xb5, 0x9c, - 0xd1, 0xba, 0x17, 0x67, 0x3c, 0x8d, 0x84, 0x51, 0x36, 0x25, 0xd2, 0xb2, 0x9c, 0xbe, 0x22, 0x7c, - 0xf9, 0x83, 0xa3, 0x89, 0x91, 0xfd, 0x7e, 0x35, 0xb2, 0x4b, 0x4e, 0xbd, 0xf3, 0xea, 0xdc, 0x93, - 0x64, 0xe9, 0x33, 0xc5, 0xe4, 0xb0, 0x35, 0x1b, 0xe0, 0x3f, 0x05, 0x72, 0x5e, 0xf3, 0x5c, 0x2e, - 0xf6, 0xf7, 0x46, 0xbb, 0xc3, 0xd1, 0x57, 0x5d, 0x87, 0x74, 0xa0, 0xd9, 0x1f, 0x0c, 0xf6, 0xf6, - 0x75, 0x66, 0x6a, 0x9a, 0xda, 0xdd, 0x1b, 0x3c, 0x1f, 0x8e, 0xf6, 0x76, 0xbb, 0xae, 0xa6, 0x06, - 0xfd, 0xd1, 0x60, 0xef, 0xf9, 0xde, 0x6e, 0xb7, 0xee, 0xfd, 0xd3, 0x31, 0x93, 0xbd, 0x00, 0x5b, - 0xc6, 0xcf, 0x5d, 0x1e, 0x08, 0xb9, 0xfa, 0xef, 0x84, 0x5b, 0xd0, 0xb2, 0xf1, 0x1c, 0x16, 0x95, - 0x36, 0x67, 0x90, 0x5f, 0xc0, 0x66, 0x68, 0xf5, 0xfd, 0x4a, 0xe5, 0x3d, 0x5a, 0xc4, 0x48, 0xcb, - 0x3e, 0xb9, 0x53, 0x2c, 0x6c, 0x78, 0x36, 0xc2, 0x0a, 0xed, 0x7d, 0x0c, 0x1b, 0x55, 0x89, 0xca, - 0x61, 0xdf, 0xab, 0x1c, 0xd6, 0xf1, 0xbe, 0x75, 0x60, 0x73, 0xe1, 0x6f, 0xd2, 0xd5, 0xd3, 0xe6, - 0x2e, 0x74, 0x42, 0x21, 0xd3, 0x88, 0xe5, 0x7e, 0xa9, 0x1f, 0xb5, 0x2d, 0x0f, 0x71, 0xf2, 0xc7, - 0x40, 0xca, 0x22, 0x7e, 0x19, 0x65, 0x77, 0x4b, 0x82, 0xd8, 0x4e, 0x2a, 0xe3, 0xab, 0xfe, 0x2e, - 0xe3, 0xeb, 0xc9, 0xfa, 0xcf, 0xdb, 0x3b, 0x9f, 0x3c, 0x2e, 0x64, 0x8f, 0xd7, 0x70, 0xf5, 0xe8, - 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x30, 0x89, 0x4d, 0x75, 0xf4, 0x17, 0x00, 0x00, + // 2306 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x59, 0x5f, 0x73, 0x1b, 0x49, + 0x11, 0xbf, 0x95, 0x14, 0x4b, 0x6a, 0xc9, 0xb2, 0x33, 0xc9, 0x25, 0x8a, 0x93, 0x54, 0x9c, 0x0d, + 0xa9, 0x0b, 0x55, 0xc1, 0x77, 0x95, 0x00, 0x07, 0x97, 0xbb, 0xe2, 0x14, 0xd9, 0x5c, 0x9c, 0x3f, + 0x8e, 0x6b, 0x6c, 0x27, 0x40, 0x51, 0xb5, 0x35, 0xde, 0x1d, 0x5b, 0x83, 0x57, 0xbb, 0xcb, 0xce, + 0xc8, 0x61, 0xf9, 0x00, 0x7c, 0x00, 0x5e, 0x78, 0xbd, 0x77, 0xde, 0xa8, 0x3a, 0x9e, 0xf8, 0x00, + 0xbc, 0xf1, 0xc0, 0x0b, 0x55, 0x50, 0x7c, 0x00, 0x3e, 0x05, 0x35, 0x3d, 0xb3, 0xda, 0x5d, 0x59, + 0x32, 0x4e, 0xf1, 0x74, 0x4f, 0x9e, 0xee, 0xe9, 0xee, 0xed, 0xe9, 0xee, 0xe9, 0xfe, 0x8d, 0x0c, + 0xcb, 0x09, 0x13, 0xa9, 0x88, 0x8e, 0x37, 0x92, 0x34, 0x56, 0x31, 0x69, 0xe1, 0x9f, 0xc3, 0xc9, + 0xd1, 0xda, 0x15, 0x99, 0x45, 0xbe, 0x27, 0xb9, 0x52, 0x22, 0x3a, 0x96, 0x66, 0x7b, 0xcd, 0x65, + 0x49, 0x12, 0x0a, 0x9f, 0x29, 0x11, 0x47, 0xde, 0x98, 0x2b, 0x16, 0x30, 0xc5, 0xbc, 0x31, 0x97, + 0x92, 0x1d, 0x73, 0x23, 0xe3, 0x32, 0xb8, 0xf9, 0x53, 0xae, 0xfc, 0x91, 0x88, 0x8e, 0x9f, 0x32, + 0xff, 0x84, 0x07, 0x07, 0xc9, 0x26, 0x53, 0x6c, 0x93, 0x2b, 0x26, 0x42, 0x49, 0xee, 0x40, 0x07, + 0x95, 0xa2, 0xc9, 0xf8, 0x90, 0xa7, 0x7d, 0x67, 0xdd, 0x79, 0xb0, 0x4c, 0x41, 0xb3, 0x76, 0x90, + 0x43, 0xee, 0x42, 0x57, 0xc5, 0x8a, 0x85, 0xb9, 0x44, 0x0d, 0x25, 0x3a, 0xc8, 0x33, 0x22, 0xee, + 0x3f, 0x1a, 0xb0, 0xa4, 0x6d, 0x4f, 0x12, 0x72, 0x15, 0x2e, 0xf9, 0x61, 0xec, 0x9f, 0xa0, 0xa1, + 0x06, 0x35, 0x04, 0xe9, 0x41, 0x4d, 0x04, 0xa8, 0xd9, 0xa6, 0x35, 0x11, 0x90, 0x9f, 0x40, 0xcb, + 0x8f, 0x23, 0xc5, 0x7c, 0x25, 0xfb, 0xf5, 0xf5, 0xfa, 0x83, 0xce, 0xa3, 0x7b, 0x1b, 0xf9, 0x49, + 0x37, 0xf6, 0xb2, 0xc8, 0xdf, 0x8e, 0xa4, 0x62, 0x61, 0x88, 0x07, 0x1b, 0x1a, 0xc9, 0x37, 0x8f, + 0xe8, 0x54, 0x89, 0xfc, 0x18, 0x3a, 0x7e, 0x3c, 0x1e, 0x4f, 0x22, 0xa1, 0x04, 0x97, 0xfd, 0x06, + 0xda, 0xb8, 0x5e, 0xb5, 0x31, 0xb4, 0x02, 0x19, 0x2d, 0xcb, 0x92, 0xd7, 0xb0, 0x92, 0x9b, 0xb1, + 0x31, 0xe8, 0x5f, 0x5a, 0x77, 0x1e, 0x74, 0x1e, 0xdd, 0x2f, 0xd4, 0xcf, 0x09, 0x18, 0x9d, 0xd5, + 0x26, 0x07, 0x40, 0x4a, 0xf6, 0x73, 0x9b, 0x4b, 0xef, 0x63, 0x73, 0x8e, 0x01, 0xf2, 0x18, 0x9a, + 0x49, 0x1a, 0x1f, 0x89, 0x90, 0xf7, 0x9b, 0x68, 0xeb, 0x46, 0x61, 0x2b, 0xb7, 0xb1, 0x6b, 0x04, + 0x68, 0x2e, 0x49, 0x5e, 0x41, 0xcf, 0x2e, 0x73, 0x3f, 0x5a, 0xef, 0xe3, 0xc7, 0x8c, 0x32, 0xf9, + 0x18, 0x9a, 0xb6, 0xe2, 0xfa, 0x6d, 0xb4, 0xf3, 0x61, 0x35, 0xc4, 0x7b, 0x66, 0x93, 0xe6, 0x52, + 0x3a, 0xb8, 0x79, 0x89, 0xe6, 0x0e, 0xc0, 0x7b, 0x05, 0x77, 0x46, 0xdb, 0xfd, 0x4b, 0x03, 0xba, + 0xaf, 0x26, 0xa1, 0x12, 0x03, 0xdf, 0x8f, 0x27, 0x91, 0x22, 0x04, 0x1a, 0x11, 0x1b, 0x73, 0xac, + 0xaf, 0x36, 0xc5, 0x35, 0xb9, 0x05, 0x6d, 0x25, 0xc6, 0x5c, 0x2a, 0x36, 0x4e, 0xb0, 0xca, 0xea, + 0xb4, 0x60, 0xe8, 0x5d, 0x11, 0xf0, 0x48, 0x09, 0x3f, 0x8e, 0xfa, 0x75, 0x54, 0x2b, 0x18, 0xe4, + 0x4b, 0x00, 0x3f, 0x0e, 0xe3, 0xd4, 0x1b, 0x31, 0x39, 0xb2, 0x85, 0x74, 0xb7, 0x70, 0xb6, 0xfc, + 0xed, 0x8d, 0x61, 0x1c, 0xc6, 0x93, 0xf4, 0x19, 0x93, 0x23, 0xda, 0x46, 0x25, 0xbd, 0x24, 0x7d, + 0x68, 0x22, 0xb1, 0x1d, 0x60, 0x21, 0xd5, 0x69, 0x4e, 0x92, 0x8f, 0x60, 0xe5, 0x84, 0x67, 0x3e, + 0x4b, 0x03, 0xcf, 0x5e, 0x6b, 0x2c, 0x8b, 0x36, 0xed, 0x59, 0xf6, 0xae, 0xe1, 0x92, 0xeb, 0xd0, + 0x3c, 0xe1, 0x99, 0x37, 0x11, 0x01, 0xe6, 0xba, 0x4d, 0x97, 0x4e, 0x78, 0x76, 0x20, 0x02, 0xf2, + 0x39, 0x2c, 0x89, 0x31, 0x3b, 0xe6, 0x3a, 0x8f, 0xda, 0xb3, 0xef, 0x2c, 0xf0, 0x6c, 0x1b, 0xcf, + 0xa3, 0xb2, 0x6d, 0x2d, 0x4c, 0xad, 0xce, 0x9a, 0x0b, 0x50, 0xb8, 0xac, 0xaf, 0xa6, 0x88, 0x02, + 0xfe, 0x9b, 0xbe, 0xb3, 0x5e, 0x7f, 0x50, 0xa7, 0x86, 0x58, 0xfb, 0xa7, 0x03, 0xcb, 0x15, 0xed, + 0xb2, 0x33, 0x4e, 0xc5, 0x99, 0x3c, 0xf4, 0xb5, 0x52, 0xe8, 0xfb, 0xd0, 0x4c, 0x58, 0x16, 0xc6, + 0x2c, 0xc0, 0xd0, 0x76, 0x69, 0x4e, 0xea, 0xcf, 0xbd, 0x13, 0x81, 0xd2, 0x31, 0xd5, 0x41, 0x31, + 0x04, 0xb9, 0x06, 0x4b, 0x23, 0x2e, 0x8e, 0x47, 0xca, 0xc6, 0xca, 0x52, 0x64, 0x0d, 0x5a, 0xba, + 0xf0, 0xa4, 0xf8, 0x2d, 0xc7, 0x18, 0xd5, 0xe9, 0x94, 0x26, 0xf7, 0x60, 0x39, 0xc5, 0x95, 0xa7, + 0x58, 0x7a, 0xcc, 0x15, 0xc6, 0xa8, 0x4e, 0xbb, 0x86, 0xb9, 0x8f, 0xbc, 0xa2, 0xf1, 0xb4, 0x4a, + 0x8d, 0xc7, 0xfd, 0xbb, 0x03, 0x57, 0x5e, 0xc6, 0x3e, 0x0b, 0x6d, 0xa4, 0x77, 0xad, 0x73, 0x3f, + 0x80, 0xc6, 0x09, 0xcf, 0x24, 0x86, 0xa2, 0x92, 0xef, 0x39, 0xc2, 0x1b, 0x2f, 0x78, 0x46, 0x51, + 0x9c, 0x7c, 0x06, 0xdd, 0xb1, 0x0e, 0x3b, 0x33, 0x61, 0xc7, 0x48, 0x74, 0x1e, 0x5d, 0x9b, 0x9f, + 0x14, 0x5a, 0x91, 0xd5, 0x27, 0x4c, 0x98, 0x94, 0xef, 0xe2, 0x34, 0xb0, 0x55, 0x38, 0xa5, 0xd7, + 0xbe, 0x07, 0xf5, 0x17, 0x3c, 0x9b, 0x5b, 0xdb, 0x04, 0x1a, 0xba, 0x19, 0xe3, 0xa7, 0xba, 0x14, + 0xd7, 0xee, 0xef, 0x1c, 0x58, 0xd5, 0x3e, 0x96, 0xbb, 0xe4, 0x82, 0xce, 0xfb, 0x11, 0xac, 0x88, + 0x92, 0x94, 0x37, 0x6d, 0xc3, 0xbd, 0x32, 0x7b, 0x3b, 0xc0, 0x39, 0xc0, 0x4f, 0x85, 0xcf, 0x3d, + 0x95, 0x25, 0xdc, 0x7a, 0x08, 0x86, 0xb5, 0x9f, 0x25, 0x7c, 0xea, 0x5c, 0xa3, 0x70, 0xce, 0xfd, + 0x8f, 0x03, 0xd7, 0x17, 0xb4, 0xeb, 0x0b, 0x4e, 0x82, 0x7b, 0xb0, 0x6c, 0x7b, 0x8e, 0x87, 0x45, + 0x6b, 0x3f, 0xdc, 0xb5, 0x4c, 0x53, 0x91, 0x37, 0xa0, 0xc5, 0x23, 0xe9, 0x95, 0x3e, 0xdf, 0xe4, + 0x91, 0xdc, 0xd1, 0xe1, 0xb9, 0x0b, 0xdd, 0x90, 0x49, 0xe5, 0x4d, 0x92, 0x80, 0x29, 0x6e, 0x6e, + 0x60, 0x83, 0x76, 0x34, 0xef, 0xc0, 0xb0, 0xf4, 0xc9, 0x64, 0x26, 0x15, 0x1f, 0x7b, 0x8a, 0x1d, + 0xeb, 0xc6, 0x5c, 0xd7, 0x27, 0x33, 0xac, 0x7d, 0x76, 0x2c, 0xc9, 0x7d, 0xe8, 0x85, 0x3a, 0xed, + 0x5e, 0x24, 0xfc, 0x13, 0xfc, 0x88, 0xb9, 0x84, 0xcb, 0xc8, 0xdd, 0xb1, 0x4c, 0xf7, 0xdf, 0x75, + 0xb8, 0xb1, 0x70, 0x36, 0x91, 0x4f, 0xe0, 0x6a, 0xd9, 0x11, 0x0f, 0x75, 0xc3, 0xcc, 0x9e, 0x9e, + 0x94, 0x1c, 0x7a, 0x69, 0x76, 0xbe, 0xc5, 0xa1, 0xd0, 0xb9, 0x65, 0x41, 0xc0, 0x03, 0x9c, 0x0a, + 0x2d, 0x6a, 0x08, 0xdd, 0x0b, 0x0e, 0x75, 0x92, 0x79, 0x80, 0x4d, 0xbf, 0x45, 0x73, 0x52, 0xcb, + 0x8f, 0x27, 0xda, 0xa7, 0x8e, 0x91, 0x47, 0x42, 0xcb, 0xa7, 0x7c, 0x1c, 0x9f, 0xf2, 0xa0, 0xdf, + 0x35, 0xf2, 0x96, 0x24, 0xeb, 0xd0, 0x1d, 0x31, 0xe9, 0xa1, 0x59, 0x6f, 0x22, 0xfb, 0xcb, 0xb8, + 0x0d, 0x23, 0x26, 0x07, 0x9a, 0x75, 0xa0, 0x27, 0xd3, 0x95, 0x53, 0x9e, 0x8a, 0xa3, 0x1c, 0xfc, + 0x48, 0xc5, 0xd4, 0x44, 0xf6, 0x7b, 0xd8, 0x19, 0x48, 0x79, 0x6b, 0x0f, 0x77, 0x10, 0xc6, 0xa4, + 0x13, 0xa9, 0x72, 0xc9, 0x15, 0x94, 0xec, 0x20, 0xcf, 0x88, 0xb8, 0xef, 0xce, 0x16, 0x73, 0x3e, + 0x75, 0xe6, 0x17, 0xf3, 0x99, 0x8c, 0xd5, 0xe6, 0x64, 0x6c, 0x36, 0x2d, 0xf5, 0x33, 0x69, 0x71, + 0x9f, 0xc2, 0xda, 0xec, 0x87, 0x77, 0x27, 0x87, 0xa1, 0xf0, 0x87, 0x23, 0x76, 0xc1, 0x8b, 0xe4, + 0x7e, 0x53, 0x87, 0xe5, 0x0a, 0xea, 0xf9, 0x9f, 0x7a, 0x5d, 0xac, 0xba, 0x3b, 0xd0, 0x49, 0x52, + 0x71, 0xca, 0x14, 0xf7, 0x4e, 0x78, 0x66, 0x9b, 0x38, 0x58, 0x96, 0x6e, 0x4a, 0xeb, 0xba, 0x31, + 0x48, 0x3f, 0x15, 0x89, 0xf6, 0x0b, 0x8b, 0xae, 0x4b, 0xcb, 0x2c, 0xdd, 0xd3, 0x7f, 0x15, 0x8b, + 0xc8, 0x96, 0x5c, 0x8b, 0x5a, 0x4a, 0x77, 0x3c, 0x93, 0x08, 0x1e, 0x60, 0x4f, 0x6f, 0xd1, 0x29, + 0x5d, 0x54, 0x44, 0xb3, 0x5c, 0x11, 0xaf, 0x61, 0x35, 0xe5, 0xbf, 0x9e, 0x70, 0xa9, 0xa4, 0xa7, + 0x62, 0x4f, 0xdb, 0xb1, 0x83, 0xef, 0xfe, 0x22, 0x6c, 0x67, 0xc5, 0xf7, 0xe3, 0xe7, 0xb1, 0x88, + 0x68, 0x2f, 0xad, 0xd0, 0xe4, 0x09, 0xb4, 0x72, 0x44, 0x61, 0x11, 0xcc, 0x9d, 0x05, 0x86, 0x2c, + 0x94, 0x91, 0x74, 0xaa, 0xa0, 0x81, 0x03, 0x8f, 0xfc, 0x34, 0x4b, 0xd4, 0xb4, 0xa2, 0x0b, 0x86, + 0xde, 0x95, 0x09, 0xf7, 0x15, 0x2b, 0xea, 0xba, 0x60, 0xe8, 0xbe, 0x6b, 0x45, 0x75, 0x75, 0xe2, + 0xac, 0xe9, 0x62, 0xe4, 0x7a, 0x05, 0xfb, 0x05, 0xcf, 0xa4, 0xfb, 0x37, 0x07, 0x6e, 0x9e, 0x73, + 0x22, 0x9b, 0x2f, 0x67, 0x9a, 0xaf, 0xdb, 0x00, 0x09, 0xd6, 0x06, 0xa6, 0xcb, 0xe4, 0xbf, 0x6d, + 0x38, 0x3a, 0x5b, 0xd3, 0xa4, 0xd7, 0xcb, 0x49, 0x3f, 0xa7, 0x6b, 0x5c, 0x87, 0xa6, 0x3f, 0x62, + 0x4a, 0x0f, 0x86, 0x4b, 0x66, 0xda, 0x6b, 0x72, 0x3b, 0xd0, 0x75, 0x9b, 0xa3, 0xd2, 0x4c, 0xef, + 0x2e, 0x99, 0xc4, 0x4f, 0x79, 0xdb, 0x98, 0x44, 0x7d, 0x9b, 0x4c, 0x93, 0x68, 0x50, 0x43, 0xb8, + 0xbf, 0xaf, 0xc1, 0xea, 0x6c, 0x39, 0x93, 0x2f, 0x4a, 0x88, 0xff, 0xcc, 0xd0, 0x5d, 0xd0, 0x55, + 0x4b, 0x78, 0xff, 0x2b, 0xe8, 0xda, 0x53, 0x6b, 0xef, 0x64, 0xbf, 0x36, 0x8b, 0x86, 0x16, 0xdf, + 0x1f, 0xda, 0x49, 0xa6, 0x6b, 0x49, 0x9e, 0x40, 0x33, 0x1f, 0xde, 0x75, 0xac, 0x87, 0x73, 0xdc, + 0xc8, 0xe7, 0x78, 0xae, 0xf1, 0x7f, 0xbc, 0x3a, 0xdc, 0x4f, 0x61, 0x05, 0x77, 0xb5, 0x43, 0xb6, + 0xc9, 0x5d, 0xec, 0x5e, 0x7f, 0x0e, 0x57, 0x73, 0xc5, 0x57, 0xe6, 0x5d, 0x27, 0x29, 0x67, 0x17, + 0xd5, 0xfe, 0x12, 0xae, 0x69, 0xed, 0x81, 0xaf, 0xc4, 0xa9, 0x50, 0xd9, 0x90, 0x47, 0x8a, 0xa7, + 0xe7, 0xe8, 0xaf, 0x42, 0x5d, 0x04, 0x26, 0xbc, 0x5d, 0xaa, 0x97, 0xee, 0xa6, 0xe9, 0x4d, 0x55, + 0x0b, 0x03, 0xdf, 0xe7, 0x78, 0x09, 0x2e, 0x6a, 0x65, 0xcb, 0x14, 0x79, 0xd5, 0xca, 0xa6, 0x90, + 0x63, 0x21, 0xe5, 0x7b, 0x98, 0xf9, 0xda, 0x81, 0xae, 0xb6, 0xf3, 0x34, 0x8e, 0x4f, 0xc6, 0x2c, + 0x3d, 0x59, 0xac, 0x38, 0x49, 0x43, 0x1b, 0x06, 0xbd, 0x9c, 0x82, 0x97, 0x7a, 0x09, 0x59, 0xdd, + 0x84, 0x36, 0x76, 0x6d, 0x4f, 0xcb, 0x9a, 0x5b, 0xd1, 0x42, 0xc6, 0x41, 0x1a, 0x96, 0x67, 0xd3, + 0xa5, 0xea, 0x6c, 0xba, 0x0d, 0x10, 0xf0, 0x90, 0xeb, 0x19, 0xcf, 0x14, 0xde, 0x8a, 0x06, 0x6d, + 0x5b, 0xce, 0x40, 0xb9, 0xcf, 0x4d, 0xf1, 0x0f, 0x43, 0xce, 0xd2, 0x67, 0x42, 0xaa, 0x38, 0xcd, + 0xca, 0x77, 0xcc, 0xa9, 0xdc, 0xb1, 0xdb, 0x00, 0xbe, 0x16, 0x34, 0xb6, 0x6a, 0xc6, 0x96, 0xe5, + 0x0c, 0x94, 0xfb, 0x57, 0x07, 0x88, 0x36, 0x66, 0x9f, 0x79, 0xbb, 0xc2, 0x57, 0x93, 0x94, 0xcf, + 0x85, 0x89, 0x25, 0x1c, 0x5e, 0x5b, 0x80, 0xc3, 0xeb, 0xf8, 0x70, 0x3f, 0x83, 0xc3, 0x1b, 0xc8, + 0xce, 0x71, 0xf8, 0x4d, 0x68, 0xe3, 0x3c, 0x43, 0x20, 0x7e, 0x09, 0xb7, 0x10, 0x88, 0xef, 0xcd, + 0x05, 0xe2, 0x4b, 0x28, 0xb0, 0x00, 0x88, 0x37, 0xcb, 0x40, 0x7c, 0x04, 0x57, 0xce, 0x9e, 0x44, + 0x2e, 0x7e, 0x6b, 0xfc, 0x08, 0x5a, 0x89, 0x15, 0xb2, 0x97, 0xfd, 0x56, 0xf5, 0x9e, 0x55, 0x2d, + 0xd1, 0xa9, 0xb4, 0xfb, 0xc7, 0x1a, 0x5c, 0xd6, 0x02, 0x6f, 0x59, 0x18, 0x72, 0x75, 0xfe, 0x00, + 0xef, 0x43, 0x93, 0x05, 0x41, 0xca, 0xa5, 0xcc, 0xa3, 0x66, 0x49, 0x1d, 0x9f, 0x77, 0x68, 0x00, + 0xc3, 0xd6, 0xa2, 0x96, 0xd2, 0xb1, 0xd7, 0xb9, 0xc3, 0xa8, 0xb5, 0x28, 0xae, 0x35, 0x0f, 0x31, + 0xb3, 0xe9, 0x9f, 0xb8, 0xd6, 0x96, 0x75, 0xee, 0x35, 0x28, 0x30, 0x4f, 0xbe, 0x9c, 0xd4, 0xd2, + 0x09, 0x53, 0x23, 0x0b, 0xac, 0x70, 0xad, 0x67, 0xc9, 0xb4, 0x85, 0xe3, 0x03, 0xa6, 0x5b, 0xee, + 0xe9, 0x79, 0xbe, 0xdb, 0xa5, 0x7c, 0xeb, 0xf3, 0xe8, 0x57, 0x26, 0xce, 0xa5, 0x36, 0x35, 0x04, + 0x66, 0x55, 0x04, 0x01, 0x8f, 0xec, 0x40, 0xb2, 0xd4, 0x62, 0xa4, 0xe5, 0xbe, 0x32, 0x15, 0x56, + 0x09, 0x96, 0x24, 0x9f, 0x42, 0xcb, 0xf6, 0xbc, 0xbc, 0x5b, 0xdf, 0xac, 0x46, 0xbf, 0x22, 0x4f, + 0xa7, 0xc2, 0xee, 0x9f, 0x1d, 0x53, 0xfe, 0x7b, 0xec, 0x94, 0x07, 0x03, 0x1b, 0xcb, 0x52, 0x94, + 0x9d, 0x6a, 0x94, 0xe7, 0xbd, 0x28, 0x6f, 0x41, 0xfb, 0x88, 0x9d, 0xc6, 0x93, 0x54, 0x28, 0x6e, + 0x83, 0x5f, 0x30, 0xf4, 0x24, 0xf3, 0x47, 0x4c, 0xe0, 0x43, 0xa6, 0x81, 0xa9, 0x6c, 0x22, 0xbd, + 0x1d, 0x9c, 0x73, 0x65, 0xef, 0x42, 0xd7, 0xa0, 0x2f, 0xaf, 0x5c, 0x99, 0x1d, 0xc3, 0x1b, 0x62, + 0x7d, 0xfe, 0xc1, 0x81, 0x0f, 0xe7, 0xe2, 0x81, 0x05, 0x95, 0x33, 0x3b, 0x1d, 0xcd, 0x09, 0x2a, + 0xd3, 0x71, 0x0b, 0xee, 0x8c, 0x4c, 0x03, 0xf0, 0x58, 0xea, 0x8f, 0xc4, 0x29, 0xf7, 0xe4, 0x24, + 0x49, 0xe2, 0x54, 0x79, 0x3c, 0x62, 0x87, 0xa1, 0xc5, 0x82, 0x2d, 0x7a, 0xcb, 0x8a, 0x0d, 0x8c, + 0xd4, 0x9e, 0x11, 0xda, 0x32, 0x32, 0xee, 0x9f, 0x1c, 0x33, 0x3a, 0xf6, 0x35, 0x52, 0xd5, 0xd8, + 0x97, 0xa7, 0x17, 0x7c, 0x5b, 0x7d, 0x01, 0x4b, 0x16, 0xec, 0xea, 0xef, 0xf4, 0x66, 0x31, 0x54, + 0xc9, 0xe0, 0xc6, 0x7e, 0x01, 0x83, 0xa9, 0x55, 0x72, 0x3f, 0x83, 0x4e, 0x89, 0x4d, 0x3a, 0xd0, + 0x3c, 0xd8, 0x79, 0xb1, 0xf3, 0xfa, 0xed, 0xce, 0xea, 0x07, 0x9a, 0xd8, 0xa7, 0x07, 0x7b, 0xfb, + 0x5b, 0x9b, 0xab, 0x0e, 0xb9, 0x0c, 0xcb, 0x07, 0x3b, 0x48, 0xbe, 0x7d, 0x4d, 0xf7, 0x9f, 0xfd, + 0x7c, 0xb5, 0xe6, 0x7e, 0x5d, 0x37, 0x58, 0xfa, 0x4d, 0x09, 0x88, 0x5b, 0x60, 0xb3, 0xc0, 0x79, + 0x02, 0x8d, 0xa3, 0x34, 0x1e, 0xe7, 0xa5, 0xa0, 0xd7, 0xfa, 0x40, 0x2a, 0xb6, 0x3d, 0xbb, 0xa6, + 0x62, 0x5d, 0x1a, 0xfe, 0x48, 0x57, 0x5e, 0x74, 0x9c, 0xe3, 0x98, 0x82, 0xa1, 0x53, 0x62, 0xd1, + 0x9f, 0x69, 0xa7, 0xf6, 0xfd, 0x33, 0xe5, 0x0d, 0xf0, 0x0d, 0x9e, 0x72, 0x99, 0xc4, 0x91, 0xcc, + 0xaf, 0xe5, 0x94, 0xd6, 0xbd, 0x38, 0xe5, 0x49, 0x28, 0x8c, 0xb2, 0x29, 0x91, 0xb6, 0xe5, 0x0c, + 0x14, 0xe1, 0xf3, 0x1f, 0x1c, 0x2d, 0x8c, 0xec, 0xf7, 0xab, 0x91, 0x9d, 0x73, 0xea, 0x8d, 0x37, + 0x67, 0x9e, 0x24, 0x73, 0x9f, 0x29, 0x26, 0x87, 0xed, 0xe9, 0x00, 0xff, 0x19, 0x90, 0xb3, 0x9a, + 0x67, 0x72, 0xb1, 0xbb, 0xb5, 0xb3, 0xb9, 0xbd, 0xf3, 0xd5, 0xaa, 0x43, 0xba, 0xd0, 0x1a, 0x0c, + 0x87, 0x5b, 0xbb, 0x3a, 0x33, 0x35, 0x4d, 0x6d, 0x6e, 0x0d, 0x5f, 0x6e, 0xef, 0x6c, 0x6d, 0xae, + 0xd6, 0x35, 0x35, 0x1c, 0xec, 0x0c, 0xb7, 0x5e, 0x6e, 0x6d, 0xae, 0x36, 0xdc, 0x7f, 0x39, 0x66, + 0xb2, 0xe7, 0x60, 0xcb, 0xf8, 0xb9, 0xc9, 0x7d, 0x21, 0x17, 0xff, 0x9c, 0x70, 0x0b, 0xda, 0x36, + 0x9e, 0xdb, 0x79, 0xa5, 0x15, 0x0c, 0xf2, 0x4b, 0x58, 0x09, 0xac, 0xbe, 0x57, 0xa9, 0xbc, 0xc7, + 0xb3, 0x18, 0x69, 0xde, 0x27, 0x37, 0xf2, 0x85, 0x0d, 0x4f, 0x2f, 0xa8, 0xd0, 0xee, 0x43, 0xe8, + 0x55, 0x25, 0x2a, 0x87, 0xfd, 0xa0, 0x72, 0x58, 0xc7, 0xfd, 0xc6, 0x81, 0x95, 0x99, 0x9f, 0x49, + 0x17, 0x4f, 0x9b, 0xbb, 0xd0, 0x0d, 0x84, 0x4c, 0x42, 0x96, 0x79, 0xa5, 0x7e, 0xd4, 0xb1, 0x3c, + 0xc4, 0xc9, 0x0f, 0x81, 0x94, 0x45, 0xbc, 0x32, 0xca, 0x5e, 0x2d, 0x09, 0x62, 0x3b, 0xa9, 0x8c, + 0xaf, 0xc6, 0x7b, 0x8d, 0x2f, 0x09, 0x40, 0xd9, 0x3b, 0x0b, 0xf5, 0xca, 0x63, 0xdd, 0xa9, 0x8e, + 0xf5, 0x17, 0xd0, 0xb1, 0xbf, 0xf3, 0xef, 0xeb, 0xd9, 0x53, 0xc3, 0x38, 0x7f, 0xb7, 0xf8, 0xc8, + 0xa0, 0xf8, 0xcf, 0xc0, 0x2b, 0xfb, 0x8f, 0x01, 0x6b, 0x74, 0x43, 0x2b, 0xd0, 0xb2, 0xb6, 0xae, + 0x85, 0x9e, 0xf6, 0xaa, 0xf4, 0xe5, 0x1f, 0x42, 0x27, 0x9d, 0x52, 0xf9, 0x14, 0xb8, 0x5a, 0xd8, + 0x2f, 0x44, 0x69, 0x59, 0x90, 0x3c, 0x82, 0xab, 0x72, 0x72, 0x98, 0x4f, 0x92, 0xe7, 0x32, 0x8e, + 0x9e, 0x66, 0x8a, 0xe7, 0xf3, 0x75, 0xee, 0x1e, 0x79, 0x08, 0x97, 0xf3, 0x47, 0x57, 0xa1, 0x60, + 0x5e, 0xa2, 0x67, 0x37, 0xc8, 0x27, 0x70, 0x25, 0x8a, 0x03, 0x3e, 0x8c, 0xa3, 0x23, 0x71, 0x5c, + 0xc8, 0x9b, 0x87, 0xe9, 0xbc, 0xad, 0xa7, 0xcb, 0xbf, 0xe8, 0x6c, 0x7c, 0xfc, 0x24, 0x77, 0xfd, + 0x70, 0x09, 0x57, 0x8f, 0xff, 0x1b, 0x00, 0x00, 0xff, 0xff, 0x26, 0xe4, 0xbd, 0xdb, 0x6c, 0x19, + 0x00, 0x00, } diff --git a/protocol/protobuf/pairing.proto b/protocol/protobuf/pairing.proto index 4b3ecb57a..e7dbc68b0 100644 --- a/protocol/protobuf/pairing.proto +++ b/protocol/protobuf/pairing.proto @@ -1,6 +1,7 @@ syntax = "proto3"; import "sync_settings.proto"; +import 'application_metadata_message.proto'; option go_package = "./;protobuf"; package protobuf; @@ -279,3 +280,17 @@ message BackedUpProfile { uint64 display_name_clock = 3; repeated SyncProfilePicture pictures = 4; } + +message RawMessage { + bytes payload = 1; + ApplicationMetadataMessage.Type messageType = 2; +} + +message SyncRawMessage { + repeated RawMessage rawMessages = 1; + + // we need these to be able to login + bytes subAccountsJsonBytes = 2; + bytes settingsJsonBytes = 3; + bytes nodeConfigJsonBytes = 4; +} diff --git a/server/certs.go b/server/certs.go index 846ae1298..f8e484700 100644 --- a/server/certs.go +++ b/server/certs.go @@ -8,15 +8,10 @@ import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" - "encoding/asn1" "encoding/pem" - "fmt" "math/big" "net" - "net/url" "time" - - "github.com/status-im/status-go/signal" ) var globalCertificate *tls.Certificate = nil @@ -90,7 +85,7 @@ func generateTLSCert() error { return err } - cert := GenerateX509Cert(sn, notBefore, notAfter, localhost) + cert := GenerateX509Cert(sn, notBefore, notAfter, Localhost) certPem, keyPem, err := GenerateX509PEMs(cert, priv) if err != nil { return err @@ -115,31 +110,6 @@ func PublicTLSCert() (string, error) { return globalPem, nil } -func GenerateCertFromKey(pk *ecdsa.PrivateKey, from time.Time, hostname string) (tls.Certificate, []byte, error) { - cert := GenerateX509Cert(makeSerialNumberFromKey(pk), from, from.Add(time.Hour), hostname) - certPem, keyPem, err := GenerateX509PEMs(cert, pk) - if err != nil { - return tls.Certificate{}, nil, err - } - - tlsCert, err := tls.X509KeyPair(certPem, keyPem) - if err != nil { - return tls.Certificate{}, nil, err - } - - block, _ := pem.Decode(certPem) - if block == nil { - return tls.Certificate{}, nil, fmt.Errorf("failed to decode certPem") - } - leaf, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return tls.Certificate{}, nil, err - } - tlsCert.Leaf = leaf - - return tlsCert, certPem, nil -} - // ToECDSA takes a []byte of D and uses it to create an ecdsa.PublicKey on the elliptic.P256 curve // this function is basically a P256 curve version of eth-node/crypto.ToECDSA without all the nice validation func ToECDSA(d []byte) *ecdsa.PrivateKey { @@ -150,82 +120,3 @@ func ToECDSA(d []byte) *ecdsa.PrivateKey { k.PublicKey.X, k.PublicKey.Y = k.PublicKey.Curve.ScalarBaseMult(d) return k } - -// verifyCertPublicKey checks that the ecdsa.PublicKey using in a x509.Certificate matches a known ecdsa.PublicKey -func verifyCertPublicKey(cert *x509.Certificate, publicKey *ecdsa.PublicKey) error { - certKey, ok := cert.PublicKey.(*ecdsa.PublicKey) - if !ok { - return fmt.Errorf("unexpected public key type, expected ecdsa.PublicKey") - } - - if !certKey.Equal(publicKey) { - return fmt.Errorf("server certificate MUST match the given public key") - } - return nil -} - -// verifyCertSig checks that a x509.Certificate's Signature verifies against x509.Certificate's PublicKey -// If the x509.Certificate's PublicKey is not an ecdsa.PublicKey an error will be thrown -func verifyCertSig(cert *x509.Certificate) (bool, error) { - var esig struct { - R, S *big.Int - } - if _, err := asn1.Unmarshal(cert.Signature, &esig); err != nil { - return false, err - } - - hash := sha256.New() - hash.Write(cert.RawTBSCertificate) - - ecKey, ok := cert.PublicKey.(*ecdsa.PublicKey) - if !ok { - return false, fmt.Errorf("certificate public is not an ecdsa.PublicKey") - } - - verified := ecdsa.Verify(ecKey, hash.Sum(nil), esig.R, esig.S) - return verified, nil -} - -// verifyCert verifies an x509.Certificate against a known ecdsa.PublicKey -// combining the checks of verifyCertPublicKey and verifyCertSig. -// If an x509.Certificate fails to verify an error is also thrown -func verifyCert(cert *x509.Certificate, publicKey *ecdsa.PublicKey) error { - err := verifyCertPublicKey(cert, publicKey) - if err != nil { - return err - } - - verified, err := verifyCertSig(cert) - if err != nil { - return err - } - if !verified { - return fmt.Errorf("server certificate signature MUST verify") - } - return nil -} - -// getServerCert pings a given tls host, extracts and returns its x509.Certificate -// the function expects there to be only 1 certificate -func getServerCert(URL *url.URL) (*x509.Certificate, error) { - conf := &tls.Config{ - InsecureSkipVerify: true, // nolint: gosec // Only skip verify to get the server's TLS cert. DO NOT skip for any other reason. - } - - conn, err := tls.Dial("tcp", URL.Host, conf) - if err != nil { - signal.SendLocalPairingEvent(Event{Type: EventConnectionError, Error: err.Error()}) - return nil, err - } - defer conn.Close() - - // No error on the dial out then the URL.Host is accessible - signal.SendLocalPairingEvent(Event{Type: EventConnectionSuccess}) - - certs := conn.ConnectionState().PeerCertificates - if len(certs) != 1 { - return nil, fmt.Errorf("expected 1 TLS certificate, received '%d'", len(certs)) - } - - return certs[0], nil -} diff --git a/server/certs_test.go b/server/certs_test.go index 875559321..e3bf83436 100644 --- a/server/certs_test.go +++ b/server/certs_test.go @@ -44,12 +44,12 @@ func (s *CertsSuite) TestGenerateX509Cert() { notBefore := time.Now() notAfter := notBefore.Add(time.Hour) - c1 := GenerateX509Cert(s.SN, notBefore, notAfter, localhost) - s.Require().Exactly([]string{localhost}, c1.DNSNames) + c1 := GenerateX509Cert(s.SN, notBefore, notAfter, Localhost) + s.Require().Exactly([]string{Localhost}, c1.DNSNames) s.Require().Nil(c1.IPAddresses) - c2 := GenerateX509Cert(s.SN, notBefore, notAfter, defaultIP.String()) + c2 := GenerateX509Cert(s.SN, notBefore, notAfter, DefaultIP.String()) s.Require().Len(c2.IPAddresses, 1) - s.Require().Equal(defaultIP.String(), c2.IPAddresses[0].String()) + s.Require().Equal(DefaultIP.String(), c2.IPAddresses[0].String()) s.Require().Nil(c2.DNSNames) } diff --git a/server/client.go b/server/client.go deleted file mode 100644 index 7a5c0b7f2..000000000 --- a/server/client.go +++ /dev/null @@ -1,205 +0,0 @@ -package server - -import ( - "bytes" - "crypto/ecdsa" - "crypto/tls" - "crypto/x509" - "encoding/json" - "encoding/pem" - "fmt" - "io/ioutil" - "net/http" - "net/http/cookiejar" - "net/url" - - "github.com/btcsuite/btcutil/base58" - - "github.com/status-im/status-go/logutils" - "github.com/status-im/status-go/multiaccounts" - "github.com/status-im/status-go/signal" -) - -type PairingClient struct { - *http.Client - PayloadManager - - baseAddress *url.URL - certPEM []byte - serverPK *ecdsa.PublicKey - serverMode Mode - serverCert *x509.Certificate - serverChallenge []byte -} - -func NewPairingClient(c *ConnectionParams, config *PairingPayloadManagerConfig) (*PairingClient, error) { - u, err := c.URL() - if err != nil { - return nil, err - } - - serverCert, err := getServerCert(u) - if err != nil { - return nil, err - } - err = verifyCert(serverCert, c.publicKey) - if err != nil { - return nil, err - } - certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCert.Raw}) - - rootCAs, err := x509.SystemCertPool() - if err != nil { - return nil, err - } - if ok := rootCAs.AppendCertsFromPEM(certPem); !ok { - return nil, fmt.Errorf("failed to append certPem to rootCAs") - } - - tr := &http.Transport{ - TLSClientConfig: &tls.Config{ - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: false, // MUST BE FALSE - RootCAs: rootCAs, - }, - } - - cj, err := cookiejar.New(nil) - if err != nil { - return nil, err - } - - pm, err := NewPairingPayloadManager(c.aesKey, config, logutils.ZapLogger().Named("PairingClient")) - if err != nil { - return nil, err - } - - return &PairingClient{ - Client: &http.Client{Transport: tr, Jar: cj}, - baseAddress: u, - certPEM: certPem, - serverCert: serverCert, - serverPK: c.publicKey, - serverMode: c.serverMode, - PayloadManager: pm, - }, nil -} - -func (c *PairingClient) PairAccount() error { - switch c.serverMode { - case Receiving: - return c.sendAccountData() - case Sending: - err := c.getChallenge() - if err != nil { - return err - } - return c.receiveAccountData() - default: - return fmt.Errorf("unrecognised server mode '%d'", c.serverMode) - } -} - -func (c *PairingClient) sendAccountData() error { - err := c.Mount() - if err != nil { - return err - } - - c.baseAddress.Path = pairingReceive - resp, err := c.Post(c.baseAddress.String(), "application/octet-stream", bytes.NewBuffer(c.PayloadManager.ToSend())) - if err != nil { - signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error()}) - return err - } - - if resp.StatusCode != http.StatusOK { - signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error()}) - return fmt.Errorf("status not ok, received '%s'", resp.Status) - } - - signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess}) - - c.PayloadManager.LockPayload() - return nil -} - -func (c *PairingClient) receiveAccountData() error { - c.baseAddress.Path = pairingSend - req, err := http.NewRequest(http.MethodGet, c.baseAddress.String(), nil) - if err != nil { - return err - } - - if c.serverChallenge != nil { - ec, err := c.PayloadManager.EncryptPlain(c.serverChallenge) - if err != nil { - return err - } - - req.Header.Set(sessionChallenge, base58.Encode(ec)) - } - - resp, err := c.Do(req) - if err != nil { - signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error()}) - return err - } - - if resp.StatusCode != http.StatusOK { - err = fmt.Errorf("status not ok, received '%s'", resp.Status) - signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error()}) - return err - } - - payload, err := ioutil.ReadAll(resp.Body) - if err != nil { - signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error()}) - return err - } - signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess}) - - err = c.PayloadManager.Receive(payload) - if err != nil { - signal.SendLocalPairingEvent(Event{Type: EventProcessError, Error: err.Error()}) - return err - } - signal.SendLocalPairingEvent(Event{Type: EventProcessSuccess}) - return nil -} - -func (c *PairingClient) getChallenge() error { - c.baseAddress.Path = pairingChallenge - resp, err := c.Get(c.baseAddress.String()) - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("status not ok, received '%s'", resp.Status) - } - - c.serverChallenge, err = ioutil.ReadAll(resp.Body) - return err -} - -func StartUpPairingClient(db *multiaccounts.Database, cs, configJSON string) error { - var conf PairingPayloadSourceConfig - err := json.Unmarshal([]byte(configJSON), &conf) - if err != nil { - return err - } - - ccp := new(ConnectionParams) - err = ccp.FromString(cs) - if err != nil { - return err - } - - c, err := NewPairingClient(ccp, &PairingPayloadManagerConfig{db, conf}) - if err != nil { - return err - } - - return c.PairAccount() -} diff --git a/server/components_test.go b/server/components_test.go index 4c243f495..b30c32859 100644 --- a/server/components_test.go +++ b/server/components_test.go @@ -3,11 +3,8 @@ package server import ( "crypto/ecdsa" "crypto/elliptic" - "crypto/rand" - "crypto/tls" "encoding/asn1" "math/big" - "net" "testing" "time" @@ -82,42 +79,6 @@ func (tcc *TestCertComponents) SetupCertComponents(t *testing.T) { tcc.NotAfter = tcc.NotBefore.Add(time.Hour) } -type TestPairingServerComponents struct { - EphemeralPK *ecdsa.PrivateKey - EphemeralAES []byte - OutboundIP net.IP - Cert tls.Certificate - PS *PairingServer -} - -func (tpsc *TestPairingServerComponents) SetupPairingServerComponents(t *testing.T) { - var err error - - // Get 4 key components for tls.cert generation - // 1) Ephemeral private key - tpsc.EphemeralPK, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - - // 2) AES encryption key - tpsc.EphemeralAES, err = makeEncryptionKey(tpsc.EphemeralPK) - require.NoError(t, err) - - // 3) Device outbound IP address - tpsc.OutboundIP, err = GetOutboundIP() - require.NoError(t, err) - - // Generate tls.Certificate and Server - tpsc.Cert, _, err = GenerateCertFromKey(tpsc.EphemeralPK, time.Now(), tpsc.OutboundIP.String()) - require.NoError(t, err) - - tpsc.PS, err = NewPairingServer(&Config{ - PK: &tpsc.EphemeralPK.PublicKey, - EK: tpsc.EphemeralAES, - Cert: &tpsc.Cert, - Hostname: tpsc.OutboundIP.String()}) - require.NoError(t, err) -} - type TestLoggerComponents struct { Logger *zap.Logger } @@ -125,33 +86,3 @@ type TestLoggerComponents struct { func (tlc *TestLoggerComponents) SetupLoggerComponents() { tlc.Logger = logutils.ZapLogger() } - -type MockEncryptOnlyPayloadManager struct { - *PayloadEncryptionManager -} - -func NewMockEncryptOnlyPayloadManager(aesKey []byte) (*MockEncryptOnlyPayloadManager, error) { - pem, err := NewPayloadEncryptionManager(aesKey, logutils.ZapLogger()) - if err != nil { - return nil, err - } - - return &MockEncryptOnlyPayloadManager{ - pem, - }, nil -} - -func (m *MockEncryptOnlyPayloadManager) Mount() error { - // Make a random payload - data := make([]byte, 32) - _, err := rand.Read(data) - if err != nil { - return err - } - - return m.Encrypt(data) -} - -func (m *MockEncryptOnlyPayloadManager) Receive(data []byte) error { - return m.Decrypt(data) -} diff --git a/server/encryption.go b/server/encryption.go deleted file mode 100644 index 58f59add7..000000000 --- a/server/encryption.go +++ /dev/null @@ -1,11 +0,0 @@ -package server - -import ( - "crypto/ecdsa" - - "github.com/status-im/status-go/protocol/common" -) - -func makeEncryptionKey(key *ecdsa.PrivateKey) ([]byte, error) { - return common.MakeECDHSharedKey(key, &key.PublicKey) -} diff --git a/server/handlers.go b/server/handlers.go index c1389aae7..612c5eabb 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -2,26 +2,21 @@ package server import ( "bytes" - "crypto/rand" "database/sql" "image" - "io/ioutil" "net/http" "net/url" "strconv" "time" - "github.com/btcsuite/btcutil/base58" "go.uber.org/zap" "github.com/status-im/status-go/ipfs" "github.com/status-im/status-go/multiaccounts" - "github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/identity/colorhash" "github.com/status-im/status-go/protocol/identity/identicon" "github.com/status-im/status-go/protocol/identity/ring" "github.com/status-im/status-go/protocol/images" - "github.com/status-im/status-go/signal" ) const ( @@ -34,15 +29,6 @@ const ( discordAttachmentsPath = basePath + "/discord/attachments" // Handler routes for pairing - pairingBase = "/pairing" - pairingSend = pairingBase + "/send" - pairingReceive = pairingBase + "/receive" - pairingChallenge = pairingBase + "/challenge" - - // Session names - sessionChallenge = "challenge" - sessionBlocked = "blocked" - accountImagesPath = "/accountImages" contactImagesPath = "/contactImages" ) @@ -398,130 +384,3 @@ func handleIPFS(downloader *ipfs.Downloader, logger *zap.Logger) http.HandlerFun } } } - -func handlePairingReceive(ps *PairingServer) http.HandlerFunc { - signal.SendLocalPairingEvent(Event{Type: EventConnectionSuccess}) - - return func(w http.ResponseWriter, r *http.Request) { - payload, err := ioutil.ReadAll(r.Body) - if err != nil { - signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error()}) - ps.logger.Error("ioutil.ReadAll(r.Body)", zap.Error(err)) - return - } - signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess}) - - err = ps.PayloadManager.Receive(payload) - if err != nil { - signal.SendLocalPairingEvent(Event{Type: EventProcessError, Error: err.Error()}) - ps.logger.Error("ps.PayloadManager.Receive(payload)", zap.Error(err)) - return - } - signal.SendLocalPairingEvent(Event{Type: EventProcessSuccess}) - } -} - -func handlePairingSend(ps *PairingServer) http.HandlerFunc { - signal.SendLocalPairingEvent(Event{Type: EventConnectionSuccess}) - - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/octet-stream") - _, err := w.Write(ps.PayloadManager.ToSend()) - if err != nil { - signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error()}) - ps.logger.Error("w.Write(ps.PayloadManager.ToSend())", zap.Error(err)) - return - } - signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess}) - - ps.PayloadManager.LockPayload() - } -} - -func challengeMiddleware(ps *PairingServer, next http.Handler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - s, err := ps.cookieStore.Get(r, sessionChallenge) - if err != nil { - ps.logger.Error("ps.cookieStore.Get(r, pairingStoreChallenge)", zap.Error(err)) - http.Error(w, "error", http.StatusInternalServerError) - return - } - - blocked, ok := s.Values[sessionBlocked].(bool) - if ok && blocked { - http.Error(w, "forbidden", http.StatusForbidden) - return - } - - // If the request header doesn't include a challenge don't punish the client, just throw a 403 - pc := r.Header.Get(sessionChallenge) - if pc == "" { - http.Error(w, "forbidden", http.StatusForbidden) - return - } - - c, err := common.Decrypt(base58.Decode(pc), ps.ek) - if err != nil { - ps.logger.Error("c, err := common.Decrypt(rc, ps.ek)", zap.Error(err)) - http.Error(w, "error", http.StatusInternalServerError) - return - } - - // If the challenge is not in the session store don't punish the client, just throw a 403 - challenge, ok := s.Values[sessionChallenge].([]byte) - if !ok { - http.Error(w, "forbidden", http.StatusForbidden) - return - } - - // Only if we have both a challenge in the session store and in the request header - // do we entertain blocking the client. Because then we know someone is trying to be sneaky. - if !bytes.Equal(c, challenge) { - s.Values[sessionBlocked] = true - err = s.Save(r, w) - if err != nil { - ps.logger.Error("err = s.Save(r, w)", zap.Error(err)) - } - - http.Error(w, "forbidden", http.StatusForbidden) - return - } - - next.ServeHTTP(w, r) - } -} - -func handlePairingChallenge(ps *PairingServer) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - s, err := ps.cookieStore.Get(r, sessionChallenge) - if err != nil { - ps.logger.Error("ps.cookieStore.Get(r, pairingStoreChallenge)", zap.Error(err)) - return - } - - var challenge []byte - challenge, ok := s.Values[sessionChallenge].([]byte) - if !ok { - challenge = make([]byte, 64) - _, err = rand.Read(challenge) - if err != nil { - ps.logger.Error("_, err = rand.Read(auth)", zap.Error(err)) - return - } - - s.Values[sessionChallenge] = challenge - err = s.Save(r, w) - if err != nil { - ps.logger.Error("err = s.Save(r, w)", zap.Error(err)) - return - } - } - - w.Header().Set("Content-Type", "application/octet-stream") - _, err = w.Write(challenge) - if err != nil { - ps.logger.Error("_, err = w.Write(challenge)", zap.Error(err)) - return - } - } -} diff --git a/server/ips.go b/server/ips.go index 48c2a9604..b1661f7a2 100644 --- a/server/ips.go +++ b/server/ips.go @@ -5,8 +5,8 @@ import ( ) var ( - defaultIP = net.IP{127, 0, 0, 1} - localhost = "localhost" + DefaultIP = net.IP{127, 0, 0, 1} + Localhost = "Localhost" ) func GetOutboundIP() (net.IP, error) { diff --git a/server/ips_test.go b/server/ips_test.go deleted file mode 100644 index 1c983c21d..000000000 --- a/server/ips_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package server - -import ( - "crypto/rand" - "encoding/hex" - "io/ioutil" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -func TestGetOutboundIPSuite(t *testing.T) { - suite.Run(t, new(GetOutboundIPSuite)) -} - -type GetOutboundIPSuite struct { - suite.Suite - TestPairingServerComponents -} - -func (s *GetOutboundIPSuite) SetupSuite() { - s.SetupPairingServerComponents(s.T()) -} - -func testHandler(t *testing.T) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - say, ok := r.URL.Query()["say"] - if !ok || len(say) == 0 { - say = append(say, "nothing") - } - - _, err := w.Write([]byte("Hello I like to be a tls server. You said: `" + say[0] + "` " + time.Now().String())) - if err != nil { - require.NoError(t, err) - } - } -} - -func makeThingToSay() (string, error) { - b := make([]byte, 32) - _, err := rand.Read(b) - if err != nil { - return "", err - } - - return hex.EncodeToString(b), nil -} - -func (s *GetOutboundIPSuite) TestGetOutboundIPWithFullServerE2e() { - s.PS.mode = Sending - s.PS.SetHandlers(HandlerPatternMap{"/hello": testHandler(s.T())}) - - err := s.PS.Start() - s.Require().NoError(err) - - // Give time for the sever to be ready, hacky I know, I'll iron this out - time.Sleep(100 * time.Millisecond) - - // Server generates a QR code connection string - cp, err := s.PS.MakeConnectionParams() - s.Require().NoError(err) - - qr := cp.ToString() - - // Client reads QR code and parses the connection string - ccp := new(ConnectionParams) - err = ccp.FromString(qr) - s.Require().NoError(err) - - c, err := NewPairingClient(ccp, nil) - s.Require().NoError(err) - - thing, err := makeThingToSay() - s.Require().NoError(err) - - response, err := c.Get(c.baseAddress.String() + "/hello?say=" + thing) - s.Require().NoError(err) - - defer response.Body.Close() - - content, err := ioutil.ReadAll(response.Body) - s.Require().NoError(err) - s.Require().Equal("Hello I like to be a tls server. You said: `"+thing+"`", string(content[:109])) -} diff --git a/server/pairing/certs.go b/server/pairing/certs.go new file mode 100644 index 000000000..a49fb6563 --- /dev/null +++ b/server/pairing/certs.go @@ -0,0 +1,168 @@ +package pairing + +import ( + "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "fmt" + "math/big" + "net" + "net/url" + "time" + + "github.com/status-im/status-go/signal" +) + +func makeSerialNumberFromKey(pk *ecdsa.PrivateKey) *big.Int { + h := sha256.New() + h.Write(append(pk.D.Bytes(), append(pk.Y.Bytes(), pk.X.Bytes()...)...)) + + return new(big.Int).SetBytes(h.Sum(nil)) +} + +func GenerateX509Cert(sn *big.Int, from, to time.Time, hostname string) *x509.Certificate { + c := &x509.Certificate{ + SerialNumber: sn, + Subject: pkix.Name{Organization: []string{"Self-signed cert"}}, + NotBefore: from, + NotAfter: to, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + ip := net.ParseIP(hostname) + if ip != nil { + c.IPAddresses = []net.IP{ip} + } else { + c.DNSNames = []string{hostname} + } + + return c +} + +func GenerateX509PEMs(cert *x509.Certificate, key *ecdsa.PrivateKey) (certPem, keyPem []byte, err error) { + derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &key.PublicKey, key) + if err != nil { + return + } + certPem = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + + privBytes, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return + } + keyPem = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) + + return +} + +func GenerateCertFromKey(pk *ecdsa.PrivateKey, from time.Time, hostname string) (tls.Certificate, []byte, error) { + cert := GenerateX509Cert(makeSerialNumberFromKey(pk), from, from.Add(time.Hour), hostname) + certPem, keyPem, err := GenerateX509PEMs(cert, pk) + if err != nil { + return tls.Certificate{}, nil, err + } + + tlsCert, err := tls.X509KeyPair(certPem, keyPem) + if err != nil { + return tls.Certificate{}, nil, err + } + + block, _ := pem.Decode(certPem) + if block == nil { + return tls.Certificate{}, nil, fmt.Errorf("failed to decode certPem") + } + leaf, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return tls.Certificate{}, nil, err + } + tlsCert.Leaf = leaf + + return tlsCert, certPem, nil +} + +// verifyCertPublicKey checks that the ecdsa.PublicKey using in a x509.Certificate matches a known ecdsa.PublicKey +func verifyCertPublicKey(cert *x509.Certificate, publicKey *ecdsa.PublicKey) error { + certKey, ok := cert.PublicKey.(*ecdsa.PublicKey) + if !ok { + return fmt.Errorf("unexpected public key type, expected ecdsa.PublicKey") + } + + if !certKey.Equal(publicKey) { + return fmt.Errorf("server certificate MUST match the given public key") + } + return nil +} + +// verifyCertSig checks that a x509.Certificate's Signature verifies against x509.Certificate's PublicKey +// If the x509.Certificate's PublicKey is not an ecdsa.PublicKey an error will be thrown +func verifyCertSig(cert *x509.Certificate) (bool, error) { + var esig struct { + R, S *big.Int + } + if _, err := asn1.Unmarshal(cert.Signature, &esig); err != nil { + return false, err + } + + hash := sha256.New() + hash.Write(cert.RawTBSCertificate) + + ecKey, ok := cert.PublicKey.(*ecdsa.PublicKey) + if !ok { + return false, fmt.Errorf("certificate public is not an ecdsa.PublicKey") + } + + verified := ecdsa.Verify(ecKey, hash.Sum(nil), esig.R, esig.S) + return verified, nil +} + +// verifyCert verifies an x509.Certificate against a known ecdsa.PublicKey +// combining the checks of verifyCertPublicKey and verifyCertSig. +// If an x509.Certificate fails to verify an error is also thrown +func verifyCert(cert *x509.Certificate, publicKey *ecdsa.PublicKey) error { + err := verifyCertPublicKey(cert, publicKey) + if err != nil { + return err + } + + verified, err := verifyCertSig(cert) + if err != nil { + return err + } + if !verified { + return fmt.Errorf("server certificate signature MUST verify") + } + return nil +} + +// getServerCert pings a given tls host, extracts and returns its x509.Certificate +// the function expects there to be only 1 certificate +func getServerCert(URL *url.URL) (*x509.Certificate, error) { + conf := &tls.Config{ + InsecureSkipVerify: true, // nolint: gosec // Only skip verify to get the server's TLS cert. DO NOT skip for any other reason. + } + + conn, err := tls.Dial("tcp", URL.Host, conf) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventConnectionError, Error: err.Error(), Action: ActionConnect}) + return nil, err + } + defer conn.Close() + + // No error on the dial out then the URL.Host is accessible + signal.SendLocalPairingEvent(Event{Type: EventConnectionSuccess, Action: ActionConnect}) + + certs := conn.ConnectionState().PeerCertificates + if len(certs) != 1 { + return nil, fmt.Errorf("expected 1 TLS certificate, received '%d'", len(certs)) + } + + return certs[0], nil +} diff --git a/server/pairing/client.go b/server/pairing/client.go new file mode 100644 index 000000000..4f7ac8ffb --- /dev/null +++ b/server/pairing/client.go @@ -0,0 +1,300 @@ +package pairing + +import ( + "bytes" + "crypto/ecdsa" + "crypto/tls" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/cookiejar" + "net/url" + + "github.com/btcsuite/btcutil/base58" + + "github.com/status-im/status-go/api" + "github.com/status-im/status-go/logutils" + "github.com/status-im/status-go/signal" +) + +type Client struct { + *http.Client + PayloadManager + rawMessagePayloadManager *RawMessagePayloadManager + + baseAddress *url.URL + certPEM []byte + serverPK *ecdsa.PublicKey + serverMode Mode + serverCert *x509.Certificate + serverChallenge []byte +} + +func NewPairingClient(backend *api.GethStatusBackend, c *ConnectionParams, config *AccountPayloadManagerConfig) (*Client, error) { + u, err := c.URL() + if err != nil { + return nil, err + } + + serverCert, err := getServerCert(u) + if err != nil { + return nil, err + } + err = verifyCert(serverCert, c.publicKey) + if err != nil { + return nil, err + } + certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCert.Raw}) + + rootCAs, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + if ok := rootCAs.AppendCertsFromPEM(certPem); !ok { + return nil, fmt.Errorf("failed to append certPem to rootCAs") + } + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: false, // MUST BE FALSE + RootCAs: rootCAs, + }, + } + + cj, err := cookiejar.New(nil) + if err != nil { + return nil, err + } + + pm, err := NewAccountPayloadManager(c.aesKey, config, logutils.ZapLogger().Named("Client")) + if err != nil { + return nil, err + } + + rmpm, err := NewRawMessagePayloadManager(logutils.ZapLogger().Named("Client"), pm.accountPayload, c.aesKey, backend, config.KeystorePath) + if err != nil { + return nil, err + } + return &Client{ + Client: &http.Client{Transport: tr, Jar: cj}, + baseAddress: u, + certPEM: certPem, + serverCert: serverCert, + serverPK: c.publicKey, + serverMode: c.serverMode, + PayloadManager: pm, + rawMessagePayloadManager: rmpm, + }, nil +} + +func (c *Client) PairAccount() error { + switch c.serverMode { + case Receiving: + return c.sendAccountData() + case Sending: + err := c.getChallenge() + if err != nil { + return err + } + return c.receiveAccountData() + default: + return fmt.Errorf("unrecognised server mode '%d'", c.serverMode) + } +} + +func (c *Client) PairSyncDevice() error { + switch c.serverMode { + case Receiving: + return c.sendSyncDeviceData() + case Sending: + return c.receiveSyncDeviceData() + default: + return fmt.Errorf("unrecognised server mode '%d'", c.serverMode) + } +} + +func (c *Client) sendSyncDeviceData() error { + err := c.rawMessagePayloadManager.Mount() + if err != nil { + return err + } + + c.baseAddress.Path = pairingSyncDeviceReceive + resp, err := c.Post(c.baseAddress.String(), "application/octet-stream", bytes.NewBuffer(c.rawMessagePayloadManager.ToSend())) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice}) + return err + } + + if resp.StatusCode != http.StatusOK { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice}) + return fmt.Errorf("status not ok, received '%s'", resp.Status) + } + + signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionSyncDevice}) + return nil +} + +func (c *Client) receiveSyncDeviceData() error { + c.baseAddress.Path = pairingSyncDeviceSend + req, err := http.NewRequest(http.MethodGet, c.baseAddress.String(), nil) + if err != nil { + return err + } + + if c.serverChallenge != nil { + ec, err := c.PayloadManager.EncryptPlain(c.serverChallenge) + if err != nil { + return err + } + + req.Header.Set(sessionChallenge, base58.Encode(ec)) + } + + resp, err := c.Do(req) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice}) + return err + } + + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("status not ok, received '%s'", resp.Status) + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice}) + return err + } + + payload, err := io.ReadAll(resp.Body) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice}) + return err + } + signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionSyncDevice}) + + err = c.rawMessagePayloadManager.Receive(payload) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventProcessError, Error: err.Error(), Action: ActionSyncDevice}) + return err + } + signal.SendLocalPairingEvent(Event{Type: EventProcessSuccess, Action: ActionSyncDevice}) + return nil +} + +func (c *Client) sendAccountData() error { + err := c.Mount() + if err != nil { + return err + } + + c.baseAddress.Path = pairingReceiveAccount + resp, err := c.Post(c.baseAddress.String(), "application/octet-stream", bytes.NewBuffer(c.PayloadManager.ToSend())) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount}) + return err + } + + if resp.StatusCode != http.StatusOK { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount}) + return fmt.Errorf("status not ok, received '%s'", resp.Status) + } + + signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionPairingAccount}) + + c.PayloadManager.LockPayload() + return nil +} + +func (c *Client) receiveAccountData() error { + c.baseAddress.Path = pairingSendAccount + req, err := http.NewRequest(http.MethodGet, c.baseAddress.String(), nil) + if err != nil { + return err + } + + if c.serverChallenge != nil { + ec, err := c.PayloadManager.EncryptPlain(c.serverChallenge) + if err != nil { + return err + } + + req.Header.Set(sessionChallenge, base58.Encode(ec)) + } + + resp, err := c.Do(req) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount}) + return err + } + + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("status not ok, received '%s'", resp.Status) + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount}) + return err + } + + payload, err := ioutil.ReadAll(resp.Body) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount}) + return err + } + signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionPairingAccount}) + + err = c.PayloadManager.Receive(payload) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventProcessError, Error: err.Error(), Action: ActionPairingAccount}) + return err + } + signal.SendLocalPairingEvent(Event{Type: EventProcessSuccess, Action: ActionPairingAccount}) + return nil +} + +func (c *Client) getChallenge() error { + c.baseAddress.Path = pairingChallenge + resp, err := c.Get(c.baseAddress.String()) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status not ok, received '%s'", resp.Status) + } + + c.serverChallenge, err = ioutil.ReadAll(resp.Body) + return err +} + +func StartUpPairingClient(backend *api.GethStatusBackend, cs, configJSON string) error { + c, err := setupClient(backend, cs, configJSON) + if err != nil { + return err + } + err = c.PairAccount() + if err != nil { + return err + } + return c.PairSyncDevice() +} + +func setupClient(backend *api.GethStatusBackend, cs string, configJSON string) (*Client, error) { + var conf PayloadSourceConfig + err := json.Unmarshal([]byte(configJSON), &conf) + if err != nil { + return nil, err + } + + ccp := new(ConnectionParams) + err = ccp.FromString(cs) + if err != nil { + return nil, err + } + + c, err := NewPairingClient(backend, ccp, &AccountPayloadManagerConfig{backend.GetMultiaccountDB(), &conf}) + if err != nil { + return nil, err + } + return c, nil +} diff --git a/server/pairing/components_test.go b/server/pairing/components_test.go new file mode 100644 index 000000000..1726a0db2 --- /dev/null +++ b/server/pairing/components_test.go @@ -0,0 +1,165 @@ +package pairing + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "encoding/asn1" + + "math/big" + "net" + "testing" + "time" + + "github.com/btcsuite/btcutil/base58" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "github.com/status-im/status-go/logutils" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/server" +) + +const ( + X = "7744735542292224619198421067303535767629647588258222392379329927711683109548" + Y = "6855516769916529066379811647277920115118980625614889267697023742462401590771" + D = "38564357061962143106230288374146033267100509055924181407058066820384455255240" + AES = "BbnZ7Gc66t54a9kEFCf7FW8SGQuYypwHVeNkRYeNoqV6" + DB58 = "6jpbvo2ucrtrnpXXF4DQYuysh697isH9ppd2aT8uSRDh" + SN = "91849736469742262272885892667727604096707836853856473239722372976236128900962" + CertTime = "eQUriVtGtkWhPJFeLZjF" +) + +type TestKeyComponents struct { + X *big.Int + Y *big.Int + D *big.Int + AES []byte + DBytes []byte + PK *ecdsa.PrivateKey +} + +func (tk *TestKeyComponents) SetupKeyComponents(t *testing.T) { + var ok bool + + tk.X, ok = new(big.Int).SetString(X, 10) + require.True(t, ok) + + tk.Y, ok = new(big.Int).SetString(Y, 10) + require.True(t, ok) + + tk.D, ok = new(big.Int).SetString(D, 10) + require.True(t, ok) + + tk.AES = base58.Decode(AES) + require.Len(t, tk.AES, 32) + + tk.DBytes = base58.Decode(DB58) + require.Exactly(t, tk.D.Bytes(), tk.DBytes) + + tk.PK = &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: tk.X, + Y: tk.Y, + }, + D: tk.D, + } +} + +type TestCertComponents struct { + NotBefore, NotAfter time.Time + SN *big.Int +} + +func (tcc *TestCertComponents) SetupCertComponents(t *testing.T) { + var ok bool + + tcc.SN, ok = new(big.Int).SetString(SN, 10) + require.True(t, ok) + + _, err := asn1.Unmarshal(base58.Decode(CertTime), &tcc.NotBefore) + require.NoError(t, err) + + tcc.NotAfter = tcc.NotBefore.Add(time.Hour) +} + +type TestPairingServerComponents struct { + EphemeralPK *ecdsa.PrivateKey + EphemeralAES []byte + OutboundIP net.IP + Cert tls.Certificate + PS *Server +} + +func (tpsc *TestPairingServerComponents) SetupPairingServerComponents(t *testing.T) { + var err error + + // Get 4 key components for tls.cert generation + // 1) Ephemeral private key + tpsc.EphemeralPK, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + // 2) AES encryption key + tpsc.EphemeralAES, err = common.MakeECDHSharedKey(tpsc.EphemeralPK, &tpsc.EphemeralPK.PublicKey) + require.NoError(t, err) + + // 3) Device outbound IP address + tpsc.OutboundIP, err = server.GetOutboundIP() + require.NoError(t, err) + + // Generate tls.Certificate and Server + tpsc.Cert, _, err = GenerateCertFromKey(tpsc.EphemeralPK, time.Now(), tpsc.OutboundIP.String()) + require.NoError(t, err) + + tpsc.PS, err = NewPairingServer(nil, &Config{ + PK: &tpsc.EphemeralPK.PublicKey, + EK: tpsc.EphemeralAES, + Cert: &tpsc.Cert, + Hostname: tpsc.OutboundIP.String(), + AccountPayloadManagerConfig: &AccountPayloadManagerConfig{ + PayloadSourceConfig: &PayloadSourceConfig{ + KeystorePath: "", + }, + }}) + require.NoError(t, err) +} + +type TestLoggerComponents struct { + Logger *zap.Logger +} + +func (tlc *TestLoggerComponents) SetupLoggerComponents() { + tlc.Logger = logutils.ZapLogger() +} + +type MockEncryptOnlyPayloadManager struct { + *PayloadEncryptionManager +} + +func NewMockEncryptOnlyPayloadManager(aesKey []byte) (*MockEncryptOnlyPayloadManager, error) { + pem, err := NewPayloadEncryptionManager(aesKey, logutils.ZapLogger()) + if err != nil { + return nil, err + } + + return &MockEncryptOnlyPayloadManager{ + pem, + }, nil +} + +func (m *MockEncryptOnlyPayloadManager) Mount() error { + // Make a random payload + data := make([]byte, 32) + _, err := rand.Read(data) + if err != nil { + return err + } + + return m.Encrypt(data) +} + +func (m *MockEncryptOnlyPayloadManager) Receive(data []byte) error { + return m.Decrypt(data) +} diff --git a/server/connection.go b/server/pairing/connection.go similarity index 99% rename from server/connection.go rename to server/pairing/connection.go index 0996590f0..4be589ef7 100644 --- a/server/connection.go +++ b/server/pairing/connection.go @@ -1,4 +1,4 @@ -package server +package pairing import ( "crypto/ecdsa" diff --git a/server/connection_test.go b/server/pairing/connection_test.go similarity index 79% rename from server/connection_test.go rename to server/pairing/connection_test.go index 83f4fe2b3..3ad7268cc 100644 --- a/server/connection_test.go +++ b/server/pairing/connection_test.go @@ -1,9 +1,11 @@ -package server +package pairing import ( "testing" "github.com/stretchr/testify/suite" + + internalServer "github.com/status-im/status-go/server" ) var ( @@ -20,7 +22,7 @@ type ConnectionParamsSuite struct { TestCertComponents TestLoggerComponents - server *PairingServer + server *Server } func (s *ConnectionParamsSuite) SetupSuite() { @@ -28,14 +30,14 @@ func (s *ConnectionParamsSuite) SetupSuite() { s.SetupCertComponents(s.T()) s.SetupLoggerComponents() - cert, _, err := GenerateCertFromKey(s.PK, s.NotBefore, defaultIP.String()) + cert, _, err := GenerateCertFromKey(s.PK, s.NotBefore, internalServer.DefaultIP.String()) s.Require().NoError(err) - bs := NewServer(&cert, defaultIP.String(), nil, s.Logger) + bs := internalServer.NewServer(&cert, internalServer.DefaultIP.String(), nil, s.Logger) err = bs.SetPort(1337) s.Require().NoError(err) - s.server = &PairingServer{ + s.server = &Server{ Server: bs, pk: &s.PK.PublicKey, ek: s.AES, @@ -62,7 +64,7 @@ func (s *ConnectionParamsSuite) TestConnectionParams_Generate() { s.Require().NoError(err) s.Require().Equal("https://127.0.0.1:1337", u.String()) - s.Require().Equal(defaultIP.String(), u.Hostname()) + s.Require().Equal(internalServer.DefaultIP.String(), u.Hostname()) s.Require().Equal("1337", u.Port()) s.Require().True(cp.publicKey.Equal(&s.PK.PublicKey)) diff --git a/server/events.go b/server/pairing/events.go similarity index 67% rename from server/events.go rename to server/pairing/events.go index eceef8651..7e3eb6244 100644 --- a/server/events.go +++ b/server/pairing/events.go @@ -1,4 +1,4 @@ -package server +package pairing // EventType type for event types. type EventType string @@ -19,6 +19,15 @@ const ( // Event is a type for transfer events. type Event struct { - Type EventType `json:"type"` - Error string `json:"error,omitempty"` + Type EventType `json:"type"` + Error string `json:"error,omitempty"` + Action Action `json:"action"` } + +type Action int + +const ( + ActionPairingAccount = iota + 1 + ActionSyncDevice + ActionConnect +) diff --git a/server/pairing/handlers.go b/server/pairing/handlers.go new file mode 100644 index 000000000..8419662cf --- /dev/null +++ b/server/pairing/handlers.go @@ -0,0 +1,205 @@ +package pairing + +import ( + "bytes" + "crypto/rand" + "io" + "net/http" + + "github.com/btcsuite/btcutil/base58" + "go.uber.org/zap" + + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/signal" +) + +const ( + // Handler routes for pairing + pairingBase = "/pairing" + pairingSendAccount = pairingBase + "/sendAccount" + pairingReceiveAccount = pairingBase + "/receiveAccount" + pairingChallenge = pairingBase + "/challenge" + pairingSyncDeviceSend = pairingBase + "/sendSyncDevice" + pairingSyncDeviceReceive = pairingBase + "/receiveSyncDevice" + + // Session names + sessionChallenge = "challenge" + sessionBlocked = "blocked" +) + +func handlePairingReceive(ps *Server) http.HandlerFunc { + signal.SendLocalPairingEvent(Event{Type: EventConnectionSuccess, Action: ActionPairingAccount}) + logger := ps.GetLogger() + return func(w http.ResponseWriter, r *http.Request) { + payload, err := io.ReadAll(r.Body) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount}) + logger.Error("ioutil.ReadAll(r.Body)", zap.Error(err)) + return + } + signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionPairingAccount}) + + err = ps.PayloadManager.Receive(payload) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventProcessError, Error: err.Error(), Action: ActionPairingAccount}) + logger.Error("ps.PayloadManager.Receive(payload)", zap.Error(err)) + return + } + signal.SendLocalPairingEvent(Event{Type: EventProcessSuccess, Action: ActionPairingAccount}) + } +} + +func handleParingSyncDeviceReceive(ps *Server) http.HandlerFunc { + signal.SendLocalPairingEvent(Event{Type: EventConnectionSuccess, Action: ActionSyncDevice}) + logger := ps.GetLogger() + return func(w http.ResponseWriter, r *http.Request) { + payload, err := io.ReadAll(r.Body) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice}) + logger.Error("io.ReadAll(r.Body)", zap.Error(err)) + return + } + signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionSyncDevice}) + + err = ps.rawMessagePayloadManager.Receive(payload) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventProcessError, Error: err.Error(), Action: ActionSyncDevice}) + logger.Error("ps.rawMessagePayloadManager.Receive(payload)", zap.Error(err)) + return + } + signal.SendLocalPairingEvent(Event{Type: EventProcessSuccess, Action: ActionSyncDevice}) + } +} + +func handlePairingSend(ps *Server) http.HandlerFunc { + signal.SendLocalPairingEvent(Event{Type: EventConnectionSuccess, Action: ActionPairingAccount}) + logger := ps.GetLogger() + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/octet-stream") + _, err := w.Write(ps.PayloadManager.ToSend()) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionPairingAccount}) + logger.Error("w.Write(ps.PayloadManager.ToSend())", zap.Error(err)) + return + } + signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionPairingAccount}) + + ps.PayloadManager.LockPayload() + } +} + +func handlePairingSyncDeviceSend(ps *Server) http.HandlerFunc { + signal.SendLocalPairingEvent(Event{Type: EventConnectionSuccess, Action: ActionSyncDevice}) + logger := ps.GetLogger() + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/octet-stream") + + err := ps.rawMessagePayloadManager.Mount() + if err != nil { + // maybe better to use a new event type here instead of EventTransferError? + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice}) + logger.Error("ps.rawMessagePayloadManager.Mount()", zap.Error(err)) + return + } + + _, err = w.Write(ps.rawMessagePayloadManager.ToSend()) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionSyncDevice}) + logger.Error("w.Write(ps.rawMessagePayloadManager.ToSend())", zap.Error(err)) + return + } + signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionSyncDevice}) + + ps.rawMessagePayloadManager.LockPayload() + } +} + +func challengeMiddleware(ps *Server, next http.Handler) http.HandlerFunc { + logger := ps.GetLogger() + return func(w http.ResponseWriter, r *http.Request) { + s, err := ps.cookieStore.Get(r, sessionChallenge) + if err != nil { + logger.Error("ps.cookieStore.Get(r, pairingStoreChallenge)", zap.Error(err)) + http.Error(w, "error", http.StatusInternalServerError) + return + } + + blocked, ok := s.Values[sessionBlocked].(bool) + if ok && blocked { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + // If the request header doesn't include a challenge don't punish the client, just throw a 403 + pc := r.Header.Get(sessionChallenge) + if pc == "" { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + c, err := common.Decrypt(base58.Decode(pc), ps.ek) + if err != nil { + logger.Error("c, err := common.Decrypt(rc, ps.ek)", zap.Error(err)) + http.Error(w, "error", http.StatusInternalServerError) + return + } + + // If the challenge is not in the session store don't punish the client, just throw a 403 + challenge, ok := s.Values[sessionChallenge].([]byte) + if !ok { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + // Only if we have both a challenge in the session store and in the request header + // do we entertain blocking the client. Because then we know someone is trying to be sneaky. + if !bytes.Equal(c, challenge) { + s.Values[sessionBlocked] = true + err = s.Save(r, w) + if err != nil { + logger.Error("err = s.Save(r, w)", zap.Error(err)) + } + + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + } +} + +func handlePairingChallenge(ps *Server) http.HandlerFunc { + logger := ps.GetLogger() + return func(w http.ResponseWriter, r *http.Request) { + s, err := ps.cookieStore.Get(r, sessionChallenge) + if err != nil { + logger.Error("ps.cookieStore.Get(r, pairingStoreChallenge)", zap.Error(err)) + return + } + + var challenge []byte + challenge, ok := s.Values[sessionChallenge].([]byte) + if !ok { + challenge = make([]byte, 64) + _, err = rand.Read(challenge) + if err != nil { + logger.Error("_, err = rand.Read(auth)", zap.Error(err)) + return + } + + s.Values[sessionChallenge] = challenge + err = s.Save(r, w) + if err != nil { + logger.Error("err = s.Save(r, w)", zap.Error(err)) + return + } + } + + w.Header().Set("Content-Type", "application/octet-stream") + _, err = w.Write(challenge) + if err != nil { + logger.Error("_, err = w.Write(challenge)", zap.Error(err)) + return + } + } +} diff --git a/server/payload_manager.go b/server/pairing/payload_manager.go similarity index 52% rename from server/payload_manager.go rename to server/pairing/payload_manager.go index 8af8e2343..e6eb8ac42 100644 --- a/server/payload_manager.go +++ b/server/pairing/payload_manager.go @@ -1,4 +1,4 @@ -package server +package pairing import ( "crypto/rand" @@ -8,6 +8,8 @@ import ( "os" "path/filepath" + "github.com/status-im/status-go/api" + "github.com/golang/protobuf/proto" "go.uber.org/zap" @@ -42,32 +44,38 @@ type PayloadManager interface { LockPayload() } -// PairingPayloadSourceConfig represents location and access data of the pairing payload +// PayloadSourceConfig represents location and access data of the pairing payload // ONLY available from the application client -type PairingPayloadSourceConfig struct { +type PayloadSourceConfig struct { + // required KeystorePath string `json:"keystorePath"` - KeyUID string `json:"keyUID"` - Password string `json:"password"` + // following 2 fields r optional. + // optional cases: + // 1. server mode is Receiving and server side doesn't contain this info + // 2. server mode is Sending and client side doesn't contain this info + // they are required in other cases + KeyUID string `json:"keyUID"` + Password string `json:"password"` } -// PairingPayloadManagerConfig represents the initialisation parameters required for a PairingPayloadManager -type PairingPayloadManagerConfig struct { +// AccountPayloadManagerConfig represents the initialisation parameters required for a AccountPayloadManager +type AccountPayloadManagerConfig struct { DB *multiaccounts.Database - PairingPayloadSourceConfig + *PayloadSourceConfig } -// PairingPayloadManager is responsible for the whole lifecycle of a PairingPayload -type PairingPayloadManager struct { - logger *zap.Logger - pp *PairingPayload +// AccountPayloadManager is responsible for the whole lifecycle of a AccountPayload +type AccountPayloadManager struct { + logger *zap.Logger + accountPayload *AccountPayload *PayloadEncryptionManager - ppm *PairingPayloadMarshaller - ppr PayloadRepository + accountPayloadMarshaller *AccountPayloadMarshaller + payloadRepository PayloadRepository } -// NewPairingPayloadManager generates a new and initialised PairingPayloadManager -func NewPairingPayloadManager(aesKey []byte, config *PairingPayloadManagerConfig, logger *zap.Logger) (*PairingPayloadManager, error) { - l := logger.Named("PairingPayloadManager") +// NewAccountPayloadManager generates a new and initialised AccountPayloadManager +func NewAccountPayloadManager(aesKey []byte, config *AccountPayloadManagerConfig, logger *zap.Logger) (*AccountPayloadManager, error) { + l := logger.Named("AccountPayloadManager") l.Debug("fired", zap.Binary("aesKey", aesKey), zap.Any("config", config)) pem, err := NewPayloadEncryptionManager(aesKey, l) @@ -75,74 +83,74 @@ func NewPairingPayloadManager(aesKey []byte, config *PairingPayloadManagerConfig return nil, err } - // A new SHARED PairingPayload - p := new(PairingPayload) + // A new SHARED AccountPayload + p := new(AccountPayload) - return &PairingPayloadManager{ + return &AccountPayloadManager{ logger: l, - pp: p, + accountPayload: p, PayloadEncryptionManager: pem, - ppm: NewPairingPayloadMarshaller(p, l), - ppr: NewPairingPayloadRepository(p, config), + accountPayloadMarshaller: NewPairingPayloadMarshaller(p, l), + payloadRepository: NewAccountPayloadRepository(p, config), }, nil } -// Mount loads and prepares the payload to be stored in the PairingPayloadManager's state ready for later access -func (ppm *PairingPayloadManager) Mount() error { - l := ppm.logger.Named("Mount()") +// Mount loads and prepares the payload to be stored in the AccountPayloadManager's state ready for later access +func (apm *AccountPayloadManager) Mount() error { + l := apm.logger.Named("Mount()") l.Debug("fired") - err := ppm.ppr.LoadFromSource() + err := apm.payloadRepository.LoadFromSource() if err != nil { return err } l.Debug("after LoadFromSource") - pb, err := ppm.ppm.MarshalToProtobuf() + pb, err := apm.accountPayloadMarshaller.MarshalToProtobuf() if err != nil { return err } l.Debug( "after MarshalToProtobuf", - zap.Any("ppm.ppm.keys", ppm.ppm.keys), - zap.Any("ppm.ppm.multiaccount", ppm.ppm.multiaccount), - zap.String("ppm.ppm.password", ppm.ppm.password), + zap.Any("accountPayloadMarshaller.accountPayloadMarshaller.keys", apm.accountPayloadMarshaller.keys), + zap.Any("accountPayloadMarshaller.accountPayloadMarshaller.multiaccount", apm.accountPayloadMarshaller.multiaccount), + zap.String("accountPayloadMarshaller.accountPayloadMarshaller.password", apm.accountPayloadMarshaller.password), zap.Binary("pb", pb), ) - return ppm.Encrypt(pb) + return apm.Encrypt(pb) } // Receive takes a []byte representing raw data, parses and stores the data -func (ppm *PairingPayloadManager) Receive(data []byte) error { - l := ppm.logger.Named("Receive()") +func (apm *AccountPayloadManager) Receive(data []byte) error { + l := apm.logger.Named("Receive()") l.Debug("fired") - err := ppm.Decrypt(data) + err := apm.Decrypt(data) if err != nil { return err } l.Debug("after Decrypt") - err = ppm.ppm.UnmarshalProtobuf(ppm.Received()) + err = apm.accountPayloadMarshaller.UnmarshalProtobuf(apm.Received()) if err != nil { return err } l.Debug( "after UnmarshalProtobuf", - zap.Any("ppm.ppm.keys", ppm.ppm.keys), - zap.Any("ppm.ppm.multiaccount", ppm.ppm.multiaccount), - zap.String("ppm.ppm.password", ppm.ppm.password), - zap.Binary("ppm.Received()", ppm.Received()), + zap.Any("accountPayloadMarshaller.accountPayloadMarshaller.keys", apm.accountPayloadMarshaller.keys), + zap.Any("accountPayloadMarshaller.accountPayloadMarshaller.multiaccount", apm.accountPayloadMarshaller.multiaccount), + zap.String("accountPayloadMarshaller.accountPayloadMarshaller.password", apm.accountPayloadMarshaller.password), + zap.Binary("accountPayloadMarshaller.Received()", apm.Received()), ) - return ppm.ppr.StoreToSource() + return apm.payloadRepository.StoreToSource() } -// ResetPayload resets all payload state managed by the PairingPayloadManager -func (ppm *PairingPayloadManager) ResetPayload() { - ppm.pp.ResetPayload() - ppm.PayloadEncryptionManager.ResetPayload() +// ResetPayload resets all payload state managed by the AccountPayloadManager +func (apm *AccountPayloadManager) ResetPayload() { + apm.accountPayload.ResetPayload() + apm.PayloadEncryptionManager.ResetPayload() } // EncryptionPayload represents the plain text and encrypted text of payload data @@ -247,28 +255,28 @@ func (pem *PayloadEncryptionManager) LockPayload() { pem.received.lock() } -// PairingPayload represents the payload structure a PairingServer handles -type PairingPayload struct { +// AccountPayload represents the payload structure a Server handles +type AccountPayload struct { keys map[string][]byte multiaccount *multiaccounts.Account password string } -func (pp *PairingPayload) ResetPayload() { - *pp = PairingPayload{} +func (ap *AccountPayload) ResetPayload() { + *ap = AccountPayload{} } -// PairingPayloadMarshaller is responsible for marshalling and unmarshalling PairingServer payload data -type PairingPayloadMarshaller struct { +// AccountPayloadMarshaller is responsible for marshalling and unmarshalling Server payload data +type AccountPayloadMarshaller struct { logger *zap.Logger - *PairingPayload + *AccountPayload } -func NewPairingPayloadMarshaller(p *PairingPayload, logger *zap.Logger) *PairingPayloadMarshaller { - return &PairingPayloadMarshaller{logger: logger, PairingPayload: p} +func NewPairingPayloadMarshaller(ap *AccountPayload, logger *zap.Logger) *AccountPayloadMarshaller { + return &AccountPayloadMarshaller{logger: logger, AccountPayload: ap} } -func (ppm *PairingPayloadMarshaller) MarshalToProtobuf() ([]byte, error) { +func (ppm *AccountPayloadMarshaller) MarshalToProtobuf() ([]byte, error) { return proto.Marshal(&protobuf.LocalPairingPayload{ Keys: ppm.accountKeysToProtobuf(), Multiaccount: ppm.multiaccount.ToProtobuf(), @@ -276,7 +284,7 @@ func (ppm *PairingPayloadMarshaller) MarshalToProtobuf() ([]byte, error) { }) } -func (ppm *PairingPayloadMarshaller) accountKeysToProtobuf() []*protobuf.LocalPairingPayload_Key { +func (ppm *AccountPayloadMarshaller) accountKeysToProtobuf() []*protobuf.LocalPairingPayload_Key { var keys []*protobuf.LocalPairingPayload_Key for name, data := range ppm.keys { keys = append(keys, &protobuf.LocalPairingPayload_Key{Name: name, Data: data}) @@ -284,7 +292,7 @@ func (ppm *PairingPayloadMarshaller) accountKeysToProtobuf() []*protobuf.LocalPa return keys } -func (ppm *PairingPayloadMarshaller) UnmarshalProtobuf(data []byte) error { +func (ppm *AccountPayloadMarshaller) UnmarshalProtobuf(data []byte) error { l := ppm.logger.Named("UnmarshalProtobuf()") l.Debug("fired") @@ -306,7 +314,7 @@ func (ppm *PairingPayloadMarshaller) UnmarshalProtobuf(data []byte) error { return nil } -func (ppm *PairingPayloadMarshaller) accountKeysFromProtobuf(pbKeys []*protobuf.LocalPairingPayload_Key) { +func (ppm *AccountPayloadMarshaller) accountKeysFromProtobuf(pbKeys []*protobuf.LocalPairingPayload_Key) { l := ppm.logger.Named("accountKeysFromProtobuf()") l.Debug("fired") @@ -320,11 +328,11 @@ func (ppm *PairingPayloadMarshaller) accountKeysFromProtobuf(pbKeys []*protobuf. l.Debug( "after for _, key := range pbKeys", zap.Any("pbKeys", pbKeys), - zap.Any("ppm.keys", ppm.keys), + zap.Any("accountPayloadMarshaller.keys", ppm.keys), ) } -func (ppm *PairingPayloadMarshaller) multiaccountFromProtobuf(pbMultiAccount *protobuf.MultiAccount) { +func (ppm *AccountPayloadMarshaller) multiaccountFromProtobuf(pbMultiAccount *protobuf.MultiAccount) { ppm.multiaccount = new(multiaccounts.Account) ppm.multiaccount.FromProtobuf(pbMultiAccount) } @@ -334,18 +342,18 @@ type PayloadRepository interface { StoreToSource() error } -// PairingPayloadRepository is responsible for loading, parsing, validating and storing PairingServer payload data -type PairingPayloadRepository struct { - *PairingPayload +// AccountPayloadRepository is responsible for loading, parsing, validating and storing Server payload data +type AccountPayloadRepository struct { + *AccountPayload multiaccountsDB *multiaccounts.Database keystorePath, keyUID string } -func NewPairingPayloadRepository(p *PairingPayload, config *PairingPayloadManagerConfig) *PairingPayloadRepository { - ppr := &PairingPayloadRepository{ - PairingPayload: p, +func NewAccountPayloadRepository(p *AccountPayload, config *AccountPayloadManagerConfig) *AccountPayloadRepository { + ppr := &AccountPayloadRepository{ + AccountPayload: p, } if config == nil { @@ -359,18 +367,18 @@ func NewPairingPayloadRepository(p *PairingPayload, config *PairingPayloadManage return ppr } -func (ppr *PairingPayloadRepository) LoadFromSource() error { - err := ppr.loadKeys(ppr.keystorePath) +func (apr *AccountPayloadRepository) LoadFromSource() error { + err := apr.loadKeys(apr.keystorePath) if err != nil { return err } - err = ppr.validateKeys(ppr.password) + err = apr.validateKeys(apr.password) if err != nil { return err } - ppr.multiaccount, err = ppr.multiaccountsDB.GetAccount(ppr.keyUID) + apr.multiaccount, err = apr.multiaccountsDB.GetAccount(apr.keyUID) if err != nil { return err } @@ -378,8 +386,8 @@ func (ppr *PairingPayloadRepository) LoadFromSource() error { return nil } -func (ppr *PairingPayloadRepository) loadKeys(keyStorePath string) error { - ppr.keys = make(map[string][]byte) +func (apr *AccountPayloadRepository) loadKeys(keyStorePath string) error { + apr.keys = make(map[string][]byte) fileWalker := func(path string, fileInfo os.FileInfo, err error) error { if err != nil { @@ -404,7 +412,7 @@ func (ppr *PairingPayloadRepository) loadKeys(keyStorePath string) error { return fmt.Errorf("account key address has invalid length '%s'", accountKey.Address) } - ppr.keys[fileInfo.Name()] = rawKeyFile + apr.keys[fileInfo.Name()] = rawKeyFile return nil } @@ -417,18 +425,18 @@ func (ppr *PairingPayloadRepository) loadKeys(keyStorePath string) error { return nil } -func (ppr *PairingPayloadRepository) StoreToSource() error { - err := ppr.validateKeys(ppr.password) +func (apr *AccountPayloadRepository) StoreToSource() error { + err := apr.validateKeys(apr.password) if err != nil { return err } - err = ppr.storeKeys(ppr.keystorePath) + err = apr.storeKeys(apr.keystorePath) if err != nil { return err } - err = ppr.storeMultiAccount() + err = apr.storeMultiAccount() if err != nil { return err } @@ -437,8 +445,8 @@ func (ppr *PairingPayloadRepository) StoreToSource() error { return nil } -func (ppr *PairingPayloadRepository) validateKeys(password string) error { - for _, key := range ppr.keys { +func (apr *AccountPayloadRepository) validateKeys(password string) error { + for _, key := range apr.keys { k, err := keystore.DecryptKey(key, password) if err != nil { return err @@ -453,7 +461,7 @@ func (ppr *PairingPayloadRepository) validateKeys(password string) error { return nil } -func (ppr *PairingPayloadRepository) storeKeys(keyStorePath string) error { +func (apr *AccountPayloadRepository) storeKeys(keyStorePath string) error { if keyStorePath == "" { return fmt.Errorf("keyStorePath can not be empty") } @@ -463,10 +471,10 @@ func (ppr *PairingPayloadRepository) storeKeys(keyStorePath string) error { // If lastDir == "keystore" we presume we need to create the rest of the keystore path // else we presume the provided keystore is valid if lastDir == "keystore" { - if ppr.multiaccount == nil || ppr.multiaccount.KeyUID == "" { + if apr.multiaccount == nil || apr.multiaccount.KeyUID == "" { return fmt.Errorf("no known Key UID") } - keyStorePath = filepath.Join(keyStorePath, ppr.multiaccount.KeyUID) + keyStorePath = filepath.Join(keyStorePath, apr.multiaccount.KeyUID) err := os.MkdirAll(keyStorePath, 0777) if err != nil { @@ -474,7 +482,7 @@ func (ppr *PairingPayloadRepository) storeKeys(keyStorePath string) error { } } - for name, data := range ppr.keys { + for name, data := range apr.keys { accountKey := new(keystore.EncryptedKeyJSONV3) if err := json.Unmarshal(data, &accountKey); err != nil { return fmt.Errorf("failed to read key file: %s", err) @@ -492,6 +500,87 @@ func (ppr *PairingPayloadRepository) storeKeys(keyStorePath string) error { return nil } -func (ppr *PairingPayloadRepository) storeMultiAccount() error { - return ppr.multiaccountsDB.SaveAccount(*ppr.multiaccount) +func (apr *AccountPayloadRepository) storeMultiAccount() error { + return apr.multiaccountsDB.SaveAccount(*apr.multiaccount) +} + +type RawMessagePayloadManager struct { + logger *zap.Logger + // reference from AccountPayloadManager#accountPayload + accountPayload *AccountPayload + *PayloadEncryptionManager + payloadRepository *RawMessageRepository +} + +func NewRawMessagePayloadManager(logger *zap.Logger, accountPayload *AccountPayload, aesKey []byte, backend *api.GethStatusBackend, keystorePath string) (*RawMessagePayloadManager, error) { + l := logger.Named("RawMessagePayloadManager") + pem, err := NewPayloadEncryptionManager(aesKey, l) + if err != nil { + return nil, err + } + return &RawMessagePayloadManager{ + logger: l, + accountPayload: accountPayload, + PayloadEncryptionManager: pem, + payloadRepository: NewRawMessageRepository(backend, keystorePath, accountPayload), + }, nil +} + +func (r *RawMessagePayloadManager) Mount() error { + err := r.payloadRepository.LoadFromSource() + if err != nil { + return err + } + return r.Encrypt(r.payloadRepository.payload) +} + +func (r *RawMessagePayloadManager) Receive(data []byte) error { + err := r.Decrypt(data) + if err != nil { + return err + } + r.payloadRepository.payload = r.Received() + return r.payloadRepository.StoreToSource() +} + +func (r *RawMessagePayloadManager) ResetPayload() { + r.payloadRepository.payload = make([]byte, 0) + r.PayloadEncryptionManager.ResetPayload() +} + +type RawMessageRepository struct { + payload []byte + syncRawMessageHandler *SyncRawMessageHandler + keystorePath string + accountPayload *AccountPayload +} + +func NewRawMessageRepository(backend *api.GethStatusBackend, keystorePath string, accountPayload *AccountPayload) *RawMessageRepository { + return &RawMessageRepository{ + syncRawMessageHandler: NewSyncRawMessageHandler(backend), + keystorePath: keystorePath, + payload: make([]byte, 0), + accountPayload: accountPayload, + } +} + +func (r *RawMessageRepository) LoadFromSource() error { + account := r.accountPayload.multiaccount + if account == nil || account.KeyUID == "" { + return fmt.Errorf("no known KeyUID when loading raw messages") + } + payload, err := r.syncRawMessageHandler.PrepareRawMessage(account.KeyUID) + if err != nil { + return err + } + r.payload = payload + return nil +} + +func (r *RawMessageRepository) StoreToSource() error { + accountPayload := r.accountPayload + if accountPayload == nil || accountPayload.multiaccount == nil { + return fmt.Errorf("no known multiaccount when storing raw messages") + } + return r.syncRawMessageHandler.HandleRawMessage(accountPayload.multiaccount, accountPayload.password, r.keystorePath, r.payload) } diff --git a/server/payload_manager_test.go b/server/pairing/payload_manager_test.go similarity index 91% rename from server/payload_manager_test.go rename to server/pairing/payload_manager_test.go index 6d2004336..2b014321f 100644 --- a/server/payload_manager_test.go +++ b/server/pairing/payload_manager_test.go @@ -1,4 +1,4 @@ -package server +package pairing import ( "bytes" @@ -44,8 +44,8 @@ type PayloadMarshallerSuite struct { teardown func() - config1 *PairingPayloadManagerConfig - config2 *PairingPayloadManagerConfig + config1 *AccountPayloadManagerConfig + config2 *AccountPayloadManagerConfig } func setupTestDB(t *testing.T) (*multiaccounts.Database, func()) { @@ -132,18 +132,18 @@ func (pms *PayloadMarshallerSuite) SetupTest() { err := db1.SaveAccount(expected) pms.Require().NoError(err) - pms.config1 = &PairingPayloadManagerConfig{ + pms.config1 = &AccountPayloadManagerConfig{ DB: db1, - PairingPayloadSourceConfig: PairingPayloadSourceConfig{ + PayloadSourceConfig: &PayloadSourceConfig{ KeystorePath: keystore1, KeyUID: keyUID, Password: password, }, } - pms.config2 = &PairingPayloadManagerConfig{ + pms.config2 = &AccountPayloadManagerConfig{ DB: db2, - PairingPayloadSourceConfig: PairingPayloadSourceConfig{ + PayloadSourceConfig: &PayloadSourceConfig{ KeystorePath: keystore2, KeyUID: keyUID, Password: password, @@ -156,11 +156,11 @@ func (pms *PayloadMarshallerSuite) TearDownTest() { } func (pms *PayloadMarshallerSuite) TestPayloadMarshaller_LoadPayloads() { - // Make a PairingPayload - pp := new(PairingPayload) + // Make a Payload + pp := new(AccountPayload) // Make and LoadFromSource PairingPayloadRepository 1 - ppr := NewPairingPayloadRepository(pp, pms.config1) + ppr := NewAccountPayloadRepository(pp, pms.config1) err := ppr.LoadFromSource() pms.Require().NoError(err) @@ -189,11 +189,11 @@ func (pms *PayloadMarshallerSuite) TestPayloadMarshaller_LoadPayloads() { } func (pms *PayloadMarshallerSuite) TestPayloadMarshaller_MarshalToProtobuf() { - // Make a PairingPayload - pp := new(PairingPayload) + // Make a Payload + pp := new(AccountPayload) // Make and LoadFromSource PairingPayloadRepository 1 - ppr := NewPairingPayloadRepository(pp, pms.config1) + ppr := NewAccountPayloadRepository(pp, pms.config1) err := ppr.LoadFromSource() pms.Require().NoError(err) @@ -218,11 +218,11 @@ func (pms *PayloadMarshallerSuite) TestPayloadMarshaller_MarshalToProtobuf() { } func (pms *PayloadMarshallerSuite) TestPayloadMarshaller_UnmarshalProtobuf() { - // Make a PairingPayload - pp := new(PairingPayload) + // Make a Payload + pp := new(AccountPayload) // Make and LoadFromSource PairingPayloadRepository 1 - ppr := NewPairingPayloadRepository(pp, pms.config1) + ppr := NewAccountPayloadRepository(pp, pms.config1) err := ppr.LoadFromSource() pms.Require().NoError(err) @@ -232,8 +232,8 @@ func (pms *PayloadMarshallerSuite) TestPayloadMarshaller_UnmarshalProtobuf() { pb, err := ppm.MarshalToProtobuf() pms.Require().NoError(err) - // Make a PairingPayload - pp2 := new(PairingPayload) + // Make a Payload + pp2 := new(AccountPayload) // Make PairingPayloadMarshaller 2 ppm2 := NewPairingPayloadMarshaller(pp2, pms.Logger) @@ -271,11 +271,11 @@ func (pms *PayloadMarshallerSuite) TestPayloadMarshaller_UnmarshalProtobuf() { } func (pms *PayloadMarshallerSuite) TestPayloadMarshaller_StorePayloads() { - // Make a PairingPayload - pp := new(PairingPayload) + // Make a Payload + pp := new(AccountPayload) // Make and LoadFromSource PairingPayloadRepository 1 - ppr := NewPairingPayloadRepository(pp, pms.config1) + ppr := NewAccountPayloadRepository(pp, pms.config1) err := ppr.LoadFromSource() pms.Require().NoError(err) @@ -285,8 +285,8 @@ func (pms *PayloadMarshallerSuite) TestPayloadMarshaller_StorePayloads() { pb, err := ppm.MarshalToProtobuf() pms.Require().NoError(err) - // Make a PairingPayload - pp2 := new(PairingPayload) + // Make a Payload + pp2 := new(AccountPayload) // Make PairingPayloadMarshaller 2 ppm2 := NewPairingPayloadMarshaller(pp2, pms.Logger) @@ -295,7 +295,7 @@ func (pms *PayloadMarshallerSuite) TestPayloadMarshaller_StorePayloads() { pms.Require().NoError(err) // Make and Load PairingPayloadRepository 2 - ppr2 := NewPairingPayloadRepository(pp2, pms.config2) + ppr2 := NewAccountPayloadRepository(pp2, pms.config2) err = ppr2.StoreToSource() pms.Require().NoError(err) diff --git a/server/pairing/raw_message_collector.go b/server/pairing/raw_message_collector.go new file mode 100644 index 000000000..6f272725a --- /dev/null +++ b/server/pairing/raw_message_collector.go @@ -0,0 +1,20 @@ +package pairing + +import ( + "context" + + "github.com/status-im/status-go/protocol/common" +) + +type RawMessageCollector struct { + rawMessages []*common.RawMessage +} + +func (r *RawMessageCollector) dispatchMessage(_ context.Context, rawMessage common.RawMessage) (common.RawMessage, error) { + r.rawMessages = append(r.rawMessages, &rawMessage) + return rawMessage, nil +} + +func (r *RawMessageCollector) getRawMessages() []*common.RawMessage { + return r.rawMessages +} diff --git a/server/pairing/raw_message_handler.go b/server/pairing/raw_message_handler.go new file mode 100644 index 000000000..4fb9d9624 --- /dev/null +++ b/server/pairing/raw_message_handler.go @@ -0,0 +1,142 @@ +package pairing + +import ( + "context" + "encoding/json" + "fmt" + "path/filepath" + + "github.com/google/uuid" + + "github.com/status-im/status-go/multiaccounts/settings" + + "github.com/golang/protobuf/proto" + + "github.com/status-im/status-go/api" + "github.com/status-im/status-go/multiaccounts" + "github.com/status-im/status-go/multiaccounts/accounts" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/protocol/protobuf" +) + +type SyncRawMessageHandler struct { + backend *api.GethStatusBackend +} + +func NewSyncRawMessageHandler(backend *api.GethStatusBackend) *SyncRawMessageHandler { + return &SyncRawMessageHandler{backend: backend} +} + +func (s *SyncRawMessageHandler) PrepareRawMessage(keyUID string) ([]byte, error) { + messenger := s.backend.Messenger() + if messenger == nil { + return nil, fmt.Errorf("messenger is nil when handlePairingSyncDeviceSend") + } + + currentAccount, err := s.backend.GetActiveAccount() + if err != nil { + return nil, err + } + if keyUID != currentAccount.KeyUID { + return nil, fmt.Errorf("keyUID not equal") + } + + messenger.SetLocalPairing(true) + defer func() { + messenger.SetLocalPairing(false) + }() + rawMessageCollector := new(RawMessageCollector) + err = messenger.SyncDevices(context.TODO(), currentAccount.Name, currentAccount.Identicon, rawMessageCollector.dispatchMessage) + if err != nil { + return nil, err + } + syncRawMessage := new(protobuf.SyncRawMessage) + for _, m := range rawMessageCollector.getRawMessages() { + rawMessage := new(protobuf.RawMessage) + rawMessage.Payload = m.Payload + rawMessage.MessageType = m.MessageType + syncRawMessage.RawMessages = append(syncRawMessage.RawMessages, rawMessage) + } + + accountService := s.backend.StatusNode().AccountService() + var ( + subAccounts []*accounts.Account + setting settings.Settings + ) + subAccounts, err = accountService.GetAccountsByKeyUID(keyUID) + if err != nil { + return nil, err + } + syncRawMessage.SubAccountsJsonBytes, err = json.Marshal(subAccounts) + if err != nil { + return nil, err + } + setting, err = accountService.GetSettings() + if err != nil { + return nil, err + } + syncRawMessage.SettingsJsonBytes, err = json.Marshal(setting) + if err != nil { + return nil, err + } + nodeConfig := s.backend.StatusNode().Config() + if syncRawMessage.NodeConfigJsonBytes, err = json.Marshal(nodeConfig); err != nil { + return nil, err + } + return proto.Marshal(syncRawMessage) +} + +func (s *SyncRawMessageHandler) HandleRawMessage(account *multiaccounts.Account, password, keystorePath string, payload []byte) error { + rawMessages, subAccounts, setting, nodeConfig, err := s.unmarshalSyncRawMessage(payload) + if err != nil { + return err + } + + newKeystoreDir := filepath.Join(keystorePath, account.KeyUID) + accountManager := s.backend.AccountManager() + err = accountManager.InitKeystore(newKeystoreDir) + if err != nil { + return err + } + + nodeConfig.RootDataDir = filepath.Dir(keystorePath) + nodeConfig.DataDir = filepath.Join(nodeConfig.RootDataDir, filepath.Base(nodeConfig.DataDir)) + nodeConfig.KeyStoreDir = newKeystoreDir + installationID := uuid.New().String() + nodeConfig.ShhextConfig.InstallationID = installationID + setting.InstallationID = installationID + + err = s.backend.StartNodeWithAccountAndInitialConfig(*account, password, *setting, nodeConfig, subAccounts) + if err != nil { + return err + } + + messenger := s.backend.Messenger() + return messenger.HandleSyncRawMessages(rawMessages) +} + +func (s *SyncRawMessageHandler) unmarshalSyncRawMessage(payload []byte) ([]*protobuf.RawMessage, []*accounts.Account, *settings.Settings, *params.NodeConfig, error) { + var ( + syncRawMessage protobuf.SyncRawMessage + subAccounts []*accounts.Account + setting *settings.Settings + nodeConfig *params.NodeConfig + ) + err := proto.Unmarshal(payload, &syncRawMessage) + if err != nil { + return nil, nil, nil, nil, err + } + err = json.Unmarshal(syncRawMessage.SubAccountsJsonBytes, &subAccounts) + if err != nil { + return nil, nil, nil, nil, err + } + err = json.Unmarshal(syncRawMessage.SettingsJsonBytes, &setting) + if err != nil { + return nil, nil, nil, nil, err + } + err = json.Unmarshal(syncRawMessage.NodeConfigJsonBytes, &nodeConfig) + if err != nil { + return nil, nil, nil, nil, err + } + return syncRawMessage.RawMessages, subAccounts, setting, nodeConfig, nil +} diff --git a/server/server_pairing.go b/server/pairing/server.go similarity index 50% rename from server/server_pairing.go rename to server/pairing/server.go index cd65af19b..8f28ae839 100644 --- a/server/server_pairing.go +++ b/server/pairing/server.go @@ -1,4 +1,4 @@ -package server +package pairing import ( "crypto/ecdsa" @@ -10,15 +10,18 @@ import ( "net" "time" + "github.com/status-im/status-go/api" + "github.com/status-im/status-go/server" + "github.com/gorilla/sessions" "github.com/status-im/status-go/logutils" - "github.com/status-im/status-go/multiaccounts" ) -type PairingServer struct { - Server +type Server struct { + server.Server PayloadManager + rawMessagePayloadManager *RawMessagePayloadManager pk *ecdsa.PublicKey ek []byte @@ -35,8 +38,8 @@ type Config struct { Hostname string Mode Mode - // Payload management fields - *PairingPayloadManagerConfig + // AccountPayload management fields + *AccountPayloadManagerConfig } func makeCookieStore() (*sessions.CookieStore, error) { @@ -55,10 +58,10 @@ func makeCookieStore() (*sessions.CookieStore, error) { return sessions.NewCookieStore(auth, enc), nil } -// NewPairingServer returns a *PairingServer init from the given *Config -func NewPairingServer(config *Config) (*PairingServer, error) { - logger := logutils.ZapLogger().Named("PairingServer") - pm, err := NewPairingPayloadManager(config.EK, config.PairingPayloadManagerConfig, logger) +// NewPairingServer returns a *Server init from the given *Config +func NewPairingServer(backend *api.GethStatusBackend, config *Config) (*Server, error) { + logger := logutils.ZapLogger().Named("Server") + pm, err := NewAccountPayloadManager(config.EK, config.AccountPayloadManagerConfig, logger) if err != nil { return nil, err } @@ -68,25 +71,32 @@ func NewPairingServer(config *Config) (*PairingServer, error) { return nil, err } - return &PairingServer{Server: NewServer( + rmpm, err := NewRawMessagePayloadManager(logger, pm.accountPayload, config.EK, backend, config.KeystorePath) + if err != nil { + return nil, err + } + + return &Server{Server: server.NewServer( config.Cert, config.Hostname, nil, logger, ), - pk: config.PK, - ek: config.EK, - mode: config.Mode, - PayloadManager: pm, - cookieStore: cs, + pk: config.PK, + ek: config.EK, + mode: config.Mode, + PayloadManager: pm, + cookieStore: cs, + rawMessagePayloadManager: rmpm, }, nil } // MakeConnectionParams generates a *ConnectionParams based on the Server's current state -func (s *PairingServer) MakeConnectionParams() (*ConnectionParams, error) { - netIP := net.ParseIP(s.hostname) +func (s *Server) MakeConnectionParams() (*ConnectionParams, error) { + hostname := s.GetHostname() + netIP := net.ParseIP(hostname) if netIP == nil { - return nil, fmt.Errorf("invalid ip address given '%s'", s.hostname) + return nil, fmt.Errorf("invalid ip address given '%s'", hostname) } netIP4 := netIP.To4() @@ -97,40 +107,42 @@ func (s *PairingServer) MakeConnectionParams() (*ConnectionParams, error) { return NewConnectionParams(netIP, s.MustGetPort(), s.pk, s.ek, s.mode), nil } -func (s *PairingServer) StartPairing() error { +func (s *Server) StartPairing() error { switch s.mode { case Receiving: - return s.startReceivingAccountData() + return s.startReceivingData() case Sending: - return s.startSendingAccountData() + return s.startSendingData() default: return fmt.Errorf("invalid server mode '%d'", s.mode) } } -func (s *PairingServer) startReceivingAccountData() error { - s.SetHandlers(HandlerPatternMap{ - pairingReceive: handlePairingReceive(s), - pairingChallenge: handlePairingChallenge(s), +func (s *Server) startReceivingData() error { + s.SetHandlers(server.HandlerPatternMap{ + pairingReceiveAccount: handlePairingReceive(s), + pairingChallenge: handlePairingChallenge(s), + pairingSyncDeviceReceive: handleParingSyncDeviceReceive(s), }) return s.Start() } -func (s *PairingServer) startSendingAccountData() error { +func (s *Server) startSendingData() error { err := s.Mount() if err != nil { return err } - s.SetHandlers(HandlerPatternMap{ - pairingSend: challengeMiddleware(s, handlePairingSend(s)), - pairingChallenge: handlePairingChallenge(s), + s.SetHandlers(server.HandlerPatternMap{ + pairingSendAccount: challengeMiddleware(s, handlePairingSend(s)), + pairingChallenge: handlePairingChallenge(s), + pairingSyncDeviceSend: challengeMiddleware(s, handlePairingSyncDeviceSend(s)), }) return s.Start() } -// MakeFullPairingServer generates a fully configured and randomly seeded PairingServer -func MakeFullPairingServer(db *multiaccounts.Database, mode Mode, storeConfig PairingPayloadSourceConfig) (*PairingServer, error) { +// MakeFullPairingServer generates a fully configured and randomly seeded Server +func MakeFullPairingServer(backend *api.GethStatusBackend, mode Mode, storeConfig *PayloadSourceConfig) (*Server, error) { tlsKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, err @@ -142,7 +154,7 @@ func MakeFullPairingServer(db *multiaccounts.Database, mode Mode, storeConfig Pa return nil, err } - outboundIP, err := GetOutboundIP() + outboundIP, err := server.GetOutboundIP() if err != nil { return nil, err } @@ -151,8 +163,7 @@ func MakeFullPairingServer(db *multiaccounts.Database, mode Mode, storeConfig Pa if err != nil { return nil, err } - - return NewPairingServer(&Config{ + return NewPairingServer(backend, &Config{ // Things that can be generated, and CANNOT come from the app client (well they could be this is better) PK: &tlsKey.PublicKey, EK: AESKey, @@ -162,26 +173,26 @@ func MakeFullPairingServer(db *multiaccounts.Database, mode Mode, storeConfig Pa // Things that can't be generated, but DO come from the app client Mode: mode, - PairingPayloadManagerConfig: &PairingPayloadManagerConfig{ + AccountPayloadManagerConfig: &AccountPayloadManagerConfig{ // Things that can't be generated, but DO NOT come from app client - DB: db, + DB: backend.GetMultiaccountDB(), // Things that can't be generated, but DO come from the app client - PairingPayloadSourceConfig: storeConfig, + PayloadSourceConfig: storeConfig, }, }) } -// StartUpPairingServer generates a PairingServer, starts the pairing server in the correct mode -// and returns the ConnectionParams string to allow a PairingClient to make a successful connection. -func StartUpPairingServer(db *multiaccounts.Database, mode Mode, configJSON string) (string, error) { - var conf PairingPayloadSourceConfig +// StartUpPairingServer generates a Server, starts the pairing server in the correct mode +// and returns the ConnectionParams string to allow a Client to make a successful connection. +func StartUpPairingServer(backend *api.GethStatusBackend, mode Mode, configJSON string) (string, error) { + var conf PayloadSourceConfig err := json.Unmarshal([]byte(configJSON), &conf) if err != nil { return "", err } - ps, err := MakeFullPairingServer(db, mode, conf) + ps, err := MakeFullPairingServer(backend, mode, &conf) if err != nil { return "", err } diff --git a/server/server_pairing_test.go b/server/pairing/server_pairing_test.go similarity index 72% rename from server/server_pairing_test.go rename to server/pairing/server_pairing_test.go index 053654e7e..dedf5a4e6 100644 --- a/server/server_pairing_test.go +++ b/server/pairing/server_pairing_test.go @@ -1,12 +1,19 @@ -package server +package pairing import ( "crypto/ecdsa" "crypto/rand" + "encoding/hex" + "io/ioutil" + "net/http" "regexp" "testing" + "time" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + + "github.com/status-im/status-go/server" ) func TestPairingServerSuite(t *testing.T) { @@ -61,12 +68,14 @@ func (s *PairingServerSuite) TestPairingServer_StartPairing() { err = ccp.FromString(qr) s.Require().NoError(err) - c, err := NewPairingClient(ccp, nil) + c, err := NewPairingClient(nil, ccp, &AccountPayloadManagerConfig{ + PayloadSourceConfig: &PayloadSourceConfig{KeystorePath: ""}, + }) s.Require().NoError(err) // Compare cert values cert := c.serverCert - cl := s.PS.cert.Leaf + cl := s.PS.GetCert().Leaf s.Require().Equal(cl.Signature, cert.Signature) s.Require().Zero(cl.PublicKey.(*ecdsa.PublicKey).X.Cmp(cert.PublicKey.(*ecdsa.PublicKey).X)) s.Require().Zero(cl.PublicKey.(*ecdsa.PublicKey).Y.Cmp(cert.PublicKey.(*ecdsa.PublicKey).Y)) @@ -102,7 +111,7 @@ func (s *PairingServerSuite) TestPairingServer_StartPairing() { } } -func (s *PairingServerSuite) sendingSetup() *PairingClient { +func (s *PairingServerSuite) sendingSetup() *Client { // Replace PairingServer.PayloadManager with a MockEncryptOnlyPayloadManager pm, err := NewMockEncryptOnlyPayloadManager(s.EphemeralAES) s.Require().NoError(err) @@ -122,7 +131,9 @@ func (s *PairingServerSuite) sendingSetup() *PairingClient { err = ccp.FromString(qr) s.Require().NoError(err) - c, err := NewPairingClient(ccp, nil) + c, err := NewPairingClient(nil, ccp, &AccountPayloadManagerConfig{ + PayloadSourceConfig: &PayloadSourceConfig{KeystorePath: ""}, + }) s.Require().NoError(err) // Replace PairingClient.PayloadManager with a MockEncryptOnlyPayloadManager @@ -187,3 +198,66 @@ func (s *PairingServerSuite) TestPairingServer_handlePairingChallengeMiddleware_ s.Require().Error(err) s.Require().Equal("status not ok, received '403 Forbidden'", err.Error()) } + +func testHandler(t *testing.T) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + say, ok := r.URL.Query()["say"] + if !ok || len(say) == 0 { + say = append(say, "nothing") + } + + _, err := w.Write([]byte("Hello I like to be a tls server. You said: `" + say[0] + "` " + time.Now().String())) + if err != nil { + require.NoError(t, err) + } + } +} + +func makeThingToSay() (string, error) { + b := make([]byte, 32) + _, err := rand.Read(b) + if err != nil { + return "", err + } + + return hex.EncodeToString(b), nil +} + +func (s *PairingServerSuite) TestGetOutboundIPWithFullServerE2e() { + s.PS.mode = Sending + s.PS.SetHandlers(server.HandlerPatternMap{"/hello": testHandler(s.T())}) + + err := s.PS.Start() + s.Require().NoError(err) + + // Give time for the sever to be ready, hacky I know, I'll iron this out + time.Sleep(100 * time.Millisecond) + + // Server generates a QR code connection string + cp, err := s.PS.MakeConnectionParams() + s.Require().NoError(err) + + qr := cp.ToString() + + // Client reads QR code and parses the connection string + ccp := new(ConnectionParams) + err = ccp.FromString(qr) + s.Require().NoError(err) + + c, err := NewPairingClient(nil, ccp, &AccountPayloadManagerConfig{ + PayloadSourceConfig: &PayloadSourceConfig{KeystorePath: ""}, + }) + s.Require().NoError(err) + + thing, err := makeThingToSay() + s.Require().NoError(err) + + response, err := c.Get(c.baseAddress.String() + "/hello?say=" + thing) + s.Require().NoError(err) + + defer response.Body.Close() + + content, err := ioutil.ReadAll(response.Body) + s.Require().NoError(err) + s.Require().Equal("Hello I like to be a tls server. You said: `"+thing+"`", string(content[:109])) +} diff --git a/server/pairing/sync_device_test.go b/server/pairing/sync_device_test.go new file mode 100644 index 000000000..8872332e1 --- /dev/null +++ b/server/pairing/sync_device_test.go @@ -0,0 +1,316 @@ +package pairing + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/status-im/status-go/account/generator" + "github.com/status-im/status-go/api" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/multiaccounts" + "github.com/status-im/status-go/multiaccounts/accounts" + "github.com/status-im/status-go/multiaccounts/settings" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/protocol/identity/alias" + "github.com/status-im/status-go/services/browsers" + "github.com/status-im/status-go/sqlite" +) + +const pathWalletRoot = "m/44'/60'/0'/0" +const pathEIP1581 = "m/43'/60'/1581'" +const pathDefaultChat = pathEIP1581 + "/0'/0" +const pathDefaultWallet = pathWalletRoot + "/0" + +var paths = []string{pathWalletRoot, pathEIP1581, pathDefaultChat, pathDefaultWallet} + +const keystoreDir = "keystore" + +func TestSyncDeviceSuite(t *testing.T) { + suite.Run(t, new(SyncDeviceSuite)) +} + +type SyncDeviceSuite struct { + suite.Suite + password string + clientAsSenderTmpdir string + clientAsReceiverTmpdir string +} + +func (s *SyncDeviceSuite) SetupTest() { + s.password = "password" + + clientAsSenderTmpdir, err := os.MkdirTemp("", "TestPairingSyncDeviceClientAsSender") + require.NoError(s.T(), err) + s.clientAsSenderTmpdir = clientAsSenderTmpdir + + clientAsReceiverTmpdir, err := os.MkdirTemp("", "TestPairingSyncDeviceClientAsReceiver") + require.NoError(s.T(), err) + s.clientAsReceiverTmpdir = clientAsReceiverTmpdir +} + +func (s *SyncDeviceSuite) TearDownTest() { + os.RemoveAll(s.clientAsSenderTmpdir) + os.RemoveAll(s.clientAsReceiverTmpdir) +} + +func (s *SyncDeviceSuite) prepareBackendWithAccount(tmpdir string) *api.GethStatusBackend { + backend := s.prepareBackendWithoutAccount(tmpdir) + accountManager := backend.AccountManager() + generator := accountManager.AccountsGenerator() + generatedAccountInfos, err := generator.GenerateAndDeriveAddresses(12, 1, "", paths) + require.NoError(s.T(), err) + generatedAccountInfo := generatedAccountInfos[0] + account := multiaccounts.Account{ + KeyUID: generatedAccountInfo.KeyUID, + KDFIterations: sqlite.ReducedKDFIterationsNumber, + } + err = accountManager.InitKeystore(filepath.Join(tmpdir, keystoreDir, account.KeyUID)) + require.NoError(s.T(), err) + err = backend.OpenAccounts() + require.NoError(s.T(), err) + derivedAddresses := generatedAccountInfo.Derived + _, err = generator.StoreDerivedAccounts(generatedAccountInfo.ID, s.password, paths) + require.NoError(s.T(), err) + + settings, err := defaultSettings(generatedAccountInfo.GeneratedAccountInfo, derivedAddresses, nil) + require.NoError(s.T(), err) + + nodeConfig, err := defaultNodeConfig(tmpdir, settings.InstallationID, account.KeyUID) + require.NoError(s.T(), err) + + walletDerivedAccount := derivedAddresses[pathDefaultWallet] + walletAccount := &accounts.Account{ + PublicKey: types.Hex2Bytes(walletDerivedAccount.PublicKey), + KeyUID: generatedAccountInfo.KeyUID, + Address: types.HexToAddress(walletDerivedAccount.Address), + Color: "", + Wallet: true, + Path: pathDefaultWallet, + Name: "Ethereum account", + } + + chatDerivedAccount := derivedAddresses[pathDefaultChat] + chatAccount := &accounts.Account{ + PublicKey: types.Hex2Bytes(chatDerivedAccount.PublicKey), + KeyUID: generatedAccountInfo.KeyUID, + Address: types.HexToAddress(chatDerivedAccount.Address), + Name: settings.Name, + Chat: true, + Path: pathDefaultChat, + } + + accounts := []*accounts.Account{walletAccount, chatAccount} + err = backend.StartNodeWithAccountAndInitialConfig(account, s.password, *settings, nodeConfig, accounts) + require.NoError(s.T(), err) + return backend +} + +func (s *SyncDeviceSuite) prepareBackendWithoutAccount(tmpdir string) *api.GethStatusBackend { + backend := api.NewGethStatusBackend() + backend.UpdateRootDataDir(tmpdir) + return backend +} + +func (s *SyncDeviceSuite) TestPairingSyncDeviceClientAsSender() { + clientTmpDir := filepath.Join(s.clientAsSenderTmpdir, "client") + clientBackend := s.prepareBackendWithAccount(clientTmpDir) + + serverTmpDir := filepath.Join(s.clientAsSenderTmpdir, "server") + serverBackend := s.prepareBackendWithoutAccount(serverTmpDir) + defer func() { + require.NoError(s.T(), serverBackend.Logout()) + }() + + err := serverBackend.AccountManager().InitKeystore(filepath.Join(serverTmpDir, keystoreDir)) + require.NoError(s.T(), err) + err = serverBackend.OpenAccounts() + require.NoError(s.T(), err) + serverKeystorePath := filepath.Join(serverTmpDir, keystoreDir) + configJSON := fmt.Sprintf(`{"KeystorePath":"%s"}`, serverKeystorePath) + cs, err := StartUpPairingServer(serverBackend, Receiving, configJSON) + require.NoError(s.T(), err) + + // generate some data for the client + clientBrowserAPI := clientBackend.StatusNode().BrowserService().APIs()[0].Service.(*browsers.API) + _, err = clientBrowserAPI.StoreBookmark(context.TODO(), browsers.Bookmark{ + Name: "status.im", + URL: "https://status.im", + }) + require.NoError(s.T(), err) + + activeAccount, err := clientBackend.GetActiveAccount() + require.NoError(s.T(), err) + clientKeystorePath := filepath.Join(clientTmpDir, keystoreDir, activeAccount.KeyUID) + var config = PayloadSourceConfig{ + KeystorePath: clientKeystorePath, + KeyUID: activeAccount.KeyUID, + Password: s.password, + } + configBytes, err := json.Marshal(config) + require.NoError(s.T(), err) + err = StartUpPairingClient(clientBackend, cs, string(configBytes)) + require.NoError(s.T(), err) + require.NoError(s.T(), clientBackend.Logout()) + + serverBrowserAPI := serverBackend.StatusNode().BrowserService().APIs()[0].Service.(*browsers.API) + bookmarks, err := serverBrowserAPI.GetBookmarks(context.TODO()) + require.NoError(s.T(), err) + require.Equal(s.T(), 1, len(bookmarks)) + require.Equal(s.T(), "status.im", bookmarks[0].Name) +} + +func (s *SyncDeviceSuite) TestPairingSyncDeviceClientAsReceiver() { + clientTmpDir := filepath.Join(s.clientAsReceiverTmpdir, "client") + clientBackend := s.prepareBackendWithoutAccount(clientTmpDir) + + serverTmpDir := filepath.Join(s.clientAsReceiverTmpdir, "server") + serverBackend := s.prepareBackendWithAccount(serverTmpDir) + defer func() { + require.NoError(s.T(), clientBackend.Logout()) + }() + + activeAccount, err := serverBackend.GetActiveAccount() + require.NoError(s.T(), err) + serverKeystorePath := filepath.Join(serverTmpDir, keystoreDir, activeAccount.KeyUID) + var config = PayloadSourceConfig{ + KeystorePath: serverKeystorePath, + KeyUID: activeAccount.KeyUID, + Password: s.password, + } + configBytes, err := json.Marshal(config) + require.NoError(s.T(), err) + cs, err := StartUpPairingServer(serverBackend, Sending, string(configBytes)) + require.NoError(s.T(), err) + + // generate some data for the server + serverBrowserAPI := serverBackend.StatusNode().BrowserService().APIs()[0].Service.(*browsers.API) + _, err = serverBrowserAPI.StoreBookmark(context.TODO(), browsers.Bookmark{ + Name: "status.im", + URL: "https://status.im", + }) + require.NoError(s.T(), err) + + err = clientBackend.AccountManager().InitKeystore(filepath.Join(clientTmpDir, keystoreDir)) + require.NoError(s.T(), err) + err = clientBackend.OpenAccounts() + require.NoError(s.T(), err) + clientKeystorePath := filepath.Join(clientTmpDir, keystoreDir) + configJSON := fmt.Sprintf(`{"KeystorePath":"%s"}`, clientKeystorePath) + err = StartUpPairingClient(clientBackend, cs, configJSON) + require.NoError(s.T(), err) + + require.NoError(s.T(), serverBackend.Logout()) + + clientBrowserAPI := clientBackend.StatusNode().BrowserService().APIs()[0].Service.(*browsers.API) + bookmarks, err := clientBrowserAPI.GetBookmarks(context.TODO()) + require.NoError(s.T(), err) + require.Equal(s.T(), 1, len(bookmarks)) + require.Equal(s.T(), "status.im", bookmarks[0].Name) +} + +func defaultSettings(generatedAccountInfo generator.GeneratedAccountInfo, derivedAddresses map[string]generator.AccountInfo, mnemonic *string) (*settings.Settings, error) { + chatKeyString := derivedAddresses[pathDefaultChat].PublicKey + + settings := &settings.Settings{} + settings.KeyUID = generatedAccountInfo.KeyUID + settings.Address = types.HexToAddress(generatedAccountInfo.Address) + settings.WalletRootAddress = types.HexToAddress(derivedAddresses[pathWalletRoot].Address) + + // Set chat key & name + name, err := alias.GenerateFromPublicKeyString(chatKeyString) + if err != nil { + return nil, err + } + settings.Name = name + settings.PublicKey = chatKeyString + + settings.DappsAddress = types.HexToAddress(derivedAddresses[pathDefaultWallet].Address) + settings.EIP1581Address = types.HexToAddress(derivedAddresses[pathEIP1581].Address) + settings.Mnemonic = mnemonic + + settings.SigningPhrase = "balabala" + + settings.SendPushNotifications = true + settings.InstallationID = uuid.New().String() + settings.UseMailservers = true + + settings.PreviewPrivacy = true + settings.Currency = "usd" + settings.ProfilePicturesVisibility = 1 + settings.LinkPreviewRequestEnabled = true + + visibleTokens := make(map[string][]string) + visibleTokens["mainnet"] = []string{"SNT"} + visibleTokensJSON, err := json.Marshal(visibleTokens) + if err != nil { + return nil, err + } + visibleTokenJSONRaw := json.RawMessage(visibleTokensJSON) + settings.WalletVisibleTokens = &visibleTokenJSONRaw + + networks := make([]map[string]string, 0) + networksJSON, err := json.Marshal(networks) + if err != nil { + return nil, err + } + networkRawMessage := json.RawMessage(networksJSON) + settings.Networks = &networkRawMessage + settings.CurrentNetwork = "mainnet_rpc" + + return settings, nil +} + +func defaultNodeConfig(tmpdir, installationID, keyUID string) (*params.NodeConfig, error) { + // Set mainnet + nodeConfig := ¶ms.NodeConfig{} + nodeConfig.NetworkID = 1 + nodeConfig.LogLevel = "ERROR" + nodeConfig.DataDir = filepath.Join(tmpdir, "ethereum/mainnet_rpc") + nodeConfig.KeyStoreDir = filepath.Join(tmpdir, keystoreDir, keyUID) + nodeConfig.UpstreamConfig = params.UpstreamRPCConfig{ + Enabled: true, + URL: "https://mainnet.infura.io/v3/800c641949d64d768a5070a1b0511938", + } + + nodeConfig.Name = "StatusIM" + clusterConfig, err := params.LoadClusterConfigFromFleet("eth.prod") + if err != nil { + return nil, err + } + nodeConfig.ClusterConfig = *clusterConfig + + nodeConfig.WalletConfig = params.WalletConfig{Enabled: false} + nodeConfig.LocalNotificationsConfig = params.LocalNotificationsConfig{Enabled: true} + nodeConfig.BrowsersConfig = params.BrowsersConfig{Enabled: false} + nodeConfig.PermissionsConfig = params.PermissionsConfig{Enabled: true} + nodeConfig.MailserversConfig = params.MailserversConfig{Enabled: true} + nodeConfig.EnableNTPSync = true + nodeConfig.WakuConfig = params.WakuConfig{ + Enabled: true, + LightClient: true, + MinimumPoW: 0.000001, + } + + nodeConfig.ShhextConfig = params.ShhextConfig{ + BackupDisabledDataDir: "", + InstallationID: installationID, + MaxMessageDeliveryAttempts: 6, + MailServerConfirmations: true, + VerifyTransactionURL: "", + VerifyENSURL: "", + VerifyENSContractAddress: "", + VerifyTransactionChainID: 1, + DataSyncEnabled: true, + PFSEnabled: true, + } + + return nodeConfig, nil +} diff --git a/server/server.go b/server/server.go index 876c9f2aa..ad71cb94b 100644 --- a/server/server.go +++ b/server/server.go @@ -34,6 +34,18 @@ func (s *Server) getHost() string { return fmt.Sprintf("%s:%d", s.hostname, s.GetPort()) } +func (s *Server) GetHostname() string { + return s.hostname +} + +func (s *Server) GetCert() *tls.Certificate { + return s.cert +} + +func (s *Server) GetLogger() *zap.Logger { + return s.logger +} + func (s *Server) mustGetHost() string { return fmt.Sprintf("%s:%d", s.hostname, s.MustGetPort()) } diff --git a/server/server_media.go b/server/server_media.go index 8ec002be9..dc9dea09d 100644 --- a/server/server_media.go +++ b/server/server_media.go @@ -28,7 +28,7 @@ func NewMediaServer(db *sql.DB, downloader *ipfs.Downloader, multiaccountsDB *mu s := &MediaServer{ Server: NewServer( globalCertificate, - localhost, + Localhost, signal.SendMediaServerStarted, logutils.ZapLogger().Named("MediaServer"), ), diff --git a/server/server_test.go b/server/server_test.go index 4a235fff9..aee3decbe 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -30,14 +30,14 @@ func (s *ServerURLSuite) SetupTest() { s.SetupLoggerComponents() s.server = &MediaServer{Server: Server{ - hostname: defaultIP.String(), + hostname: DefaultIP.String(), portManger: newPortManager(s.Logger, nil), }} err := s.server.SetPort(1337) s.Require().NoError(err) s.serverNoPort = &MediaServer{Server: Server{ - hostname: defaultIP.String(), + hostname: DefaultIP.String(), portManger: newPortManager(s.Logger, nil), }} go func() { diff --git a/services/accounts/service.go b/services/accounts/service.go index 5b02ec36e..aba4a1e71 100644 --- a/services/accounts/service.go +++ b/services/accounts/service.go @@ -4,6 +4,7 @@ import ( "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/rpc" + "github.com/status-im/status-go/multiaccounts/settings" "github.com/status-im/status-go/server" "github.com/status-im/status-go/account" @@ -68,3 +69,12 @@ func (s *Service) APIs() []rpc.API { func (s *Service) Protocols() []p2p.Protocol { return nil } + +func (s *Service) GetAccountsByKeyUID(keyUID string) ([]*accounts.Account, error) { + + return s.db.GetAccountsByKeyUID(keyUID) +} + +func (s *Service) GetSettings() (settings.Settings, error) { + return s.db.GetSettings() +} diff --git a/services/ext/api.go b/services/ext/api.go index 01a7ed2b8..86045ad5a 100644 --- a/services/ext/api.go +++ b/services/ext/api.go @@ -872,7 +872,7 @@ func (api *PublicAPI) SendPairInstallation(ctx context.Context) (*protocol.Messe } func (api *PublicAPI) SyncDevices(ctx context.Context, name, picture string) error { - return api.service.messenger.SyncDevices(ctx, name, picture) + return api.service.messenger.SyncDevices(ctx, name, picture, nil) } func (api *PublicAPI) AddBookmark(ctx context.Context, bookmark browsers.Bookmark) error {