1use 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 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 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 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 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 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 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 let mut update_plan = UpdatePlan::default();
434 let now = SystemTime::now();
442 let local_modified = now.checked_sub(Duration::from_secs(100)).unwrap();
444 let mirror_timestamp = now.checked_sub(Duration::from_secs(1000)).unwrap();
446 let server_timestamp = now;
448 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 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 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 let mut update_plan = UpdatePlan::default();
501 let now = SystemTime::now();
509 let local_modified = now.checked_sub(Duration::from_secs(1)).unwrap();
511 let mirror_timestamp = now.checked_sub(Duration::from_secs(1000)).unwrap();
513 let server_timestamp = now;
515 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 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 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 let mut update_plan = UpdatePlan::default();
568 let now = SystemTime::now();
576 let local_modified = now.checked_sub(Duration::from_secs(1)).unwrap();
578 let mirror_timestamp = now.checked_sub(Duration::from_secs(1000)).unwrap();
580 let server_timestamp = now;
582 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 insert_encrypted_login(
592 &db,
593 &login,
594 &mirror_login.login,
595 &mirror_login.server_modified,
596 );
597
598 db.delete("login").unwrap();
600
601 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 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 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 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}