Skip to content

Commit d88eb25

Browse files
committed
feat(backup)_: poc for local user data backups
Fixes status-im/status-desktop#18107 Creates a proof of concept of the local user data backups. When a back up is done, if the feature flag is active, a local file will be created with (almost) all the same backup data. Then, that file can be passed to the import function and it will import that data. A test case was added to test the flow using contacts and it works.
1 parent cd63d54 commit d88eb25

File tree

10 files changed

+228
-13
lines changed

10 files changed

+228
-13
lines changed

api/defaults.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,10 @@ func DefaultNodeConfig(installationID, keyUID string, request *requests.CreateAc
420420
overrideApiConfig(nodeConfig, request.APIConfig)
421421
}
422422

423+
nodeConfig.BackupConfig = params.BackupConfig{
424+
DataDir: filepath.Join(nodeConfig.RootDataDir, params.BackupsRelativePath),
425+
}
426+
423427
for _, opt := range opts {
424428
if err := opt(nodeConfig); err != nil {
425429
return nil, err

params/config.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,8 @@ type NodeConfig struct {
395395

396396
TorrentConfig TorrentConfig
397397

398+
BackupConfig BackupConfig
399+
398400
// RegisterTopics a list of specific topics where the peer wants to be
399401
// discoverable.
400402
RegisterTopics []discv5.Topic `json:"RegisterTopics"`
@@ -604,6 +606,12 @@ type TorrentConfig struct {
604606
TorrentDir string
605607
}
606608

609+
// BackupConfig provides configuration for the local backups
610+
type BackupConfig struct {
611+
// DataDir is the file system folder Status should use for storing local backups.
612+
DataDir string
613+
}
614+
607615
// Validate validates the ShhextConfig struct and returns an error if inconsistent values are found
608616
func (c *ShhextConfig) Validate(validate *validator.Validate) error {
609617
if err := validate.Struct(c); err != nil {
@@ -744,6 +752,10 @@ func (c *NodeConfig) UpdateWithDefaults() error {
744752
}
745753
}
746754

755+
c.BackupConfig = BackupConfig{
756+
DataDir: filepath.Join(c.RootDataDir, BackupsRelativePath),
757+
}
758+
747759
return c.setDefaultPushNotificationsServers()
748760
}
749761

params/defaults.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const (
88

99
ArchivesRelativePath = "data/archivedata"
1010
TorrentTorrentsRelativePath = "data/torrents"
11+
BackupsRelativePath = "backups"
1112

1213
// SendTransactionMethodName https://docs.walletconnect.com/advanced/rpc-reference/ethereum-rpc#eth_sendtransaction
1314
SendTransactionMethodName = "eth_sendTransaction"

protocol/common/feature_flags.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,7 @@ type FeatureFlags struct {
3535

3636
// EnableNewsFeed indicates whether we should enable the News Feed polling (this is not the user setting)
3737
EnableNewsFeed bool
38+
39+
// EnableLocalBackup indicates whether we should also back up user data locally
40+
EnableLocalBackup bool
3841
}

protocol/messenger.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3525,6 +3525,13 @@ func (m *Messenger) saveDataAndPrepareResponse(messageState *ReceivedMessageStat
35253525
if ok {
35263526
contactsToSave = append(contactsToSave, contact)
35273527
messageState.Response.AddContact(contact)
3528+
3529+
_, ok := m.allContacts.Load(id)
3530+
if !ok {
3531+
// If the contact is not in the allContacts map, it means it's a new contact
3532+
// and we need to add it to the allContacts map.
3533+
m.allContacts.Store(id, contact)
3534+
}
35283535
}
35293536
return true
35303537
})

protocol/messenger_backup.go

Lines changed: 116 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package protocol
22

33
import (
44
"context"
5+
"os"
6+
"path/filepath"
57
"time"
68

79
"github.com/golang/protobuf/proto"
@@ -13,7 +15,9 @@ import (
1315
"github.com/status-im/status-go/multiaccounts/settings"
1416
"github.com/status-im/status-go/protocol/common"
1517
"github.com/status-im/status-go/protocol/communities"
18+
"github.com/status-im/status-go/protocol/encryption"
1619
"github.com/status-im/status-go/protocol/protobuf"
20+
v1protocol "github.com/status-im/status-go/protocol/v1"
1721
)
1822

1923
const (
@@ -136,7 +140,7 @@ func (m *Messenger) BackupData(ctx context.Context) (uint64, error) {
136140
},
137141
ProfileDetails: &protobuf.FetchingBackedUpDataDetails{
138142
DataNumber: uint32(0),
139-
TotalNumber: uint32(len(profileToBackup)),
143+
TotalNumber: uint32(1), // Profile is always one
140144
},
141145
SettingsDetails: &protobuf.FetchingBackedUpDataDetails{
142146
DataNumber: uint32(0),
@@ -153,11 +157,14 @@ func (m *Messenger) BackupData(ctx context.Context) (uint64, error) {
153157
}
154158
}
155159

160+
fullBackup := &protobuf.Backup{}
161+
156162
// Update contacts messages encode and dispatch
157163
for i, d := range contactsToBackup {
158164
pb := backupDetailsOnly()
159165
pb.ContactsDetails.DataNumber = uint32(i + 1)
160166
pb.Contacts = d.Contacts
167+
fullBackup.Contacts = append(fullBackup.Contacts, d.Contacts...)
161168
err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
162169
if err != nil {
163170
return 0, err
@@ -169,28 +176,29 @@ func (m *Messenger) BackupData(ctx context.Context) (uint64, error) {
169176
pb := backupDetailsOnly()
170177
pb.CommunitiesDetails.DataNumber = uint32(i + 1)
171178
pb.Communities = d.Communities
179+
fullBackup.Communities = append(fullBackup.Communities, d.Communities...)
172180
err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
173181
if err != nil {
174182
return 0, err
175183
}
176184
}
177185

178186
// Update profile messages encode and dispatch
179-
for i, d := range profileToBackup {
180-
pb := backupDetailsOnly()
181-
pb.ProfileDetails.DataNumber = uint32(i + 1)
182-
pb.Profile = d.Profile
183-
err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
184-
if err != nil {
185-
return 0, err
186-
}
187+
pb := backupDetailsOnly()
188+
pb.ProfileDetails.DataNumber = uint32(1)
189+
pb.Profile = profileToBackup.Profile
190+
fullBackup.Profile = profileToBackup.Profile
191+
err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
192+
if err != nil {
193+
return 0, err
187194
}
188195

189196
// Update chats encode and dispatch
190197
for i, d := range chatsToBackup {
191198
pb := backupDetailsOnly()
192199
pb.ChatsDetails.DataNumber = uint32(i + 1)
193200
pb.Chats = d.Chats
201+
fullBackup.Chats = append(fullBackup.Chats, d.Chats...)
194202
err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
195203
if err != nil {
196204
return 0, err
@@ -202,6 +210,8 @@ func (m *Messenger) BackupData(ctx context.Context) (uint64, error) {
202210
pb := backupDetailsOnly()
203211
pb.SettingsDetails.DataNumber = uint32(i + 1)
204212
pb.Setting = d
213+
// TODO find a way to get all settings
214+
// fullBackup.Setting = append(fullBackup.Setting, d)
205215
err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
206216
if err != nil {
207217
return 0, err
@@ -213,6 +223,8 @@ func (m *Messenger) BackupData(ctx context.Context) (uint64, error) {
213223
pb := backupDetailsOnly()
214224
pb.KeypairDetails.DataNumber = uint32(i + 1)
215225
pb.Keypair = d.Keypair
226+
// TODO find a way to get all settings
227+
// fullBackup.Keypair = append(fullBackup.Keypair, d.Keypair)
216228
err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
217229
if err != nil {
218230
return 0, err
@@ -224,12 +236,49 @@ func (m *Messenger) BackupData(ctx context.Context) (uint64, error) {
224236
pb := backupDetailsOnly()
225237
pb.WatchOnlyAccountDetails.DataNumber = uint32(i + 1)
226238
pb.WatchOnlyAccount = d.WatchOnlyAccount
239+
// TODO find a way to get all settings
240+
// fullBackup.WatchOnlyAccount = append(fullBackup.WatchOnlyAccount, d.Keypair)
227241
err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
228242
if err != nil {
229243
return 0, err
230244
}
231245
}
232246

247+
if m.config.featureFlags.EnableLocalBackup {
248+
// TODO put file in a constant
249+
path := filepath.Join(m.config.backupConfig.DataDir, "user_data.bkp")
250+
251+
if err := os.MkdirAll(m.config.backupConfig.DataDir, 0700); err != nil {
252+
return 0, err
253+
}
254+
255+
file, err := os.Create(path)
256+
if err != nil {
257+
return 0, err
258+
}
259+
defer file.Close()
260+
261+
mashalledMessage, err := proto.Marshal(fullBackup)
262+
if err != nil {
263+
return 0, err
264+
}
265+
266+
messageSpec, err := m.encryptor.BuildDHMessage(m.identity, &m.identity.PublicKey, mashalledMessage)
267+
if err != nil {
268+
return 0, err
269+
}
270+
271+
encryptedMessage, err := proto.Marshal(messageSpec.Message)
272+
if err != nil {
273+
return 0, err
274+
}
275+
err = os.WriteFile(path, encryptedMessage, 0600)
276+
if err != nil {
277+
m.logger.Error("failed to write backup message to file", zap.Error(err), zap.String("path", path))
278+
return 0, err
279+
}
280+
}
281+
233282
chat.LastClockValue = clock
234283
err = m.saveChat(chat)
235284
if err != nil {
@@ -248,6 +297,62 @@ func (m *Messenger) BackupData(ctx context.Context) (uint64, error) {
248297
return clockInSeconds, nil
249298
}
250299

300+
func (m *Messenger) importLocalBackupFile(filePath string) (*MessengerResponse, error) {
301+
if !m.config.featureFlags.EnableLocalBackup {
302+
return nil, nil
303+
}
304+
305+
// Make sure the backup file exists
306+
content, err := os.ReadFile(filePath)
307+
if err != nil {
308+
return nil, err
309+
}
310+
311+
// Decrypt the backup file
312+
// Unmarshal the content to get the message spec
313+
var messageSpec encryption.ProtocolMessage
314+
err = proto.Unmarshal(content, &messageSpec)
315+
if err != nil {
316+
return nil, err
317+
}
318+
319+
// Decrypt the payload
320+
var defaultMessageID = []byte("default")
321+
decryptedPayload1, err := m.encryptor.HandleMessage(m.identity, &m.identity.PublicKey, &messageSpec, defaultMessageID)
322+
if err != nil {
323+
return nil, err
324+
}
325+
326+
// Unmarshal the decrypted payload to get the backup message
327+
var backupMessage protobuf.Backup
328+
err = proto.Unmarshal(decryptedPayload1.DecryptedMessage, &backupMessage)
329+
if err != nil {
330+
return nil, err
331+
}
332+
333+
// Handle the backup
334+
state := ReceivedMessageState{
335+
Response: &MessengerResponse{},
336+
AllChats: &chatMap{},
337+
AllContacts: &contactMap{
338+
me: m.selfContact,
339+
},
340+
Timesource: m.getTimesource(),
341+
ModifiedContacts: &stringBoolMap{},
342+
ModifiedInstallations: &stringBoolMap{},
343+
}
344+
err = m.HandleBackup(
345+
&state,
346+
&backupMessage,
347+
&v1protocol.StatusMessage{},
348+
)
349+
if err != nil {
350+
return nil, err
351+
}
352+
353+
return m.saveDataAndPrepareResponse(&state)
354+
}
355+
251356
func (m *Messenger) encodeAndDispatchBackupMessage(ctx context.Context, message *protobuf.Backup, chatID string) error {
252357
encodedMessage, err := proto.Marshal(message)
253358
if err != nil {
@@ -447,7 +552,7 @@ func (m *Messenger) buildSyncContactMessage(contact *Contact) *protobuf.SyncInst
447552
}
448553
}
449554

450-
func (m *Messenger) backupProfile(ctx context.Context, clock uint64) ([]*protobuf.Backup, error) {
555+
func (m *Messenger) backupProfile(ctx context.Context, clock uint64) (*protobuf.Backup, error) {
451556
displayName, err := m.settings.DisplayName()
452557
if err != nil {
453558
return nil, err
@@ -515,9 +620,7 @@ func (m *Messenger) backupProfile(ctx context.Context, clock uint64) ([]*protobu
515620
},
516621
}
517622

518-
backupMessages := []*protobuf.Backup{backupMessage}
519-
520-
return backupMessages, nil
623+
return backupMessage, nil
521624
}
522625

523626
func (m *Messenger) backupKeypairs() ([]*protobuf.Backup, error) {

protocol/messenger_backup_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"os"
8+
"path/filepath"
79
"reflect"
810
"testing"
911
"time"
@@ -13,6 +15,7 @@ import (
1315
"github.com/ethereum/go-ethereum/common"
1416
"github.com/ethereum/go-ethereum/event"
1517

18+
"github.com/status-im/status-go/params"
1619
v1protocol "github.com/status-im/status-go/protocol/v1"
1720
"github.com/status-im/status-go/protocol/wakusync"
1821
"github.com/status-im/status-go/services/accounts/accountsevent"
@@ -110,6 +113,76 @@ func (s *MessengerBackupSuite) TestBackupContacts() {
110113
s.Require().Equal(clock, lastBackup)
111114
}
112115

116+
func (s *MessengerBackupSuite) TestBackupContactsLocally() {
117+
backupOptions := []Option{
118+
WithLocalBackup(&params.BackupConfig{
119+
DataDir: filepath.Join(s.tmpdir, params.BackupsRelativePath),
120+
}),
121+
}
122+
123+
// Create bob1
124+
privateKey, err := crypto.GenerateKey()
125+
s.Require().NoError(err)
126+
bob1, err := newMessengerWithKey(s.shh, privateKey, s.logger, backupOptions)
127+
s.Require().NoError(err)
128+
defer TearDownMessenger(&s.Suite, bob1)
129+
130+
// Create bob2
131+
bob2, err := newMessengerWithKey(s.shh, bob1.identity, s.logger, backupOptions)
132+
s.Require().NoError(err)
133+
defer TearDownMessenger(&s.Suite, bob2)
134+
135+
// Make sure there is no backup at first
136+
backupFile := filepath.Join(bob1.config.backupConfig.DataDir, "user_data.bkp")
137+
err = os.RemoveAll(backupFile)
138+
s.Require().NoError(err)
139+
140+
// Create 2 contacts
141+
contact1Key, err := crypto.GenerateKey()
142+
s.Require().NoError(err)
143+
contactID1 := types.EncodeHex(crypto.FromECDSAPub(&contact1Key.PublicKey))
144+
145+
_, err = bob1.AddContact(context.Background(), &requests.AddContact{ID: contactID1})
146+
s.Require().NoError(err)
147+
148+
contact2Key, err := crypto.GenerateKey()
149+
s.Require().NoError(err)
150+
contactID2 := types.EncodeHex(crypto.FromECDSAPub(&contact2Key.PublicKey))
151+
152+
_, err = bob1.AddContact(context.Background(), &requests.AddContact{ID: contactID2})
153+
s.Require().NoError(err)
154+
155+
s.Require().Len(bob1.Contacts(), 2)
156+
157+
actualContacts := bob1.Contacts()
158+
if actualContacts[0].ID == contactID1 {
159+
s.Require().Equal(actualContacts[0].ID, contactID1)
160+
s.Require().Equal(actualContacts[1].ID, contactID2)
161+
} else {
162+
s.Require().Equal(actualContacts[0].ID, contactID2)
163+
s.Require().Equal(actualContacts[1].ID, contactID1)
164+
}
165+
166+
s.Require().Equal(ContactRequestStateSent, actualContacts[0].ContactRequestLocalState)
167+
s.Require().Equal(ContactRequestStateSent, actualContacts[1].ContactRequestLocalState)
168+
s.Require().True(actualContacts[0].added())
169+
s.Require().True(actualContacts[1].added())
170+
171+
// Backup
172+
_, err = bob1.BackupData(context.Background())
173+
s.Require().NoError(err)
174+
175+
// Safety check
176+
s.Require().Len(bob2.Contacts(), 0)
177+
178+
// Import the backup file and process it
179+
response, err := bob2.importLocalBackupFile(backupFile)
180+
s.Require().NoError(err)
181+
s.Require().NotNil(response)
182+
s.Require().Len(response.Contacts, 2)
183+
s.Require().Len(bob2.Contacts(), 2)
184+
}
185+
113186
func (s *MessengerBackupSuite) TestBackupProfile() {
114187
const bob1DisplayName = "bobby"
115188

0 commit comments

Comments
 (0)