autofill/sync/credit_card/
incoming.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*/
5
6use 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
22// Takes a raw payload, as stored in our database, and returns an InternalCreditCard
23// or a tombstone. Credit-cards store the payload as an encrypted string, so we
24// decrypt before conversion.
25fn raw_payload_to_incoming(
26    id: SyncGuid,
27    raw: String,
28    encdec: &EncryptorDecryptor,
29) -> Result<IncomingContent<InternalCreditCard>> {
30    let payload = encdec.decrypt(&raw)?;
31    // Turn it into a BSO
32    let bso = IncomingBso {
33        envelope: IncomingEnvelope {
34            id,
35            modified: ServerTimestamp::default(),
36            sortindex: None,
37            ttl: None,
38        },
39        payload,
40    };
41    // For hysterical raisins, we use an IncomingContent<CCPayload> to convert
42    // to an IncomingContent<InternalCC>
43    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    /// The first step in the "apply incoming" process - stage the records
68    fn stage_incoming(
69        &self,
70        tx: &Transaction<'_>,
71        incoming: Vec<IncomingBso>,
72        signal: &dyn Interruptee,
73    ) -> Result<()> {
74        // Convert the sync15::Payloads to encrypted strings.
75        let to_stage = incoming
76            .into_iter()
77            .map(|bso| {
78                // consider turning this into malformed?
79                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    /// The second step in the "apply incoming" process for syncing autofill CC records.
91    /// Incoming items are retrieved from the temp tables, deserialized, and
92    /// assigned `IncomingState` values.
93    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            // the 'guid' and 's_payload' rows must be non-null.
122            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                        // local record exists, check the state.
131                        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                        // no local record - maybe a tombstone?
145                        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                            // a tombstone in the mirror can be treated as though it's missing.
158                            raw_payload_to_incoming(guid, m_payload, &self.encdec)?.content()
159                        }
160                        None => None,
161                    }
162                },
163            })
164        })
165    }
166
167    /// Returns a local record that has the same values as the given incoming record (with the exception
168    /// of the `guid` values which should differ) that will be used as a local duplicate record for
169    /// syncing.
170    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        // Because we can't check the number in the sql, we fetch all matching
206        // rows and decrypt the numbers here.
207        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    /// Changes the guid of the local record for the given `old_guid` to the given `new_guid` used
236    /// for the `HasLocalDupe` incoming state, and mark the item as dirty.
237    /// We also update the mirror record if it exists in forking scenarios
238    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            // NOTE: the JSON here is the same as stored on the sync server -
278            // the superfluous `entry` is unfortunate but from desktop.
279            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            // incoming tombstone with existing tombstone in the mirror
377            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            // Add required items to the mirrors.
391            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        // Now the same record incoming - it should find the one we just added
529        // above as a dupe.
530        let mut incoming_record = test_record('C', &ci.encdec);
531        // sanity check that the encrypted numbers are different even though
532        // the decrypted numbers are identical.
533        assert_ne!(local_record.cc_number_enc, incoming_record.cc_number_enc);
534        // but the other fields the sql checks are
535        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        // change the incoming guid so we don't immediately think they are the same.
544        incoming_record.guid = SyncGuid::random();
545
546        // expect `Ok(Some(record))`
547        let dupe = ci.get_local_dupe(&tx, &incoming_record).unwrap().unwrap();
548        assert_eq!(dupe.guid, local_guid);
549    }
550
551    // largely the same test as above, but going through the entire plan + apply
552    // cycle.
553    #[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        // Now the same record incoming, but with a different guid. It should
565        // find the local one we just added above as a dupe.
566        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            // LocalRecordInfo::Missing because we don't have a local record with
581            // the incoming GUID.
582            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        // We should have found the local as a dupe.
589        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        // and apply it.
594        crate::sync::apply_incoming_action(&ci, &tx, incoming_action).expect("should apply");
595
596        // and the local record should now have the incoming guid.
597        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        // The incoming payload should've correctly deserialized any unknown_fields into a Map<String,Value>
608        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}