autofill/db/
credit_cards.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 crate::db::{
7    models::{
8        credit_card::{
9            CreditCardMeta, InternalCreditCard, UpdatableCreditCardFields,
10            UpdatableCreditCardFieldsWithMeta,
11        },
12        Metadata,
13    },
14    schema::{CREDIT_CARD_COMMON_COLS, CREDIT_CARD_COMMON_VALS},
15    timestamp_from_millis, with_savepoint, CounterUpdate,
16};
17use crate::error::*;
18
19use jwcrypto::EncryptorDecryptor;
20use rusqlite::{Connection, Transaction};
21use sync_guid::Guid;
22use types::Timestamp;
23
24pub struct CreditCardsDeletionMetrics {
25    pub total_scrubbed_records: u64,
26}
27
28pub(crate) fn add_credit_card(
29    conn: &Connection,
30    new_credit_card_fields: UpdatableCreditCardFields,
31) -> Result<InternalCreditCard> {
32    let now = Timestamp::now();
33
34    // We return an InternalCreditCard, so set it up first, including the
35    // missing fields, before we insert it.
36    let credit_card = InternalCreditCard {
37        guid: Guid::random(),
38        cc_name: new_credit_card_fields.cc_name,
39        cc_number_enc: new_credit_card_fields.cc_number_enc,
40        cc_number_last_4: new_credit_card_fields.cc_number_last_4,
41        cc_exp_month: new_credit_card_fields.cc_exp_month,
42        cc_exp_year: new_credit_card_fields.cc_exp_year,
43        // Credit card types are a fixed set of strings as defined in the link below
44        // (https://searchfox.org/mozilla-central/rev/7ef5cefd0468b8f509efe38e0212de2398f4c8b3/toolkit/modules/CreditCard.jsm#9-22)
45        cc_type: new_credit_card_fields.cc_type,
46        metadata: Metadata {
47            time_created: now,
48            time_last_modified: now,
49            ..Default::default()
50        },
51    };
52
53    let tx = conn.unchecked_transaction()?;
54    add_internal_credit_card(&tx, &credit_card)?;
55    tx.commit()?;
56    Ok(credit_card)
57}
58
59/// Adds a credit card **including metadata**, taking the guid, timestamps and
60/// sync change counter from the caller rather than generating them. Normally you
61/// will use `add_credit_card` instead; this is for importing records from
62/// another store that already have metadata.
63///
64/// `cc_number_enc` is stored exactly as given and is not checked against the
65/// store's key, matching `add_credit_card`. An importing application owns the
66/// ciphertext it supplies.
67pub(crate) fn add_credit_card_with_meta(
68    conn: &Connection,
69    fields: UpdatableCreditCardFields,
70    meta: CreditCardMeta,
71) -> Result<InternalCreditCard> {
72    let tx = conn.unchecked_transaction()?;
73    let card = internal_credit_card_from_meta(fields, &meta);
74    add_internal_credit_card(&tx, &card)?;
75    tx.commit()?;
76    Ok(card)
77}
78
79/// Adds multiple credit cards **including metadata** within a single
80/// transaction. Each record gets its own result, so a record that fails to
81/// insert is reported as `Err(message)` without aborting the rest of the batch.
82pub(crate) fn add_many_credit_cards_with_meta(
83    conn: &Connection,
84    entries: Vec<UpdatableCreditCardFieldsWithMeta>,
85) -> Result<Vec<std::result::Result<InternalCreditCard, String>>> {
86    let tx = conn.unchecked_transaction()?;
87    let mut results = Vec::with_capacity(entries.len());
88    for entry in entries {
89        let card = internal_credit_card_from_meta(entry.fields, &entry.meta);
90        match with_savepoint(&tx, || add_internal_credit_card(&tx, &card))? {
91            Ok(()) => results.push(Ok(card)),
92            Err(e) => results.push(Err(e.to_string())),
93        }
94    }
95    tx.commit()?;
96    Ok(results)
97}
98
99/// Removes every credit card and every credit card tombstone, in one
100/// transaction.
101///
102/// Deleting the rows alone is not enough. A delete leaves a tombstone behind for
103/// any guid the sync mirror knows, and the insert trigger then rejects re-adding
104/// that guid, so a wipe that kept them could not be followed by a re-import of
105/// the same records. Clearing both tables is what makes the wipe repeatable.
106pub(crate) fn delete_all_credit_cards(conn: &Connection) -> Result<()> {
107    let tx = conn.unchecked_transaction()?;
108    tx.execute("DELETE FROM credit_cards_data", [])?;
109    // After the data, so the tombstones the delete trigger just created go too.
110    tx.execute("DELETE FROM credit_cards_tombstones", [])?;
111    tx.commit()?;
112    Ok(())
113}
114
115/// Adds tombstones for records that were deleted locally but not yet uploaded,
116/// within a single transaction and with a result per record. `time_deleted` comes
117/// from the caller rather than being stamped as now, so that a deletion imported
118/// from another store keeps its original time. Without the tombstone the next
119/// sync has nothing to say the record was deleted and takes the server copy.
120pub(crate) fn add_many_credit_card_tombstones(
121    conn: &Connection,
122    tombstones: Vec<(String, i64)>,
123) -> Result<Vec<std::result::Result<String, String>>> {
124    let tx = conn.unchecked_transaction()?;
125    let mut results = Vec::with_capacity(tombstones.len());
126    for (guid, time_deleted) in tombstones {
127        let inserted = with_savepoint(&tx, || {
128            tx.execute(
129                "INSERT INTO credit_cards_tombstones (guid, time_deleted)
130                 VALUES (:guid, :time_deleted)",
131                rusqlite::named_params! {
132                    ":guid": &guid,
133                    ":time_deleted": timestamp_from_millis(time_deleted),
134                },
135            )?;
136            Ok(())
137        })?;
138        match inserted {
139            Ok(()) => results.push(Ok(guid)),
140            Err(e) => results.push(Err(e.to_string())),
141        }
142    }
143    tx.commit()?;
144    Ok(results)
145}
146
147fn internal_credit_card_from_meta(
148    fields: UpdatableCreditCardFields,
149    meta: &CreditCardMeta,
150) -> InternalCreditCard {
151    InternalCreditCard {
152        guid: Guid::new(&meta.guid),
153        cc_name: fields.cc_name,
154        cc_number_enc: fields.cc_number_enc,
155        cc_number_last_4: fields.cc_number_last_4,
156        cc_exp_month: fields.cc_exp_month,
157        cc_exp_year: fields.cc_exp_year,
158        cc_type: fields.cc_type,
159        metadata: Metadata {
160            time_created: timestamp_from_millis(meta.time_created),
161            time_last_used: timestamp_from_millis(meta.time_last_used.unwrap_or(0)),
162            time_last_modified: timestamp_from_millis(meta.time_last_modified),
163            times_used: meta.times_used,
164            sync_change_counter: meta.sync_change_counter,
165        },
166    }
167}
168
169/// Updates a credit card **including metadata**, setting both its fields and its
170/// timestamps and `times_used` to the supplied values. Normally you will use
171/// `update_credit_card` instead, which owns the metadata itself; this is for
172/// keeping a record identical to one held in another store. Errors with
173/// `NoSuchRecord` if the guid is absent.
174pub(crate) fn update_credit_card_with_meta(
175    conn: &Connection,
176    fields: UpdatableCreditCardFields,
177    meta: CreditCardMeta,
178) -> Result<()> {
179    let tx = conn.unchecked_transaction()?;
180
181    let card = internal_credit_card_from_meta(fields, &meta);
182    // Checked up front because `update_internal_credit_card` does not report
183    // how many rows it changed.
184    let exists: bool = tx.query_row(
185        "SELECT EXISTS(SELECT 1 FROM credit_cards_data WHERE guid = :guid)",
186        rusqlite::named_params! { ":guid": card.guid },
187        |row| row.get(0),
188    )?;
189    if !exists {
190        return Err(Error::NoSuchRecord(card.guid.to_string()));
191    }
192    update_internal_credit_card(
193        &tx,
194        &card,
195        CounterUpdate::Set(card.metadata.sync_change_counter),
196    )?;
197    tx.commit()?;
198    Ok(())
199}
200
201pub(crate) fn add_internal_credit_card(
202    tx: &Transaction<'_>,
203    card: &InternalCreditCard,
204) -> Result<()> {
205    tx.execute(
206        &format!(
207            "INSERT INTO credit_cards_data (
208                {common_cols},
209                sync_change_counter
210            ) VALUES (
211                {common_vals},
212                :sync_change_counter
213            )",
214            common_cols = CREDIT_CARD_COMMON_COLS,
215            common_vals = CREDIT_CARD_COMMON_VALS,
216        ),
217        rusqlite::named_params! {
218            ":guid": card.guid,
219            ":cc_name": card.cc_name,
220            ":cc_number_enc": card.cc_number_enc,
221            ":cc_number_last_4": card.cc_number_last_4,
222            ":cc_exp_month": card.cc_exp_month,
223            ":cc_exp_year": card.cc_exp_year,
224            ":cc_type": card.cc_type,
225            ":time_created": card.metadata.time_created,
226            ":time_last_used": card.metadata.time_last_used,
227            ":time_last_modified": card.metadata.time_last_modified,
228            ":times_used": card.metadata.times_used,
229            ":sync_change_counter": card.metadata.sync_change_counter,
230        },
231    )?;
232    Ok(())
233}
234
235pub(crate) fn get_credit_card(conn: &Connection, guid: &Guid) -> Result<InternalCreditCard> {
236    let sql = format!(
237        "SELECT
238            {common_cols},
239            sync_change_counter
240        FROM credit_cards_data
241        WHERE guid = :guid",
242        common_cols = CREDIT_CARD_COMMON_COLS
243    );
244
245    conn.query_row(&sql, [guid], InternalCreditCard::from_row)
246        .map_err(|e| match e {
247            rusqlite::Error::QueryReturnedNoRows => Error::NoSuchRecord(guid.to_string()),
248            e => e.into(),
249        })
250}
251
252pub(crate) fn get_all_credit_cards(conn: &Connection) -> Result<Vec<InternalCreditCard>> {
253    let sql = format!(
254        "SELECT
255            {common_cols},
256            sync_change_counter
257        FROM credit_cards_data",
258        common_cols = CREDIT_CARD_COMMON_COLS
259    );
260
261    let mut stmt = conn.prepare(&sql)?;
262    let credit_cards = stmt
263        .query_map([], InternalCreditCard::from_row)?
264        .collect::<std::result::Result<Vec<InternalCreditCard>, _>>()?;
265    Ok(credit_cards)
266}
267
268pub(crate) fn count_all_credit_cards(conn: &Connection) -> Result<i64> {
269    let sql = "SELECT COUNT(*)
270        FROM credit_cards_data";
271
272    let mut stmt = conn.prepare(sql)?;
273    let count: i64 = stmt.query_row([], |row| row.get(0))?;
274    Ok(count)
275}
276
277pub fn update_credit_card(
278    conn: &Connection,
279    guid: &Guid,
280    credit_card: &UpdatableCreditCardFields,
281) -> Result<()> {
282    let tx = conn.unchecked_transaction()?;
283    tx.execute(
284        "UPDATE credit_cards_data
285        SET cc_name                     = :cc_name,
286            cc_number_enc               = :cc_number_enc,
287            cc_number_last_4            = :cc_number_last_4,
288            cc_exp_month                = :cc_exp_month,
289            cc_exp_year                 = :cc_exp_year,
290            cc_type                     = :cc_type,
291            time_last_modified          = :time_last_modified,
292            sync_change_counter         = sync_change_counter + 1
293        WHERE guid                      = :guid",
294        rusqlite::named_params! {
295            ":cc_name": credit_card.cc_name,
296            ":cc_number_enc": credit_card.cc_number_enc,
297            ":cc_number_last_4": credit_card.cc_number_last_4,
298            ":cc_exp_month": credit_card.cc_exp_month,
299            ":cc_exp_year": credit_card.cc_exp_year,
300            ":cc_type": credit_card.cc_type,
301            ":time_last_modified": Timestamp::now(),
302            ":guid": guid,
303        },
304    )?;
305
306    tx.commit()?;
307    Ok(())
308}
309
310/// Updates all fields including metadata - although the change counter gets
311/// slightly special treatment, see `CounterUpdate`.
312pub(crate) fn update_internal_credit_card(
313    tx: &Transaction<'_>,
314    card: &InternalCreditCard,
315    counter: CounterUpdate,
316) -> Result<()> {
317    let (counter_sql, counter_value) = counter.as_sql();
318    tx.execute(
319        &format!(
320            "UPDATE credit_cards_data
321        SET cc_name                     = :cc_name,
322            cc_number_enc               = :cc_number_enc,
323            cc_number_last_4            = :cc_number_last_4,
324            cc_exp_month                = :cc_exp_month,
325            cc_exp_year                 = :cc_exp_year,
326            cc_type                     = :cc_type,
327            time_created                = :time_created,
328            time_last_used              = :time_last_used,
329            time_last_modified          = :time_last_modified,
330            times_used                  = :times_used,
331            sync_change_counter         = {counter_sql}
332        WHERE guid                      = :guid"
333        ),
334        rusqlite::named_params! {
335            ":cc_name": card.cc_name,
336            ":cc_number_enc": card.cc_number_enc,
337            ":cc_number_last_4": card.cc_number_last_4,
338            ":cc_exp_month": card.cc_exp_month,
339            ":cc_exp_year": card.cc_exp_year,
340            ":cc_type": card.cc_type,
341            ":time_created": card.metadata.time_created,
342            ":time_last_used": card.metadata.time_last_used,
343            ":time_last_modified": card.metadata.time_last_modified,
344            ":times_used": card.metadata.times_used,
345            ":counter": counter_value,
346            ":guid": card.guid,
347        },
348    )?;
349    Ok(())
350}
351
352pub fn delete_credit_card(conn: &Connection, guid: &Guid) -> Result<bool> {
353    let tx = conn.unchecked_transaction()?;
354
355    // execute returns how many rows were affected.
356    let exists = tx.execute(
357        "DELETE FROM credit_cards_data
358        WHERE guid = :guid",
359        rusqlite::named_params! {
360            ":guid": guid.as_str(),
361        },
362    )? != 0;
363
364    tx.commit()?;
365    Ok(exists)
366}
367
368pub fn scrub_encrypted_credit_card_data(conn: &Connection) -> Result<()> {
369    let tx = conn.unchecked_transaction()?;
370    tx.execute("UPDATE credit_cards_data SET cc_number_enc = ''", [])?;
371    tx.commit()?;
372    Ok(())
373}
374
375pub fn scrub_undecryptable_credit_card_data_for_remote_replacement(
376    conn: &Connection,
377    local_encryption_key: String,
378) -> Result<CreditCardsDeletionMetrics> {
379    let tx = conn.unchecked_transaction()?;
380    let mut scrubbed_records = 0;
381    let encdec = EncryptorDecryptor::new(local_encryption_key.as_str()).unwrap();
382
383    let undecryptable_record_ids = get_all_credit_cards(conn)?
384        .into_iter()
385        .filter(|credit_card| encdec.decrypt(&credit_card.cc_number_enc).is_err())
386        .map(|credit_card| credit_card.guid)
387        .collect::<Vec<_>>();
388
389    // Reset the cc_number_enc field as well as the meta fields of the record so if the record was previously synced
390    // it will be overwritten
391    sql_support::each_chunk(&undecryptable_record_ids, |chunk, _| -> Result<()> {
392        let scrubbed = tx.execute(
393            &format!(
394                "UPDATE credit_cards_data
395                SET cc_number_enc = '',
396                    time_created = 0,
397                    time_last_used = 0,
398                    time_last_modified = 0,
399                    times_used = 0,
400                    sync_change_counter = 0
401                WHERE guid IN ({})",
402                sql_support::repeat_sql_values(chunk.len())
403            ),
404            rusqlite::params_from_iter(chunk),
405        )?;
406        scrubbed_records += scrubbed;
407        Ok(())
408    })?;
409
410    tx.commit()?;
411    Ok(CreditCardsDeletionMetrics {
412        total_scrubbed_records: scrubbed_records as u64,
413    })
414}
415
416pub fn touch(conn: &Connection, guid: &Guid) -> Result<()> {
417    let tx = conn.unchecked_transaction()?;
418    let now_ms = Timestamp::now();
419
420    tx.execute(
421        "UPDATE credit_cards_data
422        SET time_last_used              = :time_last_used,
423            times_used                  = times_used + 1,
424            sync_change_counter         = sync_change_counter + 1
425        WHERE guid                      = :guid",
426        rusqlite::named_params! {
427            ":time_last_used": now_ms,
428            ":guid": guid.as_str(),
429        },
430    )?;
431
432    tx.commit()?;
433    Ok(())
434}
435
436#[cfg(test)]
437pub(crate) mod tests {
438    use super::*;
439    use crate::db::test::new_mem_db;
440    use crate::encryption::EncryptorDecryptor;
441    use nss_as::ensure_initialized;
442    use sync15::bso::IncomingBso;
443
444    fn meta_test_fields(cc_name: &str) -> UpdatableCreditCardFields {
445        UpdatableCreditCardFields {
446            cc_name: cc_name.to_string(),
447            // The `credit_cards_data` CHECK constraint requires either an empty
448            // string or more than 20 characters, real ciphertext being long.
449            cc_number_enc: "0123456789012345678901234567890".to_string(),
450            cc_number_last_4: "1234".to_string(),
451            cc_exp_month: 4,
452            cc_exp_year: 2030,
453            cc_type: "visa".to_string(),
454        }
455    }
456
457    fn meta_test_meta(guid: &str, sync_change_counter: i64) -> CreditCardMeta {
458        CreditCardMeta {
459            guid: guid.to_string(),
460            time_created: 1000,
461            time_last_used: Some(2000),
462            time_last_modified: 3000,
463            times_used: 4,
464            sync_change_counter,
465        }
466    }
467
468    fn count_cc_tombstones(conn: &Connection, guid: &str) -> Result<i64> {
469        Ok(conn.query_row(
470            "SELECT COUNT(*) FROM credit_cards_tombstones WHERE guid = :guid",
471            rusqlite::named_params! { ":guid": guid },
472            |row| row.get(0),
473        )?)
474    }
475
476    #[test]
477    fn test_credit_card_add_with_meta() -> Result<()> {
478        let db = new_mem_db();
479
480        let saved =
481            add_credit_card_with_meta(&db, meta_test_fields("Jane Doe"), meta_test_meta("abc", 2))?;
482
483        // the supplied guid is used rather than a fresh one being generated.
484        assert_eq!(saved.guid.as_str(), "abc");
485
486        let retrieved = get_credit_card(&db, &Guid::new("abc"))?;
487        assert_eq!(retrieved.cc_name, "Jane Doe");
488        assert_eq!(retrieved.metadata.time_created.as_millis(), 1000);
489        assert_eq!(retrieved.metadata.time_last_used.as_millis(), 2000);
490        assert_eq!(retrieved.metadata.time_last_modified.as_millis(), 3000);
491        assert_eq!(retrieved.metadata.times_used, 4);
492        assert_eq!(retrieved.metadata.sync_change_counter, 2);
493
494        Ok(())
495    }
496
497    #[test]
498    fn test_credit_card_add_with_meta_sanitizes_out_of_range_timestamps() -> Result<()> {
499        let db = new_mem_db();
500
501        // Negative, and the value from bug 2066257 - a negative microsecond
502        // timestamp that a JS consumer already reinterpreted as a u64 and
503        // divided by 1000, so it reaches us as a huge positive number. Both are
504        // "we don't know when", and a `.max(0)` would only catch the first.
505        for (guid, out_of_range) in [("abc", -1), ("def", 18446744071857664)] {
506            let meta = CreditCardMeta {
507                guid: guid.to_string(),
508                time_created: out_of_range,
509                time_last_used: Some(out_of_range),
510                time_last_modified: out_of_range,
511                times_used: 0,
512                sync_change_counter: 0,
513            };
514            add_credit_card_with_meta(&db, meta_test_fields("Jane Doe"), meta)?;
515
516            let retrieved = get_credit_card(&db, &Guid::new(guid))?;
517            assert_eq!(
518                retrieved.metadata.time_created.as_millis(),
519                0,
520                "{out_of_range} survived"
521            );
522            assert_eq!(retrieved.metadata.time_last_used.as_millis(), 0);
523            assert_eq!(retrieved.metadata.time_last_modified.as_millis(), 0);
524        }
525
526        Ok(())
527    }
528
529    /// Surface 2: a value already on disk, put there before the import path
530    /// sanitized anything. Reading it must repair rather than propagate it.
531    #[test]
532    fn test_credit_card_from_row_sanitizes_corrupt_timestamps() -> Result<()> {
533        let db = new_mem_db();
534
535        let card = add_credit_card(&db, meta_test_fields("Jane Doe"))?;
536        db.execute(
537            // Three shapes that are not representable dates: the u64-reinterpreted
538            // value from bug 2066257, a raw negative, and MAX_DATE_MS + 1.
539            "UPDATE credit_cards_data
540             SET time_created = 18446744071857664,
541                 time_last_used = -1,
542                 time_last_modified = 8640000000000001
543             WHERE guid = :guid",
544            rusqlite::named_params! { ":guid": card.guid },
545        )?;
546
547        let retrieved = get_credit_card(&db, &card.guid)?;
548        assert_eq!(retrieved.metadata.time_created.as_millis(), 0);
549        assert_eq!(retrieved.metadata.time_last_used.as_millis(), 0);
550        assert_eq!(retrieved.metadata.time_last_modified.as_millis(), 0);
551
552        Ok(())
553    }
554
555    #[test]
556    fn test_credit_card_update_with_meta_keeps_supplied_counter() -> Result<()> {
557        let db = new_mem_db();
558
559        add_credit_card_with_meta(&db, meta_test_fields("Jane Doe"), meta_test_meta("abc", 0))?;
560
561        // the supplied counter must be applied, not the one already in the row.
562        update_credit_card_with_meta(
563            &db,
564            meta_test_fields("Jane Q. Doe"),
565            meta_test_meta("abc", 1),
566        )?;
567
568        let retrieved = get_credit_card(&db, &Guid::new("abc"))?;
569        assert_eq!(retrieved.cc_name, "Jane Q. Doe");
570        assert_eq!(retrieved.metadata.sync_change_counter, 1);
571
572        // and back down again.
573        update_credit_card_with_meta(
574            &db,
575            meta_test_fields("Jane Q. Doe"),
576            meta_test_meta("abc", 0),
577        )?;
578        assert_eq!(
579            get_credit_card(&db, &Guid::new("abc"))?
580                .metadata
581                .sync_change_counter,
582            0
583        );
584
585        Ok(())
586    }
587
588    #[test]
589    fn test_credit_card_update_with_meta_errors_when_missing() -> Result<()> {
590        let db = new_mem_db();
591
592        let result = update_credit_card_with_meta(
593            &db,
594            meta_test_fields("Jane Doe"),
595            meta_test_meta("abc", 3),
596        );
597        assert!(matches!(result, Err(Error::NoSuchRecord(guid)) if guid == "abc"));
598        assert!(get_credit_card(&db, &Guid::new("abc")).is_err());
599
600        Ok(())
601    }
602
603    #[test]
604    fn test_credit_card_add_many_with_meta_isolates_failures() -> Result<()> {
605        let db = new_mem_db();
606
607        // the second entry has an empty guid, which the `credit_cards_data`
608        // CHECK constraint rejects. The others must still be inserted.
609        let results = add_many_credit_cards_with_meta(
610            &db,
611            vec![
612                UpdatableCreditCardFieldsWithMeta {
613                    fields: meta_test_fields("One"),
614                    meta: meta_test_meta("aaa", 1),
615                },
616                UpdatableCreditCardFieldsWithMeta {
617                    fields: meta_test_fields("Two"),
618                    meta: meta_test_meta("", 1),
619                },
620                UpdatableCreditCardFieldsWithMeta {
621                    fields: meta_test_fields("Three"),
622                    meta: meta_test_meta("ccc", 1),
623                },
624            ],
625        )?;
626
627        assert_eq!(results.len(), 3);
628        assert!(results[0].is_ok());
629        assert!(results[1].is_err());
630        assert!(results[2].is_ok());
631        assert_eq!(get_all_credit_cards(&db)?.len(), 2);
632
633        Ok(())
634    }
635
636    #[test]
637    fn test_delete_all_credit_cards_allows_a_reimport() -> Result<()> {
638        let db = new_mem_db();
639
640        // A tombstone left by an earlier import, and a record sharing no guid
641        // with it.
642        add_many_credit_card_tombstones(&db, vec![("gone".to_string(), 1234)])?;
643        let card = add_credit_card(&db, meta_test_fields("Jane Doe"))?;
644
645        delete_all_credit_cards(&db)?;
646        assert_eq!(get_all_credit_cards(&db)?.len(), 0);
647        let tombstones: i64 =
648            db.query_row("SELECT COUNT(*) FROM credit_cards_tombstones", [], |row| {
649                row.get(0)
650            })?;
651        assert_eq!(tombstones, 0, "tombstones are cleared with the records");
652
653        // The point of clearing them: re-importing the same guids succeeds,
654        // where the insert trigger would reject a guid still tombstoned.
655        let results = add_many_credit_cards_with_meta(
656            &db,
657            vec![
658                UpdatableCreditCardFieldsWithMeta {
659                    fields: meta_test_fields("Jane Doe"),
660                    meta: CreditCardMeta {
661                        guid: card.guid.to_string(),
662                        ..Default::default()
663                    },
664                },
665                UpdatableCreditCardFieldsWithMeta {
666                    fields: meta_test_fields("Gone"),
667                    meta: CreditCardMeta {
668                        guid: "gone".to_string(),
669                        ..Default::default()
670                    },
671                },
672            ],
673        )?;
674        assert!(
675            results.iter().all(|r| r.is_ok()),
676            "a previously tombstoned guid can be re-imported: {results:?}"
677        );
678
679        Ok(())
680    }
681
682    #[test]
683    fn test_credit_card_add_many_tombstones() -> Result<()> {
684        let db = new_mem_db();
685
686        let results = add_many_credit_card_tombstones(&db, vec![("aaa".to_string(), 1234)])?;
687        assert_eq!(results.len(), 1);
688        assert!(results[0].is_ok());
689
690        // the supplied deletion time is used rather than being stamped as now.
691        let time_deleted: i64 = db.query_row(
692            "SELECT time_deleted FROM credit_cards_tombstones WHERE guid = 'aaa'",
693            [],
694            |row| row.get(0),
695        )?;
696        assert_eq!(time_deleted, 1234);
697
698        Ok(())
699    }
700
701    #[test]
702    fn test_credit_card_add_many_tombstones_rejects_live_guid() -> Result<()> {
703        let db = new_mem_db();
704
705        add_credit_card_with_meta(&db, meta_test_fields("Jane Doe"), meta_test_meta("abc", 0))?;
706
707        // a guid cannot be in both `credit_cards_data` and
708        // `credit_cards_tombstones`; the trigger enforcing that must not take
709        // the rest of the batch down.
710        let results = add_many_credit_card_tombstones(
711            &db,
712            vec![("abc".to_string(), 1234), ("ddd".to_string(), 5678)],
713        )?;
714
715        assert_eq!(results.len(), 2);
716        assert!(results[0].is_err());
717        assert!(results[1].is_ok());
718
719        // the rejected tombstone must not have been committed anyway - see
720        // `with_savepoint`.
721        assert_eq!(count_cc_tombstones(&db, "abc")?, 0);
722        assert!(get_credit_card(&db, &Guid::new("abc")).is_ok());
723        assert_eq!(count_cc_tombstones(&db, "ddd")?, 1);
724
725        Ok(())
726    }
727
728    #[test]
729    fn test_credit_card_add_many_with_meta_rejects_deleted_guid() -> Result<()> {
730        let db = new_mem_db();
731
732        add_many_credit_card_tombstones(&db, vec![("aaa".to_string(), 1234)])?;
733
734        // the other side of the same invariant: a guid in
735        // `credit_cards_tombstones` cannot be inserted into
736        // `credit_cards_data`.
737        let results = add_many_credit_cards_with_meta(
738            &db,
739            vec![
740                UpdatableCreditCardFieldsWithMeta {
741                    fields: meta_test_fields("One"),
742                    meta: meta_test_meta("aaa", 1),
743                },
744                UpdatableCreditCardFieldsWithMeta {
745                    fields: meta_test_fields("Two"),
746                    meta: meta_test_meta("bbb", 1),
747                },
748            ],
749        )?;
750
751        assert_eq!(results.len(), 2);
752        assert!(results[0].is_err());
753        assert!(results[1].is_ok());
754
755        assert!(get_credit_card(&db, &Guid::new("aaa")).is_err());
756        assert_eq!(get_all_credit_cards(&db)?.len(), 1);
757
758        Ok(())
759    }
760
761    pub fn get_all(
762        conn: &Connection,
763        table_name: String,
764    ) -> rusqlite::Result<Vec<String>, rusqlite::Error> {
765        let mut stmt = conn.prepare(&format!(
766            "SELECT guid FROM {table_name}",
767            table_name = table_name
768        ))?;
769        let rows = stmt.query_map([], |row| row.get(0))?;
770
771        let mut guids = Vec::new();
772        for guid_result in rows {
773            guids.push(guid_result?);
774        }
775
776        Ok(guids)
777    }
778
779    pub fn insert_tombstone_record(
780        conn: &Connection,
781        guid: String,
782    ) -> rusqlite::Result<usize, rusqlite::Error> {
783        conn.execute(
784            "INSERT INTO credit_cards_tombstones (
785                guid,
786                time_deleted
787            ) VALUES (
788                :guid,
789                :time_deleted
790            )",
791            rusqlite::named_params! {
792                ":guid": guid,
793                ":time_deleted": Timestamp::now(),
794            },
795        )
796    }
797
798    pub(crate) fn test_insert_mirror_record(conn: &Connection, bso: IncomingBso) {
799        // This test function is a bit suspect, because credit-cards always
800        // store encrypted records, which this ignores entirely, and stores the
801        // raw payload with a cleartext cc_number.
802        // It's OK for all current test consumers, but it's a bit of a smell...
803        conn.execute(
804            "INSERT INTO credit_cards_mirror (guid, payload)
805             VALUES (:guid, :payload)",
806            rusqlite::named_params! {
807                ":guid": &bso.envelope.id,
808                ":payload": &bso.payload,
809            },
810        )
811        .expect("should insert");
812    }
813
814    #[test]
815    fn test_credit_card_create_and_read() -> Result<()> {
816        ensure_initialized();
817        let db = new_mem_db();
818
819        let saved_credit_card = add_credit_card(
820            &db,
821            UpdatableCreditCardFields {
822                cc_name: "jane doe".to_string(),
823                cc_number_enc: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
824                cc_number_last_4: "1234".to_string(),
825                cc_exp_month: 3,
826                cc_exp_year: 2022,
827                cc_type: "visa".to_string(),
828            },
829        )?;
830
831        // check that the add function populated the guid field
832        assert_ne!(Guid::default(), saved_credit_card.guid);
833
834        // check that the time created and time last modified were set
835        assert_ne!(0, saved_credit_card.metadata.time_created.as_millis());
836        assert_ne!(0, saved_credit_card.metadata.time_last_modified.as_millis());
837
838        // check that sync_change_counter was set to 0.
839        assert_eq!(0, saved_credit_card.metadata.sync_change_counter);
840
841        // get created credit card
842        let retrieved_credit_card = get_credit_card(&db, &saved_credit_card.guid)?;
843
844        assert_eq!(saved_credit_card.guid, retrieved_credit_card.guid);
845        assert_eq!(saved_credit_card.cc_name, retrieved_credit_card.cc_name);
846        assert_eq!(
847            saved_credit_card.cc_number_enc,
848            retrieved_credit_card.cc_number_enc
849        );
850        assert_eq!(
851            saved_credit_card.cc_number_last_4,
852            retrieved_credit_card.cc_number_last_4
853        );
854        assert_eq!(
855            saved_credit_card.cc_exp_month,
856            retrieved_credit_card.cc_exp_month
857        );
858        assert_eq!(
859            saved_credit_card.cc_exp_year,
860            retrieved_credit_card.cc_exp_year
861        );
862        assert_eq!(saved_credit_card.cc_type, retrieved_credit_card.cc_type);
863
864        // converting the created record into a tombstone to check that it's not returned on a second `get_credit_card` call
865        let delete_result = delete_credit_card(&db, &saved_credit_card.guid);
866        assert!(delete_result.is_ok());
867        assert!(delete_result?);
868
869        assert!(get_credit_card(&db, &saved_credit_card.guid).is_err());
870
871        Ok(())
872    }
873
874    #[test]
875    fn test_credit_card_missing_guid() {
876        ensure_initialized();
877        let db = new_mem_db();
878        let guid = Guid::random();
879        let result = get_credit_card(&db, &guid);
880
881        assert_eq!(
882            result.unwrap_err().to_string(),
883            Error::NoSuchRecord(guid.to_string()).to_string()
884        );
885    }
886
887    #[test]
888    fn test_credit_card_read_all() -> Result<()> {
889        ensure_initialized();
890        let db = new_mem_db();
891
892        let saved_credit_card = add_credit_card(
893            &db,
894            UpdatableCreditCardFields {
895                cc_name: "jane doe".to_string(),
896                cc_number_enc: "YYYYYYYYYYYYYYYYYYYYYYYYYYYYY".to_string(),
897                cc_number_last_4: "4321".to_string(),
898                cc_exp_month: 3,
899                cc_exp_year: 2022,
900                cc_type: "visa".to_string(),
901            },
902        )?;
903
904        let saved_credit_card2 = add_credit_card(
905            &db,
906            UpdatableCreditCardFields {
907                cc_name: "john deer".to_string(),
908                cc_number_enc: "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ".to_string(),
909                cc_number_last_4: "6543".to_string(),
910                cc_exp_month: 10,
911                cc_exp_year: 2025,
912                cc_type: "mastercard".to_string(),
913            },
914        )?;
915
916        // creating a third credit card with a tombstone to ensure it's not returned
917        let saved_credit_card3 = add_credit_card(
918            &db,
919            UpdatableCreditCardFields {
920                cc_name: "abraham lincoln".to_string(),
921                cc_number_enc: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
922                cc_number_last_4: "9876".to_string(),
923                cc_exp_month: 1,
924                cc_exp_year: 2024,
925                cc_type: "amex".to_string(),
926            },
927        )?;
928
929        let delete_result = delete_credit_card(&db, &saved_credit_card3.guid);
930        assert!(delete_result.is_ok());
931        assert!(delete_result?);
932
933        let retrieved_credit_cards = get_all_credit_cards(&db)?;
934
935        assert!(!retrieved_credit_cards.is_empty());
936        let expected_number_of_credit_cards = 2;
937        assert_eq!(
938            expected_number_of_credit_cards,
939            retrieved_credit_cards.len()
940        );
941
942        let credit_card_count = count_all_credit_cards(&db)?;
943        assert_eq!(expected_number_of_credit_cards, credit_card_count as usize);
944
945        let retrieved_credit_card_guids = [
946            retrieved_credit_cards[0].guid.as_str(),
947            retrieved_credit_cards[1].guid.as_str(),
948        ];
949        assert!(retrieved_credit_card_guids.contains(&saved_credit_card.guid.as_str()));
950        assert!(retrieved_credit_card_guids.contains(&saved_credit_card2.guid.as_str()));
951
952        Ok(())
953    }
954
955    #[test]
956    fn test_credit_card_update() -> Result<()> {
957        ensure_initialized();
958        let db = new_mem_db();
959
960        let saved_credit_card = add_credit_card(
961            &db,
962            UpdatableCreditCardFields {
963                cc_name: "john deer".to_string(),
964                cc_number_enc: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
965                cc_number_last_4: "4321".to_string(),
966                cc_exp_month: 10,
967                cc_exp_year: 2025,
968                cc_type: "mastercard".to_string(),
969            },
970        )?;
971
972        let expected_cc_name = "john doe".to_string();
973        let update_result = update_credit_card(
974            &db,
975            &saved_credit_card.guid,
976            &UpdatableCreditCardFields {
977                cc_name: expected_cc_name.clone(),
978                cc_number_enc: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
979                cc_number_last_4: "1234".to_string(),
980                cc_type: "mastercard".to_string(),
981                cc_exp_month: 10,
982                cc_exp_year: 2025,
983            },
984        );
985        assert!(update_result.is_ok());
986
987        let updated_credit_card = get_credit_card(&db, &saved_credit_card.guid)?;
988
989        assert_eq!(saved_credit_card.guid, updated_credit_card.guid);
990        assert_eq!(expected_cc_name, updated_credit_card.cc_name);
991
992        //check that the sync_change_counter was incremented
993        assert_eq!(1, updated_credit_card.metadata.sync_change_counter);
994
995        Ok(())
996    }
997
998    #[test]
999    fn test_credit_card_update_internal_credit_card() -> Result<()> {
1000        ensure_initialized();
1001        let mut db = new_mem_db();
1002        let tx = db.transaction()?;
1003
1004        let guid = Guid::random();
1005        add_internal_credit_card(
1006            &tx,
1007            &InternalCreditCard {
1008                guid: guid.clone(),
1009                cc_name: "john deer".to_string(),
1010                cc_number_enc: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
1011                cc_number_last_4: "1234".to_string(),
1012                cc_exp_month: 10,
1013                cc_exp_year: 2025,
1014                cc_type: "mastercard".to_string(),
1015                ..Default::default()
1016            },
1017        )?;
1018
1019        let expected_cc_exp_month = 11;
1020        update_internal_credit_card(
1021            &tx,
1022            &InternalCreditCard {
1023                guid: guid.clone(),
1024                cc_name: "john deer".to_string(),
1025                cc_number_enc: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
1026                cc_number_last_4: "1234".to_string(),
1027                cc_exp_month: expected_cc_exp_month,
1028                cc_exp_year: 2025,
1029                cc_type: "mastercard".to_string(),
1030                ..Default::default()
1031            },
1032            CounterUpdate::Leave,
1033        )?;
1034
1035        let record_exists: bool = tx.query_row(
1036            "SELECT EXISTS (
1037                SELECT 1
1038                FROM credit_cards_data
1039                WHERE guid = :guid
1040                AND cc_exp_month = :cc_exp_month
1041                AND sync_change_counter = 0
1042            )",
1043            [&guid.to_string(), &expected_cc_exp_month.to_string()],
1044            |row| row.get(0),
1045        )?;
1046        assert!(record_exists);
1047
1048        Ok(())
1049    }
1050
1051    #[test]
1052    fn test_credit_card_delete() -> Result<()> {
1053        ensure_initialized();
1054        let db = new_mem_db();
1055        let encdec = EncryptorDecryptor::new_with_random_key().unwrap();
1056
1057        let saved_credit_card = add_credit_card(
1058            &db,
1059            UpdatableCreditCardFields {
1060                cc_name: "john deer".to_string(),
1061                cc_number_enc: encdec.encrypt("1234567812345678")?,
1062                cc_number_last_4: "5678".to_string(),
1063                cc_exp_month: 10,
1064                cc_exp_year: 2025,
1065                cc_type: "mastercard".to_string(),
1066            },
1067        )?;
1068
1069        let delete_result = delete_credit_card(&db, &saved_credit_card.guid);
1070        assert!(delete_result.is_ok());
1071        assert!(delete_result?);
1072
1073        let saved_credit_card2 = add_credit_card(
1074            &db,
1075            UpdatableCreditCardFields {
1076                cc_name: "john doe".to_string(),
1077                cc_number_enc: encdec.encrypt("1234123412341234")?,
1078                cc_number_last_4: "1234".to_string(),
1079                cc_exp_month: 5,
1080                cc_exp_year: 2024,
1081                cc_type: "visa".to_string(),
1082            },
1083        )?;
1084
1085        // create a mirror record to check that a tombstone record is created upon deletion
1086        let cc2_guid = saved_credit_card2.guid.clone();
1087        let payload = saved_credit_card2.into_test_incoming_bso(&encdec, Default::default());
1088
1089        test_insert_mirror_record(&db, payload);
1090
1091        let delete_result2 = delete_credit_card(&db, &cc2_guid);
1092        assert!(delete_result2.is_ok());
1093        assert!(delete_result2?);
1094
1095        // check that a tombstone record exists since the record existed in the mirror
1096        let tombstone_exists: bool = db.query_row(
1097            "SELECT EXISTS (
1098                SELECT 1
1099                FROM credit_cards_tombstones
1100                WHERE guid = :guid
1101            )",
1102            [&cc2_guid],
1103            |row| row.get(0),
1104        )?;
1105        assert!(tombstone_exists);
1106
1107        // remove the tombstone record
1108        db.execute(
1109            "DELETE FROM credit_cards_tombstones
1110            WHERE guid = :guid",
1111            rusqlite::named_params! {
1112                ":guid": cc2_guid,
1113            },
1114        )?;
1115
1116        Ok(())
1117    }
1118
1119    #[test]
1120    fn test_scrub_encrypted_credit_card_data() -> Result<()> {
1121        ensure_initialized();
1122        let db = new_mem_db();
1123        let encdec = EncryptorDecryptor::new_with_random_key().unwrap();
1124        let mut saved_credit_cards = Vec::with_capacity(10);
1125        for _ in 0..5 {
1126            saved_credit_cards.push(add_credit_card(
1127                &db,
1128                UpdatableCreditCardFields {
1129                    cc_name: "john deer".to_string(),
1130                    cc_number_enc: encdec.encrypt("1234567812345678")?,
1131                    cc_number_last_4: "5678".to_string(),
1132                    cc_exp_month: 10,
1133                    cc_exp_year: 2025,
1134                    cc_type: "mastercard".to_string(),
1135                },
1136            )?);
1137        }
1138
1139        scrub_encrypted_credit_card_data(&db)?;
1140        for saved_credit_card in saved_credit_cards.into_iter() {
1141            let retrieved_credit_card = get_credit_card(&db, &saved_credit_card.guid)?;
1142            assert_eq!(retrieved_credit_card.cc_number_enc, "");
1143        }
1144
1145        Ok(())
1146    }
1147
1148    #[test]
1149    fn test_scrub_undecryptable_credit_card_date_for_remote_replacement() -> Result<()> {
1150        ensure_initialized();
1151        let db = new_mem_db();
1152        let old_key = EncryptorDecryptor::create_key()?;
1153        let old_encdec = EncryptorDecryptor::new(&old_key)?;
1154        let key = EncryptorDecryptor::create_key()?;
1155        let encdec = EncryptorDecryptor::new(&key)?;
1156
1157        let undecryptable_credit_card = add_credit_card(
1158            &db,
1159            UpdatableCreditCardFields {
1160                cc_name: "jane doe".to_string(),
1161                cc_number_enc: old_encdec.encrypt("2345678923456789")?,
1162                cc_number_last_4: "6789".to_string(),
1163                cc_exp_month: 9,
1164                cc_exp_year: 2027,
1165                cc_type: "visa".to_string(),
1166            },
1167        )?;
1168
1169        let encrypted_cc_number = encdec.encrypt("567812345678123456781")?;
1170        let credit_card = add_credit_card(
1171            &db,
1172            UpdatableCreditCardFields {
1173                cc_name: "john deer".to_string(),
1174                cc_number_enc: encrypted_cc_number.clone(),
1175                cc_number_last_4: "6781".to_string(),
1176                cc_exp_month: 10,
1177                cc_exp_year: 2025,
1178                cc_type: "mastercard".to_string(),
1179            },
1180        )?;
1181
1182        let metrics = scrub_undecryptable_credit_card_data_for_remote_replacement(&db.writer, key)?;
1183        assert_eq!(metrics.total_scrubbed_records, 1);
1184
1185        let credit_cards = get_all_credit_cards(&db)?;
1186        assert_eq!(credit_cards.len(), 2);
1187
1188        let retrieved_credit_card = get_credit_card(&db, &undecryptable_credit_card.guid)?;
1189        assert_eq!(retrieved_credit_card.cc_number_enc, "");
1190
1191        let retrieved_credit_card2 = get_credit_card(&db, &credit_card.guid)?;
1192        assert_eq!(retrieved_credit_card2.cc_number_enc, encrypted_cc_number);
1193
1194        Ok(())
1195    }
1196
1197    #[test]
1198    fn test_credit_card_trigger_on_create() -> Result<()> {
1199        ensure_initialized();
1200        let db = new_mem_db();
1201        let tx = db.unchecked_transaction()?;
1202        let guid = Guid::random();
1203
1204        // create a tombstone record
1205        insert_tombstone_record(&db, guid.to_string())?;
1206
1207        // create a new credit card with the tombstone's guid
1208        let credit_card = InternalCreditCard {
1209            guid,
1210            cc_name: "john deer".to_string(),
1211            cc_number_enc: "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW".to_string(),
1212            cc_number_last_4: "6543".to_string(),
1213            cc_exp_month: 10,
1214            cc_exp_year: 2025,
1215            cc_type: "mastercard".to_string(),
1216
1217            ..Default::default()
1218        };
1219
1220        let add_credit_card_result = add_internal_credit_card(&tx, &credit_card);
1221        assert!(add_credit_card_result.is_err());
1222
1223        let expected_error_message = "guid exists in `credit_cards_tombstones`";
1224        assert!(add_credit_card_result
1225            .unwrap_err()
1226            .to_string()
1227            .contains(expected_error_message));
1228
1229        Ok(())
1230    }
1231
1232    #[test]
1233    fn test_credit_card_trigger_on_delete() -> Result<()> {
1234        ensure_initialized();
1235        let db = new_mem_db();
1236        let tx = db.unchecked_transaction()?;
1237        let guid = Guid::random();
1238
1239        // create an credit card
1240        let credit_card = InternalCreditCard {
1241            guid,
1242            cc_name: "jane doe".to_string(),
1243            cc_number_enc: "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW".to_string(),
1244            cc_number_last_4: "6543".to_string(),
1245            cc_exp_month: 3,
1246            cc_exp_year: 2022,
1247            cc_type: "visa".to_string(),
1248            ..Default::default()
1249        };
1250        add_internal_credit_card(&tx, &credit_card)?;
1251
1252        // create a tombstone record with the same guid
1253        let tombstone_result = insert_tombstone_record(&db, credit_card.guid.to_string());
1254
1255        let expected_error_message = "guid exists in `credit_cards_data`";
1256        assert!(tombstone_result
1257            .unwrap_err()
1258            .to_string()
1259            .contains(expected_error_message));
1260
1261        Ok(())
1262    }
1263
1264    #[test]
1265    fn test_credit_card_touch() -> Result<()> {
1266        ensure_initialized();
1267        let db = new_mem_db();
1268        let saved_credit_card = add_credit_card(
1269            &db,
1270            UpdatableCreditCardFields {
1271                cc_name: "john doe".to_string(),
1272                cc_number_enc: "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW".to_string(),
1273                cc_number_last_4: "6543".to_string(),
1274                cc_exp_month: 5,
1275                cc_exp_year: 2024,
1276                cc_type: "visa".to_string(),
1277            },
1278        )?;
1279
1280        assert_eq!(saved_credit_card.metadata.sync_change_counter, 0);
1281        assert_eq!(saved_credit_card.metadata.times_used, 0);
1282
1283        touch(&db, &saved_credit_card.guid)?;
1284
1285        let touched_credit_card = get_credit_card(&db, &saved_credit_card.guid)?;
1286
1287        assert_eq!(touched_credit_card.metadata.sync_change_counter, 1);
1288        assert_eq!(touched_credit_card.metadata.times_used, 1);
1289
1290        Ok(())
1291    }
1292}