diff --git a/api/backend_test.go b/api/backend_test.go index 9d29f521c85..110e5db3938 100644 --- a/api/backend_test.go +++ b/api/backend_test.go @@ -6,6 +6,7 @@ import ( "database/sql" "encoding/hex" "encoding/json" + "errors" "fmt" "io/ioutil" "math/rand" @@ -17,6 +18,8 @@ import ( "testing" "time" + "github.com/status-im/status-go/protocol/tt" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -880,6 +883,107 @@ func TestLoginAccount(t *testing.T) { require.Equal(t, nameserver, b.config.WakuV2Config.Nameserver) } +func TestEnableInstallationAndPair(t *testing.T) { + // create account acc + utils.Init() + displayName := "some-display-name" + password := "some-password" + tmpdir := t.TempDir() + nameserver := "8.8.8.8" + b := NewGethStatusBackend() + createAccountRequest := &requests.CreateAccount{ + DisplayName: displayName, + CustomizationColor: "#ffffff", + Emoji: "some", + Password: password, + RootDataDir: tmpdir, + LogFilePath: tmpdir + "/log", + WakuV2Nameserver: &nameserver, + WakuV2Fleet: "status.staging", + } + acc, err := b.CreateAccountAndLogin(createAccountRequest) + require.NoError(t, err) + require.NotNil(t, acc) + _, err = b.Messenger().Start() + require.NoError(t, err) + s, err := b.GetSettings() + require.NoError(t, err) + mn := *s.Mnemonic + db, err := accounts.NewDB(b.appDB) + require.NoError(t, err) + n, err := db.GetSettingLastSynced(settings.DisplayName) + require.NoError(t, err) + require.True(t, n > 0) + + // restore account acc as acc2 use Mnemonic from acc + restoreRequest := &requests.RestoreAccount{ + Mnemonic: mn, + FetchBackup: true, + CreateAccount: requests.CreateAccount{ + Password: password, + CustomizationColor: "0x000000", + RootDataDir: t.TempDir(), + }, + } + b2 := NewGethStatusBackend() + acc2, err := b2.RestoreAccountAndLogin(restoreRequest) + require.NoError(t, err) + require.NotNil(t, acc2) + _, err = b2.Messenger().Start() + require.NoError(t, err) + s2, err := b2.GetSettings() + require.NoError(t, err) + + t.Logf("acc2 settings.name: %s", s2.Name) + // should be 3 words random name + require.Len(t, strings.Split(s2.Name, " "), 3) + require.Empty(t, acc2.Name) + require.Empty(t, s2.DisplayName) + db2, err := accounts.NewDB(b2.appDB) + require.NoError(t, err) + n, err = db2.GetSettingLastSynced(settings.DisplayName) + require.NoError(t, err) + require.True(t, n == 0) + + // pair installation + _, err = b2.Messenger().EnableInstallationAndPair(&requests.EnableInstallationAndPair{InstallationID: s.InstallationID}) + require.NoError(t, err) + // ensure acc received the installation from acc2 + err = tt.RetryWithBackOff(func() error { + r, err := b.Messenger().RetrieveAll() + require.NoError(t, err) + if len(r.Installations()) > 0 { + return nil + } + return errors.New("new installation not received yet") + }) + require.NoError(t, err) + + // sync data from acc to acc2 + err = b.Messenger().EnableAndSyncInstallation(&requests.EnableAndSyncInstallation{InstallationID: s2.InstallationID}) + require.NoError(t, err) + // ensure acc2's display name get synced + err = tt.RetryWithBackOff(func() error { + r, err := b2.Messenger().RetrieveAll() + require.NoError(t, err) + for _, ss := range r.Settings { + if ss.GetDBName() == "display_name" { + return nil + } + } + return errors.New("display name setting not received yet") + }) + require.NoError(t, err) + + // check display name for acc2 + s2, err = b2.GetSettings() + require.NoError(t, err) + require.Equal(t, displayName, s2.DisplayName) + acc2, err = b2.GetActiveAccount() + require.NoError(t, err) + require.Equal(t, displayName, acc2.Name) +} + func TestVerifyDatabasePassword(t *testing.T) { utils.Init() diff --git a/api/geth_backend.go b/api/geth_backend.go index 2179c5e39bb..f9d2ce306cd 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -2663,6 +2663,22 @@ func (b *GethStatusBackend) injectAccountsIntoWakuService(w types.WakuKeyManager return nil } +func (b *GethStatusBackend) InstallationID() string { + m := b.Messenger() + if m != nil { + return m.InstallationID() + } + return "" +} + +func (b *GethStatusBackend) KeyUID() string { + m := b.Messenger() + if m != nil { + return m.KeyUID() + } + return "" +} + func (b *GethStatusBackend) injectAccountsIntoServices() error { if b.statusNode.WakuService() != nil { return b.injectAccountsIntoWakuService(b.statusNode.WakuService(), func() *ext.Service { diff --git a/mobile/status.go b/mobile/status.go index fd986954611..2beffe27b91 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -1188,6 +1188,18 @@ func GetConnectionStringForBootstrappingAnotherDevice(configJSON string) string return cs } +type inputConnectionStringForBootstrappingResponse struct { + InstallationID string `json:"installationId"` + KeyUID string `json:"keyUID"` + Error error `json:"error"` +} + +func (i *inputConnectionStringForBootstrappingResponse) toJSON(err error) string { + i.Error = err + j, _ := json.Marshal(i) + return string(j) +} + // InputConnectionStringForBootstrapping starts a pairing.ReceiverClient // The given server.ConnectionParams string will determine the server.Mode // @@ -1202,18 +1214,30 @@ func InputConnectionStringForBootstrapping(cs, configJSON string) string { return makeJSONResponse(fmt.Errorf("no config given, ReceiverClientConfig is expected")) } + params := &pairing.ConnectionParams{} + err = params.FromString(cs) + if err != nil { + response := &inputConnectionStringForBootstrappingResponse{} + return response.toJSON(fmt.Errorf("could not parse connection string")) + } + response := &inputConnectionStringForBootstrappingResponse{ + InstallationID: params.InstallationID(), + KeyUID: params.KeyUID(), + } + err = statusBackend.LocalPairingStateManager.StartPairing(cs) defer func() { statusBackend.LocalPairingStateManager.StopPairing(cs, err) }() if err != nil { - return makeJSONResponse(err) + return response.toJSON(err) } err = pairing.StartUpReceivingClient(statusBackend, cs, configJSON) if err != nil { - return makeJSONResponse(err) + return response.toJSON(err) + } - return makeJSONResponse(statusBackend.Logout()) + return response.toJSON(statusBackend.Logout()) } // InputConnectionStringForBootstrappingAnotherDevice starts a pairing.SendingClient diff --git a/protocol/encryption/multidevice/multidevice.go b/protocol/encryption/multidevice/multidevice.go index 46037ac2d95..8c4593a5e40 100644 --- a/protocol/encryption/multidevice/multidevice.go +++ b/protocol/encryption/multidevice/multidevice.go @@ -31,6 +31,10 @@ type Installation struct { InstallationMetadata *InstallationMetadata `json:"metadata"` } +func (i *Installation) UniqueKey() string { + return i.ID + i.Identity +} + type Config struct { MaxInstallations int ProtocolVersion uint32 diff --git a/protocol/encryption/protocol.go b/protocol/encryption/protocol.go index 2d49549f65f..4a94711dbd9 100644 --- a/protocol/encryption/protocol.go +++ b/protocol/encryption/protocol.go @@ -540,6 +540,10 @@ func (p *Protocol) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle * return p.multidevice.AddInstallations(bundle.GetIdentity(), bundle.GetTimestamp(), installations, enabled) } +func (p *Protocol) AddInstallation(identity []byte, timestamp int64, installation *multidevice.Installation, enabled bool) ([]*multidevice.Installation, error) { + return p.multidevice.AddInstallations(identity, timestamp, []*multidevice.Installation{installation}, enabled) +} + func (p *Protocol) GetMultiDevice() *multidevice.Multidevice { return p.multidevice } diff --git a/protocol/messenger.go b/protocol/messenger.go index 7f7ff65bd57..f6976f6d59b 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -1730,24 +1730,6 @@ func (m *Messenger) handlePushNotificationClientRegistrations(c chan struct{}) { }() } -func (m *Messenger) InitInstallations() error { - installations, err := m.encryptor.GetOurInstallations(&m.identity.PublicKey) - if err != nil { - return err - } - - for _, installation := range installations { - m.allInstallations.Store(installation.ID, installation) - } - - err = m.setInstallationHostname() - if err != nil { - return err - } - - return nil -} - // InitFilters analyzes chats and contacts in order to setup filters // which are responsible for retrieving messages. func (m *Messenger) InitFilters() error { @@ -1996,74 +1978,6 @@ func (m *Messenger) Shutdown() (err error) { return } -func (m *Messenger) EnableInstallation(id string) error { - installation, ok := m.allInstallations.Load(id) - if !ok { - return errors.New("no installation found") - } - - err := m.encryptor.EnableInstallation(&m.identity.PublicKey, id) - if err != nil { - return err - } - installation.Enabled = true - // TODO(samyoul) remove storing of an updated reference pointer? - m.allInstallations.Store(id, installation) - return nil -} - -func (m *Messenger) DisableInstallation(id string) error { - installation, ok := m.allInstallations.Load(id) - if !ok { - return errors.New("no installation found") - } - - err := m.encryptor.DisableInstallation(&m.identity.PublicKey, id) - if err != nil { - return err - } - installation.Enabled = false - // TODO(samyoul) remove storing of an updated reference pointer? - m.allInstallations.Store(id, installation) - return nil -} - -func (m *Messenger) Installations() []*multidevice.Installation { - installations := make([]*multidevice.Installation, m.allInstallations.Len()) - - var i = 0 - m.allInstallations.Range(func(installationID string, installation *multidevice.Installation) (shouldContinue bool) { - installations[i] = installation - i++ - return true - }) - return installations -} - -func (m *Messenger) setInstallationMetadata(id string, data *multidevice.InstallationMetadata) error { - installation, ok := m.allInstallations.Load(id) - if !ok { - return errors.New("no installation found") - } - - installation.InstallationMetadata = data - return m.encryptor.SetInstallationMetadata(m.IdentityPublicKey(), id, data) -} - -func (m *Messenger) SetInstallationMetadata(id string, data *multidevice.InstallationMetadata) error { - return m.setInstallationMetadata(id, data) -} - -func (m *Messenger) SetInstallationName(id string, name string) error { - installation, ok := m.allInstallations.Load(id) - if !ok { - return errors.New("no installation found") - } - - installation.InstallationMetadata.Name = name - return m.encryptor.SetInstallationName(m.IdentityPublicKey(), id, name) -} - // NOT IMPLEMENTED func (m *Messenger) SelectMailserver(id string) error { return ErrNotImplemented @@ -2640,348 +2554,12 @@ func (m *Messenger) ShareImageMessage(request *requests.ShareImageMessage) (*Mes return response, nil } -func (m *Messenger) syncProfilePicturesFromDatabase(rawMessageHandler RawMessageHandler) error { - keyUID := m.account.KeyUID - identityImages, err := m.multiAccounts.GetIdentityImages(keyUID) - if err != nil { - return err - } - return m.syncProfilePictures(rawMessageHandler, identityImages) -} - -func (m *Messenger) syncProfilePictures(rawMessageHandler RawMessageHandler, identityImages []*images.IdentityImage) error { - if !m.hasPairedDevices() { - return nil - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - pictures := make([]*protobuf.SyncProfilePicture, len(identityImages)) - clock, chat := m.getLastClockWithRelatedChat() - for i, image := range identityImages { - p := &protobuf.SyncProfilePicture{} - p.Name = image.Name - p.Payload = image.Payload - p.Width = uint32(image.Width) - p.Height = uint32(image.Height) - p.FileSize = uint32(image.FileSize) - p.ResizeTarget = uint32(image.ResizeTarget) - if image.Clock == 0 { - p.Clock = clock - } else { - p.Clock = image.Clock - } - pictures[i] = p - } - - message := &protobuf.SyncProfilePictures{} - message.KeyUid = m.account.KeyUID - message.Pictures = pictures - - encodedMessage, err := proto.Marshal(message) - if err != nil { - return err - } - - rawMessage := common.RawMessage{ - LocalChatID: chat.ID, - Payload: encodedMessage, - MessageType: protobuf.ApplicationMetadataMessage_SYNC_PROFILE_PICTURES, - ResendType: common.ResendTypeDataSync, - } - - _, err = rawMessageHandler(ctx, rawMessage) - if err != nil { - return err - } - - chat.LastClockValue = clock - return m.saveChat(chat) -} - -// 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, rawMessageHandler RawMessageHandler) (err error) { - if rawMessageHandler == nil { - rawMessageHandler = m.dispatchMessage - } - - myID := contactIDFromPublicKey(&m.identity.PublicKey) - - displayName, err := m.settings.DisplayName() - if err != nil { - return err - } - - if _, err = m.sendContactUpdate(ctx, myID, displayName, ensName, photoPath, m.account.GetCustomizationColor(), rawMessageHandler); err != nil { - return err - } - - m.allChats.Range(func(chatID string, chat *Chat) bool { - if !chat.shouldBeSynced() { - return true - - } - err = m.syncChat(ctx, chat, rawMessageHandler) - return err == nil - }) - if err != nil { - return err - } - - m.allContacts.Range(func(contactID string, contact *Contact) bool { - if contact.ID == myID { - return true - } - if contact.LocalNickname != "" || contact.added() || contact.hasAddedUs() || contact.Blocked { - if err = m.syncContact(ctx, contact, rawMessageHandler); err != nil { - return false - } - } - return true - }) - - cs, err := m.communitiesManager.JoinedAndPendingCommunitiesWithRequests() - if err != nil { - return err - } - for _, c := range cs { - if err = m.syncCommunity(ctx, c, rawMessageHandler); err != nil { - return err - } - } - - bookmarks, err := m.browserDatabase.GetBookmarks() - if err != nil { - return err - } - for _, b := range bookmarks { - if err = m.SyncBookmark(ctx, b, rawMessageHandler); err != nil { - return err - } - } - - trustedUsers, err := m.verificationDatabase.GetAllTrustStatus() - if err != nil { - return err - } - for id, ts := range trustedUsers { - if err = m.SyncTrustedUser(ctx, id, ts, rawMessageHandler); err != nil { - return err - } - } - - verificationRequests, err := m.verificationDatabase.GetVerificationRequests() - if err != nil { - return err - } - for i := range verificationRequests { - if err = m.SyncVerificationRequest(ctx, &verificationRequests[i], rawMessageHandler); err != nil { - return err - } - } - - err = m.syncSettings(rawMessageHandler) - if err != nil { - return err - } - - err = m.syncProfilePicturesFromDatabase(rawMessageHandler) - if err != nil { - return err - } - - if err = m.syncLatestContactRequests(ctx, rawMessageHandler); err != nil { - return err - } - - // we have to sync deleted keypairs as well - keypairs, err := m.settings.GetAllKeypairs() - if err != nil { - return err - } - - for _, kp := range keypairs { - err = m.syncKeypair(kp, rawMessageHandler) - if err != nil { - return err - } - } - - // we have to sync deleted watch only accounts as well - woAccounts, err := m.settings.GetAllWatchOnlyAccounts() - if err != nil { - return err - } - - for _, woAcc := range woAccounts { - err = m.syncWalletAccount(woAcc, rawMessageHandler) - if err != nil { - return err - } - } - - savedAddresses, err := m.savedAddressesManager.GetRawSavedAddresses() - if err != nil { - return err - } - - for i := range savedAddresses { - sa := savedAddresses[i] - - err = m.syncSavedAddress(ctx, sa, rawMessageHandler) - if err != nil { - return err - } - } - - if err = m.syncEnsUsernameDetails(ctx, rawMessageHandler); err != nil { - return err - } - - if err = m.syncDeleteForMeMessage(ctx, rawMessageHandler); err != nil { - return err - } - - err = m.syncAccountsPositions(rawMessageHandler) - if err != nil { - return err - } - - err = m.syncProfileShowcasePreferences(context.Background(), rawMessageHandler) - if err != nil { - return err - } - - return nil -} - -func (m *Messenger) syncLatestContactRequests(ctx context.Context, rawMessageHandler RawMessageHandler) error { - latestContactRequests, err := m.persistence.LatestContactRequests() - - if err != nil { - return err - } - - for _, r := range latestContactRequests { - if r.ContactRequestState == common.ContactRequestStateAccepted || r.ContactRequestState == common.ContactRequestStateDismissed { - accepted := r.ContactRequestState == common.ContactRequestStateAccepted - err = m.syncContactRequestDecision(ctx, r.MessageID, r.ContactID, accepted, rawMessageHandler) - if err != nil { - return err - } - } - } - return nil +func (m *Messenger) InstallationID() string { + return m.installationID } -func (m *Messenger) syncContactRequestDecision(ctx context.Context, requestID, contactId string, accepted bool, rawMessageHandler RawMessageHandler) error { - m.logger.Info("syncContactRequestDecision", zap.Any("from", requestID)) - if !m.hasPairedDevices() { - return nil - } - - clock, chat := m.getLastClockWithRelatedChat() - - var status protobuf.SyncContactRequestDecision_DecisionStatus - if accepted { - status = protobuf.SyncContactRequestDecision_ACCEPTED - } else { - status = protobuf.SyncContactRequestDecision_DECLINED - } - - message := &protobuf.SyncContactRequestDecision{ - RequestId: requestID, - ContactId: contactId, - Clock: clock, - DecisionStatus: status, - } - - encodedMessage, err := proto.Marshal(message) - if err != nil { - return err - } - - rawMessage := common.RawMessage{ - LocalChatID: chat.ID, - Payload: encodedMessage, - MessageType: protobuf.ApplicationMetadataMessage_SYNC_CONTACT_REQUEST_DECISION, - ResendType: common.ResendTypeDataSync, - } - - _, err = rawMessageHandler(ctx, rawMessage) - if err != nil { - return err - } - - return nil -} - -func (m *Messenger) getLastClockWithRelatedChat() (uint64, *Chat) { - chatID := contactIDFromPublicKey(&m.identity.PublicKey) - - chat, ok := m.allChats.Load(chatID) - if !ok { - chat = OneToOneFromPublicKey(&m.identity.PublicKey, m.getTimesource()) - // We don't want to show the chat to the user - chat.Active = false - } - - m.allChats.Store(chat.ID, chat) - clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) - - return clock, chat -} - -// SendPairInstallation sends a pair installation message -func (m *Messenger) SendPairInstallation(ctx context.Context, rawMessageHandler RawMessageHandler) (*MessengerResponse, error) { - var err error - var response MessengerResponse - - installation, ok := m.allInstallations.Load(m.installationID) - if !ok { - return nil, errors.New("no installation found") - } - - if installation.InstallationMetadata == nil { - return nil, errors.New("no installation metadata") - } - - clock, chat := m.getLastClockWithRelatedChat() - - pairMessage := &protobuf.SyncPairInstallation{ - Clock: clock, - Name: installation.InstallationMetadata.Name, - InstallationId: installation.ID, - DeviceType: installation.InstallationMetadata.DeviceType, - Version: installation.Version} - encodedMessage, err := proto.Marshal(pairMessage) - if err != nil { - return nil, err - } - - if rawMessageHandler == nil { - rawMessageHandler = m.dispatchPairInstallationMessage - } - _, err = rawMessageHandler(ctx, common.RawMessage{ - LocalChatID: chat.ID, - Payload: encodedMessage, - MessageType: protobuf.ApplicationMetadataMessage_SYNC_PAIR_INSTALLATION, - ResendType: common.ResendTypeDataSync, - }) - if err != nil { - return nil, err - } - - response.AddChat(chat) - - chat.LastClockValue = clock - err = m.saveChat(chat) - if err != nil { - return nil, err - } - return &response, nil +func (m *Messenger) KeyUID() string { + return m.account.KeyUID } // syncChat sync a chat with paired devices @@ -4077,7 +3655,7 @@ func (m *Messenger) saveDataAndPrepareResponse(messageState *ReceivedMessageStat messageState.ModifiedInstallations.Range(func(id string, value bool) (shouldContinue bool) { installation, _ := messageState.AllInstallations.Load(id) - messageState.Response.Installations = append(messageState.Response.Installations, installation) + messageState.Response.AddInstallation(installation) if installation.InstallationMetadata != nil { err = m.setInstallationMetadata(id, installation.InstallationMetadata) if err != nil { diff --git a/protocol/messenger_delete_message_for_me_test.go b/protocol/messenger_delete_message_for_me_test.go index 829ce793276..77288316ad1 100644 --- a/protocol/messenger_delete_message_for_me_test.go +++ b/protocol/messenger_delete_message_for_me_test.go @@ -90,12 +90,12 @@ func (s *MessengerDeleteMessageForMeSuite) Pair() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.alice1, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(s.alice2.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("alice2", actualInstallation.InstallationMetadata.Name) diff --git a/protocol/messenger_identity_display_name_test.go b/protocol/messenger_identity_display_name_test.go index 25a6874340e..b2048b12714 100644 --- a/protocol/messenger_identity_display_name_test.go +++ b/protocol/messenger_identity_display_name_test.go @@ -122,12 +122,12 @@ func (s *MessengerProfileDisplayNameHandlerSuite) TestDisplayNameSync() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.m, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(alicesOtherDevice.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("alice's-other-device", actualInstallation.InstallationMetadata.Name) diff --git a/protocol/messenger_installations_test.go b/protocol/messenger_installations_test.go index b222da29325..48eb44cf942 100644 --- a/protocol/messenger_installations_test.go +++ b/protocol/messenger_installations_test.go @@ -52,12 +52,12 @@ func (s *MessengerInstallationSuite) TestReceiveInstallation() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.m, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(theirMessenger.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("their-name", actualInstallation.InstallationMetadata.Name) @@ -251,12 +251,12 @@ func (s *MessengerInstallationSuite) TestSyncInstallation() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.m, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(theirMessenger.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("their-name", actualInstallation.InstallationMetadata.Name) @@ -375,12 +375,12 @@ func (s *MessengerInstallationSuite) TestSyncInstallationNewMessages() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( bob1, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(bob2.installationID, actualInstallation.ID) err = bob1.EnableInstallation(bob2.installationID) s.Require().NoError(err) diff --git a/protocol/messenger_pairing_and_syncing.go b/protocol/messenger_pairing_and_syncing.go new file mode 100644 index 00000000000..e071f559164 --- /dev/null +++ b/protocol/messenger_pairing_and_syncing.go @@ -0,0 +1,487 @@ +package protocol + +import ( + "context" + "errors" + "time" + + "github.com/golang/protobuf/proto" + "go.uber.org/zap" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/images" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/encryption/multidevice" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/protocol/requests" +) + +func (m *Messenger) EnableAndSyncInstallation(request *requests.EnableAndSyncInstallation) error { + if err := request.Validate(); err != nil { + return err + } + err := m.EnableInstallation(request.InstallationID) + if err != nil { + return err + } + return m.SyncDevices(context.Background(), "", "", nil) +} + +func (m *Messenger) EnableInstallationAndPair(request *requests.EnableInstallationAndPair) (*MessengerResponse, error) { + if err := request.Validate(); err != nil { + return nil, err + } + + myIdentity := crypto.CompressPubkey(&m.identity.PublicKey) + timestamp := time.Now().UnixNano() + + installation := &multidevice.Installation{ + ID: request.InstallationID, + Enabled: true, + Version: 2, + Timestamp: timestamp, + } + + _, err := m.encryptor.AddInstallation(myIdentity, timestamp, installation, true) + if err != nil { + return nil, err + } + i, ok := m.allInstallations.Load(request.InstallationID) + if !ok { + i = installation + } else { + i.Enabled = true + } + m.allInstallations.Store(request.InstallationID, i) + return m.SendPairInstallation(context.Background(), nil) +} + +// SendPairInstallation sends a pair installation message +func (m *Messenger) SendPairInstallation(ctx context.Context, rawMessageHandler RawMessageHandler) (*MessengerResponse, error) { + var err error + var response MessengerResponse + + installation, ok := m.allInstallations.Load(m.installationID) + if !ok { + return nil, errors.New("no installation found") + } + + if installation.InstallationMetadata == nil { + return nil, errors.New("no installation metadata") + } + + clock, chat := m.getLastClockWithRelatedChat() + + pairMessage := &protobuf.SyncPairInstallation{ + Clock: clock, + Name: installation.InstallationMetadata.Name, + InstallationId: installation.ID, + DeviceType: installation.InstallationMetadata.DeviceType, + Version: installation.Version} + encodedMessage, err := proto.Marshal(pairMessage) + if err != nil { + return nil, err + } + + if rawMessageHandler == nil { + rawMessageHandler = m.dispatchPairInstallationMessage + } + _, err = rawMessageHandler(ctx, common.RawMessage{ + LocalChatID: chat.ID, + Payload: encodedMessage, + MessageType: protobuf.ApplicationMetadataMessage_SYNC_PAIR_INSTALLATION, + ResendType: common.ResendTypeDataSync, + }) + if err != nil { + return nil, err + } + + response.AddChat(chat) + + chat.LastClockValue = clock + err = m.saveChat(chat) + if err != nil { + return nil, err + } + return &response, nil +} + +// 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, rawMessageHandler RawMessageHandler) (err error) { + if rawMessageHandler == nil { + rawMessageHandler = m.dispatchMessage + } + + myID := contactIDFromPublicKey(&m.identity.PublicKey) + + displayName, err := m.settings.DisplayName() + if err != nil { + return err + } + + if _, err = m.sendContactUpdate(ctx, myID, displayName, ensName, photoPath, m.account.GetCustomizationColor(), rawMessageHandler); err != nil { + return err + } + + m.allChats.Range(func(chatID string, chat *Chat) bool { + if !chat.shouldBeSynced() { + return true + + } + err = m.syncChat(ctx, chat, rawMessageHandler) + return err == nil + }) + if err != nil { + return err + } + + m.allContacts.Range(func(contactID string, contact *Contact) bool { + if contact.ID == myID { + return true + } + if contact.LocalNickname != "" || contact.added() || contact.hasAddedUs() || contact.Blocked { + if err = m.syncContact(ctx, contact, rawMessageHandler); err != nil { + return false + } + } + return true + }) + + cs, err := m.communitiesManager.JoinedAndPendingCommunitiesWithRequests() + if err != nil { + return err + } + for _, c := range cs { + if err = m.syncCommunity(ctx, c, rawMessageHandler); err != nil { + return err + } + } + + bookmarks, err := m.browserDatabase.GetBookmarks() + if err != nil { + return err + } + for _, b := range bookmarks { + if err = m.SyncBookmark(ctx, b, rawMessageHandler); err != nil { + return err + } + } + + trustedUsers, err := m.verificationDatabase.GetAllTrustStatus() + if err != nil { + return err + } + for id, ts := range trustedUsers { + if err = m.SyncTrustedUser(ctx, id, ts, rawMessageHandler); err != nil { + return err + } + } + + verificationRequests, err := m.verificationDatabase.GetVerificationRequests() + if err != nil { + return err + } + for i := range verificationRequests { + if err = m.SyncVerificationRequest(ctx, &verificationRequests[i], rawMessageHandler); err != nil { + return err + } + } + + err = m.syncSettings(rawMessageHandler) + if err != nil { + return err + } + + err = m.syncProfilePicturesFromDatabase(rawMessageHandler) + if err != nil { + return err + } + + if err = m.syncLatestContactRequests(ctx, rawMessageHandler); err != nil { + return err + } + + // we have to sync deleted keypairs as well + keypairs, err := m.settings.GetAllKeypairs() + if err != nil { + return err + } + + for _, kp := range keypairs { + err = m.syncKeypair(kp, rawMessageHandler) + if err != nil { + return err + } + } + + // we have to sync deleted watch only accounts as well + woAccounts, err := m.settings.GetAllWatchOnlyAccounts() + if err != nil { + return err + } + + for _, woAcc := range woAccounts { + err = m.syncWalletAccount(woAcc, rawMessageHandler) + if err != nil { + return err + } + } + + savedAddresses, err := m.savedAddressesManager.GetRawSavedAddresses() + if err != nil { + return err + } + + for i := range savedAddresses { + sa := savedAddresses[i] + + err = m.syncSavedAddress(ctx, sa, rawMessageHandler) + if err != nil { + return err + } + } + + if err = m.syncEnsUsernameDetails(ctx, rawMessageHandler); err != nil { + return err + } + + if err = m.syncDeleteForMeMessage(ctx, rawMessageHandler); err != nil { + return err + } + + err = m.syncAccountsPositions(rawMessageHandler) + if err != nil { + return err + } + + err = m.syncProfileShowcasePreferences(context.Background(), rawMessageHandler) + if err != nil { + return err + } + + return nil +} + +func (m *Messenger) syncProfilePictures(rawMessageHandler RawMessageHandler, identityImages []*images.IdentityImage) error { + if !m.hasPairedDevices() { + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + pictures := make([]*protobuf.SyncProfilePicture, len(identityImages)) + clock, chat := m.getLastClockWithRelatedChat() + for i, image := range identityImages { + p := &protobuf.SyncProfilePicture{} + p.Name = image.Name + p.Payload = image.Payload + p.Width = uint32(image.Width) + p.Height = uint32(image.Height) + p.FileSize = uint32(image.FileSize) + p.ResizeTarget = uint32(image.ResizeTarget) + if image.Clock == 0 { + p.Clock = clock + } else { + p.Clock = image.Clock + } + pictures[i] = p + } + + message := &protobuf.SyncProfilePictures{} + message.KeyUid = m.account.KeyUID + message.Pictures = pictures + + encodedMessage, err := proto.Marshal(message) + if err != nil { + return err + } + + rawMessage := common.RawMessage{ + LocalChatID: chat.ID, + Payload: encodedMessage, + MessageType: protobuf.ApplicationMetadataMessage_SYNC_PROFILE_PICTURES, + ResendType: common.ResendTypeDataSync, + } + + _, err = rawMessageHandler(ctx, rawMessage) + if err != nil { + return err + } + + chat.LastClockValue = clock + return m.saveChat(chat) +} + +func (m *Messenger) syncLatestContactRequests(ctx context.Context, rawMessageHandler RawMessageHandler) error { + latestContactRequests, err := m.persistence.LatestContactRequests() + + if err != nil { + return err + } + + for _, r := range latestContactRequests { + if r.ContactRequestState == common.ContactRequestStateAccepted || r.ContactRequestState == common.ContactRequestStateDismissed { + accepted := r.ContactRequestState == common.ContactRequestStateAccepted + err = m.syncContactRequestDecision(ctx, r.MessageID, r.ContactID, accepted, rawMessageHandler) + if err != nil { + return err + } + } + } + return nil +} + +func (m *Messenger) syncContactRequestDecision(ctx context.Context, requestID, contactId string, accepted bool, rawMessageHandler RawMessageHandler) error { + m.logger.Info("syncContactRequestDecision", zap.Any("from", requestID)) + if !m.hasPairedDevices() { + return nil + } + + clock, chat := m.getLastClockWithRelatedChat() + + var status protobuf.SyncContactRequestDecision_DecisionStatus + if accepted { + status = protobuf.SyncContactRequestDecision_ACCEPTED + } else { + status = protobuf.SyncContactRequestDecision_DECLINED + } + + message := &protobuf.SyncContactRequestDecision{ + RequestId: requestID, + ContactId: contactId, + Clock: clock, + DecisionStatus: status, + } + + encodedMessage, err := proto.Marshal(message) + if err != nil { + return err + } + + rawMessage := common.RawMessage{ + LocalChatID: chat.ID, + Payload: encodedMessage, + MessageType: protobuf.ApplicationMetadataMessage_SYNC_CONTACT_REQUEST_DECISION, + ResendType: common.ResendTypeDataSync, + } + + _, err = rawMessageHandler(ctx, rawMessage) + if err != nil { + return err + } + + return nil +} + +func (m *Messenger) getLastClockWithRelatedChat() (uint64, *Chat) { + chatID := contactIDFromPublicKey(&m.identity.PublicKey) + + chat, ok := m.allChats.Load(chatID) + if !ok { + chat = OneToOneFromPublicKey(&m.identity.PublicKey, m.getTimesource()) + // We don't want to show the chat to the user + chat.Active = false + } + + m.allChats.Store(chat.ID, chat) + clock, _ := chat.NextClockAndTimestamp(m.getTimesource()) + + return clock, chat +} + +func (m *Messenger) syncProfilePicturesFromDatabase(rawMessageHandler RawMessageHandler) error { + keyUID := m.account.KeyUID + identityImages, err := m.multiAccounts.GetIdentityImages(keyUID) + if err != nil { + return err + } + return m.syncProfilePictures(rawMessageHandler, identityImages) +} + +func (m *Messenger) InitInstallations() error { + installations, err := m.encryptor.GetOurInstallations(&m.identity.PublicKey) + if err != nil { + return err + } + + for _, installation := range installations { + m.allInstallations.Store(installation.ID, installation) + } + + err = m.setInstallationHostname() + if err != nil { + return err + } + + return nil +} + +func (m *Messenger) Installations() []*multidevice.Installation { + installations := make([]*multidevice.Installation, m.allInstallations.Len()) + + var i = 0 + m.allInstallations.Range(func(installationID string, installation *multidevice.Installation) (shouldContinue bool) { + installations[i] = installation + i++ + return true + }) + return installations +} + +func (m *Messenger) setInstallationMetadata(id string, data *multidevice.InstallationMetadata) error { + installation, ok := m.allInstallations.Load(id) + if !ok { + return errors.New("no installation found") + } + + installation.InstallationMetadata = data + return m.encryptor.SetInstallationMetadata(m.IdentityPublicKey(), id, data) +} + +func (m *Messenger) SetInstallationMetadata(id string, data *multidevice.InstallationMetadata) error { + return m.setInstallationMetadata(id, data) +} + +func (m *Messenger) SetInstallationName(id string, name string) error { + installation, ok := m.allInstallations.Load(id) + if !ok { + return errors.New("no installation found") + } + + installation.InstallationMetadata.Name = name + return m.encryptor.SetInstallationName(m.IdentityPublicKey(), id, name) +} + +func (m *Messenger) EnableInstallation(id string) error { + installation, ok := m.allInstallations.Load(id) + if !ok { + return errors.New("no installation found") + } + + err := m.encryptor.EnableInstallation(&m.identity.PublicKey, id) + if err != nil { + return err + } + installation.Enabled = true + // TODO(samyoul) remove storing of an updated reference pointer? + m.allInstallations.Store(id, installation) + return nil +} + +func (m *Messenger) DisableInstallation(id string) error { + installation, ok := m.allInstallations.Load(id) + if !ok { + return errors.New("no installation found") + } + + err := m.encryptor.DisableInstallation(&m.identity.PublicKey, id) + if err != nil { + return err + } + installation.Enabled = false + // TODO(samyoul) remove storing of an updated reference pointer? + m.allInstallations.Store(id, installation) + return nil +} diff --git a/protocol/messenger_pairing_and_syncing_test.go b/protocol/messenger_pairing_and_syncing_test.go new file mode 100644 index 00000000000..21bc971400c --- /dev/null +++ b/protocol/messenger_pairing_and_syncing_test.go @@ -0,0 +1,88 @@ +package protocol + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/suite" + + "github.com/status-im/status-go/protocol/encryption/multidevice" + "github.com/status-im/status-go/protocol/requests" +) + +func TestMessengerPairingTest(t *testing.T) { + suite.Run(t, new(MessengerPairingSuite)) +} + +type MessengerPairingSuite struct { + MessengerBaseTestSuite +} + +func (s *MessengerPairingSuite) TestEnableNonExistingInstallation() { + installationID := uuid.New().String() + _, err := s.m.EnableInstallationAndPair(&requests.EnableInstallationAndPair{InstallationID: installationID}) + s.Require().NoError(err) + + installations := s.m.Installations() + s.Require().NoError(err) + + s.Require().Len(installations, 2) + var theirInstallation *multidevice.Installation + for _, i := range installations { + if i.ID == installationID { + theirInstallation = i + break + } else { + s.Require().NotNil(i.InstallationMetadata) + } + } + s.Require().NotNil(theirInstallation) + s.Require().True(theirInstallation.Enabled) + + installationsFromDB, err := s.m.encryptor.GetOurActiveInstallations(&s.m.identity.PublicKey) + s.Require().NoError(err) + s.Require().Len(installationsFromDB, 2) + for _, i := range installationsFromDB { + s.Require().True(i.Enabled) + if i.ID == installationID { + theirInstallation = i + break + } + } + s.Require().NotNil(theirInstallation) + s.Require().True(theirInstallation.Enabled) + +} + +func (s *MessengerPairingSuite) TestMessengerPairAfterSeedPhrase() { + // assuming alice2 want to sync with alice1 + // alice1 generated the connection string for bootstraping alice2 + // alice2 failed to connect to alice1 and restored from seed phrase + // alice2 get the installationID1 from alice1 via parsing the connection string + alice1 := s.m + alice2, err := newMessengerWithKey(s.shh, s.privateKey, s.logger, nil) + s.Require().NoError(err) + defer TearDownMessenger(&s.Suite, alice2) + installationID1 := alice1.installationID + installationID2 := alice2.installationID + s.Require().NotEqual(installationID1, installationID2) + _, err = alice2.EnableInstallationAndPair(&requests.EnableInstallationAndPair{InstallationID: installationID1}) + s.Require().NoError(err) + + // alice1 should get the installationID1 from alice2 + _, err = WaitOnMessengerResponse( + alice2, + func(r *MessengerResponse) bool { + for _, i := range r.Installations() { + if i.ID == installationID2 { + return true + } + } + return false + }, + "no messages", + ) + + s.Require().NoError(err) + +} diff --git a/protocol/messenger_response.go b/protocol/messenger_response.go index 583b7e85e0c..3bed555cccc 100644 --- a/protocol/messenger_response.go +++ b/protocol/messenger_response.go @@ -46,7 +46,6 @@ type SeenUnseenMessages struct { type MessengerResponse struct { Contacts []*Contact - Installations []*multidevice.Installation Invitations []*GroupChatInvitation CommunityChanges []*communities.CommunityChanges AnonymousMetrics []*appmetrics.AppMetric @@ -68,6 +67,7 @@ type MessengerResponse struct { // notifications a list of notifications derived from messenger events // that are useful to notify the user about + installations map[string]*multidevice.Installation notifications map[string]*localnotifications.Notification requestsToJoinCommunity map[string]*communities.RequestToJoin chats map[string]*Chat @@ -143,7 +143,7 @@ func (r *MessengerResponse) MarshalJSON() ([]byte, error) { SeenAndUnseenMessages []*SeenUnseenMessages `json:"seenAndUnseenMessages,omitempty"` }{ Contacts: r.Contacts, - Installations: r.Installations, + Installations: r.Installations(), Invitations: r.Invitations, CommunityChanges: r.CommunityChanges, RequestsToJoinCommunity: r.RequestsToJoinCommunity(), @@ -196,6 +196,14 @@ func (r *MessengerResponse) Chats() []*Chat { return chats } +func (r *MessengerResponse) Installations() []*multidevice.Installation { + var is []*multidevice.Installation + for _, i := range r.installations { + is = append(is, i) + } + return is +} + func (r *MessengerResponse) RemovedChats() []string { var chats []string for chatID := range r.removedChats { @@ -301,7 +309,7 @@ func (r *MessengerResponse) IsEmpty() bool { len(r.Bookmarks)+ len(r.clearedHistories)+ len(r.Settings)+ - len(r.Installations)+ + len(r.installations)+ len(r.Invitations)+ len(r.emojiReactions)+ len(r.communities)+ @@ -358,7 +366,7 @@ func (r *MessengerResponse) Merge(response *MessengerResponse) error { r.AddActivityCenterNotifications(response.ActivityCenterNotifications()) r.SetActivityCenterState(response.ActivityCenterState()) r.AddEmojiReactions(response.EmojiReactions()) - r.AddInstallations(response.Installations) + r.AddInstallations(response.Installations()) r.AddSavedAddresses(response.SavedAddresses()) r.AddEnsUsernameDetails(response.EnsUsernameDetails()) r.AddRequestsToJoinCommunity(response.RequestsToJoinCommunity()) @@ -795,15 +803,10 @@ func (r *MessengerResponse) AddContacts(contacts []*Contact) { } func (r *MessengerResponse) AddInstallation(i *multidevice.Installation) { - - for idx, i1 := range r.Installations { - if i1.ID == i.ID { - r.Installations[idx] = i - return - } + if len(r.installations) == 0 { + r.installations = make(map[string]*multidevice.Installation) } - - r.Installations = append(r.Installations, i) + r.installations[i.UniqueKey()] = i } func (r *MessengerResponse) AddInstallations(installations []*multidevice.Installation) { diff --git a/protocol/messenger_sync_bookmark_test.go b/protocol/messenger_sync_bookmark_test.go index 10646c59397..f9e7e93a84d 100644 --- a/protocol/messenger_sync_bookmark_test.go +++ b/protocol/messenger_sync_bookmark_test.go @@ -49,12 +49,12 @@ func (s *MessengerSyncBookmarkSuite) TestSyncBookmark() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.m, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(theirMessenger.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("their-name", actualInstallation.InstallationMetadata.Name) diff --git a/protocol/messenger_sync_chat_test.go b/protocol/messenger_sync_chat_test.go index 9a99ad630a6..af4d913ce93 100644 --- a/protocol/messenger_sync_chat_test.go +++ b/protocol/messenger_sync_chat_test.go @@ -91,12 +91,12 @@ func (s *MessengerSyncChatSuite) Pair() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.alice1, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(s.alice2.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("alice2", actualInstallation.InstallationMetadata.Name) diff --git a/protocol/messenger_sync_clear_history_test.go b/protocol/messenger_sync_clear_history_test.go index 93e9cbc1a72..691630fcf03 100644 --- a/protocol/messenger_sync_clear_history_test.go +++ b/protocol/messenger_sync_clear_history_test.go @@ -38,12 +38,12 @@ func (s *MessengerSyncClearHistory) pair() *Messenger { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.m, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(theirMessenger.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("their-name", actualInstallation.InstallationMetadata.Name) diff --git a/protocol/messenger_sync_keycard_change_test.go b/protocol/messenger_sync_keycard_change_test.go index c901ba1dbbe..324e7dc0312 100644 --- a/protocol/messenger_sync_keycard_change_test.go +++ b/protocol/messenger_sync_keycard_change_test.go @@ -65,7 +65,7 @@ func (s *MessengerSyncKeycardChangeSuite) SetupTest() { // Wait for the message to reach its destination _, err = WaitOnMessengerResponse( s.main, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) diff --git a/protocol/messenger_sync_keycards_state_test.go b/protocol/messenger_sync_keycards_state_test.go index fa34ea840e0..689fae29d0d 100644 --- a/protocol/messenger_sync_keycards_state_test.go +++ b/protocol/messenger_sync_keycards_state_test.go @@ -65,7 +65,7 @@ func (s *MessengerSyncKeycardsStateSuite) SetupTest() { // Wait for the message to reach its destination _, err = WaitOnMessengerResponse( s.main, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) diff --git a/protocol/messenger_sync_profile_picture_test.go b/protocol/messenger_sync_profile_picture_test.go index f3b952f2fed..96023738d3b 100644 --- a/protocol/messenger_sync_profile_picture_test.go +++ b/protocol/messenger_sync_profile_picture_test.go @@ -44,12 +44,12 @@ func (s *MessengerSyncProfilePictureSuite) TestSyncProfilePicture() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.m, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(theirMessenger.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("their-name", actualInstallation.InstallationMetadata.Name) diff --git a/protocol/messenger_sync_saved_addresses_test.go b/protocol/messenger_sync_saved_addresses_test.go index c2177118cbb..53f7d35ea54 100644 --- a/protocol/messenger_sync_saved_addresses_test.go +++ b/protocol/messenger_sync_saved_addresses_test.go @@ -69,7 +69,7 @@ func (s *MessengerSyncSavedAddressesSuite) SetupTest() { // Wait for the message to reach its destination _, err = WaitOnMessengerResponse( s.main, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) diff --git a/protocol/messenger_sync_verification_test.go b/protocol/messenger_sync_verification_test.go index a2beee706c8..c6978aa803f 100644 --- a/protocol/messenger_sync_verification_test.go +++ b/protocol/messenger_sync_verification_test.go @@ -52,12 +52,12 @@ func (s *MessengerSyncVerificationRequests) TestSyncVerificationRequests() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.m, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(theirMessenger.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("their-name", actualInstallation.InstallationMetadata.Name) @@ -116,12 +116,12 @@ func (s *MessengerSyncVerificationRequests) TestSyncTrust() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.m, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(theirMessenger.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("their-name", actualInstallation.InstallationMetadata.Name) diff --git a/protocol/messenger_sync_wallets_test.go b/protocol/messenger_sync_wallets_test.go index c8749fc5a8a..50cfef6f5a1 100644 --- a/protocol/messenger_sync_wallets_test.go +++ b/protocol/messenger_sync_wallets_test.go @@ -110,12 +110,12 @@ func (s *MessengerSyncWalletSuite) TestSyncWallets() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.m, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(alicesOtherDevice.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("alice's-other-device", actualInstallation.InstallationMetadata.Name) @@ -330,12 +330,12 @@ func (s *MessengerSyncWalletSuite) TestSyncWalletAccountsReorder() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.m, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(alicesOtherDevice.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("alice's-other-device", actualInstallation.InstallationMetadata.Name) @@ -520,12 +520,12 @@ func (s *MessengerSyncWalletSuite) TestSyncWalletAccountOrderAfterDeletion() { // Wait for the message to reach its destination response, err = WaitOnMessengerResponse( s.m, - func(r *MessengerResponse) bool { return len(r.Installations) > 0 }, + func(r *MessengerResponse) bool { return len(r.Installations()) > 0 }, "installation not received", ) s.Require().NoError(err) - actualInstallation := response.Installations[0] + actualInstallation := response.Installations()[0] s.Require().Equal(alicesOtherDevice.installationID, actualInstallation.ID) s.Require().NotNil(actualInstallation.InstallationMetadata) s.Require().Equal("alice's-other-device", actualInstallation.InstallationMetadata.Name) diff --git a/protocol/messenger_testing_utils.go b/protocol/messenger_testing_utils.go index f04d182a9d7..bd482fe763e 100644 --- a/protocol/messenger_testing_utils.go +++ b/protocol/messenger_testing_utils.go @@ -287,7 +287,7 @@ func PairDevices(s *suite.Suite, device1, device2 *Messenger) { response, err = WaitOnMessengerResponse( device2, func(r *MessengerResponse) bool { - for _, installation := range r.Installations { + for _, installation := range r.Installations() { if installation.ID == device1.installationID { return installation.InstallationMetadata != nil && i.InstallationMetadata.Name == installation.InstallationMetadata.Name && diff --git a/protocol/requests/enable_and_sync_installation.go b/protocol/requests/enable_and_sync_installation.go new file mode 100644 index 00000000000..da4e30a2def --- /dev/null +++ b/protocol/requests/enable_and_sync_installation.go @@ -0,0 +1,19 @@ +package requests + +import ( + "errors" +) + +var ErrEnableAndSyncInstallationInvalidID = errors.New("enable and sync installation: invalid installation id") + +type EnableAndSyncInstallation struct { + InstallationID string `json:"installationId"` +} + +func (j *EnableAndSyncInstallation) Validate() error { + if len(j.InstallationID) == 0 { + return ErrEnableAndSyncInstallationInvalidID + } + + return nil +} diff --git a/protocol/requests/enable_installation_and_pair.go b/protocol/requests/enable_installation_and_pair.go new file mode 100644 index 00000000000..5a9453d43f7 --- /dev/null +++ b/protocol/requests/enable_installation_and_pair.go @@ -0,0 +1,19 @@ +package requests + +import ( + "errors" +) + +var ErrEnableInstallationAndPairInvalidID = errors.New("enable installation and pair: invalid installation id") + +type EnableInstallationAndPair struct { + InstallationID string `json:"installationId"` +} + +func (j *EnableInstallationAndPair) Validate() error { + if len(j.InstallationID) == 0 { + return ErrEnableInstallationAndPairInvalidID + } + + return nil +} diff --git a/server/pairing/config.go b/server/pairing/config.go index 62e6cee5903..7e323155b8a 100644 --- a/server/pairing/config.go +++ b/server/pairing/config.go @@ -64,11 +64,13 @@ type ServerConfig struct { // Connection fields, not json (un)marshalled // Required for the server, but MUST NOT come from client - PK *ecdsa.PublicKey `json:"-"` - EK []byte `json:"-"` - Cert *tls.Certificate `json:"-"` - ListenIP net.IP `json:"-"` - IPAddresses []net.IP `json:"-"` + PK *ecdsa.PublicKey `json:"-"` + EK []byte `json:"-"` + Cert *tls.Certificate `json:"-"` + ListenIP net.IP `json:"-"` + IPAddresses []net.IP `json:"-"` + InstallationID string `json:"-"` + KeyUID string `json:"-"` } type ClientConfig struct{} diff --git a/server/pairing/connection.go b/server/pairing/connection.go index 5630c7f9614..0dbc8f4c5ce 100644 --- a/server/pairing/connection.go +++ b/server/pairing/connection.go @@ -4,12 +4,14 @@ import ( "crypto/ecdsa" "crypto/elliptic" "fmt" + "log" "math/big" "net" "net/url" "strings" "github.com/btcsuite/btcutil/base58" + "github.com/google/uuid" "github.com/status-im/status-go/server/pairing/versioning" ) @@ -19,20 +21,24 @@ const ( ) type ConnectionParams struct { - version versioning.ConnectionParamVersion - netIPs []net.IP - port int - publicKey *ecdsa.PublicKey - aesKey []byte + version versioning.ConnectionParamVersion + netIPs []net.IP + port int + publicKey *ecdsa.PublicKey + aesKey []byte + installationID string + keyUID string } -func NewConnectionParams(netIPs []net.IP, port int, publicKey *ecdsa.PublicKey, aesKey []byte) *ConnectionParams { +func NewConnectionParams(netIPs []net.IP, port int, publicKey *ecdsa.PublicKey, aesKey []byte, installationID, keyUID string) *ConnectionParams { cp := new(ConnectionParams) cp.version = versioning.LatestConnectionParamVer cp.netIPs = netIPs cp.port = port cp.publicKey = publicKey cp.aesKey = aesKey + cp.installationID = installationID + cp.keyUID = keyUID return cp } @@ -45,13 +51,21 @@ func NewConnectionParams(netIPs []net.IP, port int, publicKey *ecdsa.PublicKey, // - string type identifier // - version // - net.IP -// - version 1: a single net.IP -// - version 2: array of IPs in next form: +// - array of IPs in next form: // | 1 byte | 4*N bytes | 1 byte | 16*N bytes | // | N | N * IPv4 | M | M * IPv6 | // - port // - ecdsa CompressedPublicKey // - AES encryption key +// - string InstallationID of the sending device +// - string KeyUID of the sending device +// NOTE: +// - append(accrete) parameters instead of changing(breaking) existing parameters. Appending should **never** break, modifying existing parameters will break. Watch this before making changes: https://www.youtube.com/watch?v=oyLBGkS5ICk +// - never strictly check version, unless you really want to break + +// This flag is used to keep compatibility with 2.29. It will output a 5 parameters connection string with version 3. +var keep229Compatibility bool = true + func (cp *ConnectionParams) ToString() string { v := base58.Encode(new(big.Int).SetInt64(int64(cp.version)).Bytes()) ips := base58.Encode(SerializeNetIps(cp.netIPs)) @@ -59,7 +73,39 @@ func (cp *ConnectionParams) ToString() string { k := base58.Encode(elliptic.MarshalCompressed(cp.publicKey.Curve, cp.publicKey.X, cp.publicKey.Y)) ek := base58.Encode(cp.aesKey) - return fmt.Sprintf("%s%s:%s:%s:%s:%s", connectionStringID, v, ips, p, k, ek) + if keep229Compatibility { + return fmt.Sprintf("%s%s:%s:%s:%s:%s", connectionStringID, v, ips, p, k, ek) + } + + var i string + if cp.installationID != "" { + + u, err := uuid.Parse(cp.installationID) + if err != nil { + log.Fatalf("Failed to parse UUID: %v", err) + } else { + + // Convert UUID to byte slice + byteSlice := u[:] + i = base58.Encode(byteSlice) + } + } + + var kuid string + if cp.keyUID != "" { + kuid = base58.Encode([]byte(cp.keyUID)) + + } + + return fmt.Sprintf("%s%s:%s:%s:%s:%s:%s:%s", connectionStringID, v, ips, p, k, ek, i, kuid) +} + +func (cp *ConnectionParams) InstallationID() string { + return cp.installationID +} + +func (cp *ConnectionParams) KeyUID() string { + return cp.keyUID } func SerializeNetIps(ips []net.IP) []byte { @@ -128,26 +174,17 @@ func (cp *ConnectionParams) FromString(s string) error { requiredParams := 5 sData := strings.Split(s[2:], ":") - if len(sData) != requiredParams { + // NOTE: always allow extra parameters for forward compatibility, error on not enough required parameters or failing to parse + if len(sData) < requiredParams { return fmt.Errorf("expected data '%s' to have length of '%d', received '%d'", s, requiredParams, len(sData)) } - cp.version = versioning.ConnectionParamVersion(new(big.Int).SetBytes(base58.Decode(sData[0])).Int64()) - netIpsBytes := base58.Decode(sData[1]) - switch cp.version { - case versioning.ConnectionParamsV1: - if len(netIpsBytes) != net.IPv4len { - return fmt.Errorf("invalid IP size: '%d' bytes, expected: '%d' bytes", len(netIpsBytes), net.IPv4len) - } - cp.netIPs = []net.IP{netIpsBytes} - case versioning.ConnectionParamsV2: - netIps, err := ParseNetIps(netIpsBytes) - if err != nil { - return err - } - cp.netIPs = netIps + netIps, err := ParseNetIps(netIpsBytes) + if err != nil { + return err } + cp.netIPs = netIps cp.port = int(new(big.Int).SetBytes(base58.Decode(sData[2])).Int64()) cp.publicKey = new(ecdsa.PublicKey) @@ -155,16 +192,25 @@ func (cp *ConnectionParams) FromString(s string) error { cp.publicKey.Curve = elliptic.P256() cp.aesKey = base58.Decode(sData[4]) + if len(sData) > 5 && len(sData[5]) != 0 { + installationIDBytes := base58.Decode(sData[5]) + installationID, err := uuid.FromBytes(installationIDBytes) + if err != nil { + return err + } + cp.installationID = installationID.String() + } + + if len(sData) > 6 && len(sData[6]) != 0 { + decodedBytes := base58.Decode(sData[6]) + cp.keyUID = string(decodedBytes) + } + return cp.validate() } func (cp *ConnectionParams) validate() error { - err := cp.validateVersion() - if err != nil { - return err - } - - err = cp.validateNetIP() + err := cp.validateNetIP() if err != nil { return err } @@ -182,13 +228,6 @@ func (cp *ConnectionParams) validate() error { return cp.validateAESKey() } -func (cp *ConnectionParams) validateVersion() error { - if cp.version <= versioning.LatestConnectionParamVer { - return nil - } - return fmt.Errorf("unsupported version '%d'", cp.version) -} - func (cp *ConnectionParams) validateNetIP() error { for _, ip := range cp.netIPs { if ok := net.ParseIP(ip.String()); ok == nil { diff --git a/server/pairing/connection_test.go b/server/pairing/connection_test.go index 3763a86f306..3640212e9cc 100644 --- a/server/pairing/connection_test.go +++ b/server/pairing/connection_test.go @@ -14,9 +14,10 @@ import ( ) const ( - connectionStringV1 = "cs2:4FHRnp:Q4:uqnnMwVUfJc2Fkcaojet8F1ufKC3hZdGEt47joyBx9yd:BbnZ7Gc66t54a9kEFCf7FW8SGQuYypwHVeNkRYeNoqV6" - connectionStringV2 = "cs3:kDDauj5:Q4:uqnnMwVUfJc2Fkcaojet8F1ufKC3hZdGEt47joyBx9yd:BbnZ7Gc66t54a9kEFCf7FW8SGQuYypwHVeNkRYeNoqV6" - port = 1337 + connectionString = "cs3:kDDauj5:Q4:uqnnMwVUfJc2Fkcaojet8F1ufKC3hZdGEt47joyBx9yd:BbnZ7Gc66t54a9kEFCf7FW8SGQuYypwHVeNkRYeNoqV6:XxovYsfDefUHFJy8U98wtV:3BM1jGMPFuHMhJqEB" + + connectionString229Compatibility = "cs3:kDDauj5:Q4:uqnnMwVUfJc2Fkcaojet8F1ufKC3hZdGEt47joyBx9yd:BbnZ7Gc66t54a9kEFCf7FW8SGQuYypwHVeNkRYeNoqV6" + port = 1337 ) func TestConnectionParamsSuite(t *testing.T) { @@ -44,11 +45,13 @@ func (s *ConnectionParamsSuite) SetupSuite() { s.Require().NoError(err) sc := ServerConfig{ - PK: &s.PK.PublicKey, - EK: s.AES, - Cert: &cert, - IPAddresses: ips, - ListenIP: net.IPv4zero, + PK: &s.PK.PublicKey, + EK: s.AES, + Cert: &cert, + IPAddresses: ips, + ListenIP: net.IPv4zero, + InstallationID: "fabcfc11-6ed9-46d1-b723-de5d4ad1657a", + KeyUID: "some-key-uid", } bs := server.NewServer(&cert, net.IPv4zero.String(), nil, s.Logger) @@ -62,11 +65,12 @@ func (s *ConnectionParamsSuite) SetupSuite() { } func (s *ConnectionParamsSuite) TestConnectionParams_ToString() { + keep229Compatibility = false cp, err := s.server.MakeConnectionParams() s.Require().NoError(err) cps := cp.ToString() - s.Require().Equal(connectionStringV2, cps) + s.Require().Equal(connectionString, cps) } func (s *ConnectionParamsSuite) TestConnectionParams_Generate() { @@ -75,14 +79,13 @@ func (s *ConnectionParamsSuite) TestConnectionParams_Generate() { description string cs string }{ - {description: "ConnectionString_version1", cs: connectionStringV1}, - {description: "ConnectionString_version2", cs: connectionStringV2}, + {description: "ConnectionString", cs: connectionString}, } for _, tc := range testCases { s.T().Run(tc.description, func(t *testing.T) { cp := new(ConnectionParams) - err := cp.FromString(connectionStringV2) + err := cp.FromString(connectionString) s.Require().NoError(err) u, err := cp.URL(0) @@ -136,3 +139,17 @@ func (s *ConnectionParamsSuite) TestConnectionParams_ParseNetIps() { s.Require().Equal(in, out) } + +func (s *ConnectionParamsSuite) TestParse229() { + cp := new(ConnectionParams) + s.Require().NoError(cp.FromString(connectionString229Compatibility)) +} + +func (s *ConnectionParamsSuite) TestParseConnectionStringWithKeyUIDAndInstallationID() { + cp := new(ConnectionParams) + err := cp.FromString(connectionString) + s.Require().NoError(err) + + s.Require().NotEmpty(cp.InstallationID) + s.Require().NotEmpty(cp.KeyUID) +} diff --git a/server/pairing/peers/payload.go b/server/pairing/peers/payload.go index 4adcfc0305a..81dece652b9 100644 --- a/server/pairing/peers/payload.go +++ b/server/pairing/peers/payload.go @@ -9,7 +9,6 @@ import ( udpp2p "github.com/schollz/peerdiscovery" "github.com/status-im/status-go/protocol/protobuf" - "github.com/status-im/status-go/server/pairing/versioning" ) type LocalPairingPeerHello struct { @@ -20,7 +19,6 @@ type LocalPairingPeerHello struct { func NewLocalPairingPeerHello(id []byte, name, deviceType string, k *ecdsa.PrivateKey) (*LocalPairingPeerHello, error) { h := new(LocalPairingPeerHello) - h.PairingVersion = int32(versioning.LatestLocalPairingVer) h.PeerId = id h.DeviceName = name h.DeviceType = deviceType @@ -35,17 +33,15 @@ func NewLocalPairingPeerHello(id []byte, name, deviceType string, k *ecdsa.Priva func (h *LocalPairingPeerHello) MarshalJSON() ([]byte, error) { alias := struct { - PairingVersion int32 - PeerID []byte - DeviceName string - DeviceType string - Address string + PeerID []byte + DeviceName string + DeviceType string + Address string }{ - PairingVersion: h.PairingVersion, - PeerID: h.PeerId, - DeviceName: h.DeviceName, - DeviceType: h.DeviceType, - Address: h.Discovered.Address, + PeerID: h.PeerId, + DeviceName: h.DeviceName, + DeviceType: h.DeviceType, + Address: h.Discovered.Address, } return json.Marshal(alias) diff --git a/server/pairing/server.go b/server/pairing/server.go index 925b7d205f7..96016b3be32 100644 --- a/server/pairing/server.go +++ b/server/pairing/server.go @@ -58,7 +58,7 @@ func NewBaseServer(logger *zap.Logger, e *PayloadEncryptor, config *ServerConfig // MakeConnectionParams generates a *ConnectionParams based on the Server's current state func (s *BaseServer) MakeConnectionParams() (*ConnectionParams, error) { - return NewConnectionParams(s.config.IPAddresses, s.MustGetPort(), s.config.PK, s.config.EK), nil + return NewConnectionParams(s.config.IPAddresses, s.MustGetPort(), s.config.PK, s.config.EK, s.config.InstallationID, s.config.KeyUID), nil } func MakeServerConfig(config *ServerConfig) error { @@ -160,6 +160,8 @@ func (s *SenderServer) startSendingData() error { // MakeFullSenderServer generates a fully configured and randomly seeded SenderServer func MakeFullSenderServer(backend *api.GethStatusBackend, config *SenderServerConfig) (*SenderServer, error) { + config.ServerConfig.InstallationID = backend.InstallationID() + config.ServerConfig.KeyUID = backend.KeyUID() err := MakeServerConfig(config.ServerConfig) if err != nil { return nil, err @@ -268,6 +270,9 @@ func (s *ReceiverServer) startReceivingData() error { // MakeFullReceiverServer generates a fully configured and randomly seeded ReceiverServer func MakeFullReceiverServer(backend *api.GethStatusBackend, config *ReceiverServerConfig) (*ReceiverServer, error) { + config.ServerConfig.InstallationID = backend.InstallationID() + config.ServerConfig.KeyUID = backend.KeyUID() + err := MakeServerConfig(config.ServerConfig) if err != nil { return nil, err @@ -370,6 +375,9 @@ func (s *KeystoreFilesSenderServer) startSendingData() error { // MakeFullSenderServer generates a fully configured and randomly seeded KeystoreFilesSenderServer func MakeKeystoreFilesSenderServer(backend *api.GethStatusBackend, config *KeystoreFilesSenderServerConfig) (*KeystoreFilesSenderServer, error) { + config.ServerConfig.InstallationID = backend.InstallationID() + config.ServerConfig.KeyUID = backend.KeyUID() + err := MakeServerConfig(config.ServerConfig) if err != nil { return nil, err diff --git a/server/pairing/server_pairing_test.go b/server/pairing/server_pairing_test.go index b8dbb87cb3c..d1d5ddaefde 100644 --- a/server/pairing/server_pairing_test.go +++ b/server/pairing/server_pairing_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -320,3 +321,50 @@ func (s *PairingServerSuite) TestGetOutboundIPWithFullServerE2e() { s.Require().NoError(err) s.Require().Equal("Hello I like to be a tls server. You said: `"+thing+"`", string(content[:109])) } + +func (s *PairingServerSuite) TestFromStringInstallationID() { + pm := NewMockPayloadMounter(s.EphemeralAES) + s.SS.accountMounter = pm + + err := s.SS.startSendingData() + s.Require().NoError(err) + + // Server generates a QR code connection string + cp, err := s.SS.MakeConnectionParams() + s.Require().NoError(err) + + installationID := uuid.New().String() + cp.installationID = installationID + qr := cp.ToString() + + // Client reads QR code and parses the connection string + ccp := new(ConnectionParams) + err = ccp.FromString(qr) + s.Require().NoError(err) + + s.Require().Equal(installationID, ccp.installationID) +} + +func (s *PairingServerSuite) TestFromStringForwardCompatibility() { + pm := NewMockPayloadMounter(s.EphemeralAES) + s.SS.accountMounter = pm + + err := s.SS.startSendingData() + s.Require().NoError(err) + + // Server generates a QR code connection string + cp, err := s.SS.MakeConnectionParams() + s.Require().NoError(err) + + installationID := uuid.New().String() + cp.installationID = installationID + qr := cp.ToString() + + qr += ":for-gods-sake-this-should-not-break:anything" + + // Client reads QR code and parses the connection string + ccp := new(ConnectionParams) + err = ccp.FromString(qr) + s.Require().NoError(err) + s.Require().NotEmpty(ccp.netIPs) +} diff --git a/server/pairing/sync_device_test.go b/server/pairing/sync_device_test.go index 8e37ef40664..0a1746c29d0 100644 --- a/server/pairing/sync_device_test.go +++ b/server/pairing/sync_device_test.go @@ -1035,7 +1035,7 @@ func (s *SyncDeviceSuite) TestTransferringKeystoreFilesAfterStopUisngKeycard() { response, err = protocol.WaitOnMessengerResponse( serverMessenger, func(r *protocol.MessengerResponse) bool { - for _, i := range r.Installations { + for _, i := range r.Installations() { if i.ID == settings.InstallationID { return true } @@ -1048,7 +1048,7 @@ func (s *SyncDeviceSuite) TestTransferringKeystoreFilesAfterStopUisngKeycard() { s.Require().NoError(err) found := false - for _, i := range response.Installations { + for _, i := range response.Installations() { found = i.ID == settings.InstallationID && i.InstallationMetadata != nil && i.InstallationMetadata.Name == im1.Name && diff --git a/services/ext/api.go b/services/ext/api.go index 8104755d099..ed8dc7c57d3 100644 --- a/services/ext/api.go +++ b/services/ext/api.go @@ -1060,6 +1060,14 @@ func (api *PublicAPI) SyncDevices(ctx context.Context, name, picture string) err return api.service.messenger.SyncDevices(ctx, name, picture, nil) } +func (api *PublicAPI) EnableAndSyncInstallation(request *requests.EnableAndSyncInstallation) error { + return api.service.messenger.EnableAndSyncInstallation(request) +} + +func (api *PublicAPI) EnableInstallationAndPair(request *requests.EnableInstallationAndPair) (*protocol.MessengerResponse, error) { + return api.service.messenger.EnableInstallationAndPair(request) +} + func (api *PublicAPI) AddBookmark(ctx context.Context, bookmark browsers.Bookmark) error { return api.service.messenger.AddBookmark(ctx, bookmark) }