1use crate::encryption::EncryptorDecryptor;
26use crate::error::*;
27use crate::login::*;
28use crate::schema;
29use crate::sync::SyncStatus;
30use crate::util;
31use interrupt_support::{SqlInterruptHandle, SqlInterruptScope};
32use lazy_static::lazy_static;
33use rusqlite::{
34 named_params,
35 types::{FromSql, ToSql},
36 Connection,
37};
38use sql_support::ConnExt;
39use std::ops::Deref;
40use std::path::Path;
41use std::sync::Arc;
42use std::time::SystemTime;
43use sync_guid::Guid;
44use url::{Host, Url};
45
46pub struct LoginDb {
47 pub db: Connection,
48 pub encdec: Arc<dyn EncryptorDecryptor>,
49 interrupt_handle: Arc<SqlInterruptHandle>,
50}
51
52pub struct LoginsDeletionMetrics {
53 pub local_deleted: u64,
54 pub mirror_deleted: u64,
55}
56
57impl LoginDb {
58 pub fn with_connection(db: Connection, encdec: Arc<dyn EncryptorDecryptor>) -> Result<Self> {
59 #[cfg(test)]
60 {
61 util::init_test_logging();
62 }
63
64 db.set_pragma("temp_store", 2)?;
69
70 let mut logins = Self {
71 interrupt_handle: Arc::new(SqlInterruptHandle::new(&db)),
72 encdec,
73 db,
74 };
75 let tx = logins.db.transaction()?;
76 schema::init(&tx)?;
77 tx.commit()?;
78 Ok(logins)
79 }
80
81 pub fn open(path: impl AsRef<Path>, encdec: Arc<dyn EncryptorDecryptor>) -> Result<Self> {
82 Self::with_connection(Connection::open(path)?, encdec)
83 }
84
85 #[cfg(test)]
86 pub fn open_in_memory() -> Self {
87 let encdec: Arc<dyn EncryptorDecryptor> =
88 crate::encryption::test_utils::TEST_ENCDEC.clone();
89 Self::with_connection(Connection::open_in_memory().unwrap(), encdec).unwrap()
90 }
91
92 pub fn new_interrupt_handle(&self) -> Arc<SqlInterruptHandle> {
93 Arc::clone(&self.interrupt_handle)
94 }
95
96 #[inline]
97 pub fn begin_interrupt_scope(&self) -> Result<SqlInterruptScope> {
98 Ok(self.interrupt_handle.begin_interrupt_scope()?)
99 }
100}
101
102impl ConnExt for LoginDb {
103 #[inline]
104 fn conn(&self) -> &Connection {
105 &self.db
106 }
107}
108
109impl Deref for LoginDb {
110 type Target = Connection;
111 #[inline]
112 fn deref(&self) -> &Connection {
113 &self.db
114 }
115}
116
117impl LoginDb {
120 pub(crate) fn put_meta(&self, key: &str, value: &dyn ToSql) -> Result<()> {
121 self.execute_cached(
122 "REPLACE INTO loginsSyncMeta (key, value) VALUES (:key, :value)",
123 named_params! { ":key": key, ":value": value },
124 )?;
125 Ok(())
126 }
127
128 pub(crate) fn get_meta<T: FromSql>(&self, key: &str) -> Result<Option<T>> {
129 self.try_query_row(
130 "SELECT value FROM loginsSyncMeta WHERE key = :key",
131 named_params! { ":key": key },
132 |row| Ok::<_, Error>(row.get(0)?),
133 true,
134 )
135 }
136
137 pub(crate) fn delete_meta(&self, key: &str) -> Result<()> {
138 self.execute_cached(
139 "DELETE FROM loginsSyncMeta WHERE key = :key",
140 named_params! { ":key": key },
141 )?;
142 Ok(())
143 }
144
145 pub fn count_all(&self) -> Result<i64> {
146 let mut stmt = self.db.prepare_cached(&COUNT_ALL_SQL)?;
147
148 let count: i64 = stmt.query_row([], |row| row.get(0))?;
149 Ok(count)
150 }
151
152 pub fn count_by_origin(&self, origin: &str) -> Result<i64> {
153 match LoginEntry::validate_and_fixup_origin(origin) {
154 Ok(result) => {
155 let origin = result.unwrap_or(origin.to_string());
156 let mut stmt = self.db.prepare_cached(&COUNT_BY_ORIGIN_SQL)?;
157 let count: i64 =
158 stmt.query_row(named_params! { ":origin": origin }, |row| row.get(0))?;
159 Ok(count)
160 }
161 Err(e) => {
162 warn!("count_by_origin was passed an invalid origin: {}", e);
164 Ok(0)
165 }
166 }
167 }
168
169 pub fn count_by_form_action_origin(&self, form_action_origin: &str) -> Result<i64> {
170 match LoginEntry::validate_and_normalize_form_action_origin(form_action_origin) {
171 Ok(result) => {
172 let form_action_origin = result.unwrap_or(form_action_origin.to_string());
173 let mut stmt = self.db.prepare_cached(&COUNT_BY_FORM_ACTION_ORIGIN_SQL)?;
174 let count: i64 = stmt.query_row(
175 named_params! { ":form_action_origin": form_action_origin },
176 |row| row.get(0),
177 )?;
178 Ok(count)
179 }
180 Err(e) => {
181 warn!(
183 "count_by_form_action_origin was passed an invalid origin: {}",
184 e
185 );
186 Ok(0)
187 }
188 }
189 }
190
191 pub fn get_all(&self) -> Result<Vec<EncryptedLogin>> {
192 let mut stmt = self.db.prepare_cached(&GET_ALL_SQL)?;
193 let rows = stmt.query_and_then([], EncryptedLogin::from_row)?;
194 rows.collect::<Result<_>>()
195 }
196
197 pub fn get_by_base_domain(&self, base_domain: &str) -> Result<Vec<EncryptedLogin>> {
198 let base_host = match Host::parse(base_domain) {
200 Ok(d) => d,
201 Err(e) => {
202 warn!("get_by_base_domain was passed an invalid domain: {}", e);
204 return Ok(vec![]);
205 }
206 };
207 let mut stmt = self.db.prepare_cached(&GET_ALL_SQL)?;
214 let rows = stmt
215 .query_and_then([], EncryptedLogin::from_row)?
216 .filter(|r| {
217 let login = r
218 .as_ref()
219 .ok()
220 .and_then(|login| Url::parse(&login.fields.origin).ok());
221 let this_host = login.as_ref().and_then(|url| url.host());
222 match (&base_host, this_host) {
223 (Host::Domain(base), Some(Host::Domain(look))) => {
224 let mut rev_input = base.chars().rev();
228 let mut rev_host = look.chars().rev();
229 loop {
230 match (rev_input.next(), rev_host.next()) {
231 (Some(ref a), Some(ref b)) if a == b => continue,
232 (None, None) => return true, (None, Some(ref h)) => return *h == '.',
234 _ => return false,
235 }
236 }
237 }
238 (Host::Ipv4(base), Some(Host::Ipv4(look))) => *base == look,
240 (Host::Ipv6(base), Some(Host::Ipv6(look))) => *base == look,
241 _ => false,
243 }
244 });
245 rows.collect::<Result<_>>()
246 }
247
248 pub fn get_by_id(&self, id: &str) -> Result<Option<EncryptedLogin>> {
249 self.try_query_row(
250 &GET_BY_GUID_SQL,
251 &[(":guid", &id as &dyn ToSql)],
252 EncryptedLogin::from_row,
253 true,
254 )
255 }
256
257 pub fn find_login_to_update(&self, look: LoginEntry) -> Result<Option<Login>> {
269 let look = look.fixup()?;
270 let logins = self
271 .get_by_entry_target(&look)?
272 .into_iter()
273 .map(|enc_login| enc_login.decrypt(self.encdec.as_ref()))
274 .collect::<Result<Vec<Login>>>()?;
275 Ok(logins
276 .iter()
278 .find(|login| login.username == look.username)
279 .or_else(|| logins.iter().find(|login| login.username.is_empty()))
281 .cloned())
283 }
284
285 pub fn touch(&self, id: &str) -> Result<()> {
286 let tx = self.unchecked_transaction()?;
287 self.ensure_local_overlay_exists(id)?;
288 self.mark_mirror_overridden(id)?;
289 let now_ms = util::system_time_ms_i64(SystemTime::now());
290 self.execute_cached(
293 "UPDATE loginsL
294 SET timeLastUsed = :now_millis,
295 timesUsed = timesUsed + 1,
296 local_modified = :now_millis
297 WHERE guid = :guid
298 AND is_deleted = 0",
299 named_params! {
300 ":now_millis": now_ms,
301 ":guid": id,
302 },
303 )?;
304 tx.commit()?;
305 Ok(())
306 }
307
308 pub fn record_potentially_vulnerable_passwords(&self, passwords: Vec<String>) -> Result<()> {
313 let tx = self.unchecked_transaction()?;
314 self.insert_potentially_vulnerable_passwords(passwords)?;
315 tx.commit()?;
316 Ok(())
317 }
318
319 fn insert_potentially_vulnerable_passwords(&self, passwords: Vec<String>) -> Result<()> {
320 let encrypted_existing_potentially_vulnerable_passwords: Vec<String> = self
321 .db
322 .query_rows_and_then_cached("SELECT encryptedPassword FROM breachesL", [], |row| {
323 row.get(0)
324 })?;
325 let existing_potentially_vulnerable_passwords: Result<Vec<String>> =
326 encrypted_existing_potentially_vulnerable_passwords
327 .iter()
328 .map(|ciphertext| {
329 let decrypted_bytes = self
330 .encdec
331 .decrypt(ciphertext.as_bytes().into())
332 .map_err(|e| {
333 Error::DecryptionFailed(format!(
334 "Failed to decrypt password from breachesL: {}",
335 e
336 ))
337 })?;
338
339 let password = std::str::from_utf8(&decrypted_bytes).map_err(|e| {
340 Error::DecryptionFailed(format!(
341 "Decrypted password from breachesL is not valid UTF-8: {}",
342 e
343 ))
344 })?;
345
346 Ok(password.into())
347 })
348 .collect();
349
350 let existing: std::collections::HashSet<String> =
351 existing_potentially_vulnerable_passwords?
352 .into_iter()
353 .collect();
354 let difference: Vec<_> = passwords
355 .iter()
356 .filter(|item| !existing.contains(item.as_str()))
357 .collect();
358
359 for password in difference {
360 let encrypted_password_bytes = self
361 .encdec
362 .encrypt(password.as_bytes().into())
363 .map_err(|e| Error::EncryptionFailed(format!("{e} (encrypting password)")))?;
364 let encrypted_password =
365 std::str::from_utf8(&encrypted_password_bytes).map_err(|e| {
366 Error::EncryptionFailed(format!("{e} (encrypting password: data not utf8)"))
367 })?;
368
369 self.execute_cached(
370 "INSERT INTO breachesL (encryptedPassword) VALUES (:encrypted_password)",
371 named_params! {
372 ":encrypted_password": encrypted_password,
373 },
374 )?;
375 }
376
377 Ok(())
378 }
379
380 pub fn are_potentially_vulnerable_passwords(&self, guids: &[&str]) -> Result<Vec<String>> {
390 if guids.is_empty() {
391 return Ok(Vec::new());
392 }
393
394 let all_encrypted_passwords: Vec<String> = self.db.query_rows_and_then_cached(
396 "SELECT encryptedPassword FROM breachesL",
397 [],
398 |row| row.get(0),
399 )?;
400
401 let mut breached_passwords = std::collections::HashSet::new();
402 for ciphertext in &all_encrypted_passwords {
403 let decrypted_bytes =
404 self.encdec
405 .decrypt(ciphertext.as_bytes().into())
406 .map_err(|e| {
407 Error::DecryptionFailed(format!(
408 "Failed to decrypt password from breachesL: {}",
409 e
410 ))
411 })?;
412
413 let decrypted_password = std::str::from_utf8(&decrypted_bytes).map_err(|e| {
414 Error::DecryptionFailed(format!(
415 "Decrypted password from breachesL is not valid UTF-8: {}",
416 e
417 ))
418 })?;
419
420 breached_passwords.insert(decrypted_password.to_string());
421 }
422
423 let mut vulnerable_guids = Vec::new();
425 for guid in guids {
426 if let Some(login) = self.get_by_id(guid)? {
427 let decrypted_login = login.decrypt(self.encdec.as_ref())?;
428 if breached_passwords.contains(&decrypted_login.password) {
429 vulnerable_guids.push(guid.to_string());
430 }
431 }
432 }
433
434 Ok(vulnerable_guids)
435 }
436
437 pub fn is_potentially_vulnerable_password(&self, guid: &str) -> Result<bool> {
438 let vulnerable = self.are_potentially_vulnerable_passwords(&[guid])?;
440 Ok(!vulnerable.is_empty())
441 }
442
443 pub fn reset_all_breaches(&self) -> Result<()> {
444 let tx = self.unchecked_transaction()?;
445 self.execute_cached("DELETE FROM breachesL", [])?;
446 tx.commit()?;
447 Ok(())
448 }
449
450 pub fn record_breach_alert_dismissal(&self, id: &str) -> Result<()> {
455 let timestamp = util::system_time_ms_i64(SystemTime::now());
456 self.record_breach_alert_dismissal_time(id, timestamp)
457 }
458
459 pub fn record_breach_alert_dismissal_time(&self, id: &str, timestamp: i64) -> Result<()> {
465 let tx = self.unchecked_transaction()?;
466 self.ensure_local_overlay_exists(id)?;
467 self.mark_mirror_overridden(id)?;
468 self.execute_cached(
469 "UPDATE loginsL
470 SET timeLastBreachAlertDismissed = :now_millis
471 WHERE guid = :guid",
472 named_params! {
473 ":now_millis": timestamp,
474 ":guid": id,
475 },
476 )?;
477 tx.commit()?;
478 Ok(())
479 }
480
481 fn insert_new_login(&self, login: &EncryptedLogin) -> Result<()> {
484 let sql = format!(
485 "INSERT OR REPLACE INTO loginsL (
486 origin,
487 httpRealm,
488 formActionOrigin,
489 usernameField,
490 passwordField,
491 timesUsed,
492 secFields,
493 guid,
494 timeCreated,
495 timeLastUsed,
496 timePasswordChanged,
497 timeLastBreachAlertDismissed,
498 local_modified,
499 is_deleted,
500 sync_status
501 ) VALUES (
502 :origin,
503 :http_realm,
504 :form_action_origin,
505 :username_field,
506 :password_field,
507 :times_used,
508 :sec_fields,
509 :guid,
510 :time_created,
511 :time_last_used,
512 :time_password_changed,
513 :time_last_breach_alert_dismissed,
514 :local_modified,
515 0, -- is_deleted
516 {new} -- sync_status
517 )",
518 new = SyncStatus::New as u8
519 );
520
521 self.execute(
522 &sql,
523 named_params! {
524 ":origin": login.fields.origin,
525 ":http_realm": login.fields.http_realm,
526 ":form_action_origin": login.fields.form_action_origin,
527 ":username_field": login.fields.username_field,
528 ":password_field": login.fields.password_field,
529 ":time_created": login.meta.time_created,
530 ":times_used": login.meta.times_used,
531 ":time_last_used": login.meta.time_last_used,
532 ":time_password_changed": login.meta.time_password_changed,
533 ":local_modified": login.meta.time_created,
534 ":time_last_breach_alert_dismissed": login.meta.time_last_breach_alert_dismissed,
535 ":sec_fields": login.sec_fields,
536 ":guid": login.guid(),
537 },
538 )?;
539 Ok(())
540 }
541
542 fn update_existing_login(&self, login: &EncryptedLogin) -> Result<()> {
543 let now_ms = util::system_time_ms_i64(SystemTime::now());
545 let sql = format!(
546 "UPDATE loginsL
547 SET local_modified = :now_millis,
548 timeLastUsed = :time_last_used,
549 timePasswordChanged = :time_password_changed,
550 httpRealm = :http_realm,
551 formActionOrigin = :form_action_origin,
552 usernameField = :username_field,
553 passwordField = :password_field,
554 timesUsed = :times_used,
555 secFields = :sec_fields,
556 origin = :origin,
557 -- leave New records as they are, otherwise update them to `changed`
558 sync_status = max(sync_status, {changed})
559 WHERE guid = :guid",
560 changed = SyncStatus::Changed as u8
561 );
562
563 self.db.execute(
564 &sql,
565 named_params! {
566 ":origin": login.fields.origin,
567 ":http_realm": login.fields.http_realm,
568 ":form_action_origin": login.fields.form_action_origin,
569 ":username_field": login.fields.username_field,
570 ":password_field": login.fields.password_field,
571 ":time_last_used": login.meta.time_last_used,
572 ":times_used": login.meta.times_used,
573 ":time_password_changed": login.meta.time_password_changed,
574 ":sec_fields": login.sec_fields,
575 ":guid": &login.meta.id,
576 ":now_millis": now_ms,
577 },
578 )?;
579 Ok(())
580 }
581
582 pub fn add_many(&self, entries: Vec<LoginEntry>) -> Result<Vec<Result<EncryptedLogin>>> {
584 let now_ms = util::system_time_ms_i64(SystemTime::now());
585
586 let entries_with_meta = entries
587 .into_iter()
588 .map(|entry| {
589 let guid = Guid::random();
590 LoginEntryWithMeta {
591 entry,
592 meta: LoginMeta {
593 id: guid.to_string(),
594 time_created: now_ms,
595 time_password_changed: now_ms,
596 time_last_used: now_ms,
597 times_used: 1,
598 time_last_breach_alert_dismissed: None,
599 },
600 }
601 })
602 .collect();
603
604 self.add_many_with_meta(entries_with_meta)
605 }
606
607 pub fn add_many_with_meta(
612 &self,
613 entries_with_meta: Vec<LoginEntryWithMeta>,
614 ) -> Result<Vec<Result<EncryptedLogin>>> {
615 let tx = self.unchecked_transaction()?;
616 let mut results = vec![];
617 for mut entry_with_meta in entries_with_meta {
618 let guid = match Self::validate_or_fixup_guid(Guid::from_string(
619 entry_with_meta.meta.id.clone(),
620 )) {
621 Ok(guid) => guid,
622 Err(err) => {
623 results.push(Err(err));
624 continue;
625 }
626 };
627 entry_with_meta.meta.id = guid.to_string();
630 match self.fixup_and_check_for_dupes(&guid, entry_with_meta.entry) {
631 Ok(new_entry) => {
632 let sec_fields = SecureLoginFields {
633 username: new_entry.username,
634 password: new_entry.password,
635 }
636 .encrypt(self.encdec.as_ref(), &entry_with_meta.meta.id)?;
637 let encrypted_login = EncryptedLogin {
638 meta: entry_with_meta.meta,
639 fields: LoginFields {
640 origin: new_entry.origin,
641 form_action_origin: new_entry.form_action_origin,
642 http_realm: new_entry.http_realm,
643 username_field: new_entry.username_field,
644 password_field: new_entry.password_field,
645 },
646 sec_fields,
647 };
648 let result = self
649 .insert_new_login(&encrypted_login)
650 .map(|_| encrypted_login);
651 results.push(result);
652 }
653
654 Err(error) => results.push(Err(error)),
655 }
656 }
657
658 tx.commit()?;
659
660 Ok(results)
661 }
662
663 fn validate_or_fixup_guid(guid: Guid) -> Result<Guid> {
673 if guid.is_valid_for_sync_server() {
674 return Ok(guid);
675 }
676 #[cfg(feature = "fixup_invalid_guids")]
677 {
678 warn!("regenerating a login guid that is invalid for the sync server");
679 Ok(Guid::random())
680 }
681 #[cfg(not(feature = "fixup_invalid_guids"))]
682 {
683 Err(InvalidLogin::IllegalFieldValue {
684 field_info: "guid is not valid for the sync server".into(),
685 }
686 .into())
687 }
688 }
689
690 pub fn add(&self, entry: LoginEntry) -> Result<EncryptedLogin> {
691 let guid = Guid::random();
692 let now_ms = util::system_time_ms_i64(SystemTime::now());
693
694 let entry_with_meta = LoginEntryWithMeta {
695 entry,
696 meta: LoginMeta {
697 id: guid.to_string(),
698 time_created: now_ms,
699 time_password_changed: now_ms,
700 time_last_used: now_ms,
701 times_used: 1,
702 time_last_breach_alert_dismissed: None,
703 },
704 };
705
706 self.add_with_meta(entry_with_meta)
707 }
708
709 pub fn add_with_meta(&self, entry_with_meta: LoginEntryWithMeta) -> Result<EncryptedLogin> {
713 let mut results = self.add_many_with_meta(vec![entry_with_meta])?;
714 results.pop().expect("there should be a single result")
715 }
716
717 pub fn update(&self, sguid: &str, entry: LoginEntry) -> Result<EncryptedLogin> {
718 let guid = Guid::new(sguid);
719 let now_ms = util::system_time_ms_i64(SystemTime::now());
720 let tx = self.unchecked_transaction()?;
721
722 let entry = entry.fixup()?;
723
724 if self.check_for_dupes(&guid, &entry).is_err() {
730 let has_mirror_row: bool = self
732 .db
733 .conn_ext_query_one("SELECT EXISTS (SELECT 1 FROM loginsM)")?;
734 let has_http_realm = entry.http_realm.is_some();
735 let has_form_action_origin = entry.form_action_origin.is_some();
736 report_error!(
737 "logins-duplicate-in-update",
738 "(mirror: {has_mirror_row}, realm: {has_http_realm}, form_origin: {has_form_action_origin})");
739 }
740
741 self.ensure_local_overlay_exists(&guid)?;
743 self.mark_mirror_overridden(&guid)?;
744
745 let existing = match self.get_by_id(sguid)? {
747 Some(e) => e.decrypt(self.encdec.as_ref())?,
748 None => return Err(Error::NoSuchRecord(sguid.to_owned())),
749 };
750 let time_password_changed = if existing.password == entry.password {
751 existing.time_password_changed
752 } else {
753 now_ms
754 };
755
756 let sec_fields = SecureLoginFields {
758 username: entry.username,
759 password: entry.password,
760 }
761 .encrypt(self.encdec.as_ref(), &existing.id)?;
762 let result = EncryptedLogin {
763 meta: LoginMeta {
764 id: existing.id,
765 time_created: existing.time_created,
766 time_password_changed,
767 time_last_used: existing.time_last_used,
769 times_used: existing.times_used,
770 time_last_breach_alert_dismissed: None,
771 },
772 fields: LoginFields {
773 origin: entry.origin,
774 form_action_origin: entry.form_action_origin,
775 http_realm: entry.http_realm,
776 username_field: entry.username_field,
777 password_field: entry.password_field,
778 },
779 sec_fields,
780 };
781
782 self.update_existing_login(&result)?;
783 tx.commit()?;
784 Ok(result)
785 }
786
787 pub fn add_or_update(&self, entry: LoginEntry) -> Result<EncryptedLogin> {
788 let entry = entry.fixup()?;
790 match self.find_login_to_update(entry.clone())? {
791 Some(login) => self.update(&login.id, entry),
792 None => self.add(entry),
793 }
794 }
795
796 pub fn fixup_and_check_for_dupes(&self, guid: &Guid, entry: LoginEntry) -> Result<LoginEntry> {
797 let entry = entry.fixup()?;
798 self.check_for_dupes(guid, &entry)?;
799 Ok(entry)
800 }
801
802 pub fn check_for_dupes(&self, guid: &Guid, entry: &LoginEntry) -> Result<()> {
803 if self.dupe_exists(guid, entry)? {
804 return Err(InvalidLogin::DuplicateLogin.into());
805 }
806 Ok(())
807 }
808
809 pub fn dupe_exists(&self, guid: &Guid, entry: &LoginEntry) -> Result<bool> {
810 Ok(self.find_dupe(guid, entry)?.is_some())
811 }
812
813 pub fn find_dupe(&self, guid: &Guid, entry: &LoginEntry) -> Result<Option<Guid>> {
814 for possible in self.get_by_entry_target(entry)? {
815 if possible.guid() != *guid {
816 let pos_sec_fields = possible.decrypt_fields(self.encdec.as_ref())?;
817 if pos_sec_fields.username == entry.username {
818 return Ok(Some(possible.guid()));
819 }
820 }
821 }
822 Ok(None)
823 }
824
825 fn get_by_entry_target(&self, entry: &LoginEntry) -> Result<Vec<EncryptedLogin>> {
835 lazy_static::lazy_static! {
837 static ref GET_BY_FORM_ACTION_ORIGIN: String = format!(
838 "SELECT {common_cols} FROM loginsL
839 WHERE is_deleted = 0
840 AND origin = :origin
841 AND formActionOrigin = :form_action_origin
842
843 UNION ALL
844
845 SELECT {common_cols} FROM loginsM
846 WHERE is_overridden = 0
847 AND origin = :origin
848 AND formActionOrigin = :form_action_origin
849 ",
850 common_cols = schema::COMMON_COLS
851 );
852 static ref GET_BY_HTTP_REALM: String = format!(
853 "SELECT {common_cols} FROM loginsL
854 WHERE is_deleted = 0
855 AND origin = :origin
856 AND httpRealm = :http_realm
857
858 UNION ALL
859
860 SELECT {common_cols} FROM loginsM
861 WHERE is_overridden = 0
862 AND origin = :origin
863 AND httpRealm = :http_realm
864 ",
865 common_cols = schema::COMMON_COLS
866 );
867 }
868 match (entry.form_action_origin.as_ref(), entry.http_realm.as_ref()) {
869 (Some(form_action_origin), None) => {
870 let params = named_params! {
871 ":origin": &entry.origin,
872 ":form_action_origin": form_action_origin,
873 };
874 self.db
875 .prepare_cached(&GET_BY_FORM_ACTION_ORIGIN)?
876 .query_and_then(params, EncryptedLogin::from_row)?
877 .collect()
878 }
879 (None, Some(http_realm)) => {
880 let params = named_params! {
881 ":origin": &entry.origin,
882 ":http_realm": http_realm,
883 };
884 self.db
885 .prepare_cached(&GET_BY_HTTP_REALM)?
886 .query_and_then(params, EncryptedLogin::from_row)?
887 .collect()
888 }
889 (Some(_), Some(_)) => Err(InvalidLogin::BothTargets.into()),
890 (None, None) => Err(InvalidLogin::NoTarget.into()),
891 }
892 }
893
894 pub fn exists(&self, id: &str) -> Result<bool> {
895 Ok(self.db.query_row(
896 "SELECT EXISTS(
897 SELECT 1 FROM loginsL
898 WHERE guid = :guid AND is_deleted = 0
899 UNION ALL
900 SELECT 1 FROM loginsM
901 WHERE guid = :guid AND is_overridden IS NOT 1
902 )",
903 named_params! { ":guid": id },
904 |row| row.get(0),
905 )?)
906 }
907
908 pub fn delete(&self, id: &str) -> Result<bool> {
911 let mut results = self.delete_many(vec![id])?;
912 Ok(results.pop().expect("there should be a single result"))
913 }
914
915 pub fn delete_all(&self) -> Result<Vec<String>> {
917 let ids: Vec<String> = self.db.query_rows_and_then_cached(
918 "SELECT guid FROM loginsL WHERE is_deleted = 0
919 UNION ALL
920 SELECT guid FROM loginsM WHERE is_overridden = 0",
921 [],
922 |row| row.get(0),
923 )?;
924 self.delete_many(ids.iter().map(String::as_str).collect())?;
925 Ok(ids)
926 }
927
928 pub fn delete_all_except_fxa(&self) -> Result<Vec<String>> {
931 let ids: Vec<String> = self.db.query_rows_and_then_cached(
932 "SELECT guid FROM loginsL WHERE is_deleted = 0 AND origin != :fxa_origin
933 UNION ALL
934 SELECT guid FROM loginsM WHERE is_overridden = 0 AND origin != :fxa_origin",
935 named_params! { ":fxa_origin": FXA_CREDENTIALS_ORIGIN },
936 |row| row.get(0),
937 )?;
938 self.delete_many(ids.iter().map(String::as_str).collect())?;
939 Ok(ids)
940 }
941
942 pub fn delete_many(&self, ids: Vec<&str>) -> Result<Vec<bool>> {
945 let tx = self.unchecked_transaction_imm()?;
946 let sql = format!(
947 "
948 UPDATE loginsL
949 SET local_modified = :now_ms,
950 sync_status = {status_changed},
951 is_deleted = 1,
952 secFields = '',
953 origin = '',
954 httpRealm = NULL,
955 formActionOrigin = NULL
956 WHERE guid = :guid AND is_deleted IS FALSE
957 ",
958 status_changed = SyncStatus::Changed as u8
959 );
960 let mut stmt = self.db.prepare_cached(&sql)?;
961
962 let mut result = vec![];
963
964 for id in ids {
965 let now_ms = util::system_time_ms_i64(SystemTime::now());
966
967 let update_result = stmt.execute(named_params! { ":now_ms": now_ms, ":guid": id })?;
969
970 let exists = update_result == 1;
971
972 self.execute(
974 "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
975 named_params! { ":guid": id },
976 )?;
977
978 self.execute(&format!("
981 INSERT OR IGNORE INTO loginsL
982 (guid, local_modified, is_deleted, sync_status, origin, timeCreated, timePasswordChanged, secFields)
983 SELECT guid, :now_ms, 1, {changed}, '', timeCreated, :now_ms, ''
984 FROM loginsM
985 WHERE guid = :guid",
986 changed = SyncStatus::Changed as u8),
987 named_params! { ":now_ms": now_ms, ":guid": id })?;
988
989 result.push(exists);
990 }
991
992 tx.commit()?;
993
994 Ok(result)
995 }
996
997 pub fn delete_undecryptable_records_for_remote_replacement(
998 &self,
999 ) -> Result<LoginsDeletionMetrics> {
1000 let corrupted_logins = self
1002 .get_all()?
1003 .into_iter()
1004 .filter(|login| login.clone().decrypt(self.encdec.as_ref()).is_err())
1005 .collect::<Vec<_>>();
1006 let ids = corrupted_logins
1007 .iter()
1008 .map(|login| login.guid_str())
1009 .collect::<Vec<_>>();
1010
1011 self.delete_local_records_for_remote_replacement(ids)
1012 }
1013
1014 pub fn delete_local_records_for_remote_replacement(
1015 &self,
1016 ids: Vec<&str>,
1017 ) -> Result<LoginsDeletionMetrics> {
1018 let tx = self.unchecked_transaction_imm()?;
1019 let mut local_deleted = 0;
1020 let mut mirror_deleted = 0;
1021
1022 sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
1023 let deleted = self.execute(
1024 &format!(
1025 "DELETE FROM loginsL WHERE guid IN ({})",
1026 sql_support::repeat_sql_values(chunk.len())
1027 ),
1028 rusqlite::params_from_iter(chunk),
1029 )?;
1030 local_deleted += deleted;
1031 Ok(())
1032 })?;
1033
1034 sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
1035 let deleted = self.execute(
1036 &format!(
1037 "DELETE FROM loginsM WHERE guid IN ({})",
1038 sql_support::repeat_sql_values(chunk.len())
1039 ),
1040 rusqlite::params_from_iter(chunk),
1041 )?;
1042 mirror_deleted += deleted;
1043 Ok(())
1044 })?;
1045
1046 tx.commit()?;
1047 Ok(LoginsDeletionMetrics {
1048 local_deleted: local_deleted as u64,
1049 mirror_deleted: mirror_deleted as u64,
1050 })
1051 }
1052
1053 fn mark_mirror_overridden(&self, guid: &str) -> Result<()> {
1054 self.execute_cached(
1055 "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
1056 named_params! { ":guid": guid },
1057 )?;
1058 Ok(())
1059 }
1060
1061 fn ensure_local_overlay_exists(&self, guid: &str) -> Result<()> {
1062 let already_have_local: bool = self.db.query_row(
1063 "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid)",
1064 named_params! { ":guid": guid },
1065 |row| row.get(0),
1066 )?;
1067
1068 if already_have_local {
1069 return Ok(());
1070 }
1071
1072 debug!("No overlay; cloning one for {:?}.", guid);
1073 let changed = self.clone_mirror_to_overlay(guid)?;
1074 if changed == 0 {
1075 report_error!(
1076 "logins-local-overlay-error",
1077 "Failed to create local overlay for GUID {guid:?}."
1078 );
1079 return Err(Error::NoSuchRecord(guid.to_owned()));
1080 }
1081 Ok(())
1082 }
1083
1084 fn clone_mirror_to_overlay(&self, guid: &str) -> Result<usize> {
1085 Ok(self.execute_cached(&CLONE_SINGLE_MIRROR_SQL, &[(":guid", &guid as &dyn ToSql)])?)
1086 }
1087
1088 pub fn wipe_local(&self) -> Result<usize> {
1090 info!("Executing wipe_local on password engine!");
1091 let tx = self.unchecked_transaction()?;
1092 let mut row_count = 0;
1093 row_count += self.execute("DELETE FROM loginsL", [])?;
1094 row_count += self.execute("DELETE FROM loginsM", [])?;
1095 row_count += self.execute("DELETE FROM loginsSyncMeta", [])?;
1096 row_count += self.execute("DELETE FROM breachesL", [])?;
1097 tx.commit()?;
1098 Ok(row_count)
1099 }
1100
1101 pub fn wipe_local_except_fxa(&self) -> Result<usize> {
1103 info!("Executing wipe_local_except_fxa on password engine!");
1104 let tx = self.unchecked_transaction()?;
1105 let mut row_count = 0;
1106 row_count += self.execute(
1107 "DELETE FROM loginsL WHERE origin != :fxa_origin",
1108 named_params! { ":fxa_origin": FXA_CREDENTIALS_ORIGIN },
1109 )?;
1110 row_count += self.execute(
1111 "DELETE FROM loginsM WHERE origin != :fxa_origin",
1112 named_params! { ":fxa_origin": FXA_CREDENTIALS_ORIGIN },
1113 )?;
1114 row_count += self.execute("DELETE FROM loginsSyncMeta", [])?;
1115 row_count += self.execute("DELETE FROM breachesL", [])?;
1116 tx.commit()?;
1117 Ok(row_count)
1118 }
1119
1120 pub fn shutdown(self) -> Result<()> {
1121 self.db.close().map_err(|(_, e)| Error::SqlError(e))
1122 }
1123}
1124
1125lazy_static! {
1126 static ref GET_ALL_SQL: String = format!(
1127 "SELECT {common_cols} FROM loginsL WHERE is_deleted = 0
1128 UNION ALL
1129 SELECT {common_cols} FROM loginsM WHERE is_overridden = 0",
1130 common_cols = schema::COMMON_COLS,
1131 );
1132 static ref COUNT_ALL_SQL: String = format!(
1133 "SELECT COUNT(*) FROM (
1134 SELECT guid FROM loginsL WHERE is_deleted = 0
1135 UNION ALL
1136 SELECT guid FROM loginsM WHERE is_overridden = 0
1137 )"
1138 );
1139 static ref COUNT_BY_ORIGIN_SQL: String = format!(
1140 "SELECT COUNT(*) FROM (
1141 SELECT guid FROM loginsL WHERE is_deleted = 0 AND origin = :origin
1142 UNION ALL
1143 SELECT guid FROM loginsM WHERE is_overridden = 0 AND origin = :origin
1144 )"
1145 );
1146 static ref COUNT_BY_FORM_ACTION_ORIGIN_SQL: String = format!(
1147 "SELECT COUNT(*) FROM (
1148 SELECT guid FROM loginsL WHERE is_deleted = 0 AND formActionOrigin = :form_action_origin
1149 UNION ALL
1150 SELECT guid FROM loginsM WHERE is_overridden = 0 AND formActionOrigin = :form_action_origin
1151 )"
1152 );
1153 static ref GET_BY_GUID_SQL: String = format!(
1154 "SELECT {common_cols}
1155 FROM loginsL
1156 WHERE is_deleted = 0
1157 AND guid = :guid
1158
1159 UNION ALL
1160
1161 SELECT {common_cols}
1162 FROM loginsM
1163 WHERE is_overridden IS NOT 1
1164 AND guid = :guid
1165 ORDER BY origin ASC
1166
1167 LIMIT 1",
1168 common_cols = schema::COMMON_COLS,
1169 );
1170 pub static ref CLONE_ENTIRE_MIRROR_SQL: String = format!(
1171 "INSERT OR IGNORE INTO loginsL ({common_cols}, local_modified, is_deleted, sync_status)
1172 SELECT {common_cols}, NULL AS local_modified, 0 AS is_deleted, 0 AS sync_status
1173 FROM loginsM",
1174 common_cols = schema::COMMON_COLS,
1175 );
1176 static ref CLONE_SINGLE_MIRROR_SQL: String =
1177 format!("{} WHERE guid = :guid", &*CLONE_ENTIRE_MIRROR_SQL,);
1178}
1179
1180#[cfg(not(feature = "keydb"))]
1181#[cfg(test)]
1182pub mod test_utils {
1183 use super::*;
1184 use crate::encryption::test_utils::decrypt_struct;
1185 use crate::login::test_utils::enc_login;
1186 use crate::SecureLoginFields;
1187 use sync15::ServerTimestamp;
1188
1189 pub fn insert_login(
1193 db: &LoginDb,
1194 guid: &str,
1195 local_login: Option<&str>,
1196 mirror_login: Option<&str>,
1197 ) {
1198 if let Some(password) = mirror_login {
1199 add_mirror(
1200 db,
1201 &enc_login(guid, password),
1202 &ServerTimestamp(util::system_time_ms_i64(std::time::SystemTime::now())),
1203 local_login.is_some(),
1204 )
1205 .unwrap();
1206 }
1207 if let Some(password) = local_login {
1208 db.insert_new_login(&enc_login(guid, password)).unwrap();
1209 }
1210 }
1211
1212 pub fn insert_encrypted_login(
1213 db: &LoginDb,
1214 local: &EncryptedLogin,
1215 mirror: &EncryptedLogin,
1216 server_modified: &ServerTimestamp,
1217 ) {
1218 db.insert_new_login(local).unwrap();
1219 add_mirror(db, mirror, server_modified, true).unwrap();
1220 }
1221
1222 pub fn add_mirror(
1223 db: &LoginDb,
1224 login: &EncryptedLogin,
1225 server_modified: &ServerTimestamp,
1226 is_overridden: bool,
1227 ) -> Result<()> {
1228 let sql = "
1229 INSERT OR IGNORE INTO loginsM (
1230 is_overridden,
1231 server_modified,
1232
1233 httpRealm,
1234 formActionOrigin,
1235 usernameField,
1236 passwordField,
1237 secFields,
1238 origin,
1239
1240 timesUsed,
1241 timeLastUsed,
1242 timePasswordChanged,
1243 timeCreated,
1244
1245 timeLastBreachAlertDismissed,
1246
1247 guid
1248 ) VALUES (
1249 :is_overridden,
1250 :server_modified,
1251
1252 :http_realm,
1253 :form_action_origin,
1254 :username_field,
1255 :password_field,
1256 :sec_fields,
1257 :origin,
1258
1259 :times_used,
1260 :time_last_used,
1261 :time_password_changed,
1262 :time_created,
1263
1264 :time_last_breach_alert_dismissed,
1265
1266 :guid
1267 )";
1268 let mut stmt = db.prepare_cached(sql)?;
1269
1270 stmt.execute(named_params! {
1271 ":is_overridden": is_overridden,
1272 ":server_modified": server_modified.as_millis(),
1273 ":http_realm": login.fields.http_realm,
1274 ":form_action_origin": login.fields.form_action_origin,
1275 ":username_field": login.fields.username_field,
1276 ":password_field": login.fields.password_field,
1277 ":origin": login.fields.origin,
1278 ":sec_fields": login.sec_fields,
1279 ":times_used": login.meta.times_used,
1280 ":time_last_used": login.meta.time_last_used,
1281 ":time_password_changed": login.meta.time_password_changed,
1282 ":time_created": login.meta.time_created,
1283 ":time_last_breach_alert_dismissed": login.meta.time_last_breach_alert_dismissed,
1284 ":guid": login.guid_str(),
1285 })?;
1286 Ok(())
1287 }
1288
1289 pub fn get_local_guids(db: &LoginDb) -> Vec<String> {
1290 get_guids(db, "SELECT guid FROM loginsL")
1291 }
1292
1293 pub fn get_mirror_guids(db: &LoginDb) -> Vec<String> {
1294 get_guids(db, "SELECT guid FROM loginsM")
1295 }
1296
1297 fn get_guids(db: &LoginDb, sql: &str) -> Vec<String> {
1298 let mut stmt = db.prepare_cached(sql).unwrap();
1299 let mut res: Vec<String> = stmt
1300 .query_map([], |r| r.get(0))
1301 .unwrap()
1302 .map(|r| r.unwrap())
1303 .collect();
1304 res.sort();
1305 res
1306 }
1307
1308 pub fn get_server_modified(db: &LoginDb, guid: &str) -> i64 {
1309 db.conn_ext_query_one(&format!(
1310 "SELECT server_modified FROM loginsM WHERE guid='{}'",
1311 guid
1312 ))
1313 .unwrap()
1314 }
1315
1316 pub fn check_local_login(db: &LoginDb, guid: &str, password: &str, local_modified_gte: i64) {
1317 let row: (String, i64, bool) = db
1318 .query_row(
1319 "SELECT secFields, local_modified, is_deleted FROM loginsL WHERE guid=?",
1320 [guid],
1321 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1322 )
1323 .unwrap();
1324 let enc: SecureLoginFields = decrypt_struct(row.0);
1325 assert_eq!(enc.password, password);
1326 assert!(row.1 >= local_modified_gte);
1327 assert!(!row.2);
1328 }
1329
1330 pub fn check_mirror_login(
1331 db: &LoginDb,
1332 guid: &str,
1333 password: &str,
1334 server_modified: i64,
1335 is_overridden: bool,
1336 ) {
1337 let row: (String, i64, bool) = db
1338 .query_row(
1339 "SELECT secFields, server_modified, is_overridden FROM loginsM WHERE guid=?",
1340 [guid],
1341 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1342 )
1343 .unwrap();
1344 let enc: SecureLoginFields = decrypt_struct(row.0);
1345 assert_eq!(enc.password, password);
1346 assert_eq!(row.1, server_modified);
1347 assert_eq!(row.2, is_overridden);
1348 }
1349}
1350
1351#[cfg(not(feature = "keydb"))]
1352#[cfg(test)]
1353mod tests {
1354 use super::*;
1355 use crate::db::test_utils::{get_local_guids, get_mirror_guids};
1356 use crate::encryption::test_utils::TEST_ENCDEC;
1357 use crate::sync::merge::LocalLogin;
1358 use nss_as::ensure_initialized;
1359 use std::{thread, time};
1360
1361 #[test]
1362 fn test_username_dupe_semantics() {
1363 ensure_initialized();
1364 let mut login = LoginEntry {
1365 origin: "https://www.example.com".into(),
1366 http_realm: Some("https://www.example.com".into()),
1367 username: "test".into(),
1368 password: "sekret".into(),
1369 ..LoginEntry::default()
1370 };
1371
1372 let db = LoginDb::open_in_memory();
1373 db.add(login.clone())
1374 .expect("should be able to add first login");
1375
1376 let exp_err = "Invalid login: Login already exists";
1378 assert_eq!(db.add(login.clone()).unwrap_err().to_string(), exp_err);
1379
1380 login.username = "".to_string();
1382 db.add(login.clone()).expect("empty login isn't a dupe");
1383
1384 assert_eq!(db.add(login).unwrap_err().to_string(), exp_err);
1385
1386 assert_eq!(db.get_all().unwrap().len(), 2);
1388 }
1389
1390 #[test]
1391 fn test_add_many() {
1392 ensure_initialized();
1393
1394 let login_a = LoginEntry {
1395 origin: "https://a.example.com".into(),
1396 http_realm: Some("https://www.example.com".into()),
1397 username: "test".into(),
1398 password: "sekret".into(),
1399 ..LoginEntry::default()
1400 };
1401
1402 let login_b = LoginEntry {
1403 origin: "https://b.example.com".into(),
1404 http_realm: Some("https://www.example.com".into()),
1405 username: "test".into(),
1406 password: "sekret".into(),
1407 ..LoginEntry::default()
1408 };
1409
1410 let db = LoginDb::open_in_memory();
1411 let added = db
1412 .add_many(vec![login_a.clone(), login_b.clone()])
1413 .expect("should be able to add logins");
1414
1415 let [added_a, added_b] = added.as_slice() else {
1416 panic!("there should really be 2")
1417 };
1418
1419 let fetched_a = db
1420 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1421 .expect("should work")
1422 .expect("should get a record");
1423
1424 assert_eq!(fetched_a.fields.origin, login_a.origin);
1425
1426 let fetched_b = db
1427 .get_by_id(&added_b.as_ref().unwrap().meta.id)
1428 .expect("should work")
1429 .expect("should get a record");
1430
1431 assert_eq!(fetched_b.fields.origin, login_b.origin);
1432
1433 assert_eq!(db.count_all().unwrap(), 2);
1434 }
1435
1436 #[test]
1437 fn test_count_by_origin() {
1438 ensure_initialized();
1439
1440 let origin_a = "https://a.example.com";
1441 let login_a = LoginEntry {
1442 origin: origin_a.into(),
1443 http_realm: Some("https://www.example.com".into()),
1444 username: "test".into(),
1445 password: "sekret".into(),
1446 ..LoginEntry::default()
1447 };
1448
1449 let login_b = LoginEntry {
1450 origin: "https://b.example.com".into(),
1451 http_realm: Some("https://www.example.com".into()),
1452 username: "test".into(),
1453 password: "sekret".into(),
1454 ..LoginEntry::default()
1455 };
1456
1457 let origin_umlaut = "https://bücher.example.com";
1458 let login_umlaut = LoginEntry {
1459 origin: origin_umlaut.into(),
1460 http_realm: Some("https://www.example.com".into()),
1461 username: "test".into(),
1462 password: "sekret".into(),
1463 ..LoginEntry::default()
1464 };
1465
1466 let db = LoginDb::open_in_memory();
1467 db.add_many(vec![login_a.clone(), login_b.clone(), login_umlaut.clone()])
1468 .expect("should be able to add logins");
1469
1470 assert_eq!(db.count_by_origin(origin_a).unwrap(), 1);
1471 assert_eq!(db.count_by_origin(origin_umlaut).unwrap(), 1);
1472 }
1473
1474 #[test]
1475 fn test_count_by_form_action_origin() {
1476 ensure_initialized();
1477
1478 let origin_a = "https://a.example.com";
1479 let login_a = LoginEntry {
1480 origin: origin_a.into(),
1481 form_action_origin: Some(origin_a.into()),
1482 http_realm: Some("https://www.example.com".into()),
1483 username: "test".into(),
1484 password: "sekret".into(),
1485 ..LoginEntry::default()
1486 };
1487
1488 let login_b = LoginEntry {
1489 origin: "https://b.example.com".into(),
1490 form_action_origin: Some("https://b.example.com".into()),
1491 http_realm: Some("https://www.example.com".into()),
1492 username: "test".into(),
1493 password: "sekret".into(),
1494 ..LoginEntry::default()
1495 };
1496
1497 let origin_umlaut = "https://bücher.example.com";
1498 let login_umlaut = LoginEntry {
1499 origin: origin_umlaut.into(),
1500 form_action_origin: Some(origin_umlaut.into()),
1501 http_realm: Some("https://www.example.com".into()),
1502 username: "test".into(),
1503 password: "sekret".into(),
1504 ..LoginEntry::default()
1505 };
1506
1507 let db = LoginDb::open_in_memory();
1508 db.add_many(vec![login_a.clone(), login_b.clone(), login_umlaut.clone()])
1509 .expect("should be able to add logins");
1510
1511 assert_eq!(db.count_by_form_action_origin(origin_a).unwrap(), 1);
1512 assert_eq!(db.count_by_form_action_origin(origin_umlaut).unwrap(), 1);
1513 }
1514
1515 #[test]
1516 #[cfg(feature = "ignore_form_action_origin_validation_errors")]
1517 fn test_count_by_invalid_form_action_origin() {
1518 ensure_initialized();
1519
1520 let login = LoginEntry {
1521 origin: "https://example.com".into(),
1522 form_action_origin: Some("email".into()),
1523 username: "test".into(),
1524 password: "sekret".into(),
1525 ..LoginEntry::default()
1526 };
1527
1528 let db = LoginDb::open_in_memory();
1529 db.add(login)
1530 .expect("should be able to add login with invalid form_action_origin");
1531 assert_eq!(db.count_by_form_action_origin("email").unwrap(), 1);
1532 }
1533
1534 #[test]
1535 fn test_add_many_with_failed_constraint() {
1536 ensure_initialized();
1537
1538 let login_a = LoginEntry {
1539 origin: "https://example.com".into(),
1540 http_realm: Some("https://www.example.com".into()),
1541 username: "test".into(),
1542 password: "sekret".into(),
1543 ..LoginEntry::default()
1544 };
1545
1546 let login_b = LoginEntry {
1547 origin: "https://example.com".into(),
1549 http_realm: Some("https://www.example.com".into()),
1550 username: "test".into(),
1551 password: "sekret".into(),
1552 ..LoginEntry::default()
1553 };
1554
1555 let db = LoginDb::open_in_memory();
1556 let added = db
1557 .add_many(vec![login_a.clone(), login_b.clone()])
1558 .expect("should be able to add logins");
1559
1560 let [added_a, added_b] = added.as_slice() else {
1561 panic!("there should really be 2")
1562 };
1563
1564 let fetched_a = db
1566 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1567 .expect("should work")
1568 .expect("should get a record");
1569
1570 assert_eq!(fetched_a.fields.origin, login_a.origin);
1571
1572 assert!(!added_b.is_ok());
1574 }
1575
1576 #[test]
1577 fn test_add_with_meta() {
1578 ensure_initialized();
1579
1580 let guid = Guid::random();
1581 let now_ms = util::system_time_ms_i64(SystemTime::now());
1582 let login = LoginEntry {
1583 origin: "https://www.example.com".into(),
1584 http_realm: Some("https://www.example.com".into()),
1585 username: "test".into(),
1586 password: "sekret".into(),
1587 ..LoginEntry::default()
1588 };
1589 let meta = LoginMeta {
1590 id: guid.to_string(),
1591 time_created: now_ms,
1592 time_password_changed: now_ms + 100,
1593 time_last_used: now_ms + 10,
1594 times_used: 42,
1595 time_last_breach_alert_dismissed: None,
1596 };
1597
1598 let db = LoginDb::open_in_memory();
1599 let entry_with_meta = LoginEntryWithMeta {
1600 entry: login.clone(),
1601 meta: meta.clone(),
1602 };
1603
1604 db.add_with_meta(entry_with_meta)
1605 .expect("should be able to add login with record");
1606
1607 let fetched = db
1608 .get_by_id(&guid)
1609 .expect("should work")
1610 .expect("should get a record");
1611
1612 assert_eq!(fetched.meta, meta);
1613 }
1614
1615 #[test]
1616 fn test_add_with_meta_invalid_guid() {
1617 ensure_initialized();
1618
1619 let now_ms = util::system_time_ms_i64(SystemTime::now());
1620 let meta = LoginMeta {
1622 id: "invalid,guid".to_string(),
1623 time_created: now_ms,
1624 time_password_changed: now_ms,
1625 time_last_used: now_ms,
1626 times_used: 1,
1627 time_last_breach_alert_dismissed: None,
1628 };
1629 let db = LoginDb::open_in_memory();
1630 let result = db.add_with_meta(LoginEntryWithMeta {
1631 entry: LoginEntry {
1632 origin: "https://www.example.com".into(),
1633 http_realm: Some("https://www.example.com".into()),
1634 username: "test".into(),
1635 password: "sekret".into(),
1636 ..LoginEntry::default()
1637 },
1638 meta,
1639 });
1640
1641 #[cfg(not(feature = "fixup_invalid_guids"))]
1644 assert!(result.is_err());
1645
1646 #[cfg(feature = "fixup_invalid_guids")]
1647 {
1648 let login = result.expect("invalid guid should be repaired");
1649 assert!(Guid::new(&login.meta.id).is_valid_for_sync_server());
1650 }
1651 }
1652
1653 #[test]
1654 fn test_add_with_meta_duplicate_id() {
1655 ensure_initialized();
1656
1657 let guid = Guid::random();
1658 let now_ms = util::system_time_ms_i64(SystemTime::now());
1659 let meta = LoginMeta {
1660 id: guid.to_string(),
1661 time_created: now_ms,
1662 time_password_changed: now_ms,
1663 time_last_used: now_ms,
1664 times_used: 1,
1665 time_last_breach_alert_dismissed: None,
1666 };
1667
1668 let db = LoginDb::open_in_memory();
1669 db.add_with_meta(LoginEntryWithMeta {
1670 entry: LoginEntry {
1671 origin: "https://www.example.com".into(),
1672 http_realm: Some("https://www.example.com".into()),
1673 username: "test".into(),
1674 password: "sekret".into(),
1675 ..LoginEntry::default()
1676 },
1677 meta: meta.clone(),
1678 })
1679 .expect("should be able to add login with record");
1680
1681 db.add_with_meta(LoginEntryWithMeta {
1684 entry: LoginEntry {
1685 origin: "https://www.other.com".into(),
1686 http_realm: Some("https://www.other.com".into()),
1687 username: "test".into(),
1688 password: "sekret".into(),
1689 ..LoginEntry::default()
1690 },
1691 meta,
1692 })
1693 .expect("should be able to re-add a login with the same id");
1694
1695 let fetched = db
1696 .get_by_id(&guid)
1697 .expect("should work")
1698 .expect("should get a record");
1699 assert_eq!(fetched.fields.origin, "https://www.other.com");
1700 }
1701
1702 #[test]
1703 fn test_record_potentially_vulnerable_passwords() {
1704 ensure_initialized();
1705 let db = LoginDb::open_in_memory();
1706
1707 let count: i64 = db
1709 .db
1710 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1711 .unwrap();
1712 assert_eq!(count, 0);
1713
1714 db.record_potentially_vulnerable_passwords(vec![
1716 "password1".into(),
1717 "password2".into(),
1718 "password3".into(),
1719 ])
1720 .unwrap();
1721
1722 let count: i64 = db
1724 .db
1725 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1726 .unwrap();
1727 assert_eq!(count, 3);
1728
1729 db.record_potentially_vulnerable_passwords(vec!["password1".into(), "password4".into()])
1731 .unwrap();
1732
1733 let count: i64 = db
1735 .db
1736 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1737 .unwrap();
1738 assert_eq!(count, 4);
1739
1740 db.record_potentially_vulnerable_passwords(vec!["password1".into(), "password2".into()])
1742 .unwrap();
1743
1744 let count: i64 = db
1745 .db
1746 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1747 .unwrap();
1748 assert_eq!(count, 4);
1749 }
1750
1751 #[test]
1752 fn test_add_with_meta_deleted() {
1753 ensure_initialized();
1754
1755 let guid = Guid::random();
1756 let now_ms = util::system_time_ms_i64(SystemTime::now());
1757 let login = LoginEntry {
1758 origin: "https://www.example.com".into(),
1759 http_realm: Some("https://www.example.com".into()),
1760 username: "test".into(),
1761 password: "sekret".into(),
1762 ..LoginEntry::default()
1763 };
1764 let meta = LoginMeta {
1765 id: guid.to_string(),
1766 time_created: now_ms,
1767 time_password_changed: now_ms + 100,
1768 time_last_used: now_ms + 10,
1769 times_used: 42,
1770 time_last_breach_alert_dismissed: None,
1771 };
1772
1773 let db = LoginDb::open_in_memory();
1774 let entry_with_meta = LoginEntryWithMeta {
1775 entry: login.clone(),
1776 meta: meta.clone(),
1777 };
1778
1779 db.add_with_meta(entry_with_meta)
1780 .expect("should be able to add login with record");
1781
1782 db.delete(&guid).expect("should be able to delete login");
1783
1784 let entry_with_meta2 = LoginEntryWithMeta {
1785 entry: login.clone(),
1786 meta: meta.clone(),
1787 };
1788
1789 db.add_with_meta(entry_with_meta2)
1790 .expect("should be able to re-add login with record");
1791
1792 let fetched = db
1793 .get_by_id(&guid)
1794 .expect("should work")
1795 .expect("should get a record");
1796
1797 assert_eq!(fetched.meta, meta);
1798 }
1799
1800 #[test]
1801 fn test_unicode_submit() {
1802 ensure_initialized();
1803 let db = LoginDb::open_in_memory();
1804 let added = db
1805 .add(LoginEntry {
1806 form_action_origin: Some("http://😍.com".into()),
1807 origin: "http://😍.com".into(),
1808 http_realm: None,
1809 username_field: "😍".into(),
1810 password_field: "😍".into(),
1811 username: "😍".into(),
1812 password: "😍".into(),
1813 })
1814 .unwrap();
1815 let fetched = db
1816 .get_by_id(&added.meta.id)
1817 .expect("should work")
1818 .expect("should get a record");
1819 assert_eq!(added, fetched);
1820 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1821 assert_eq!(
1822 fetched.fields.form_action_origin,
1823 Some("http://xn--r28h.com".to_string())
1824 );
1825 assert_eq!(fetched.fields.username_field, "😍");
1826 assert_eq!(fetched.fields.password_field, "😍");
1827 let sec_fields = fetched.decrypt_fields(db.encdec.as_ref()).unwrap();
1828 assert_eq!(sec_fields.username, "😍");
1829 assert_eq!(sec_fields.password, "😍");
1830 }
1831
1832 #[test]
1833 fn test_unicode_realm() {
1834 ensure_initialized();
1835 let db = LoginDb::open_in_memory();
1836 let added = db
1837 .add(LoginEntry {
1838 form_action_origin: None,
1839 origin: "http://😍.com".into(),
1840 http_realm: Some("😍😍".into()),
1841 username: "😍".into(),
1842 password: "😍".into(),
1843 ..Default::default()
1844 })
1845 .unwrap();
1846 let fetched = db
1847 .get_by_id(&added.meta.id)
1848 .expect("should work")
1849 .expect("should get a record");
1850 assert_eq!(added, fetched);
1851 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1852 assert_eq!(fetched.fields.http_realm.unwrap(), "😍😍");
1853 }
1854
1855 fn check_matches(db: &LoginDb, query: &str, expected: &[&str]) {
1856 let mut results = db
1857 .get_by_base_domain(query)
1858 .unwrap()
1859 .into_iter()
1860 .map(|l| l.fields.origin)
1861 .collect::<Vec<String>>();
1862 results.sort_unstable();
1863 let mut sorted = expected.to_owned();
1864 sorted.sort_unstable();
1865 assert_eq!(sorted, results);
1866 }
1867
1868 fn check_good_bad(
1869 good: Vec<&str>,
1870 bad: Vec<&str>,
1871 good_queries: Vec<&str>,
1872 zero_queries: Vec<&str>,
1873 ) {
1874 let db = LoginDb::open_in_memory();
1875 for h in good.iter().chain(bad.iter()) {
1876 db.add(LoginEntry {
1877 origin: (*h).into(),
1878 http_realm: Some((*h).into()),
1879 password: "test".into(),
1880 ..Default::default()
1881 })
1882 .unwrap();
1883 }
1884 for query in good_queries {
1885 check_matches(&db, query, &good);
1886 }
1887 for query in zero_queries {
1888 check_matches(&db, query, &[]);
1889 }
1890 }
1891
1892 #[test]
1893 fn test_get_by_base_domain_invalid() {
1894 ensure_initialized();
1895 check_good_bad(
1896 vec!["https://example.com"],
1897 vec![],
1898 vec![],
1899 vec!["invalid query"],
1900 );
1901 }
1902
1903 #[test]
1904 fn test_get_by_base_domain() {
1905 ensure_initialized();
1906 check_good_bad(
1907 vec![
1908 "https://example.com",
1909 "https://www.example.com",
1910 "http://www.example.com",
1911 "http://www.example.com:8080",
1912 "http://sub.example.com:8080",
1913 "https://sub.example.com:8080",
1914 "https://sub.sub.example.com",
1915 "ftp://sub.example.com",
1916 ],
1917 vec![
1918 "https://badexample.com",
1919 "https://example.co",
1920 "https://example.com.au",
1921 ],
1922 vec!["example.com"],
1923 vec!["foo.com"],
1924 );
1925 }
1926
1927 #[test]
1928 fn test_get_by_base_domain_punicode() {
1929 ensure_initialized();
1930 check_good_bad(
1933 vec![
1934 "http://xn--r28h.com", ],
1936 vec!["http://💖.com"],
1937 vec!["😍.com", "xn--r28h.com"],
1938 vec![],
1939 );
1940 }
1941
1942 #[test]
1943 fn test_get_by_base_domain_ipv4() {
1944 ensure_initialized();
1945 check_good_bad(
1946 vec!["http://127.0.0.1", "https://127.0.0.1:8000"],
1947 vec!["https://127.0.0.0", "https://example.com"],
1948 vec!["127.0.0.1"],
1949 vec!["127.0.0.2"],
1950 );
1951 }
1952
1953 #[test]
1954 fn test_get_by_base_domain_ipv6() {
1955 ensure_initialized();
1956 check_good_bad(
1957 vec!["http://[::1]", "https://[::1]:8000"],
1958 vec!["https://[0:0:0:0:0:0:1:1]", "https://example.com"],
1959 vec!["[::1]", "[0:0:0:0:0:0:0:1]"],
1960 vec!["[0:0:0:0:0:0:1:2]"],
1961 );
1962 }
1963
1964 #[test]
1965 fn test_add() {
1966 ensure_initialized();
1967 let db = LoginDb::open_in_memory();
1968 let to_add = LoginEntry {
1969 origin: "https://www.example.com".into(),
1970 http_realm: Some("https://www.example.com".into()),
1971 username: "test_user".into(),
1972 password: "test_password".into(),
1973 ..Default::default()
1974 };
1975 let login = db.add(to_add).unwrap();
1976 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1977
1978 assert_eq!(login.fields.origin, login2.fields.origin);
1979 assert_eq!(login.fields.http_realm, login2.fields.http_realm);
1980 assert_eq!(login.sec_fields, login2.sec_fields);
1981 }
1982
1983 #[test]
1984 fn test_update() {
1985 ensure_initialized();
1986 let db = LoginDb::open_in_memory();
1987 let login = db
1988 .add(LoginEntry {
1989 origin: "https://www.example.com".into(),
1990 http_realm: Some("https://www.example.com".into()),
1991 username: "user1".into(),
1992 password: "password1".into(),
1993 ..Default::default()
1994 })
1995 .unwrap();
1996 db.update(
1997 &login.meta.id,
1998 LoginEntry {
1999 origin: "https://www.example2.com".into(),
2000 http_realm: Some("https://www.example2.com".into()),
2001 username: "user2".into(),
2002 password: "password2".into(),
2003 ..Default::default() },
2005 )
2006 .unwrap();
2007
2008 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
2009
2010 assert_eq!(login2.fields.origin, "https://www.example2.com");
2011 assert_eq!(
2012 login2.fields.http_realm,
2013 Some("https://www.example2.com".into())
2014 );
2015 let sec_fields = login2.decrypt_fields(db.encdec.as_ref()).unwrap();
2016 assert_eq!(sec_fields.username, "user2");
2017 assert_eq!(sec_fields.password, "password2");
2018 }
2019
2020 #[test]
2021 fn test_touch() {
2022 ensure_initialized();
2023 let db = LoginDb::open_in_memory();
2024 let login = db
2025 .add(LoginEntry {
2026 origin: "https://www.example.com".into(),
2027 http_realm: Some("https://www.example.com".into()),
2028 username: "user1".into(),
2029 password: "password1".into(),
2030 ..Default::default()
2031 })
2032 .unwrap();
2033 thread::sleep(time::Duration::from_millis(50));
2035 db.touch(&login.meta.id).unwrap();
2036 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
2037 assert!(login2.meta.time_last_used > login.meta.time_last_used);
2038 assert_eq!(login2.meta.times_used, login.meta.times_used + 1);
2039 }
2040
2041 #[test]
2042 fn test_update_does_not_count_as_use() {
2043 ensure_initialized();
2047 let db = LoginDb::open_in_memory();
2048 let login = db
2049 .add(LoginEntry {
2050 origin: "https://www.example.com".into(),
2051 http_realm: Some("https://www.example.com".into()),
2052 username: "user1".into(),
2053 password: "password1".into(),
2054 ..Default::default()
2055 })
2056 .unwrap();
2057 thread::sleep(time::Duration::from_millis(50));
2059 db.update(
2060 &login.meta.id,
2061 LoginEntry {
2062 origin: "https://www.example.com".into(),
2063 http_realm: Some("https://www.example.com".into()),
2064 username: "user1".into(),
2065 password: "password2".into(),
2066 ..Default::default()
2067 },
2068 )
2069 .unwrap();
2070 let updated = db.get_by_id(&login.meta.id).unwrap().unwrap();
2071 assert_eq!(updated.meta.times_used, login.meta.times_used);
2073 assert_eq!(updated.meta.time_last_used, login.meta.time_last_used);
2075 }
2076
2077 #[test]
2078 fn test_breach_alert_dismissal() {
2079 ensure_initialized();
2080 let db = LoginDb::open_in_memory();
2081 let login = db
2082 .add(LoginEntry {
2083 origin: "https://www.example.com".into(),
2084 http_realm: Some("https://www.example.com".into()),
2085 username: "user1".into(),
2086 password: "password1".into(),
2087 ..Default::default()
2088 })
2089 .unwrap();
2090 assert!(login.meta.time_last_breach_alert_dismissed.is_none());
2092
2093 db.record_breach_alert_dismissal(&login.meta.id).unwrap();
2095 let login1 = db.get_by_id(&login.meta.id).unwrap().unwrap();
2096 assert!(login1.meta.time_last_breach_alert_dismissed.is_some());
2097 }
2098
2099 #[test]
2100 fn test_breach_alert_dismissal_with_specific_timestamp() {
2101 ensure_initialized();
2102 let db = LoginDb::open_in_memory();
2103 let login = db
2104 .add(LoginEntry {
2105 origin: "https://www.example.com".into(),
2106 http_realm: Some("https://www.example.com".into()),
2107 username: "user1".into(),
2108 password: "password1".into(),
2109 ..Default::default()
2110 })
2111 .unwrap();
2112
2113 let dismiss_time = login.meta.time_password_changed + 1000;
2114 db.record_breach_alert_dismissal_time(&login.meta.id, dismiss_time)
2115 .unwrap();
2116
2117 let retrieved = db
2118 .get_by_id(&login.meta.id)
2119 .unwrap()
2120 .unwrap()
2121 .decrypt(db.encdec.as_ref())
2122 .unwrap();
2123 assert_eq!(
2124 retrieved.time_last_breach_alert_dismissed,
2125 Some(dismiss_time)
2126 );
2127 }
2128
2129 #[test]
2130 fn test_delete() {
2131 ensure_initialized();
2132 let db = LoginDb::open_in_memory();
2133 let login = db
2134 .add(LoginEntry {
2135 origin: "https://www.example.com".into(),
2136 http_realm: Some("https://www.example.com".into()),
2137 username: "test_user".into(),
2138 password: "test_password".into(),
2139 ..Default::default()
2140 })
2141 .unwrap();
2142
2143 assert!(db.delete(login.guid_str()).unwrap());
2144
2145 let local_login = db
2146 .query_row(
2147 "SELECT * FROM loginsL WHERE guid = :guid",
2148 named_params! { ":guid": login.guid_str() },
2149 |row| Ok(LocalLogin::test_raw_from_row(row).unwrap()),
2150 )
2151 .unwrap();
2152 assert_eq!(local_login.fields.http_realm, None);
2153 assert_eq!(local_login.fields.form_action_origin, None);
2154
2155 assert!(!db.exists(login.guid_str()).unwrap());
2156 }
2157
2158 #[test]
2159 fn test_delete_many() {
2160 ensure_initialized();
2161 let db = LoginDb::open_in_memory();
2162
2163 let login_a = db
2164 .add(LoginEntry {
2165 origin: "https://a.example.com".into(),
2166 http_realm: Some("https://www.example.com".into()),
2167 username: "test_user".into(),
2168 password: "test_password".into(),
2169 ..Default::default()
2170 })
2171 .unwrap();
2172
2173 let login_b = db
2174 .add(LoginEntry {
2175 origin: "https://b.example.com".into(),
2176 http_realm: Some("https://www.example.com".into()),
2177 username: "test_user".into(),
2178 password: "test_password".into(),
2179 ..Default::default()
2180 })
2181 .unwrap();
2182
2183 let result = db
2184 .delete_many(vec![login_a.guid_str(), login_b.guid_str()])
2185 .unwrap();
2186 assert!(result[0]);
2187 assert!(result[1]);
2188 assert!(!db.exists(login_a.guid_str()).unwrap());
2189 assert!(!db.exists(login_b.guid_str()).unwrap());
2190 }
2191
2192 #[test]
2193 fn test_subsequent_delete_many() {
2194 ensure_initialized();
2195 let db = LoginDb::open_in_memory();
2196
2197 let login = db
2198 .add(LoginEntry {
2199 origin: "https://a.example.com".into(),
2200 http_realm: Some("https://www.example.com".into()),
2201 username: "test_user".into(),
2202 password: "test_password".into(),
2203 ..Default::default()
2204 })
2205 .unwrap();
2206
2207 let result = db.delete_many(vec![login.guid_str()]).unwrap();
2208 assert!(result[0]);
2209 assert!(!db.exists(login.guid_str()).unwrap());
2210
2211 let result = db.delete_many(vec![login.guid_str()]).unwrap();
2212 assert!(!result[0]);
2213 }
2214
2215 #[test]
2216 fn test_delete_many_with_non_existent_id() {
2217 ensure_initialized();
2218 let db = LoginDb::open_in_memory();
2219
2220 let result = db.delete_many(vec![&Guid::random()]).unwrap();
2221 assert!(!result[0]);
2222 }
2223
2224 #[test]
2225 fn test_delete_all() {
2226 ensure_initialized();
2227 let db = LoginDb::open_in_memory();
2228 let login_a = db
2229 .add(LoginEntry {
2230 origin: "https://a.example.com".into(),
2231 http_realm: Some("https://www.example.com".into()),
2232 username: "test_user".into(),
2233 password: "test_password".into(),
2234 ..Default::default()
2235 })
2236 .unwrap();
2237 let login_b = db
2238 .add(LoginEntry {
2239 origin: "https://b.example.com".into(),
2240 http_realm: Some("https://www.example.com".into()),
2241 username: "test_user".into(),
2242 password: "test_password".into(),
2243 ..Default::default()
2244 })
2245 .unwrap();
2246
2247 let mut deleted = db.delete_all().unwrap();
2248 deleted.sort();
2249 let mut expected = vec![login_a.meta.id.clone(), login_b.meta.id.clone()];
2250 expected.sort();
2251 assert_eq!(deleted, expected);
2252 assert!(!db.exists(login_a.guid_str()).unwrap());
2253 assert!(!db.exists(login_b.guid_str()).unwrap());
2254
2255 assert_eq!(db.delete_all().unwrap(), Vec::<String>::new());
2257 }
2258
2259 #[test]
2260 fn test_delete_all_except_fxa() {
2261 ensure_initialized();
2262 let db = LoginDb::open_in_memory();
2263 let login = db
2264 .add(LoginEntry {
2265 origin: "https://a.example.com".into(),
2266 http_realm: Some("https://www.example.com".into()),
2267 username: "test_user".into(),
2268 password: "test_password".into(),
2269 ..Default::default()
2270 })
2271 .unwrap();
2272 let fxa_login = db
2273 .add(LoginEntry {
2274 origin: FXA_CREDENTIALS_ORIGIN.into(),
2275 http_realm: Some("https://www.example.com".into()),
2276 username: "test_user".into(),
2277 password: "test_password".into(),
2278 ..Default::default()
2279 })
2280 .unwrap();
2281
2282 let deleted = db.delete_all_except_fxa().unwrap();
2283 assert_eq!(deleted, vec![login.meta.id.clone()]);
2284
2285 assert!(!db.exists(login.guid_str()).unwrap());
2287 assert!(db.exists(fxa_login.guid_str()).unwrap());
2288 }
2289
2290 #[test]
2291 fn test_wipe_local_except_fxa() {
2292 ensure_initialized();
2293 let db = LoginDb::open_in_memory();
2294 let login = db
2295 .add(LoginEntry {
2296 origin: "https://a.example.com".into(),
2297 http_realm: Some("https://www.example.com".into()),
2298 username: "test_user".into(),
2299 password: "test_password".into(),
2300 ..Default::default()
2301 })
2302 .unwrap();
2303 let fxa_login = db
2304 .add(LoginEntry {
2305 origin: FXA_CREDENTIALS_ORIGIN.into(),
2306 http_realm: Some("https://www.example.com".into()),
2307 username: "test_user".into(),
2308 password: "test_password".into(),
2309 ..Default::default()
2310 })
2311 .unwrap();
2312
2313 db.wipe_local_except_fxa().unwrap();
2314
2315 assert!(!db.exists(login.guid_str()).unwrap());
2317 assert!(db.exists(fxa_login.guid_str()).unwrap());
2318 }
2319
2320 #[test]
2321 fn test_delete_local_for_remote_replacement() {
2322 ensure_initialized();
2323 let db = LoginDb::open_in_memory();
2324 let login = db
2325 .add(LoginEntry {
2326 origin: "https://www.example.com".into(),
2327 http_realm: Some("https://www.example.com".into()),
2328 username: "test_user".into(),
2329 password: "test_password".into(),
2330 ..Default::default()
2331 })
2332 .unwrap();
2333
2334 let result = db
2335 .delete_local_records_for_remote_replacement(vec![login.guid_str()])
2336 .unwrap();
2337
2338 let local_guids = get_local_guids(&db);
2339 assert_eq!(local_guids.len(), 0);
2340
2341 let mirror_guids = get_mirror_guids(&db);
2342 assert_eq!(mirror_guids.len(), 0);
2343
2344 assert_eq!(result.local_deleted, 1);
2345 }
2346
2347 mod test_find_login_to_update {
2348 use super::*;
2349
2350 fn make_entry(username: &str, password: &str) -> LoginEntry {
2351 LoginEntry {
2352 origin: "https://www.example.com".into(),
2353 http_realm: Some("the website".into()),
2354 username: username.into(),
2355 password: password.into(),
2356 ..Default::default()
2357 }
2358 }
2359
2360 fn make_saved_login(db: &LoginDb, username: &str, password: &str) -> Login {
2361 db.add(make_entry(username, password))
2362 .unwrap()
2363 .decrypt(db.encdec.as_ref())
2364 .unwrap()
2365 }
2366
2367 #[test]
2368 fn test_match() {
2369 ensure_initialized();
2370 let db = LoginDb::open_in_memory();
2371 let login = make_saved_login(&db, "user", "pass");
2372 assert_eq!(
2373 Some(login),
2374 db.find_login_to_update(make_entry("user", "pass")).unwrap(),
2375 );
2376 }
2377
2378 #[test]
2379 fn test_non_matches() {
2380 ensure_initialized();
2381 let db = LoginDb::open_in_memory();
2382 make_saved_login(&db, "other-user", "pass");
2384 db.add(LoginEntry {
2386 origin: "https://www.example.com".into(),
2387 http_realm: Some("the other website".into()),
2388 username: "user".into(),
2389 password: "pass".into(),
2390 ..Default::default()
2391 })
2392 .unwrap();
2393 db.add(LoginEntry {
2395 origin: "https://www.example.com".into(),
2396 form_action_origin: Some("https://www.example.com/".into()),
2397 username: "user".into(),
2398 password: "pass".into(),
2399 ..Default::default()
2400 })
2401 .unwrap();
2402 assert_eq!(
2403 None,
2404 db.find_login_to_update(make_entry("user", "pass")).unwrap(),
2405 );
2406 }
2407
2408 #[test]
2409 fn test_match_blank_password() {
2410 ensure_initialized();
2411 let db = LoginDb::open_in_memory();
2412 let login = make_saved_login(&db, "", "pass");
2413 assert_eq!(
2414 Some(login),
2415 db.find_login_to_update(make_entry("user", "pass")).unwrap(),
2416 );
2417 }
2418
2419 #[test]
2420 fn test_username_match_takes_precedence_over_blank_username() {
2421 ensure_initialized();
2422 let db = LoginDb::open_in_memory();
2423 make_saved_login(&db, "", "pass");
2424 let username_match = make_saved_login(&db, "user", "pass");
2425 assert_eq!(
2426 Some(username_match),
2427 db.find_login_to_update(make_entry("user", "pass")).unwrap(),
2428 );
2429 }
2430
2431 #[test]
2432 fn test_invalid_login() {
2433 ensure_initialized();
2434 let db = LoginDb::open_in_memory();
2435 assert!(db
2436 .find_login_to_update(LoginEntry {
2437 http_realm: None,
2438 form_action_origin: None,
2439 ..LoginEntry::default()
2440 })
2441 .is_err());
2442 }
2443
2444 #[test]
2445 fn test_update_with_duplicate_login() {
2446 ensure_initialized();
2447 let db = LoginDb::open_in_memory();
2450 let login = make_saved_login(&db, "user", "pass");
2451 let mut dupe = login.clone().encrypt(&*TEST_ENCDEC).unwrap();
2452 dupe.meta.id = "different-guid".to_string();
2453 db.insert_new_login(&dupe).unwrap();
2454
2455 let mut entry = login.entry();
2456 entry.password = "pass2".to_string();
2457 db.update(&login.id, entry).unwrap();
2458
2459 let mut entry = login.entry();
2460 entry.password = "pass3".to_string();
2461 db.add_or_update(entry).unwrap();
2462 }
2463
2464 #[test]
2465 fn test_password_reuse_detection() {
2466 ensure_initialized();
2467 let db = LoginDb::open_in_memory();
2468
2469 let login1 = db
2471 .add(LoginEntry {
2472 origin: "https://site1.com".into(),
2473 http_realm: Some("realm".into()),
2474 username: "user1".into(),
2475 password: "shared_password".into(),
2476 ..Default::default()
2477 })
2478 .unwrap();
2479
2480 let login2 = db
2481 .add(LoginEntry {
2482 origin: "https://site2.com".into(),
2483 http_realm: Some("realm".into()),
2484 username: "user2".into(),
2485 password: "shared_password".into(),
2486 ..Default::default()
2487 })
2488 .unwrap();
2489
2490 assert!(!db
2492 .is_potentially_vulnerable_password(&login1.meta.id)
2493 .unwrap());
2494 assert!(!db
2495 .is_potentially_vulnerable_password(&login2.meta.id)
2496 .unwrap());
2497 let vulnerable = db
2499 .are_potentially_vulnerable_passwords(&[&login1.meta.id, &login2.meta.id])
2500 .unwrap();
2501 assert_eq!(vulnerable.len(), 0);
2502
2503 db.record_potentially_vulnerable_passwords(vec!["shared_password".into()])
2505 .unwrap();
2506
2507 assert!(db
2509 .is_potentially_vulnerable_password(&login2.meta.id)
2510 .unwrap());
2511 let vulnerable = db
2513 .are_potentially_vulnerable_passwords(&[&login1.meta.id, &login2.meta.id])
2514 .unwrap();
2515 assert_eq!(vulnerable.len(), 2);
2516 assert!(vulnerable.contains(&login1.meta.id));
2517 assert!(vulnerable.contains(&login2.meta.id));
2518
2519 db.update(
2521 &login2.meta.id,
2522 LoginEntry {
2523 origin: "https://site2.com".into(),
2524 http_realm: Some("realm".into()),
2525 username: "user2".into(),
2526 password: "different_password".into(),
2527 ..Default::default()
2528 },
2529 )
2530 .unwrap();
2531
2532 assert!(!db
2533 .is_potentially_vulnerable_password(&login2.meta.id)
2534 .unwrap());
2535 }
2536
2537 #[test]
2538 fn test_reset_all_breaches_clears_breach_table() {
2539 ensure_initialized();
2540 let db = LoginDb::open_in_memory();
2541
2542 let login = db
2543 .add(LoginEntry {
2544 origin: "https://example.com".into(),
2545 http_realm: Some("realm".into()),
2546 username: "user".into(),
2547 password: "password123".into(),
2548 ..Default::default()
2549 })
2550 .unwrap();
2551
2552 db.record_potentially_vulnerable_passwords(vec!["password123".into()])
2553 .unwrap();
2554
2555 let count: i64 = db
2557 .db
2558 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
2559 .unwrap();
2560 assert_eq!(count, 1);
2561 let vulnerable = db
2563 .are_potentially_vulnerable_passwords(&[&login.meta.id])
2564 .unwrap();
2565 assert_eq!(vulnerable.len(), 1);
2566 assert_eq!(vulnerable[0], login.meta.id);
2567
2568 db.reset_all_breaches().unwrap();
2570
2571 let count: i64 = db
2573 .db
2574 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
2575 .unwrap();
2576 assert_eq!(count, 0);
2577 let vulnerable = db
2579 .are_potentially_vulnerable_passwords(&[&login.meta.id])
2580 .unwrap();
2581 assert_eq!(vulnerable.len(), 0);
2582 }
2583
2584 #[test]
2585 fn test_different_passwords_not_vulnerable() {
2586 ensure_initialized();
2587 let db = LoginDb::open_in_memory();
2588
2589 let login1 = db
2590 .add(LoginEntry {
2591 origin: "https://site1.com".into(),
2592 http_realm: Some("realm".into()),
2593 username: "user".into(),
2594 password: "password_A".into(),
2595 ..Default::default()
2596 })
2597 .unwrap();
2598
2599 let login2 = db
2600 .add(LoginEntry {
2601 origin: "https://site2.com".into(),
2602 http_realm: Some("realm".into()),
2603 username: "user".into(),
2604 password: "password_B".into(),
2605 ..Default::default()
2606 })
2607 .unwrap();
2608
2609 db.record_potentially_vulnerable_passwords(vec!["password_A".into()])
2610 .unwrap();
2611
2612 assert!(!db
2614 .is_potentially_vulnerable_password(&login2.meta.id)
2615 .unwrap());
2616 let vulnerable = db
2619 .are_potentially_vulnerable_passwords(&[&login1.meta.id, &login2.meta.id])
2620 .unwrap();
2621 assert_eq!(vulnerable.len(), 1);
2622 assert!(vulnerable.contains(&login1.meta.id));
2623 }
2624 }
2625}