1use crate::db::{
7 models::{
8 credit_card::{InternalCreditCard, UpdatableCreditCardFields},
9 Metadata,
10 },
11 schema::{CREDIT_CARD_COMMON_COLS, CREDIT_CARD_COMMON_VALS},
12};
13use crate::error::*;
14
15use rusqlite::{Connection, Transaction};
16use sync_guid::Guid;
17use types::Timestamp;
18
19pub(crate) fn add_credit_card(
20 conn: &Connection,
21 new_credit_card_fields: UpdatableCreditCardFields,
22) -> Result<InternalCreditCard> {
23 let now = Timestamp::now();
24
25 let credit_card = InternalCreditCard {
28 guid: Guid::random(),
29 cc_name: new_credit_card_fields.cc_name,
30 cc_number_enc: new_credit_card_fields.cc_number_enc,
31 cc_number_last_4: new_credit_card_fields.cc_number_last_4,
32 cc_exp_month: new_credit_card_fields.cc_exp_month,
33 cc_exp_year: new_credit_card_fields.cc_exp_year,
34 cc_type: new_credit_card_fields.cc_type,
37 metadata: Metadata {
38 time_created: now,
39 time_last_modified: now,
40 ..Default::default()
41 },
42 };
43
44 let tx = conn.unchecked_transaction()?;
45 add_internal_credit_card(&tx, &credit_card)?;
46 tx.commit()?;
47 Ok(credit_card)
48}
49
50pub(crate) fn add_internal_credit_card(
51 tx: &Transaction<'_>,
52 card: &InternalCreditCard,
53) -> Result<()> {
54 tx.execute(
55 &format!(
56 "INSERT INTO credit_cards_data (
57 {common_cols},
58 sync_change_counter
59 ) VALUES (
60 {common_vals},
61 :sync_change_counter
62 )",
63 common_cols = CREDIT_CARD_COMMON_COLS,
64 common_vals = CREDIT_CARD_COMMON_VALS,
65 ),
66 rusqlite::named_params! {
67 ":guid": card.guid,
68 ":cc_name": card.cc_name,
69 ":cc_number_enc": card.cc_number_enc,
70 ":cc_number_last_4": card.cc_number_last_4,
71 ":cc_exp_month": card.cc_exp_month,
72 ":cc_exp_year": card.cc_exp_year,
73 ":cc_type": card.cc_type,
74 ":time_created": card.metadata.time_created,
75 ":time_last_used": card.metadata.time_last_used,
76 ":time_last_modified": card.metadata.time_last_modified,
77 ":times_used": card.metadata.times_used,
78 ":sync_change_counter": card.metadata.sync_change_counter,
79 },
80 )?;
81 Ok(())
82}
83
84pub(crate) fn get_credit_card(conn: &Connection, guid: &Guid) -> Result<InternalCreditCard> {
85 let sql = format!(
86 "SELECT
87 {common_cols},
88 sync_change_counter
89 FROM credit_cards_data
90 WHERE guid = :guid",
91 common_cols = CREDIT_CARD_COMMON_COLS
92 );
93
94 conn.query_row(&sql, [guid], InternalCreditCard::from_row)
95 .map_err(|e| match e {
96 rusqlite::Error::QueryReturnedNoRows => Error::NoSuchRecord(guid.to_string()),
97 e => e.into(),
98 })
99}
100
101pub(crate) fn get_all_credit_cards(conn: &Connection) -> Result<Vec<InternalCreditCard>> {
102 let sql = format!(
103 "SELECT
104 {common_cols},
105 sync_change_counter
106 FROM credit_cards_data",
107 common_cols = CREDIT_CARD_COMMON_COLS
108 );
109
110 let mut stmt = conn.prepare(&sql)?;
111 let credit_cards = stmt
112 .query_map([], InternalCreditCard::from_row)?
113 .collect::<std::result::Result<Vec<InternalCreditCard>, _>>()?;
114 Ok(credit_cards)
115}
116
117pub fn update_credit_card(
118 conn: &Connection,
119 guid: &Guid,
120 credit_card: &UpdatableCreditCardFields,
121) -> Result<()> {
122 let tx = conn.unchecked_transaction()?;
123 tx.execute(
124 "UPDATE credit_cards_data
125 SET cc_name = :cc_name,
126 cc_number_enc = :cc_number_enc,
127 cc_number_last_4 = :cc_number_last_4,
128 cc_exp_month = :cc_exp_month,
129 cc_exp_year = :cc_exp_year,
130 cc_type = :cc_type,
131 time_last_modified = :time_last_modified,
132 sync_change_counter = sync_change_counter + 1
133 WHERE guid = :guid",
134 rusqlite::named_params! {
135 ":cc_name": credit_card.cc_name,
136 ":cc_number_enc": credit_card.cc_number_enc,
137 ":cc_number_last_4": credit_card.cc_number_last_4,
138 ":cc_exp_month": credit_card.cc_exp_month,
139 ":cc_exp_year": credit_card.cc_exp_year,
140 ":cc_type": credit_card.cc_type,
141 ":time_last_modified": Timestamp::now(),
142 ":guid": guid,
143 },
144 )?;
145
146 tx.commit()?;
147 Ok(())
148}
149
150pub(crate) fn update_internal_credit_card(
154 tx: &Transaction<'_>,
155 card: &InternalCreditCard,
156 flag_as_changed: bool,
157) -> Result<()> {
158 let change_counter_increment = flag_as_changed as u32; tx.execute(
160 "UPDATE credit_cards_data
161 SET cc_name = :cc_name,
162 cc_number_enc = :cc_number_enc,
163 cc_number_last_4 = :cc_number_last_4,
164 cc_exp_month = :cc_exp_month,
165 cc_exp_year = :cc_exp_year,
166 cc_type = :cc_type,
167 time_created = :time_created,
168 time_last_used = :time_last_used,
169 time_last_modified = :time_last_modified,
170 times_used = :times_used,
171 sync_change_counter = sync_change_counter + :change_incr
172 WHERE guid = :guid",
173 rusqlite::named_params! {
174 ":cc_name": card.cc_name,
175 ":cc_number_enc": card.cc_number_enc,
176 ":cc_number_last_4": card.cc_number_last_4,
177 ":cc_exp_month": card.cc_exp_month,
178 ":cc_exp_year": card.cc_exp_year,
179 ":cc_type": card.cc_type,
180 ":time_created": card.metadata.time_created,
181 ":time_last_used": card.metadata.time_last_used,
182 ":time_last_modified": card.metadata.time_last_modified,
183 ":times_used": card.metadata.times_used,
184 ":change_incr": change_counter_increment,
185 ":guid": card.guid,
186 },
187 )?;
188 Ok(())
189}
190
191pub fn delete_credit_card(conn: &Connection, guid: &Guid) -> Result<bool> {
192 let tx = conn.unchecked_transaction()?;
193
194 let exists = tx.execute(
196 "DELETE FROM credit_cards_data
197 WHERE guid = :guid",
198 rusqlite::named_params! {
199 ":guid": guid.as_str(),
200 },
201 )? != 0;
202
203 tx.commit()?;
204 Ok(exists)
205}
206
207pub fn scrub_encrypted_credit_card_data(conn: &Connection) -> Result<()> {
208 let tx = conn.unchecked_transaction()?;
209 tx.execute("UPDATE credit_cards_data SET cc_number_enc = ''", [])?;
210 tx.commit()?;
211 Ok(())
212}
213
214pub fn touch(conn: &Connection, guid: &Guid) -> Result<()> {
215 let tx = conn.unchecked_transaction()?;
216 let now_ms = Timestamp::now();
217
218 tx.execute(
219 "UPDATE credit_cards_data
220 SET time_last_used = :time_last_used,
221 times_used = times_used + 1,
222 sync_change_counter = sync_change_counter + 1
223 WHERE guid = :guid",
224 rusqlite::named_params! {
225 ":time_last_used": now_ms,
226 ":guid": guid.as_str(),
227 },
228 )?;
229
230 tx.commit()?;
231 Ok(())
232}
233
234#[cfg(test)]
235pub(crate) mod tests {
236 use super::*;
237 use crate::db::test::new_mem_db;
238 use crate::encryption::EncryptorDecryptor;
239 use nss::ensure_initialized;
240 use sync15::bso::IncomingBso;
241
242 pub fn get_all(
243 conn: &Connection,
244 table_name: String,
245 ) -> rusqlite::Result<Vec<String>, rusqlite::Error> {
246 let mut stmt = conn.prepare(&format!(
247 "SELECT guid FROM {table_name}",
248 table_name = table_name
249 ))?;
250 let rows = stmt.query_map([], |row| row.get(0))?;
251
252 let mut guids = Vec::new();
253 for guid_result in rows {
254 guids.push(guid_result?);
255 }
256
257 Ok(guids)
258 }
259
260 pub fn insert_tombstone_record(
261 conn: &Connection,
262 guid: String,
263 ) -> rusqlite::Result<usize, rusqlite::Error> {
264 conn.execute(
265 "INSERT INTO credit_cards_tombstones (
266 guid,
267 time_deleted
268 ) VALUES (
269 :guid,
270 :time_deleted
271 )",
272 rusqlite::named_params! {
273 ":guid": guid,
274 ":time_deleted": Timestamp::now(),
275 },
276 )
277 }
278
279 pub(crate) fn test_insert_mirror_record(conn: &Connection, bso: IncomingBso) {
280 conn.execute(
285 "INSERT INTO credit_cards_mirror (guid, payload)
286 VALUES (:guid, :payload)",
287 rusqlite::named_params! {
288 ":guid": &bso.envelope.id,
289 ":payload": &bso.payload,
290 },
291 )
292 .expect("should insert");
293 }
294
295 #[test]
296 fn test_credit_card_create_and_read() -> Result<()> {
297 ensure_initialized();
298 let db = new_mem_db();
299
300 let saved_credit_card = add_credit_card(
301 &db,
302 UpdatableCreditCardFields {
303 cc_name: "jane doe".to_string(),
304 cc_number_enc: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".to_string(),
305 cc_number_last_4: "1234".to_string(),
306 cc_exp_month: 3,
307 cc_exp_year: 2022,
308 cc_type: "visa".to_string(),
309 },
310 )?;
311
312 assert_ne!(Guid::default(), saved_credit_card.guid);
314
315 assert_ne!(0, saved_credit_card.metadata.time_created.as_millis());
317 assert_ne!(0, saved_credit_card.metadata.time_last_modified.as_millis());
318
319 assert_eq!(0, saved_credit_card.metadata.sync_change_counter);
321
322 let retrieved_credit_card = get_credit_card(&db, &saved_credit_card.guid)?;
324
325 assert_eq!(saved_credit_card.guid, retrieved_credit_card.guid);
326 assert_eq!(saved_credit_card.cc_name, retrieved_credit_card.cc_name);
327 assert_eq!(
328 saved_credit_card.cc_number_enc,
329 retrieved_credit_card.cc_number_enc
330 );
331 assert_eq!(
332 saved_credit_card.cc_number_last_4,
333 retrieved_credit_card.cc_number_last_4
334 );
335 assert_eq!(
336 saved_credit_card.cc_exp_month,
337 retrieved_credit_card.cc_exp_month
338 );
339 assert_eq!(
340 saved_credit_card.cc_exp_year,
341 retrieved_credit_card.cc_exp_year
342 );
343 assert_eq!(saved_credit_card.cc_type, retrieved_credit_card.cc_type);
344
345 let delete_result = delete_credit_card(&db, &saved_credit_card.guid);
347 assert!(delete_result.is_ok());
348 assert!(delete_result?);
349
350 assert!(get_credit_card(&db, &saved_credit_card.guid).is_err());
351
352 Ok(())
353 }
354
355 #[test]
356 fn test_credit_card_missing_guid() {
357 ensure_initialized();
358 let db = new_mem_db();
359 let guid = Guid::random();
360 let result = get_credit_card(&db, &guid);
361
362 assert_eq!(
363 result.unwrap_err().to_string(),
364 Error::NoSuchRecord(guid.to_string()).to_string()
365 );
366 }
367
368 #[test]
369 fn test_credit_card_read_all() -> Result<()> {
370 ensure_initialized();
371 let db = new_mem_db();
372
373 let saved_credit_card = add_credit_card(
374 &db,
375 UpdatableCreditCardFields {
376 cc_name: "jane doe".to_string(),
377 cc_number_enc: "YYYYYYYYYYYYYYYYYYYYYYYYYYYYY".to_string(),
378 cc_number_last_4: "4321".to_string(),
379 cc_exp_month: 3,
380 cc_exp_year: 2022,
381 cc_type: "visa".to_string(),
382 },
383 )?;
384
385 let saved_credit_card2 = add_credit_card(
386 &db,
387 UpdatableCreditCardFields {
388 cc_name: "john deer".to_string(),
389 cc_number_enc: "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ".to_string(),
390 cc_number_last_4: "6543".to_string(),
391 cc_exp_month: 10,
392 cc_exp_year: 2025,
393 cc_type: "mastercard".to_string(),
394 },
395 )?;
396
397 let saved_credit_card3 = add_credit_card(
399 &db,
400 UpdatableCreditCardFields {
401 cc_name: "abraham lincoln".to_string(),
402 cc_number_enc: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
403 cc_number_last_4: "9876".to_string(),
404 cc_exp_month: 1,
405 cc_exp_year: 2024,
406 cc_type: "amex".to_string(),
407 },
408 )?;
409
410 let delete_result = delete_credit_card(&db, &saved_credit_card3.guid);
411 assert!(delete_result.is_ok());
412 assert!(delete_result?);
413
414 let retrieved_credit_cards = get_all_credit_cards(&db)?;
415
416 assert!(!retrieved_credit_cards.is_empty());
417 let expected_number_of_credit_cards = 2;
418 assert_eq!(
419 expected_number_of_credit_cards,
420 retrieved_credit_cards.len()
421 );
422
423 let retrieved_credit_card_guids = [
424 retrieved_credit_cards[0].guid.as_str(),
425 retrieved_credit_cards[1].guid.as_str(),
426 ];
427 assert!(retrieved_credit_card_guids.contains(&saved_credit_card.guid.as_str()));
428 assert!(retrieved_credit_card_guids.contains(&saved_credit_card2.guid.as_str()));
429
430 Ok(())
431 }
432
433 #[test]
434 fn test_credit_card_update() -> Result<()> {
435 ensure_initialized();
436 let db = new_mem_db();
437
438 let saved_credit_card = add_credit_card(
439 &db,
440 UpdatableCreditCardFields {
441 cc_name: "john deer".to_string(),
442 cc_number_enc: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
443 cc_number_last_4: "4321".to_string(),
444 cc_exp_month: 10,
445 cc_exp_year: 2025,
446 cc_type: "mastercard".to_string(),
447 },
448 )?;
449
450 let expected_cc_name = "john doe".to_string();
451 let update_result = update_credit_card(
452 &db,
453 &saved_credit_card.guid,
454 &UpdatableCreditCardFields {
455 cc_name: expected_cc_name.clone(),
456 cc_number_enc: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
457 cc_number_last_4: "1234".to_string(),
458 cc_type: "mastercard".to_string(),
459 cc_exp_month: 10,
460 cc_exp_year: 2025,
461 },
462 );
463 assert!(update_result.is_ok());
464
465 let updated_credit_card = get_credit_card(&db, &saved_credit_card.guid)?;
466
467 assert_eq!(saved_credit_card.guid, updated_credit_card.guid);
468 assert_eq!(expected_cc_name, updated_credit_card.cc_name);
469
470 assert_eq!(1, updated_credit_card.metadata.sync_change_counter);
472
473 Ok(())
474 }
475
476 #[test]
477 fn test_credit_card_update_internal_credit_card() -> Result<()> {
478 ensure_initialized();
479 let mut db = new_mem_db();
480 let tx = db.transaction()?;
481
482 let guid = Guid::random();
483 add_internal_credit_card(
484 &tx,
485 &InternalCreditCard {
486 guid: guid.clone(),
487 cc_name: "john deer".to_string(),
488 cc_number_enc: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
489 cc_number_last_4: "1234".to_string(),
490 cc_exp_month: 10,
491 cc_exp_year: 2025,
492 cc_type: "mastercard".to_string(),
493 ..Default::default()
494 },
495 )?;
496
497 let expected_cc_exp_month = 11;
498 update_internal_credit_card(
499 &tx,
500 &InternalCreditCard {
501 guid: guid.clone(),
502 cc_name: "john deer".to_string(),
503 cc_number_enc: "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(),
504 cc_number_last_4: "1234".to_string(),
505 cc_exp_month: expected_cc_exp_month,
506 cc_exp_year: 2025,
507 cc_type: "mastercard".to_string(),
508 ..Default::default()
509 },
510 false,
511 )?;
512
513 let record_exists: bool = tx.query_row(
514 "SELECT EXISTS (
515 SELECT 1
516 FROM credit_cards_data
517 WHERE guid = :guid
518 AND cc_exp_month = :cc_exp_month
519 AND sync_change_counter = 0
520 )",
521 [&guid.to_string(), &expected_cc_exp_month.to_string()],
522 |row| row.get(0),
523 )?;
524 assert!(record_exists);
525
526 Ok(())
527 }
528
529 #[test]
530 fn test_credit_card_delete() -> Result<()> {
531 ensure_initialized();
532 let db = new_mem_db();
533 let encdec = EncryptorDecryptor::new_with_random_key().unwrap();
534
535 let saved_credit_card = add_credit_card(
536 &db,
537 UpdatableCreditCardFields {
538 cc_name: "john deer".to_string(),
539 cc_number_enc: encdec.encrypt("1234567812345678")?,
540 cc_number_last_4: "5678".to_string(),
541 cc_exp_month: 10,
542 cc_exp_year: 2025,
543 cc_type: "mastercard".to_string(),
544 },
545 )?;
546
547 let delete_result = delete_credit_card(&db, &saved_credit_card.guid);
548 assert!(delete_result.is_ok());
549 assert!(delete_result?);
550
551 let saved_credit_card2 = add_credit_card(
552 &db,
553 UpdatableCreditCardFields {
554 cc_name: "john doe".to_string(),
555 cc_number_enc: encdec.encrypt("1234123412341234")?,
556 cc_number_last_4: "1234".to_string(),
557 cc_exp_month: 5,
558 cc_exp_year: 2024,
559 cc_type: "visa".to_string(),
560 },
561 )?;
562
563 let cc2_guid = saved_credit_card2.guid.clone();
565 let payload = saved_credit_card2.into_test_incoming_bso(&encdec, Default::default());
566
567 test_insert_mirror_record(&db, payload);
568
569 let delete_result2 = delete_credit_card(&db, &cc2_guid);
570 assert!(delete_result2.is_ok());
571 assert!(delete_result2?);
572
573 let tombstone_exists: bool = db.query_row(
575 "SELECT EXISTS (
576 SELECT 1
577 FROM credit_cards_tombstones
578 WHERE guid = :guid
579 )",
580 [&cc2_guid],
581 |row| row.get(0),
582 )?;
583 assert!(tombstone_exists);
584
585 db.execute(
587 "DELETE FROM credit_cards_tombstones
588 WHERE guid = :guid",
589 rusqlite::named_params! {
590 ":guid": cc2_guid,
591 },
592 )?;
593
594 Ok(())
595 }
596
597 #[test]
598 fn test_scrub_encrypted_credit_card_data() -> Result<()> {
599 ensure_initialized();
600 let db = new_mem_db();
601 let encdec = EncryptorDecryptor::new_with_random_key().unwrap();
602 let mut saved_credit_cards = Vec::with_capacity(10);
603 for _ in 0..5 {
604 saved_credit_cards.push(add_credit_card(
605 &db,
606 UpdatableCreditCardFields {
607 cc_name: "john deer".to_string(),
608 cc_number_enc: encdec.encrypt("1234567812345678")?,
609 cc_number_last_4: "5678".to_string(),
610 cc_exp_month: 10,
611 cc_exp_year: 2025,
612 cc_type: "mastercard".to_string(),
613 },
614 )?);
615 }
616
617 scrub_encrypted_credit_card_data(&db)?;
618 for saved_credit_card in saved_credit_cards.into_iter() {
619 let retrieved_credit_card = get_credit_card(&db, &saved_credit_card.guid)?;
620 assert_eq!(retrieved_credit_card.cc_number_enc, "");
621 }
622
623 Ok(())
624 }
625
626 #[test]
627 fn test_credit_card_trigger_on_create() -> Result<()> {
628 ensure_initialized();
629 let db = new_mem_db();
630 let tx = db.unchecked_transaction()?;
631 let guid = Guid::random();
632
633 insert_tombstone_record(&db, guid.to_string())?;
635
636 let credit_card = InternalCreditCard {
638 guid,
639 cc_name: "john deer".to_string(),
640 cc_number_enc: "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW".to_string(),
641 cc_number_last_4: "6543".to_string(),
642 cc_exp_month: 10,
643 cc_exp_year: 2025,
644 cc_type: "mastercard".to_string(),
645
646 ..Default::default()
647 };
648
649 let add_credit_card_result = add_internal_credit_card(&tx, &credit_card);
650 assert!(add_credit_card_result.is_err());
651
652 let expected_error_message = "guid exists in `credit_cards_tombstones`";
653 assert!(add_credit_card_result
654 .unwrap_err()
655 .to_string()
656 .contains(expected_error_message));
657
658 Ok(())
659 }
660
661 #[test]
662 fn test_credit_card_trigger_on_delete() -> Result<()> {
663 ensure_initialized();
664 let db = new_mem_db();
665 let tx = db.unchecked_transaction()?;
666 let guid = Guid::random();
667
668 let credit_card = InternalCreditCard {
670 guid,
671 cc_name: "jane doe".to_string(),
672 cc_number_enc: "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW".to_string(),
673 cc_number_last_4: "6543".to_string(),
674 cc_exp_month: 3,
675 cc_exp_year: 2022,
676 cc_type: "visa".to_string(),
677 ..Default::default()
678 };
679 add_internal_credit_card(&tx, &credit_card)?;
680
681 let tombstone_result = insert_tombstone_record(&db, credit_card.guid.to_string());
683
684 let expected_error_message = "guid exists in `credit_cards_data`";
685 assert!(tombstone_result
686 .unwrap_err()
687 .to_string()
688 .contains(expected_error_message));
689
690 Ok(())
691 }
692
693 #[test]
694 fn test_credit_card_touch() -> Result<()> {
695 ensure_initialized();
696 let db = new_mem_db();
697 let saved_credit_card = add_credit_card(
698 &db,
699 UpdatableCreditCardFields {
700 cc_name: "john doe".to_string(),
701 cc_number_enc: "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW".to_string(),
702 cc_number_last_4: "6543".to_string(),
703 cc_exp_month: 5,
704 cc_exp_year: 2024,
705 cc_type: "visa".to_string(),
706 },
707 )?;
708
709 assert_eq!(saved_credit_card.metadata.sync_change_counter, 0);
710 assert_eq!(saved_credit_card.metadata.times_used, 0);
711
712 touch(&db, &saved_credit_card.guid)?;
713
714 let touched_credit_card = get_credit_card(&db, &saved_credit_card.guid)?;
715
716 assert_eq!(touched_credit_card.metadata.sync_change_counter, 1);
717 assert_eq!(touched_credit_card.metadata.times_used, 1);
718
719 Ok(())
720 }
721}