logins/sync/
update_plan.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use super::merge::{LocalLogin, MirrorLogin};
6use super::{IncomingLogin, SyncStatus};
7use crate::encryption::EncryptorDecryptor;
8use crate::error::*;
9use crate::util;
10use interrupt_support::SqlInterruptScope;
11use rusqlite::{named_params, Connection};
12use std::time::SystemTime;
13use sync15::ServerTimestamp;
14use sync_guid::Guid;
15
16#[derive(Default, Debug)]
17pub(super) struct UpdatePlan {
18    pub delete_mirror: Vec<Guid>,
19    pub delete_local: Vec<Guid>,
20    pub local_updates: Vec<MirrorLogin>,
21    // the bool is the `is_overridden` flag, the i64 is ServerTimestamp in millis
22    pub mirror_inserts: Vec<(IncomingLogin, i64, bool)>,
23    pub mirror_updates: Vec<(IncomingLogin, i64)>,
24}
25
26impl UpdatePlan {
27    pub fn plan_two_way_merge(
28        &mut self,
29        local: LocalLogin,
30        upstream: (IncomingLogin, ServerTimestamp),
31    ) {
32        match &local {
33            LocalLogin::Tombstone { .. } => {
34                debug!("  ignoring local tombstone, inserting into mirror");
35                self.delete_local.push(upstream.0.guid());
36                self.plan_mirror_insert(upstream.0, upstream.1, false);
37            }
38            LocalLogin::Alive { login, .. } => {
39                debug!("  Conflicting record without shared parent, using newer");
40                let is_override =
41                    login.meta.time_password_changed > upstream.0.login.meta.time_password_changed;
42                self.plan_mirror_insert(upstream.0, upstream.1, is_override);
43                if !is_override {
44                    self.delete_local.push(login.guid());
45                }
46            }
47        }
48    }
49
50    pub fn plan_three_way_merge(
51        &mut self,
52        local: LocalLogin,
53        shared: MirrorLogin,
54        upstream: IncomingLogin,
55        upstream_time: ServerTimestamp,
56        server_now: ServerTimestamp,
57        encdec: &dyn EncryptorDecryptor,
58    ) -> Result<()> {
59        let local_age = SystemTime::now()
60            .duration_since(local.local_modified())
61            .unwrap_or_default();
62        let remote_age = server_now.duration_since(upstream_time).unwrap_or_default();
63
64        let delta = {
65            let upstream_delta = upstream.login.delta(&shared.login, encdec)?;
66            match local {
67                LocalLogin::Tombstone { .. } => {
68                    // If the login was deleted locally, the merged delta is the
69                    // upstream delta. We do this because a user simultaneously deleting their
70                    // login and updating it has two possible outcomes:
71                    //   - A login that was intended to be deleted remains because another update was
72                    //   there
73                    //   - A login that was intended to be updated got deleted
74                    //
75                    //   The second case is arguably worse, where a user could lose their login
76                    //   indefinitely
77                    // So, just like desktop, this acts as though the local login doesn't exist at all.
78                    upstream_delta
79                }
80                LocalLogin::Alive { login, .. } => {
81                    let local_delta = login.delta(&shared.login, encdec)?;
82                    local_delta.merge(upstream_delta, remote_age < local_age)
83                }
84            }
85        };
86
87        // Update mirror to upstream
88        self.mirror_updates
89            .push((upstream, upstream_time.as_millis()));
90        let mut new = shared;
91
92        new.login.apply_delta(delta, encdec)?;
93        new.server_modified = upstream_time;
94        self.local_updates.push(new);
95        Ok(())
96    }
97
98    pub fn plan_delete(&mut self, id: Guid) {
99        self.delete_local.push(id.clone());
100        self.delete_mirror.push(id);
101    }
102
103    pub fn plan_mirror_update(&mut self, upstream: IncomingLogin, time: ServerTimestamp) {
104        self.mirror_updates.push((upstream, time.as_millis()));
105    }
106
107    pub fn plan_mirror_insert(
108        &mut self,
109        upstream: IncomingLogin,
110        time: ServerTimestamp,
111        is_override: bool,
112    ) {
113        self.mirror_inserts
114            .push((upstream, time.as_millis(), is_override));
115    }
116
117    fn perform_deletes(&self, conn: &Connection, scope: &SqlInterruptScope) -> Result<()> {
118        sql_support::each_chunk(&self.delete_local, |chunk, _| -> Result<()> {
119            conn.execute(
120                &format!(
121                    "DELETE FROM loginsL WHERE guid IN ({vars})",
122                    vars = sql_support::repeat_sql_vars(chunk.len())
123                ),
124                rusqlite::params_from_iter(chunk),
125            )?;
126            scope.err_if_interrupted()?;
127            Ok(())
128        })?;
129
130        sql_support::each_chunk(&self.delete_mirror, |chunk, _| {
131            conn.execute(
132                &format!(
133                    "DELETE FROM loginsM WHERE guid IN ({vars})",
134                    vars = sql_support::repeat_sql_vars(chunk.len())
135                ),
136                rusqlite::params_from_iter(chunk),
137            )?;
138            Ok(())
139        })
140    }
141
142    // These aren't batched but probably should be.
143    fn perform_mirror_updates(&self, conn: &Connection, scope: &SqlInterruptScope) -> Result<()> {
144        let sql = "
145            UPDATE loginsM
146            SET server_modified  = :server_modified,
147                enc_unknown_fields = :enc_unknown_fields,
148                httpRealm        = :http_realm,
149                formActionOrigin = :form_action_origin,
150                usernameField    = :username_field,
151                passwordField    = :password_field,
152                origin           = :origin,
153                secFields        = :sec_fields,
154                -- Avoid zeroes if the remote has been overwritten by an older client.
155                timesUsed           = coalesce(nullif(:times_used,            0), timesUsed),
156                timeLastUsed        = coalesce(nullif(:time_last_used,        0), timeLastUsed),
157                timePasswordChanged = coalesce(nullif(:time_password_changed, 0), timePasswordChanged),
158                timeCreated         = coalesce(nullif(:time_created,          0), timeCreated)
159            WHERE guid = :guid
160        ";
161        let mut stmt = conn.prepare_cached(sql)?;
162        for (upstream, timestamp) in &self.mirror_updates {
163            let login = &upstream.login;
164            trace!("Updating mirror {:?}", login.guid_str());
165            stmt.execute(named_params! {
166                ":server_modified": *timestamp,
167                ":enc_unknown_fields": upstream.unknown,
168                ":http_realm": login.fields.http_realm,
169                ":form_action_origin": login.fields.form_action_origin,
170                ":username_field": login.fields.username_field,
171                ":password_field": login.fields.password_field,
172                ":origin": login.fields.origin,
173                ":times_used": login.meta.times_used,
174                ":time_last_used": login.meta.time_last_used,
175                ":time_password_changed": login.meta.time_password_changed,
176                ":time_created": login.meta.time_created,
177                ":guid": login.guid_str(),
178                ":sec_fields": login.sec_fields,
179            })?;
180            scope.err_if_interrupted()?;
181        }
182        Ok(())
183    }
184
185    fn perform_mirror_inserts(&self, conn: &Connection, scope: &SqlInterruptScope) -> Result<()> {
186        let sql = "
187            INSERT OR IGNORE INTO loginsM (
188                is_overridden,
189                server_modified,
190                enc_unknown_fields,
191
192                httpRealm,
193                formActionOrigin,
194                usernameField,
195                passwordField,
196                origin,
197                secFields,
198
199                timesUsed,
200                timeLastUsed,
201                timePasswordChanged,
202                timeCreated,
203
204                guid
205            ) VALUES (
206                :is_overridden,
207                :server_modified,
208                :enc_unknown_fields,
209
210                :http_realm,
211                :form_action_origin,
212                :username_field,
213                :password_field,
214                :origin,
215                :sec_fields,
216
217                :times_used,
218                :time_last_used,
219                :time_password_changed,
220                :time_created,
221
222                :guid
223            )";
224        let mut stmt = conn.prepare_cached(sql)?;
225
226        for (upstream, timestamp, is_overridden) in &self.mirror_inserts {
227            let login = &upstream.login;
228            trace!("Inserting mirror {:?}", login.guid_str());
229            stmt.execute(named_params! {
230                ":is_overridden": *is_overridden,
231                ":server_modified": *timestamp,
232                ":enc_unknown_fields": upstream.unknown,
233                ":http_realm": login.fields.http_realm,
234                ":form_action_origin": login.fields.form_action_origin,
235                ":username_field": login.fields.username_field,
236                ":password_field": login.fields.password_field,
237                ":origin": login.fields.origin,
238                ":times_used": login.meta.times_used,
239                ":time_last_used": login.meta.time_last_used,
240                ":time_password_changed": login.meta.time_password_changed,
241                ":time_created": login.meta.time_created,
242                ":guid": login.guid_str(),
243                ":sec_fields": login.sec_fields,
244            })?;
245            scope.err_if_interrupted()?;
246        }
247        Ok(())
248    }
249
250    fn perform_local_updates(&self, conn: &Connection, scope: &SqlInterruptScope) -> Result<()> {
251        let sql = format!(
252            "UPDATE loginsL
253             SET local_modified      = :local_modified,
254                 httpRealm           = :http_realm,
255                 formActionOrigin    = :form_action_origin,
256                 usernameField       = :username_field,
257                 passwordField       = :password_field,
258                 timeLastUsed        = :time_last_used,
259                 timePasswordChanged = :time_password_changed,
260                 timesUsed           = :times_used,
261                 origin              = :origin,
262                 secFields           = :sec_fields,
263                 sync_status         = {changed},
264                 is_deleted          = 0
265             WHERE guid = :guid",
266            changed = SyncStatus::Changed as u8
267        );
268        let mut stmt = conn.prepare_cached(&sql)?;
269        // XXX OutgoingChangeset should no longer have timestamp.
270        let local_ms: i64 = util::system_time_ms_i64(SystemTime::now());
271        for l in &self.local_updates {
272            trace!("Updating local {:?}", l.guid_str());
273            stmt.execute(named_params! {
274                ":local_modified": local_ms,
275                ":http_realm": l.login.fields.http_realm,
276                ":form_action_origin": l.login.fields.form_action_origin,
277                ":username_field": l.login.fields.username_field,
278                ":password_field": l.login.fields.password_field,
279                ":origin": l.login.fields.origin,
280                ":time_last_used": l.login.meta.time_last_used,
281                ":time_password_changed": l.login.meta.time_password_changed,
282                ":times_used": l.login.meta.times_used,
283                ":guid": l.guid_str(),
284                ":sec_fields": l.login.sec_fields,
285            })?;
286            scope.err_if_interrupted()?;
287        }
288        Ok(())
289    }
290
291    pub fn execute(&self, conn: &Connection, scope: &SqlInterruptScope) -> Result<()> {
292        debug!(
293            "UpdatePlan: deleting {} records...",
294            self.delete_local.len()
295        );
296        self.perform_deletes(conn, scope)?;
297        debug!(
298            "UpdatePlan: Updating {} existing mirror records...",
299            self.mirror_updates.len()
300        );
301        self.perform_mirror_updates(conn, scope)?;
302        debug!(
303            "UpdatePlan: Inserting {} new mirror records...",
304            self.mirror_inserts.len()
305        );
306        self.perform_mirror_inserts(conn, scope)?;
307        debug!(
308            "UpdatePlan: Updating {} reconciled local records...",
309            self.local_updates.len()
310        );
311        self.perform_local_updates(conn, scope)?;
312        Ok(())
313    }
314}
315
316#[cfg(not(feature = "keydb"))]
317#[cfg(test)]
318mod tests {
319    use nss::ensure_initialized;
320    use std::time::Duration;
321
322    use super::*;
323    use crate::db::test_utils::{
324        check_local_login, check_mirror_login, get_local_guids, get_mirror_guids,
325        get_server_modified, insert_encrypted_login, insert_login,
326    };
327    use crate::db::LoginDb;
328    use crate::encryption::test_utils::TEST_ENCDEC;
329    use crate::login::test_utils::enc_login;
330
331    fn inc_login(id: &str, password: &str) -> crate::sync::IncomingLogin {
332        IncomingLogin {
333            login: enc_login(id, password),
334            unknown: Default::default(),
335        }
336    }
337
338    #[test]
339    fn test_deletes() {
340        ensure_initialized();
341        let db = LoginDb::open_in_memory();
342        insert_login(&db, "login1", Some("password"), Some("password"));
343        insert_login(&db, "login2", Some("password"), Some("password"));
344        insert_login(&db, "login3", Some("password"), Some("password"));
345        insert_login(&db, "login4", Some("password"), Some("password"));
346
347        UpdatePlan {
348            delete_mirror: vec![Guid::new("login1"), Guid::new("login2")],
349            delete_local: vec![Guid::new("login2"), Guid::new("login3")],
350            ..UpdatePlan::default()
351        }
352        .execute(&db, &db.begin_interrupt_scope().unwrap())
353        .unwrap();
354
355        assert_eq!(get_local_guids(&db), vec!["login1", "login4"]);
356        assert_eq!(get_mirror_guids(&db), vec!["login3", "login4"]);
357    }
358
359    #[test]
360    fn test_mirror_updates() {
361        ensure_initialized();
362        let db = LoginDb::open_in_memory();
363        insert_login(&db, "unchanged", None, Some("password"));
364        insert_login(&db, "changed", None, Some("password"));
365        insert_login(
366            &db,
367            "changed2",
368            Some("new-local-password"),
369            Some("password"),
370        );
371        let initial_modified = get_server_modified(&db, "unchanged");
372
373        UpdatePlan {
374            mirror_updates: vec![
375                (inc_login("changed", "new-password"), 20000),
376                (inc_login("changed2", "new-password2"), 21000),
377            ],
378            ..UpdatePlan::default()
379        }
380        .execute(&db, &db.begin_interrupt_scope().unwrap())
381        .unwrap();
382        check_mirror_login(&db, "unchanged", "password", initial_modified, false);
383        check_mirror_login(&db, "changed", "new-password", 20000, false);
384        check_mirror_login(&db, "changed2", "new-password2", 21000, true);
385    }
386
387    #[test]
388    fn test_mirror_inserts() {
389        ensure_initialized();
390        let db = LoginDb::open_in_memory();
391        UpdatePlan {
392            mirror_inserts: vec![
393                (inc_login("login1", "new-password"), 20000, false),
394                (inc_login("login2", "new-password2"), 21000, true),
395            ],
396            ..UpdatePlan::default()
397        }
398        .execute(&db, &db.begin_interrupt_scope().unwrap())
399        .unwrap();
400        check_mirror_login(&db, "login1", "new-password", 20000, false);
401        check_mirror_login(&db, "login2", "new-password2", 21000, true);
402    }
403
404    #[test]
405    fn test_local_updates() {
406        ensure_initialized();
407        let db = LoginDb::open_in_memory();
408        insert_login(&db, "login", Some("password"), Some("password"));
409        let before_update = util::system_time_ms_i64(SystemTime::now());
410
411        UpdatePlan {
412            local_updates: vec![MirrorLogin {
413                login: enc_login("login", "new-password"),
414                server_modified: ServerTimestamp(10000),
415            }],
416            ..UpdatePlan::default()
417        }
418        .execute(&db, &db.begin_interrupt_scope().unwrap())
419        .unwrap();
420        check_local_login(&db, "login", "new-password", before_update);
421    }
422
423    #[test]
424    fn test_plan_three_way_merge_server_wins() {
425        ensure_initialized();
426        let db = LoginDb::open_in_memory();
427        // First we create our expected logins
428        let login = enc_login("login", "old local password");
429        let mirror_login = enc_login("login", "mirror password");
430        let server_login = enc_login("login", "new upstream password");
431
432        // Then, we create a new, empty update plan
433        let mut update_plan = UpdatePlan::default();
434        // Here, we define all the timestamps, remember, if difference between the
435        // upstream record timestamp and the server timestamp is less than the
436        // difference between the local record timestamp and time **now** then the server wins.
437        //
438        // In other words, if server_time - upstream_time < now - local_record_time then the server
439        // wins. This is because we determine which record to "prefer" based on the "age" of the
440        // update
441        let now = SystemTime::now();
442        // local record's timestamps is now - 100 second, so the local age is 100
443        let local_modified = now.checked_sub(Duration::from_secs(100)).unwrap();
444        // mirror timestamp is not too relevant here, but we set it for completeness
445        let mirror_timestamp = now.checked_sub(Duration::from_secs(1000)).unwrap();
446        // Server's timestamp is now
447        let server_timestamp = now;
448        // Server's record timestamp is now - 1 second, so the server age is: 1
449        // And since the local age is 100, then the server should win.
450        let server_record_timestamp = now.checked_sub(Duration::from_secs(1)).unwrap();
451        let local_login = LocalLogin::Alive {
452            login: login.clone(),
453            local_modified,
454        };
455
456        let mirror_login = MirrorLogin {
457            login: mirror_login,
458            server_modified: mirror_timestamp.try_into().unwrap(),
459        };
460
461        // Lets make sure our local login is in the database, so that it can be updated later
462        insert_encrypted_login(
463            &db,
464            &login,
465            &mirror_login.login,
466            &mirror_login.server_modified,
467        );
468        let upstream_login = IncomingLogin {
469            login: server_login,
470            unknown: None,
471        };
472
473        update_plan
474            .plan_three_way_merge(
475                local_login,
476                mirror_login,
477                upstream_login,
478                server_record_timestamp.try_into().unwrap(),
479                server_timestamp.try_into().unwrap(),
480                &*TEST_ENCDEC,
481            )
482            .unwrap();
483        update_plan
484            .execute(&db, &db.begin_interrupt_scope().unwrap())
485            .unwrap();
486
487        check_local_login(&db, "login", "new upstream password", 0);
488    }
489
490    #[test]
491    fn test_plan_three_way_merge_local_wins() {
492        ensure_initialized();
493        let db = LoginDb::open_in_memory();
494        // First we create our expected logins
495        let login = enc_login("login", "new local password");
496        let mirror_login = enc_login("login", "mirror password");
497        let server_login = enc_login("login", "old upstream password");
498
499        // Then, we create a new, empty update plan
500        let mut update_plan = UpdatePlan::default();
501        // Here, we define all the timestamps, remember, if difference between the
502        // upstream record timestamp and the server timestamp is less than the
503        // difference between the local record timestamp and time **now** then the server wins.
504        //
505        // In other words, if server_time - upstream_time < now - local_record_time then the server
506        // wins. This is because we determine which record to "prefer" based on the "age" of the
507        // update
508        let now = SystemTime::now();
509        // local record's timestamps is now - 1 second, so the local age is 1
510        let local_modified = now.checked_sub(Duration::from_secs(1)).unwrap();
511        // mirror timestamp is not too relevant here, but we set it for completeness
512        let mirror_timestamp = now.checked_sub(Duration::from_secs(1000)).unwrap();
513        // Server's timestamp is now
514        let server_timestamp = now;
515        // Server's record timestamp is now - 500 second, so the server age is: 500
516        // And since the local age is 1, the local record should win!
517        let server_record_timestamp = now.checked_sub(Duration::from_secs(500)).unwrap();
518        let local_login = LocalLogin::Alive {
519            login: login.clone(),
520            local_modified,
521        };
522        let mirror_login = MirrorLogin {
523            login: mirror_login,
524            server_modified: mirror_timestamp.try_into().unwrap(),
525        };
526
527        // Lets make sure our local login is in the database, so that it can be updated later
528        insert_encrypted_login(
529            &db,
530            &login,
531            &mirror_login.login,
532            &mirror_login.server_modified,
533        );
534
535        let upstream_login = IncomingLogin {
536            login: server_login,
537            unknown: None,
538        };
539
540        update_plan
541            .plan_three_way_merge(
542                local_login,
543                mirror_login,
544                upstream_login,
545                server_record_timestamp.try_into().unwrap(),
546                server_timestamp.try_into().unwrap(),
547                &*TEST_ENCDEC,
548            )
549            .unwrap();
550        update_plan
551            .execute(&db, &db.begin_interrupt_scope().unwrap())
552            .unwrap();
553
554        check_local_login(&db, "login", "new local password", 0);
555    }
556
557    #[test]
558    fn test_plan_three_way_merge_local_tombstone_loses() {
559        ensure_initialized();
560        let db = LoginDb::open_in_memory();
561        // First we create our expected logins
562        let login = enc_login("login", "new local password");
563        let mirror_login = enc_login("login", "mirror password");
564        let server_login = enc_login("login", "old upstream password");
565
566        // Then, we create a new, empty update plan
567        let mut update_plan = UpdatePlan::default();
568        // Here, we define all the timestamps, remember, if difference between the
569        // upstream record timestamp and the server timestamp is less than the
570        // difference between the local record timestamp and time **now** then the server wins.
571        //
572        // In other words, if server_time - upstream_time < now - local_record_time then the server
573        // wins. This is because we determine which record to "prefer" based on the "age" of the
574        // update
575        let now = SystemTime::now();
576        // local record's timestamps is now - 1 second, so the local age is 1
577        let local_modified = now.checked_sub(Duration::from_secs(1)).unwrap();
578        // mirror timestamp is not too relevant here, but we set it for completeness
579        let mirror_timestamp = now.checked_sub(Duration::from_secs(1000)).unwrap();
580        // Server's timestamp is now
581        let server_timestamp = now;
582        // Server's record timestamp is now - 500 second, so the server age is: 500
583        // And since the local age is 1, the local record should win!
584        let server_record_timestamp = now.checked_sub(Duration::from_secs(500)).unwrap();
585        let mirror_login = MirrorLogin {
586            login: mirror_login,
587            server_modified: mirror_timestamp.try_into().unwrap(),
588        };
589
590        // Lets make sure our local login is in the database, so that it can be updated later
591        insert_encrypted_login(
592            &db,
593            &login,
594            &mirror_login.login,
595            &mirror_login.server_modified,
596        );
597
598        // Now, lets delete our local login
599        db.delete("login").unwrap();
600
601        // Then, lets set our tombstone
602        let local_login = LocalLogin::Tombstone {
603            id: login.meta.id.clone(),
604            local_modified,
605        };
606
607        let upstream_login = IncomingLogin {
608            login: server_login,
609            unknown: None,
610        };
611
612        update_plan
613            .plan_three_way_merge(
614                local_login,
615                mirror_login,
616                upstream_login,
617                server_record_timestamp.try_into().unwrap(),
618                server_timestamp.try_into().unwrap(),
619                &*TEST_ENCDEC,
620            )
621            .unwrap();
622        update_plan
623            .execute(&db, &db.begin_interrupt_scope().unwrap())
624            .unwrap();
625
626        // Now we verify that even though our login deletion was "younger"
627        // then the upstream modification, the upstream modification wins because
628        // modifications always beat tombstones
629        check_local_login(&db, "login", "old upstream password", 0);
630    }
631
632    #[test]
633    fn test_plan_two_way_merge_local_tombstone_loses() {
634        ensure_initialized();
635        let mut update_plan = UpdatePlan::default();
636        // Ensure the local tombstone is newer than the incoming - it still loses.
637        let local = LocalLogin::Tombstone {
638            id: "login-id".to_string(),
639            local_modified: SystemTime::now(),
640        };
641        let incoming = IncomingLogin {
642            login: enc_login("login-id", "new local password"),
643            unknown: None,
644        };
645
646        update_plan.plan_two_way_merge(local, (incoming, ServerTimestamp::from_millis(1234)));
647
648        // Plan should be to apply the incoming.
649        assert_eq!(update_plan.mirror_inserts.len(), 1);
650        assert_eq!(update_plan.delete_local.len(), 1);
651        assert_eq!(update_plan.delete_mirror.len(), 0);
652    }
653}