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