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::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
23// Takes a raw payload, as stored in our database, and returns an InternalCreditCard
24// or a tombstone. Credit-cards store the payload as an encrypted string, so we
25// decrypt before conversion.
26fn raw_payload_to_incoming(
27    id: SyncGuid,
28    raw: String,
29    encdec: &EncryptorDecryptor,
30) -> Result<IncomingContent<InternalCreditCard>> {
31    let payload = encdec.decrypt(&raw)?;
32    // Turn it into a BSO
33    let bso = IncomingBso {
34        envelope: IncomingEnvelope {
35            id,
36            modified: ServerTimestamp::default(),
37            sortindex: None,
38            ttl: None,
39        },
40        payload,
41    };
42    // For hysterical raisins, we use an IncomingContent<CCPayload> to convert
43    // to an IncomingContent<InternalCC>
44    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    /// The first step in the "apply incoming" process - stage the records
69    fn stage_incoming(
70        &self,
71        tx: &Transaction<'_>,
72        incoming: Vec<IncomingBso>,
73        signal: &dyn Interruptee,
74    ) -> Result<()> {
75        // Convert the sync15::Payloads to encrypted strings.
76        let to_stage = incoming
77            .into_iter()
78            .map(|bso| {
79                // consider turning this into malformed?
80                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    /// The second step in the "apply incoming" process for syncing autofill CC records.
92    /// Incoming items are retrieved from the temp tables, deserialized, and
93    /// assigned `IncomingState` values.
94    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            // the 'guid' and 's_payload' rows must be non-null.
123            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                        // local record exists, check the state.
132                        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                        // no local record - maybe a tombstone?
146                        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                            // a tombstone in the mirror can be treated as though it's missing.
159                            raw_payload_to_incoming(guid, m_payload, &self.encdec)?.content()
160                        }
161                        None => None,
162                    }
163                },
164            })
165        })
166    }
167
168    /// Returns a local record that has the same values as the given incoming record (with the exception
169    /// of the `guid` values which should differ) that will be used as a local duplicate record for
170    /// syncing.
171    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        // Because we can't check the number in the sql, we fetch all matching
207        // rows and decrypt the numbers here.
208        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    /// Changes the guid of the local record for the given `old_guid` to the given `new_guid` used
245    /// for the `HasLocalDupe` incoming state, and mark the item as dirty.
246    /// We also update the mirror record if it exists in forking scenarios
247    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            // NOTE: the JSON here is the same as stored on the sync server -
287            // the superfluous `entry` is unfortunate but from desktop.
288            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            // incoming tombstone with existing tombstone in the mirror
386            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            // Add required items to the mirrors.
400            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        // Now the same record incoming - it should find the one we just added
538        // above as a dupe.
539        let mut incoming_record = test_record('C', &ci.encdec);
540        // sanity check that the encrypted numbers are different even though
541        // the decrypted numbers are identical.
542        assert_ne!(local_record.cc_number_enc, incoming_record.cc_number_enc);
543        // but the other fields the sql checks are
544        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        // change the incoming guid so we don't immediately think they are the same.
553        incoming_record.guid = SyncGuid::random();
554
555        // expect `Ok(Some(record))`
556        let dupe = ci.get_local_dupe(&tx, &incoming_record).unwrap().unwrap();
557        assert_eq!(dupe.guid, local_guid);
558    }
559
560    // largely the same test as above, but going through the entire plan + apply
561    // cycle.
562    #[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        // Now the same record incoming, but with a different guid. It should
574        // find the local one we just added above as a dupe.
575        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            // LocalRecordInfo::Missing because we don't have a local record with
590            // the incoming GUID.
591            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        // We should have found the local as a dupe.
598        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        // and apply it.
603        crate::sync::apply_incoming_action(&ci, &tx, incoming_action).expect("should apply");
604
605        // and the local record should now have the incoming guid.
606        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        // The incoming payload should've correctly deserialized any unknown_fields into a Map<String,Value>
617        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}