1use super::CreditCardPayload;
7use crate::db::credit_cards::{add_internal_credit_card, update_internal_credit_card};
8use crate::db::models::credit_card::InternalCreditCard;
9use crate::db::schema::CREDIT_CARD_COMMON_COLS;
10use crate::encryption::EncryptorDecryptor;
11use crate::error::*;
12use crate::sync::common::*;
13use crate::sync::{
14 IncomingBso, IncomingContent, IncomingEnvelope, IncomingKind, IncomingState, LocalRecordInfo,
15 ProcessIncomingRecordImpl, ServerTimestamp, SyncRecord,
16};
17use interrupt_support::Interruptee;
18use rusqlite::{named_params, Transaction};
19use sql_support::ConnExt;
20use sync_guid::Guid as SyncGuid;
21
22fn raw_payload_to_incoming(
26 id: SyncGuid,
27 raw: String,
28 encdec: &EncryptorDecryptor,
29) -> Result<IncomingContent<InternalCreditCard>> {
30 let payload = encdec.decrypt(&raw)?;
31 let bso = IncomingBso {
33 envelope: IncomingEnvelope {
34 id,
35 modified: ServerTimestamp::default(),
36 sortindex: None,
37 ttl: None,
38 },
39 payload,
40 };
41 let payload_content = bso.into_content::<CreditCardPayload>();
44 Ok(match payload_content.kind {
45 IncomingKind::Content(content) => IncomingContent {
46 envelope: payload_content.envelope,
47 kind: IncomingKind::Content(InternalCreditCard::from_payload(content, encdec)?),
48 },
49 IncomingKind::Tombstone => IncomingContent {
50 envelope: payload_content.envelope,
51 kind: IncomingKind::Tombstone,
52 },
53 IncomingKind::Malformed => IncomingContent {
54 envelope: payload_content.envelope,
55 kind: IncomingKind::Malformed,
56 },
57 })
58}
59
60pub(super) struct IncomingCreditCardsImpl {
61 pub(super) encdec: EncryptorDecryptor,
62}
63
64impl ProcessIncomingRecordImpl for IncomingCreditCardsImpl {
65 type Record = InternalCreditCard;
66
67 fn stage_incoming(
69 &self,
70 tx: &Transaction<'_>,
71 incoming: Vec<IncomingBso>,
72 signal: &dyn Interruptee,
73 ) -> Result<()> {
74 let to_stage = incoming
76 .into_iter()
77 .map(|bso| {
78 let encrypted = self.encdec.encrypt(&bso.payload)?;
80 Ok((bso.envelope.id, encrypted, bso.envelope.modified))
81 })
82 .collect::<Result<_>>()?;
83 common_stage_incoming_records(tx, "credit_cards_sync_staging", to_stage, signal)
84 }
85
86 fn finish_incoming(&self, tx: &Transaction<'_>) -> Result<()> {
87 common_mirror_staged_records(tx, "credit_cards_sync_staging", "credit_cards_mirror")
88 }
89
90 fn fetch_incoming_states(
94 &self,
95 tx: &Transaction<'_>,
96 ) -> Result<Vec<IncomingState<Self::Record>>> {
97 let sql = "
98 SELECT
99 s.guid as guid,
100 l.guid as l_guid,
101 t.guid as t_guid,
102 s.payload as s_payload,
103 m.payload as m_payload,
104 l.cc_name,
105 l.cc_number_enc,
106 l.cc_number_last_4,
107 l.cc_exp_month,
108 l.cc_exp_year,
109 l.cc_type,
110 l.time_created,
111 l.time_last_used,
112 l.time_last_modified,
113 l.times_used,
114 l.sync_change_counter
115 FROM temp.credit_cards_sync_staging s
116 LEFT JOIN credit_cards_mirror m ON s.guid = m.guid
117 LEFT JOIN credit_cards_data l ON s.guid = l.guid
118 LEFT JOIN credit_cards_tombstones t ON s.guid = t.guid";
119
120 tx.query_rows_and_then(sql, [], |row| -> Result<IncomingState<Self::Record>> {
121 let guid: SyncGuid = row.get("guid")?;
123 let incoming =
124 raw_payload_to_incoming(guid.clone(), row.get("s_payload")?, &self.encdec)?;
125 Ok(IncomingState {
126 incoming,
127 local: match row.get_unwrap::<_, Option<String>>("l_guid") {
128 Some(l_guid) => {
129 assert_eq!(l_guid, guid);
130 let record = InternalCreditCard::from_row(row)?;
132 if record.has_scrubbed_data() {
133 LocalRecordInfo::Scrubbed { record }
134 } else {
135 let has_changes = record.metadata().sync_change_counter != 0;
136 if has_changes {
137 LocalRecordInfo::Modified { record }
138 } else {
139 LocalRecordInfo::Unmodified { record }
140 }
141 }
142 }
143 None => {
144 match row.get::<_, Option<String>>("t_guid")? {
146 Some(t_guid) => {
147 assert_eq!(guid, t_guid);
148 LocalRecordInfo::Tombstone { guid: guid.clone() }
149 }
150 None => LocalRecordInfo::Missing,
151 }
152 }
153 },
154 mirror: {
155 match row.get::<_, Option<String>>("m_payload")? {
156 Some(m_payload) => {
157 raw_payload_to_incoming(guid, m_payload, &self.encdec)?.content()
159 }
160 None => None,
161 }
162 },
163 })
164 })
165 }
166
167 fn get_local_dupe(
171 &self,
172 tx: &Transaction<'_>,
173 incoming: &Self::Record,
174 ) -> Result<Option<Self::Record>> {
175 let sql = format!("
176 SELECT
177 {common_cols},
178 sync_change_counter
179 FROM credit_cards_data
180 WHERE
181 -- `guid <> :guid` is a pre-condition for this being called, but...
182 guid <> :guid
183 -- only non-synced records are candidates, which means can't already be in the mirror.
184 AND guid NOT IN (
185 SELECT guid
186 FROM credit_cards_mirror
187 )
188 -- and sql can check the field values (but note we can not meaningfully
189 -- check the encrypted value, as it's different each time it is encrypted)
190 AND cc_name == :cc_name
191 AND cc_number_last_4 == :cc_number_last_4
192 AND cc_exp_month == :cc_exp_month
193 AND cc_exp_year == :cc_exp_year
194 AND cc_type == :cc_type", common_cols = CREDIT_CARD_COMMON_COLS);
195
196 let params = named_params! {
197 ":guid": incoming.guid,
198 ":cc_name": incoming.cc_name,
199 ":cc_number_last_4": incoming.cc_number_last_4,
200 ":cc_exp_month": incoming.cc_exp_month,
201 ":cc_exp_year": incoming.cc_exp_year,
202 ":cc_type": incoming.cc_type,
203 };
204
205 let records = tx.query_rows_and_then(&sql, params, |row| -> Result<Self::Record> {
208 Ok(Self::Record::from_row(row)?)
209 })?;
210
211 let incoming_cc_number = self.encdec.decrypt(&incoming.cc_number_enc)?;
212 for record in records {
213 if self.encdec.decrypt(&record.cc_number_enc)? == incoming_cc_number {
214 return Ok(Some(record));
215 }
216 }
217 Ok(None)
218 }
219
220 fn update_local_record(
221 &self,
222 tx: &Transaction<'_>,
223 new_record: Self::Record,
224 flag_as_changed: bool,
225 ) -> Result<()> {
226 update_internal_credit_card(tx, &new_record, flag_as_changed)?;
227 Ok(())
228 }
229
230 fn insert_local_record(&self, tx: &Transaction<'_>, new_record: Self::Record) -> Result<()> {
231 add_internal_credit_card(tx, &new_record)?;
232 Ok(())
233 }
234
235 fn change_record_guid(
239 &self,
240 tx: &Transaction<'_>,
241 old_guid: &SyncGuid,
242 new_guid: &SyncGuid,
243 ) -> Result<()> {
244 common_change_guid(
245 tx,
246 "credit_cards_data",
247 "credit_cards_mirror",
248 old_guid,
249 new_guid,
250 )
251 }
252
253 fn remove_record(&self, tx: &Transaction<'_>, guid: &SyncGuid) -> Result<()> {
254 common_remove_record(tx, "credit_cards_data", guid)
255 }
256
257 fn remove_tombstone(&self, tx: &Transaction<'_>, guid: &SyncGuid) -> Result<()> {
258 common_remove_record(tx, "credit_cards_tombstones", guid)
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::super::super::test::new_syncable_mem_db;
265 use super::*;
266 use crate::db::credit_cards::get_credit_card;
267 use crate::sync::common::tests::*;
268
269 use error_support::{info, trace};
270 use interrupt_support::NeverInterrupts;
271 use nss::ensure_initialized;
272 use serde_json::{json, Map, Value};
273 use sql_support::ConnExt;
274
275 lazy_static::lazy_static! {
276 static ref TEST_JSON_RECORDS: Map<String, Value> = {
277 let val = json! {{
280 "A" : {
281 "id": expand_test_guid('A'),
282 "entry": {
283 "cc-name": "Mr Me A Person",
284 "cc-number": "1234567812345678",
285 "cc-exp_month": 12,
286 "cc-exp_year": 2021,
287 "cc-type": "Cash!",
288 "version": 3,
289 }
290 },
291 "C" : {
292 "id": expand_test_guid('C'),
293 "entry": {
294 "cc-name": "Mr Me Another Person",
295 "cc-number": "8765432112345678",
296 "cc-exp-month": 1,
297 "cc-exp-year": 2020,
298 "cc-type": "visa",
299 "timeCreated": 0,
300 "timeLastUsed": 0,
301 "timeLastModified": 0,
302 "timesUsed": 0,
303 "version": 3,
304 }
305 },
306 "D" : {
307 "id": expand_test_guid('D'),
308 "entry": {
309 "cc-name": "Mr Me Another Person",
310 "cc-number": "8765432112345678",
311 "cc-exp-month": 1,
312 "cc-exp-year": 2020,
313 "cc-type": "visa",
314 "timeCreated": 0,
315 "timeLastUsed": 0,
316 "timeLastModified": 0,
317 "timesUsed": 0,
318 "version": 3,
319 "foo": "bar",
320 "baz": "qux",
321 }
322 }
323 }};
324 val.as_object().expect("literal is an object").clone()
325 };
326 }
327
328 fn test_json_record(guid_prefix: char) -> Value {
329 TEST_JSON_RECORDS
330 .get(&guid_prefix.to_string())
331 .expect("should exist")
332 .clone()
333 }
334
335 fn test_record(guid_prefix: char, encdec: &EncryptorDecryptor) -> InternalCreditCard {
336 let json = test_json_record(guid_prefix);
337 let payload = serde_json::from_value(json).unwrap();
338 InternalCreditCard::from_payload(payload, encdec).expect("should be valid")
339 }
340
341 #[test]
342 fn test_stage_incoming() -> Result<()> {
343 ensure_initialized();
344 error_support::init_for_tests();
345 let mut db = new_syncable_mem_db();
346 struct TestCase {
347 incoming_records: Vec<Value>,
348 mirror_records: Vec<Value>,
349 expected_record_count: usize,
350 expected_tombstone_count: usize,
351 }
352
353 let test_cases = vec![
354 TestCase {
355 incoming_records: vec![test_json_record('A')],
356 mirror_records: vec![],
357 expected_record_count: 1,
358 expected_tombstone_count: 0,
359 },
360 TestCase {
361 incoming_records: vec![test_json_tombstone('A')],
362 mirror_records: vec![],
363 expected_record_count: 0,
364 expected_tombstone_count: 1,
365 },
366 TestCase {
367 incoming_records: vec![
368 test_json_record('A'),
369 test_json_record('C'),
370 test_json_tombstone('B'),
371 ],
372 mirror_records: vec![],
373 expected_record_count: 2,
374 expected_tombstone_count: 1,
375 },
376 TestCase {
378 incoming_records: vec![test_json_tombstone('B')],
379 mirror_records: vec![test_json_tombstone('B')],
380 expected_record_count: 0,
381 expected_tombstone_count: 1,
382 },
383 ];
384
385 for tc in test_cases {
386 info!("starting new testcase");
387 let tx = db.transaction().unwrap();
388 let encdec = EncryptorDecryptor::new_with_random_key().unwrap();
389
390 let mirror_sql = "INSERT OR REPLACE INTO credit_cards_mirror (guid, payload)
392 VALUES (:guid, :payload)";
393 for payload in tc.mirror_records {
394 tx.execute(
395 mirror_sql,
396 rusqlite::named_params! {
397 ":guid": payload["id"].as_str().unwrap(),
398 ":payload": encdec.encrypt(&payload.to_string())?,
399 },
400 )
401 .expect("should insert mirror record");
402 }
403
404 let ri = IncomingCreditCardsImpl { encdec };
405 ri.stage_incoming(
406 &tx,
407 array_to_incoming(tc.incoming_records),
408 &NeverInterrupts,
409 )?;
410
411 let records = tx.conn().query_rows_and_then(
412 "SELECT * FROM temp.credit_cards_sync_staging;",
413 [],
414 |row| -> Result<IncomingContent<InternalCreditCard>> {
415 let guid: SyncGuid = row.get_unwrap("guid");
416 let enc_payload: String = row.get_unwrap("payload");
417 raw_payload_to_incoming(guid, enc_payload, &ri.encdec)
418 },
419 )?;
420
421 let record_count = records
422 .iter()
423 .filter(|p| !matches!(p.kind, IncomingKind::Tombstone))
424 .count();
425 let tombstone_count = records.len() - record_count;
426 trace!("record count: {record_count}, tombstone count: {tombstone_count}");
427
428 assert_eq!(record_count, tc.expected_record_count);
429 assert_eq!(tombstone_count, tc.expected_tombstone_count);
430
431 ri.fetch_incoming_states(&tx)?;
432
433 tx.execute("DELETE FROM temp.credit_cards_sync_staging;", [])?;
434 }
435 Ok(())
436 }
437
438 #[test]
439 fn test_change_record_guid() -> Result<()> {
440 ensure_initialized();
441 let mut db = new_syncable_mem_db();
442 let tx = db.transaction()?;
443 let ri = IncomingCreditCardsImpl {
444 encdec: EncryptorDecryptor::new_with_random_key().unwrap(),
445 };
446
447 ri.insert_local_record(&tx, test_record('C', &ri.encdec))?;
448
449 ri.change_record_guid(
450 &tx,
451 &SyncGuid::new(&expand_test_guid('C')),
452 &SyncGuid::new(&expand_test_guid('B')),
453 )?;
454 tx.commit()?;
455 assert!(get_credit_card(&db.writer, &expand_test_guid('C').into()).is_err());
456 assert!(get_credit_card(&db.writer, &expand_test_guid('B').into()).is_ok());
457 Ok(())
458 }
459
460 #[test]
461 fn test_get_incoming() {
462 ensure_initialized();
463 let mut db = new_syncable_mem_db();
464 let tx = db.transaction().expect("should get tx");
465 let ci = IncomingCreditCardsImpl {
466 encdec: EncryptorDecryptor::new_with_random_key().unwrap(),
467 };
468 let record = test_record('C', &ci.encdec);
469 let bso = record
470 .clone()
471 .into_test_incoming_bso(&ci.encdec, Default::default());
472 do_test_incoming_same(&ci, &tx, record, bso);
473 }
474
475 #[test]
476 fn test_incoming_tombstone() {
477 ensure_initialized();
478 let mut db = new_syncable_mem_db();
479 let tx = db.transaction().expect("should get tx");
480 let ci = IncomingCreditCardsImpl {
481 encdec: EncryptorDecryptor::new_with_random_key().unwrap(),
482 };
483 do_test_incoming_tombstone(&ci, &tx, test_record('C', &ci.encdec));
484 }
485
486 #[test]
487 fn test_local_data_scrubbed() {
488 ensure_initialized();
489 let mut db = new_syncable_mem_db();
490 let tx = db.transaction().expect("should get tx");
491 let ci = IncomingCreditCardsImpl {
492 encdec: EncryptorDecryptor::new_with_random_key().unwrap(),
493 };
494 let mut scrubbed_record = test_record('A', &ci.encdec);
495 let bso = scrubbed_record
496 .clone()
497 .into_test_incoming_bso(&ci.encdec, Default::default());
498 scrubbed_record.cc_number_enc = "".to_string();
499 do_test_scrubbed_local_data(&ci, &tx, scrubbed_record, bso);
500 }
501
502 #[test]
503 fn test_staged_to_mirror() {
504 ensure_initialized();
505 let mut db = new_syncable_mem_db();
506 let tx = db.transaction().expect("should get tx");
507 let ci = IncomingCreditCardsImpl {
508 encdec: EncryptorDecryptor::new_with_random_key().unwrap(),
509 };
510 let record = test_record('C', &ci.encdec);
511 let bso = record
512 .clone()
513 .into_test_incoming_bso(&ci.encdec, Default::default());
514 do_test_staged_to_mirror(&ci, &tx, record, bso, "credit_cards_mirror");
515 }
516
517 #[test]
518 fn test_find_dupe() {
519 ensure_initialized();
520 let mut db = new_syncable_mem_db();
521 let tx = db.transaction().expect("should get tx");
522 let encdec = EncryptorDecryptor::new_with_random_key().unwrap();
523 let ci = IncomingCreditCardsImpl { encdec };
524 let local_record = test_record('C', &ci.encdec);
525 let local_guid = local_record.guid.clone();
526 ci.insert_local_record(&tx, local_record.clone()).unwrap();
527
528 let mut incoming_record = test_record('C', &ci.encdec);
531 assert_ne!(local_record.cc_number_enc, incoming_record.cc_number_enc);
534 assert_eq!(local_record.cc_name, incoming_record.cc_name);
536 assert_eq!(
537 local_record.cc_number_last_4,
538 incoming_record.cc_number_last_4
539 );
540 assert_eq!(local_record.cc_exp_month, incoming_record.cc_exp_month);
541 assert_eq!(local_record.cc_exp_year, incoming_record.cc_exp_year);
542 assert_eq!(local_record.cc_type, incoming_record.cc_type);
543 incoming_record.guid = SyncGuid::random();
545
546 let dupe = ci.get_local_dupe(&tx, &incoming_record).unwrap().unwrap();
548 assert_eq!(dupe.guid, local_guid);
549 }
550
551 #[test]
554 fn test_find_dupe_applied() {
555 ensure_initialized();
556 let mut db = new_syncable_mem_db();
557 let tx = db.transaction().expect("should get tx");
558 let encdec = EncryptorDecryptor::new_with_random_key().unwrap();
559 let ci = IncomingCreditCardsImpl { encdec };
560 let local_record = test_record('C', &ci.encdec);
561 let local_guid = local_record.guid.clone();
562 ci.insert_local_record(&tx, local_record.clone()).unwrap();
563
564 let incoming_guid = SyncGuid::new(&expand_test_guid('I'));
567 let mut incoming = local_record;
568 incoming.guid = incoming_guid.clone();
569
570 let incoming_state = IncomingState {
571 incoming: IncomingContent {
572 envelope: IncomingEnvelope {
573 id: incoming_guid.clone(),
574 modified: ServerTimestamp::default(),
575 sortindex: None,
576 ttl: None,
577 },
578 kind: IncomingKind::Content(incoming),
579 },
580 local: LocalRecordInfo::Missing,
583 mirror: None,
584 };
585
586 let incoming_action =
587 crate::sync::plan_incoming(&ci, &tx, incoming_state).expect("should get action");
588 assert!(
590 matches!(incoming_action, crate::sync::IncomingAction::UpdateLocalGuid { ref old_guid, record: ref incoming } if *old_guid == local_guid && incoming.guid == incoming_guid)
591 );
592
593 crate::sync::apply_incoming_action(&ci, &tx, incoming_action).expect("should apply");
595
596 tx.commit().expect("should commit");
598 assert!(get_credit_card(&db.writer, &local_guid).is_err());
599 assert!(get_credit_card(&db.writer, &incoming_guid).is_ok());
600 }
601
602 #[test]
603 fn test_get_incoming_unknown_fields() {
604 ensure_initialized();
605 let json = test_json_record('D');
606 let cc_payload = serde_json::from_value::<CreditCardPayload>(json).unwrap();
607 assert_eq!(cc_payload.entry.unknown_fields.len(), 2);
609 assert_eq!(
610 cc_payload
611 .entry
612 .unknown_fields
613 .get("foo")
614 .unwrap()
615 .as_str()
616 .unwrap(),
617 "bar"
618 );
619 }
620}