From 5335a2b4fd35c877ed5215d8bc3101b7c42dd102 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Wed, 26 Jun 2019 11:32:59 +0200 Subject: [PATCH] Move installations to status-go (#1499) * Move installations to status-go This commit moves installations management/storage to status-go. We remove the native binding and provide RPC endpoints to set the metadata and return a list of our own installations. --- VERSION | 2 +- api/backend.go | 40 ------ lib/library.go | 36 ----- mobile/status.go | 34 ----- services/shhext/api.go | 21 +++ services/shhext/chat/db/migrations/bindata.go | 50 ++++++- .../chat/encryption_multi_device_test.go | 25 ++-- services/shhext/chat/encryption_test.go | 4 +- .../shhext/chat/multidevice/persistence.go | 6 +- services/shhext/chat/multidevice/service.go | 73 +++++++--- .../chat/multidevice/sql_lite_persistence.go | 135 ++++++++++++++++-- .../multidevice/sql_lite_persistence_test.go | 111 +++++++++++--- services/shhext/chat/protocol.go | 16 ++- services/shhext/chat/protocol_test.go | 2 +- services/shhext/publisher/service.go | 92 +++++++++--- ...1368210_add_installation_metadata.down.sql | 1 + ...561368210_add_installation_metadata.up.sql | 8 ++ 17 files changed, 456 insertions(+), 200 deletions(-) create mode 100644 static/chat_db_migrations/1561368210_add_installation_metadata.down.sql create mode 100644 static/chat_db_migrations/1561368210_add_installation_metadata.up.sql diff --git a/VERSION b/VERSION index 4f7eae858..c4930afb7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.27.0-beta.1 +0.28.0-beta.0 diff --git a/api/backend.go b/api/backend.go index b2a5bec6d..4bd9dafab 100644 --- a/api/backend.go +++ b/api/backend.go @@ -658,46 +658,6 @@ func (b *StatusBackend) SignGroupMembership(content string) (string, error) { return crypto.Sign(content, selectedChatAccount.AccountKey.PrivateKey) } -// EnableInstallation enables an installation for multi-device sync. -func (b *StatusBackend) EnableInstallation(installationID string) error { - selectedChatAccount, err := b.AccountManager().SelectedChatAccount() - if err != nil { - return err - } - - st, err := b.statusNode.ShhExtService() - if err != nil { - return err - } - - if err := st.EnableInstallation(&selectedChatAccount.AccountKey.PrivateKey.PublicKey, installationID); err != nil { - b.log.Error("error enabling installation", "err", err) - return err - } - - return nil -} - -// DisableInstallation disables an installation for multi-device sync. -func (b *StatusBackend) DisableInstallation(installationID string) error { - selectedChatAccount, err := b.AccountManager().SelectedChatAccount() - if err != nil { - return err - } - - st, err := b.statusNode.ShhExtService() - if err != nil { - return err - } - - if err := st.DisableInstallation(&selectedChatAccount.AccountKey.PrivateKey.PublicKey, installationID); err != nil { - b.log.Error("error disabling installation", "err", err) - return err - } - - return nil -} - // UpdateMailservers on ShhExtService. func (b *StatusBackend) UpdateMailservers(enodes []string) error { st, err := b.statusNode.ShhExtService() diff --git a/lib/library.go b/lib/library.go index b1faeef9f..568b228b8 100644 --- a/lib/library.go +++ b/lib/library.go @@ -93,42 +93,6 @@ func SignGroupMembership(content *C.char) *C.char { return C.CString(string(data)) } -// EnableInstallation enables an installation for multi-device sync. -//export EnableInstallation -func EnableInstallation(installationID *C.char) *C.char { - err := statusBackend.EnableInstallation(C.GoString(installationID)) - if err != nil { - return makeJSONResponse(err) - } - - data, err := json.Marshal(struct { - Response string `json:"response"` - }{Response: "ok"}) - if err != nil { - return makeJSONResponse(err) - } - - return C.CString(string(data)) -} - -// DisableInstallation disables an installation for multi-device sync. -//export DisableInstallation -func DisableInstallation(installationID *C.char) *C.char { - err := statusBackend.DisableInstallation(C.GoString(installationID)) - if err != nil { - return makeJSONResponse(err) - } - - data, err := json.Marshal(struct { - Response string `json:"response"` - }{Response: "ok"}) - if err != nil { - return makeJSONResponse(err) - } - - return C.CString(string(data)) -} - //ValidateNodeConfig validates config for status node //export ValidateNodeConfig func ValidateNodeConfig(configJSON *C.char) *C.char { diff --git a/mobile/status.go b/mobile/status.go index 598affc54..1941793a3 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -104,40 +104,6 @@ func SignGroupMembership(content string) string { return string(data) } -// EnableInstallation enables an installation for multi-device sync. -func EnableInstallation(installationID string) string { - err := statusBackend.EnableInstallation(installationID) - if err != nil { - return makeJSONResponse(err) - } - - data, err := json.Marshal(struct { - Response string `json:"response"` - }{Response: "ok"}) - if err != nil { - return makeJSONResponse(err) - } - - return string(data) -} - -// DisableInstallation disables an installation for multi-device sync. -func DisableInstallation(installationID string) string { - err := statusBackend.DisableInstallation(installationID) - if err != nil { - return makeJSONResponse(err) - } - - data, err := json.Marshal(struct { - Response string `json:"response"` - }{Response: "ok"}) - if err != nil { - return makeJSONResponse(err) - } - - return string(data) -} - // ValidateNodeConfig validates config for the Status node. func ValidateNodeConfig(configJSON string) string { var resp APIDetailedResponse diff --git a/services/shhext/api.go b/services/shhext/api.go index 7ba6ceef2..3ff51391d 100644 --- a/services/shhext/api.go +++ b/services/shhext/api.go @@ -18,6 +18,7 @@ import ( "github.com/status-im/status-go/db" "github.com/status-im/status-go/mailserver" "github.com/status-im/status-go/services/shhext/chat" + "github.com/status-im/status-go/services/shhext/chat/multidevice" "github.com/status-im/status-go/services/shhext/dedup" "github.com/status-im/status-go/services/shhext/filter" "github.com/status-im/status-go/services/shhext/mailservers" @@ -608,6 +609,26 @@ func (api *PublicAPI) RemoveFilters(parent context.Context, chats []*filter.Chat return api.service.RemoveFilters(chats) } +// EnableInstallation enables an installation for multi-device sync. +func (api *PublicAPI) EnableInstallation(installationID string) error { + return api.service.EnableInstallation(installationID) +} + +// DisableInstallation disables an installation for multi-device sync. +func (api *PublicAPI) DisableInstallation(installationID string) error { + return api.service.DisableInstallation(installationID) +} + +// GetOurInstallations returns all the installations available given an identity +func (api *PublicAPI) GetOurInstallations() ([]*multidevice.Installation, error) { + return api.service.GetOurInstallations() +} + +// SetInstallationMetadata sets the metadata for our own installation +func (api *PublicAPI) SetInstallationMetadata(installationID string, data *multidevice.InstallationMetadata) error { + return api.service.SetInstallationMetadata(installationID, data) +} + // ----- // HELPER // ----- diff --git a/services/shhext/chat/db/migrations/bindata.go b/services/shhext/chat/db/migrations/bindata.go index afb375175..1f2583249 100644 --- a/services/shhext/chat/db/migrations/bindata.go +++ b/services/shhext/chat/db/migrations/bindata.go @@ -15,6 +15,8 @@ // 1559627659_add_contact_code.up.sql // 1561059285_add_whisper_keys.down.sql // 1561059285_add_whisper_keys.up.sql +// 1561368210_add_installation_metadata.down.sql +// 1561368210_add_installation_metadata.up.sql // static.go // DO NOT EDIT! @@ -358,7 +360,7 @@ func _1561059285_add_whisper_keysDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1561059285_add_whisper_keys.down.sql", size: 25, mode: os.FileMode(420), modTime: time.Unix(1561059394, 0)} + info := bindataFileInfo{name: "1561059285_add_whisper_keys.down.sql", size: 25, mode: os.FileMode(420), modTime: time.Unix(1561361219, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -378,7 +380,47 @@ func _1561059285_add_whisper_keysUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1561059285_add_whisper_keys.up.sql", size: 112, mode: os.FileMode(420), modTime: time.Unix(1561097945, 0)} + info := bindataFileInfo{name: "1561059285_add_whisper_keys.up.sql", size: 112, mode: os.FileMode(420), modTime: time.Unix(1561361219, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var __1561368210_add_installation_metadataDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\xc8\xcc\x2b\x2e\x49\xcc\xc9\x49\x2c\xc9\xcc\xcf\x2b\x8e\xcf\x4d\x2d\x49\x4c\x49\x2c\x49\xb4\xe6\x02\x04\x00\x00\xff\xff\x03\x72\x7f\x08\x23\x00\x00\x00") + +func _1561368210_add_installation_metadataDownSqlBytes() ([]byte, error) { + return bindataRead( + __1561368210_add_installation_metadataDownSql, + "1561368210_add_installation_metadata.down.sql", + ) +} + +func _1561368210_add_installation_metadataDownSql() (*asset, error) { + bytes, err := _1561368210_add_installation_metadataDownSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "1561368210_add_installation_metadata.down.sql", size: 35, mode: os.FileMode(420), modTime: time.Unix(1561459323, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var __1561368210_add_installation_metadataUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\xce\xc1\x8a\x83\x30\x10\xc6\xf1\xbb\x4f\xf1\xdd\x54\xf0\x0d\xf6\x14\xb3\x23\x08\x21\xd9\x95\x04\x7a\x93\x60\x52\x08\xd5\x58\xe8\x50\xf0\xed\x8b\x87\x42\xed\xc1\xeb\xcc\xef\x83\xbf\x1c\x48\x58\x82\x15\xad\x22\xa4\xfc\x60\x3f\xcf\x9e\xd3\x9a\xc7\x25\xb2\x0f\x9e\x3d\x50\x15\x40\x0a\x31\x73\xe2\x0d\xad\x32\x2d\xb4\xb1\xd0\x4e\xa9\x66\xff\x7c\x8e\x52\x80\xa5\x8b\x3d\x80\xec\x97\x78\xbc\xe2\x97\x3a\xe1\x94\x45\x59\xee\x20\xc4\x67\x9a\xe2\xc8\xdb\xfd\xdc\x5d\xa7\x65\xe4\xf5\x16\xf3\xa9\x72\xba\xff\x77\x54\xbd\x83\x9b\xef\xc0\x1a\x46\x43\x1a\xdd\xa9\x5e\x5a\x0c\xf4\xa7\x84\xa4\xa2\xfe\x29\x5e\x01\x00\x00\xff\xff\x5d\x6f\xe6\xd3\x0b\x01\x00\x00") + +func _1561368210_add_installation_metadataUpSqlBytes() ([]byte, error) { + return bindataRead( + __1561368210_add_installation_metadataUpSql, + "1561368210_add_installation_metadata.up.sql", + ) +} + +func _1561368210_add_installation_metadataUpSql() (*asset, error) { + bytes, err := _1561368210_add_installation_metadataUpSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "1561368210_add_installation_metadata.up.sql", size: 267, mode: os.FileMode(420), modTime: time.Unix(1561459769, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -470,6 +512,8 @@ var _bindata = map[string]func() (*asset, error){ "1559627659_add_contact_code.up.sql": _1559627659_add_contact_codeUpSql, "1561059285_add_whisper_keys.down.sql": _1561059285_add_whisper_keysDownSql, "1561059285_add_whisper_keys.up.sql": _1561059285_add_whisper_keysUpSql, + "1561368210_add_installation_metadata.down.sql": _1561368210_add_installation_metadataDownSql, + "1561368210_add_installation_metadata.up.sql": _1561368210_add_installation_metadataUpSql, "static.go": staticGo, } @@ -528,6 +572,8 @@ var _bintree = &bintree{nil, map[string]*bintree{ "1559627659_add_contact_code.up.sql": &bintree{_1559627659_add_contact_codeUpSql, map[string]*bintree{}}, "1561059285_add_whisper_keys.down.sql": &bintree{_1561059285_add_whisper_keysDownSql, map[string]*bintree{}}, "1561059285_add_whisper_keys.up.sql": &bintree{_1561059285_add_whisper_keysUpSql, map[string]*bintree{}}, + "1561368210_add_installation_metadata.down.sql": &bintree{_1561368210_add_installation_metadataDownSql, map[string]*bintree{}}, + "1561368210_add_installation_metadata.up.sql": &bintree{_1561368210_add_installation_metadataUpSql, map[string]*bintree{}}, "static.go": &bintree{staticGo, map[string]*bintree{}}, }} diff --git a/services/shhext/chat/encryption_multi_device_test.go b/services/shhext/chat/encryption_multi_device_test.go index 16554671f..fe72cf829 100644 --- a/services/shhext/chat/encryption_multi_device_test.go +++ b/services/shhext/chat/encryption_multi_device_test.go @@ -6,7 +6,6 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/suite" "os" - "sort" "testing" "github.com/status-im/status-go/services/shhext/chat/multidevice" @@ -69,7 +68,7 @@ func setupUser(user string, s *EncryptionServiceMultiDeviceSuite, n int) error { DefaultEncryptionServiceConfig(installationID)), sharedSecretService, multideviceService, - func(s []*multidevice.IdentityAndID) {}, + func(s []*multidevice.Installation) {}, func(s []*sharedsecret.Secret) {}, ) @@ -111,12 +110,20 @@ func (s *EncryptionServiceMultiDeviceSuite) TestProcessPublicBundle() { // Add alice2 bundle response, err := s.services[aliceUser].services[0].ProcessPublicBundle(aliceKey, alice2Bundle) s.Require().NoError(err) - s.Require().Equal(multidevice.IdentityAndID{alice2Identity, "alice2"}, *response[0]) + s.Require().Equal(multidevice.Installation{ + Identity: alice2Identity, + Version: 1, + ID: "alice2", + }, *response[0]) // Add alice3 bundle response, err = s.services[aliceUser].services[0].ProcessPublicBundle(aliceKey, alice3Bundle) s.Require().NoError(err) - s.Require().Equal(multidevice.IdentityAndID{alice3Identity, "alice3"}, *response[0]) + s.Require().Equal(multidevice.Installation{ + Identity: alice3Identity, + Version: 1, + ID: "alice3", + }, *response[0]) // No installation is enabled alice1MergedBundle1, err := s.services[aliceUser].services[0].GetBundle(aliceKey) @@ -141,16 +148,6 @@ func (s *EncryptionServiceMultiDeviceSuite) TestProcessPublicBundle() { s.Require().NotNil(alice1MergedBundle2.GetSignedPreKeys()["alice2"]) s.Require().NotNil(alice1MergedBundle2.GetSignedPreKeys()["alice3"]) - response, err = s.services[aliceUser].services[0].ProcessPublicBundle(aliceKey, alice1MergedBundle2) - s.Require().NoError(err) - sort.Slice(response, func(i, j int) bool { - return response[i].ID < response[j].ID - }) - // We only get back installationIDs not equal to us - s.Require().Equal(2, len(response)) - s.Require().Equal(multidevice.IdentityAndID{alice2Identity, "alice2"}, *response[0]) - s.Require().Equal(multidevice.IdentityAndID{alice2Identity, "alice3"}, *response[1]) - // We disable the installations err = s.services[aliceUser].services[0].DisableInstallation(&aliceKey.PublicKey, "alice2") s.Require().NoError(err) diff --git a/services/shhext/chat/encryption_test.go b/services/shhext/chat/encryption_test.go index 808233e96..acdb57be9 100644 --- a/services/shhext/chat/encryption_test.go +++ b/services/shhext/chat/encryption_test.go @@ -80,7 +80,7 @@ func (s *EncryptionServiceTestSuite) initDatabases(baseConfig *EncryptionService aliceEncryptionService, aliceSharedSecretService, aliceMultideviceService, - func(s []*multidevice.IdentityAndID) {}, + func(s []*multidevice.Installation) {}, func(s []*sharedsecret.Secret) {}, ) @@ -106,7 +106,7 @@ func (s *EncryptionServiceTestSuite) initDatabases(baseConfig *EncryptionService bobEncryptionService, bobSharedSecretService, bobMultideviceService, - func(s []*multidevice.IdentityAndID) {}, + func(s []*multidevice.Installation) {}, func(s []*sharedsecret.Secret) {}, ) diff --git a/services/shhext/chat/multidevice/persistence.go b/services/shhext/chat/multidevice/persistence.go index 27017a745..e853b6839 100644 --- a/services/shhext/chat/multidevice/persistence.go +++ b/services/shhext/chat/multidevice/persistence.go @@ -8,5 +8,9 @@ type Persistence interface { // DisableInstallation disable the installation. DisableInstallation(identity []byte, installationID string) error // AddInstallations adds the installations for a given identity, maintaining the enabled flag - AddInstallations(identity []byte, timestamp int64, installations []*Installation, defaultEnabled bool) error + AddInstallations(identity []byte, timestamp int64, installations []*Installation, defaultEnabled bool) ([]*Installation, error) + // GetInstallations returns all the installations for a given identity + GetInstallations(identity []byte) ([]*Installation, error) + // SetInstallationMetadata sets the metadata for a given installation + SetInstallationMetadata(identity []byte, installationID string, data *InstallationMetadata) error } diff --git a/services/shhext/chat/multidevice/service.go b/services/shhext/chat/multidevice/service.go index b2f6b0b23..a0277c20a 100644 --- a/services/shhext/chat/multidevice/service.go +++ b/services/shhext/chat/multidevice/service.go @@ -7,11 +7,28 @@ import ( "github.com/status-im/status-go/services/shhext/chat/protobuf" ) +type InstallationMetadata struct { + // The name of the device + Name string `json:"name"` + // The type of device + DeviceType string `json:"deviceType"` + // The FCMToken for mobile devices + FCMToken string `json:"fcmToken"` +} + type Installation struct { + // Identity is the string identity of the owner + Identity string `json:"identity"` // The installation-id of the device - ID string + ID string `json:"id"` // The last known protocol version of the device - Version uint32 + Version uint32 `json:"version"` + // Enabled is whether the installation is enabled + Enabled bool `json:"enabled"` + // Timestamp is the last time we saw this device + Timestamp int64 `json:"timestamp"` + // InstallationMetadata + InstallationMetadata *InstallationMetadata `json:"metadata"` } type Config struct { @@ -32,11 +49,6 @@ type Service struct { config *Config } -type IdentityAndID struct { - Identity string - ID string -} - func (s *Service) GetActiveInstallations(identity *ecdsa.PublicKey) ([]*Installation, error) { identityC := crypto.CompressPubkey(identity) return s.persistence.GetActiveInstallations(s.config.MaxInstallations, identityC) @@ -57,6 +69,38 @@ func (s *Service) GetOurActiveInstallations(identity *ecdsa.PublicKey) ([]*Insta return installations, nil } +func (s *Service) GetOurInstallations(identity *ecdsa.PublicKey) ([]*Installation, error) { + var found bool + identityC := crypto.CompressPubkey(identity) + installations, err := s.persistence.GetInstallations(identityC) + if err != nil { + return nil, err + } + + for _, installation := range installations { + if installation.ID == s.config.InstallationID { + found = true + installation.Enabled = true + installation.Version = s.config.ProtocolVersion + } + + } + if !found { + installations = append(installations, &Installation{ + ID: s.config.InstallationID, + Enabled: true, + Version: s.config.ProtocolVersion, + }) + } + + return installations, nil +} + +func (s *Service) SetInstallationMetadata(identity *ecdsa.PublicKey, installationID string, metadata *InstallationMetadata) error { + identityC := crypto.CompressPubkey(identity) + return s.persistence.SetInstallationMetadata(identityC, installationID, metadata) +} + func (s *Service) EnableInstallation(identity *ecdsa.PublicKey, installationID string) error { identityC := crypto.CompressPubkey(identity) return s.persistence.EnableInstallation(identityC, installationID) @@ -68,9 +112,8 @@ func (s *Service) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installati } // ProcessPublicBundle persists a bundle and returns a list of tuples identity/installationID -func (s *Service) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, theirIdentity *ecdsa.PublicKey, b *protobuf.Bundle) ([]*IdentityAndID, error) { +func (s *Service) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, theirIdentity *ecdsa.PublicKey, b *protobuf.Bundle) ([]*Installation, error) { signedPreKeys := b.GetSignedPreKeys() - var response []*IdentityAndID var installations []*Installation myIdentityStr := fmt.Sprintf("0x%x", crypto.FromECDSAPub(&myIdentityKey.PublicKey)) @@ -83,16 +126,12 @@ func (s *Service) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, theirIden for installationID, signedPreKey := range signedPreKeys { if installationID != s.config.InstallationID { installations = append(installations, &Installation{ - ID: installationID, - Version: signedPreKey.GetProtocolVersion(), + Identity: theirIdentityStr, + ID: installationID, + Version: signedPreKey.GetProtocolVersion(), }) - response = append(response, &IdentityAndID{theirIdentityStr, installationID}) } } - if err := s.persistence.AddInstallations(b.GetIdentity(), b.GetTimestamp(), installations, fromOurIdentity); err != nil { - return nil, err - } - - return response, nil + return s.persistence.AddInstallations(b.GetIdentity(), b.GetTimestamp(), installations, fromOurIdentity) } diff --git a/services/shhext/chat/multidevice/sql_lite_persistence.go b/services/shhext/chat/multidevice/sql_lite_persistence.go index 7333e846e..5dd49da1d 100644 --- a/services/shhext/chat/multidevice/sql_lite_persistence.go +++ b/services/shhext/chat/multidevice/sql_lite_persistence.go @@ -32,8 +32,10 @@ func (s *SQLLitePersistence) GetActiveInstallations(maxInstallations int, identi } for rows.Next() { - var installationID string - var version uint32 + var ( + installationID string + version uint32 + ) err = rows.Scan( &installationID, &version, @@ -44,6 +46,7 @@ func (s *SQLLitePersistence) GetActiveInstallations(maxInstallations int, identi installations = append(installations, &Installation{ ID: installationID, Version: version, + Enabled: true, }) } @@ -52,20 +55,116 @@ func (s *SQLLitePersistence) GetActiveInstallations(maxInstallations int, identi } +// GetInstallations returns all the installations for a given identity +// we both return the installations & the metadata +// metadata is currently stored in a separate table, as in some cases we +// might have metadata for a device, but no other information on the device +func (s *SQLLitePersistence) GetInstallations(identity []byte) ([]*Installation, error) { + installationMap := make(map[string]*Installation) + var installations []*Installation + + // We query both tables as sqlite does not support full outer joins + installationsStmt, err := s.db.Prepare(`SELECT installation_id, version, enabled, timestamp FROM installations WHERE identity = ?`) + if err != nil { + return nil, err + } + defer installationsStmt.Close() + + installationRows, err := installationsStmt.Query(identity) + if err != nil { + return nil, err + } + + for installationRows.Next() { + var ( + installationID string + version uint32 + enabled bool + timestamp int64 + ) + err = installationRows.Scan( + &installationID, + &version, + &enabled, + ×tamp, + ) + if err != nil { + return nil, err + } + installationMap[installationID] = &Installation{ + ID: installationID, + Version: version, + Enabled: enabled, + Timestamp: timestamp, + InstallationMetadata: &InstallationMetadata{}, + } + + } + + metadataStmt, err := s.db.Prepare(`SELECT installation_id, name, device_type, fcm_token FROM installation_metadata WHERE identity = ?`) + if err != nil { + return nil, err + } + defer metadataStmt.Close() + + metadataRows, err := metadataStmt.Query(identity) + if err != nil { + return nil, err + } + + for metadataRows.Next() { + var ( + installationID string + name sql.NullString + deviceType sql.NullString + fcmToken sql.NullString + installation *Installation + ) + err = metadataRows.Scan( + &installationID, + &name, + &deviceType, + &fcmToken, + ) + if err != nil { + return nil, err + } + if _, ok := installationMap[installationID]; ok { + installation = installationMap[installationID] + } else { + installation = &Installation{ID: installationID} + } + installation.InstallationMetadata = &InstallationMetadata{ + Name: name.String, + DeviceType: deviceType.String, + FCMToken: fcmToken.String, + } + installationMap[installationID] = installation + } + + for _, installation := range installationMap { + installations = append(installations, installation) + } + + return installations, nil +} + // AddInstallations adds the installations for a given identity, maintaining the enabled flag -func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, installations []*Installation, defaultEnabled bool) error { +func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, installations []*Installation, defaultEnabled bool) ([]*Installation, error) { tx, err := s.db.Begin() if err != nil { - return nil + return nil, err } + var insertedInstallations []*Installation + for _, installation := range installations { stmt, err := tx.Prepare(`SELECT enabled, version FROM installations WHERE identity = ? AND installation_id = ? LIMIT 1`) if err != nil { - return err + return nil, err } defer stmt.Close() @@ -76,14 +175,14 @@ func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, err = stmt.QueryRow(identity, installation.ID).Scan(&oldEnabled, &oldVersion) if err != nil && err != sql.ErrNoRows { - return err + return nil, err } if err == sql.ErrNoRows { stmt, err = tx.Prepare(`INSERT INTO installations(identity, installation_id, timestamp, enabled, version) VALUES (?, ?, ?, ?, ?)`) if err != nil { - return err + return nil, err } defer stmt.Close() @@ -95,8 +194,9 @@ func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, latestVersion, ) if err != nil { - return err + return nil, err } + insertedInstallations = append(insertedInstallations, installation) } else { // We update timestamp if present without changing enabled, only if this is a new bundle // and we set the version to the latest we ever saw @@ -110,7 +210,7 @@ func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, AND installation_id = ? AND timestamp < ?`) if err != nil { - return err + return nil, err } defer stmt.Close() @@ -123,7 +223,7 @@ func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, timestamp, ) if err != nil { - return err + return nil, err } } @@ -131,10 +231,10 @@ func (s *SQLLitePersistence) AddInstallations(identity []byte, timestamp int64, if err := tx.Commit(); err != nil { _ = tx.Rollback() - return err + return nil, err } - return nil + return insertedInstallations, nil } @@ -165,3 +265,14 @@ func (s *SQLLitePersistence) DisableInstallation(identity []byte, installationID _, err = stmt.Exec(identity, installationID) return err } + +// SetInstallationMetadata sets the metadata for a given installation +func (s *SQLLitePersistence) SetInstallationMetadata(identity []byte, installationID string, metadata *InstallationMetadata) error { + stmt, err := s.db.Prepare(`INSERT INTO installation_metadata(name, device_type, fcm_token, identity, installation_id) VALUES(?,?,?,?,?)`) + if err != nil { + return err + } + + _, err = stmt.Exec(metadata.Name, metadata.DeviceType, metadata.FCMToken, identity, installationID) + return err +} diff --git a/services/shhext/chat/multidevice/sql_lite_persistence_test.go b/services/shhext/chat/multidevice/sql_lite_persistence_test.go index 1f359cfc8..f56a90579 100644 --- a/services/shhext/chat/multidevice/sql_lite_persistence_test.go +++ b/services/shhext/chat/multidevice/sql_lite_persistence_test.go @@ -36,30 +36,30 @@ func (s *SQLLitePersistenceTestSuite) SetupTest() { func (s *SQLLitePersistenceTestSuite) TestAddInstallations() { identity := []byte("alice") installations := []*Installation{ - {ID: "alice-1", Version: 1}, - {ID: "alice-2", Version: 2}, + {ID: "alice-1", Version: 1, Enabled: true}, + {ID: "alice-2", Version: 2, Enabled: true}, } - err := s.service.AddInstallations( + addedInstallations, err := s.service.AddInstallations( identity, 1, installations, true, ) - s.Require().NoError(err) enabledInstallations, err := s.service.GetActiveInstallations(5, identity) s.Require().NoError(err) s.Require().Equal(installations, enabledInstallations) + s.Require().Equal(installations, addedInstallations) } func (s *SQLLitePersistenceTestSuite) TestAddInstallationVersions() { identity := []byte("alice") installations := []*Installation{ - {ID: "alice-1", Version: 1}, + {ID: "alice-1", Version: 1, Enabled: true}, } - err := s.service.AddInstallations( + _, err := s.service.AddInstallations( identity, 1, installations, @@ -77,7 +77,7 @@ func (s *SQLLitePersistenceTestSuite) TestAddInstallationVersions() { {ID: "alice-1", Version: 0}, } - err = s.service.AddInstallations( + _, err = s.service.AddInstallations( identity, 3, installationsWithDowngradedVersion, @@ -98,7 +98,7 @@ func (s *SQLLitePersistenceTestSuite) TestAddInstallationsLimit() { {ID: "alice-2", Version: 2}, } - err := s.service.AddInstallations( + _, err := s.service.AddInstallations( identity, 1, installations, @@ -111,7 +111,7 @@ func (s *SQLLitePersistenceTestSuite) TestAddInstallationsLimit() { {ID: "alice-3", Version: 3}, } - err = s.service.AddInstallations( + _, err = s.service.AddInstallations( identity, 2, installations, @@ -120,12 +120,12 @@ func (s *SQLLitePersistenceTestSuite) TestAddInstallationsLimit() { s.Require().NoError(err) installations = []*Installation{ - {ID: "alice-2", Version: 2}, - {ID: "alice-3", Version: 3}, - {ID: "alice-4", Version: 4}, + {ID: "alice-2", Version: 2, Enabled: true}, + {ID: "alice-3", Version: 3, Enabled: true}, + {ID: "alice-4", Version: 4, Enabled: true}, } - err = s.service.AddInstallations( + _, err = s.service.AddInstallations( identity, 3, installations, @@ -147,7 +147,7 @@ func (s *SQLLitePersistenceTestSuite) TestAddInstallationsDisabled() { {ID: "alice-2", Version: 2}, } - err := s.service.AddInstallations( + _, err := s.service.AddInstallations( identity, 1, installations, @@ -169,7 +169,7 @@ func (s *SQLLitePersistenceTestSuite) TestDisableInstallation() { {ID: "alice-2", Version: 2}, } - err := s.service.AddInstallations( + _, err := s.service.AddInstallations( identity, 1, installations, @@ -186,18 +186,19 @@ func (s *SQLLitePersistenceTestSuite) TestDisableInstallation() { {ID: "alice-2", Version: 2}, } - err = s.service.AddInstallations( + addedInstallations, err := s.service.AddInstallations( identity, 1, installations, true, ) s.Require().NoError(err) + s.Require().Equal(0, len(addedInstallations)) actualInstallations, err := s.service.GetActiveInstallations(3, identity) s.Require().NoError(err) - expected := []*Installation{{ID: "alice-2", Version: 2}} + expected := []*Installation{{ID: "alice-2", Version: 2, Enabled: true}} s.Require().Equal(expected, actualInstallations) } @@ -209,7 +210,7 @@ func (s *SQLLitePersistenceTestSuite) TestEnableInstallation() { {ID: "alice-2", Version: 2}, } - err := s.service.AddInstallations( + _, err := s.service.AddInstallations( identity, 1, installations, @@ -223,7 +224,7 @@ func (s *SQLLitePersistenceTestSuite) TestEnableInstallation() { actualInstallations, err := s.service.GetActiveInstallations(3, identity) s.Require().NoError(err) - expected := []*Installation{{ID: "alice-2", Version: 2}} + expected := []*Installation{{ID: "alice-2", Version: 2, Enabled: true}} s.Require().Equal(expected, actualInstallations) err = s.service.EnableInstallation(identity, "alice-1") @@ -233,9 +234,79 @@ func (s *SQLLitePersistenceTestSuite) TestEnableInstallation() { s.Require().NoError(err) expected = []*Installation{ + {ID: "alice-1", Version: 1, Enabled: true}, + {ID: "alice-2", Version: 2, Enabled: true}, + } + s.Require().Equal(expected, actualInstallations) +} + +func (s *SQLLitePersistenceTestSuite) TestGetInstallations() { + identity := []byte("alice") + + installations := []*Installation{ {ID: "alice-1", Version: 1}, {ID: "alice-2", Version: 2}, } - s.Require().Equal(expected, actualInstallations) + _, err := s.service.AddInstallations( + identity, + 1, + installations, + true, + ) + s.Require().NoError(err) + + err = s.service.DisableInstallation(identity, "alice-1") + s.Require().NoError(err) + + actualInstallations, err := s.service.GetInstallations(identity) + s.Require().NoError(err) + + emptyMetadata := &InstallationMetadata{} + + expected := []*Installation{ + {ID: "alice-1", Version: 1, Timestamp: 1, Enabled: false, InstallationMetadata: emptyMetadata}, + {ID: "alice-2", Version: 2, Timestamp: 1, Enabled: true, InstallationMetadata: emptyMetadata}, + } + s.Require().Equal(2, len(actualInstallations)) + s.Require().ElementsMatch(expected, actualInstallations) +} + +func (s *SQLLitePersistenceTestSuite) TestSetMetadata() { + identity := []byte("alice") + + installations := []*Installation{ + {ID: "alice-1", Version: 1}, + {ID: "alice-2", Version: 2}, + } + + _, err := s.service.AddInstallations( + identity, + 1, + installations, + true, + ) + s.Require().NoError(err) + + err = s.service.DisableInstallation(identity, "alice-1") + s.Require().NoError(err) + + emptyMetadata := &InstallationMetadata{} + setMetadata := &InstallationMetadata{ + Name: "a", + FCMToken: "b", + DeviceType: "c", + } + + err = s.service.SetInstallationMetadata(identity, "alice-2", setMetadata) + s.Require().NoError(err) + + actualInstallations, err := s.service.GetInstallations(identity) + s.Require().NoError(err) + + expected := []*Installation{ + {ID: "alice-1", Version: 1, Timestamp: 1, Enabled: false, InstallationMetadata: emptyMetadata}, + {ID: "alice-2", Version: 2, Timestamp: 1, Enabled: true, InstallationMetadata: setMetadata}, + } + s.Require().ElementsMatch(expected, actualInstallations) } diff --git a/services/shhext/chat/protocol.go b/services/shhext/chat/protocol.go index 0e790983a..5c3381f63 100644 --- a/services/shhext/chat/protocol.go +++ b/services/shhext/chat/protocol.go @@ -27,7 +27,7 @@ type ProtocolService struct { encryption *EncryptionService secret *sharedsecret.Service multidevice *multidevice.Service - addedBundlesHandler func([]*multidevice.IdentityAndID) + addedBundlesHandler func([]*multidevice.Installation) onNewSharedSecretHandler func([]*sharedsecret.Secret) Enabled bool } @@ -35,7 +35,7 @@ type ProtocolService struct { var ErrNotProtocolMessage = errors.New("Not a protocol message") // NewProtocolService creates a new ProtocolService instance -func NewProtocolService(encryption *EncryptionService, secret *sharedsecret.Service, multidevice *multidevice.Service, addedBundlesHandler func([]*multidevice.IdentityAndID), onNewSharedSecretHandler func([]*sharedsecret.Secret)) *ProtocolService { +func NewProtocolService(encryption *EncryptionService, secret *sharedsecret.Service, multidevice *multidevice.Service, addedBundlesHandler func([]*multidevice.Installation), onNewSharedSecretHandler func([]*sharedsecret.Secret)) *ProtocolService { return &ProtocolService{ log: log.New("package", "status-go/services/sshext.chat"), encryption: encryption, @@ -193,7 +193,7 @@ func (p *ProtocolService) BuildDHMessage(myIdentityKey *ecdsa.PrivateKey, destin } // ProcessPublicBundle processes a received X3DH bundle. -func (p *ProtocolService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *protobuf.Bundle) ([]*multidevice.IdentityAndID, error) { +func (p *ProtocolService) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *protobuf.Bundle) ([]*multidevice.Installation, error) { if err := p.encryption.ProcessPublicBundle(myIdentityKey, bundle); err != nil { return nil, err } @@ -227,6 +227,16 @@ func (p *ProtocolService) DisableInstallation(myIdentityKey *ecdsa.PublicKey, in return p.multidevice.DisableInstallation(myIdentityKey, installationID) } +// GetOurInstallations returns all the installations available given an identity +func (p *ProtocolService) GetOurInstallations(myIdentityKey *ecdsa.PublicKey) ([]*multidevice.Installation, error) { + return p.multidevice.GetOurInstallations(myIdentityKey) +} + +// SetInstallationMetadata sets the metadata for our own installation +func (p *ProtocolService) SetInstallationMetadata(myIdentityKey *ecdsa.PublicKey, installationID string, data *multidevice.InstallationMetadata) error { + return p.multidevice.SetInstallationMetadata(myIdentityKey, installationID, data) +} + // GetPublicBundle retrieves a public bundle given an identity func (p *ProtocolService) GetPublicBundle(theirIdentityKey *ecdsa.PublicKey) (*protobuf.Bundle, error) { installations, err := p.multidevice.GetActiveInstallations(theirIdentityKey) diff --git a/services/shhext/chat/protocol_test.go b/services/shhext/chat/protocol_test.go index fe6e36efb..e1f1331cc 100644 --- a/services/shhext/chat/protocol_test.go +++ b/services/shhext/chat/protocol_test.go @@ -39,7 +39,7 @@ func (s *ProtocolServiceTestSuite) SetupTest() { panic(err) } - addedBundlesHandler := func(addedBundles []*multidevice.IdentityAndID) {} + addedBundlesHandler := func(addedBundles []*multidevice.Installation) {} onNewSharedSecretHandler := func(secret []*sharedsecret.Secret) {} aliceMultideviceConfig := &multidevice.Config{ diff --git a/services/shhext/publisher/service.go b/services/shhext/publisher/service.go index 0a104e4f3..238beb0a8 100644 --- a/services/shhext/publisher/service.go +++ b/services/shhext/publisher/service.go @@ -37,6 +37,7 @@ var ( // ErrPFSNotEnabled is returned when an endpoint PFS only is called but // PFS is disabled ErrPFSNotEnabled = errors.New("pfs not enabled") + errNoKeySelected = errors.New("no key selected") ) type Service struct { @@ -134,7 +135,7 @@ func (s *Service) initProtocol(address, encKey, password string) error { return err } - addedBundlesHandler := func(addedBundles []*multidevice.IdentityAndID) { + addedBundlesHandler := func(addedBundles []*multidevice.Installation) { handler := SignalHandler{} for _, bundle := range addedBundles { handler.BundleAdded(bundle.Identity, bundle.ID) @@ -142,7 +143,6 @@ func (s *Service) initProtocol(address, encKey, password string) error { } // Initialize persistence - s.persistence = NewSQLLitePersistence(persistence.DB) // Initialize sharedsecret @@ -171,7 +171,7 @@ func (s *Service) initProtocol(address, encKey, password string) error { return nil } -func (s *Service) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *protobuf.Bundle) ([]*multidevice.IdentityAndID, error) { +func (s *Service) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *protobuf.Bundle) ([]*multidevice.Installation, error) { if s.protocol == nil { return nil, errProtocolNotInitialized } @@ -188,12 +188,79 @@ func (s *Service) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*protobuf.Bundle, } // EnableInstallation enables an installation for multi-device sync. -func (s *Service) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error { +func (s *Service) EnableInstallation(installationID string) error { if s.protocol == nil { return errProtocolNotInitialized } - return s.protocol.EnableInstallation(myIdentityKey, installationID) + privateKeyID := s.whisper.SelectedKeyPairID() + if privateKeyID == "" { + return errNoKeySelected + } + + privateKey, err := s.whisper.GetPrivateKey(privateKeyID) + if err != nil { + return err + } + + return s.protocol.EnableInstallation(&privateKey.PublicKey, installationID) +} + +// DisableInstallation disables an installation for multi-device sync. +func (s *Service) DisableInstallation(installationID string) error { + if s.protocol == nil { + return errProtocolNotInitialized + } + + privateKeyID := s.whisper.SelectedKeyPairID() + if privateKeyID == "" { + return errNoKeySelected + } + + privateKey, err := s.whisper.GetPrivateKey(privateKeyID) + if err != nil { + return err + } + + return s.protocol.DisableInstallation(&privateKey.PublicKey, installationID) +} + +// GetOurInstallations returns all the installations available given an identity +func (s *Service) GetOurInstallations() ([]*multidevice.Installation, error) { + if s.protocol == nil { + return nil, errProtocolNotInitialized + } + + privateKeyID := s.whisper.SelectedKeyPairID() + if privateKeyID == "" { + return nil, errNoKeySelected + } + + privateKey, err := s.whisper.GetPrivateKey(privateKeyID) + if err != nil { + return nil, err + } + + return s.protocol.GetOurInstallations(&privateKey.PublicKey) +} + +// SetInstallationMetadata sets the metadata for our own installation +func (s *Service) SetInstallationMetadata(installationID string, data *multidevice.InstallationMetadata) error { + if s.protocol == nil { + return errProtocolNotInitialized + } + + privateKeyID := s.whisper.SelectedKeyPairID() + if privateKeyID == "" { + return errNoKeySelected + } + + privateKey, err := s.whisper.GetPrivateKey(privateKeyID) + if err != nil { + return err + } + + return s.protocol.SetInstallationMetadata(&privateKey.PublicKey, installationID, data) } func (s *Service) GetPublicBundle(identityKey *ecdsa.PublicKey) (*protobuf.Bundle, error) { @@ -204,15 +271,6 @@ func (s *Service) GetPublicBundle(identityKey *ecdsa.PublicKey) (*protobuf.Bundl return s.protocol.GetPublicBundle(identityKey) } -// DisableInstallation disables an installation for multi-device sync. -func (s *Service) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error { - if s.protocol == nil { - return errProtocolNotInitialized - } - - return s.protocol.DisableInstallation(myIdentityKey, installationID) -} - func (s *Service) Start(online func() bool, startTicker bool) error { s.online = online if startTicker { @@ -283,7 +341,7 @@ func (s *Service) ProcessMessage(dedupMessage dedup.DeduplicateMessage) error { privateKeyID := s.whisper.SelectedKeyPairID() if privateKeyID == "" { - return errors.New("no key selected") + return errNoKeySelected } privateKey, err := s.whisper.GetPrivateKey(privateKeyID) @@ -425,7 +483,7 @@ func (s *Service) CreatePublicMessage(signature string, chatID string, payload [ if wrap { privateKeyID := s.whisper.SelectedKeyPairID() if privateKeyID == "" { - return nil, errors.New("no key selected") + return nil, errNoKeySelected } privateKey, err := s.whisper.GetPrivateKey(privateKeyID) @@ -501,7 +559,7 @@ func (s *Service) sendContactCode() (*whisper.NewMessage, error) { privateKeyID := s.whisper.SelectedKeyPairID() if privateKeyID == "" { - return nil, errors.New("no key selected") + return nil, errNoKeySelected } privateKey, err := s.whisper.GetPrivateKey(privateKeyID) diff --git a/static/chat_db_migrations/1561368210_add_installation_metadata.down.sql b/static/chat_db_migrations/1561368210_add_installation_metadata.down.sql new file mode 100644 index 000000000..ba2c563de --- /dev/null +++ b/static/chat_db_migrations/1561368210_add_installation_metadata.down.sql @@ -0,0 +1 @@ +DROP TABLE installations_metadata; diff --git a/static/chat_db_migrations/1561368210_add_installation_metadata.up.sql b/static/chat_db_migrations/1561368210_add_installation_metadata.up.sql new file mode 100644 index 000000000..84f84f7f4 --- /dev/null +++ b/static/chat_db_migrations/1561368210_add_installation_metadata.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE installation_metadata ( + identity BLOB NOT NULL, + installation_id TEXT NOT NULL, + name TEXT NOT NULL DEFAULT '', + device_type TEXT NOT NULL DEFAULT '', + fcm_token TEXT NOT NULL DEFAULT '', + UNIQUE(identity, installation_id) ON CONFLICT REPLACE +);