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(
269 &self,
270 look: LoginEntry,
271 encdec: &dyn EncryptorDecryptor,
272 ) -> Result<Option<Login>> {
273 let look = look.fixup()?;
274 let logins = self
275 .get_by_entry_target(&look)?
276 .into_iter()
277 .map(|enc_login| enc_login.decrypt(encdec))
278 .collect::<Result<Vec<Login>>>()?;
279 Ok(logins
280 .iter()
282 .find(|login| login.username == look.username)
283 .or_else(|| logins.iter().find(|login| login.username.is_empty()))
285 .cloned())
287 }
288
289 pub fn touch(&self, id: &str) -> Result<()> {
290 let tx = self.unchecked_transaction()?;
291 self.ensure_local_overlay_exists(id)?;
292 self.mark_mirror_overridden(id)?;
293 let now_ms = util::system_time_ms_i64(SystemTime::now());
294 self.execute_cached(
297 "UPDATE loginsL
298 SET timeLastUsed = :now_millis,
299 timesUsed = timesUsed + 1,
300 local_modified = :now_millis
301 WHERE guid = :guid
302 AND is_deleted = 0",
303 named_params! {
304 ":now_millis": now_ms,
305 ":guid": id,
306 },
307 )?;
308 tx.commit()?;
309 Ok(())
310 }
311
312 pub fn record_potentially_vulnerable_passwords(
317 &self,
318 passwords: Vec<String>,
319 encdec: &dyn EncryptorDecryptor,
320 ) -> Result<()> {
321 let tx = self.unchecked_transaction()?;
322 self.insert_potentially_vulnerable_passwords(passwords, encdec)?;
323 tx.commit()?;
324 Ok(())
325 }
326
327 fn insert_potentially_vulnerable_passwords(
328 &self,
329 passwords: Vec<String>,
330 encdec: &dyn EncryptorDecryptor,
331 ) -> Result<()> {
332 let encrypted_existing_potentially_vulnerable_passwords: Vec<String> = self
333 .db
334 .query_rows_and_then_cached("SELECT encryptedPassword FROM breachesL", [], |row| {
335 row.get(0)
336 })?;
337 let existing_potentially_vulnerable_passwords: Result<Vec<String>> =
338 encrypted_existing_potentially_vulnerable_passwords
339 .iter()
340 .map(|ciphertext| {
341 let decrypted_bytes =
342 encdec.decrypt(ciphertext.as_bytes().into()).map_err(|e| {
343 Error::DecryptionFailed(format!(
344 "Failed to decrypt password from breachesL: {}",
345 e
346 ))
347 })?;
348
349 let password = std::str::from_utf8(&decrypted_bytes).map_err(|e| {
350 Error::DecryptionFailed(format!(
351 "Decrypted password from breachesL is not valid UTF-8: {}",
352 e
353 ))
354 })?;
355
356 Ok(password.into())
357 })
358 .collect();
359
360 let existing: std::collections::HashSet<String> =
361 existing_potentially_vulnerable_passwords?
362 .into_iter()
363 .collect();
364 let difference: Vec<_> = passwords
365 .iter()
366 .filter(|item| !existing.contains(item.as_str()))
367 .collect();
368
369 for password in difference {
370 let encrypted_password_bytes = encdec
371 .encrypt(password.as_bytes().into())
372 .map_err(|e| Error::EncryptionFailed(format!("{e} (encrypting password)")))?;
373 let encrypted_password =
374 std::str::from_utf8(&encrypted_password_bytes).map_err(|e| {
375 Error::EncryptionFailed(format!("{e} (encrypting password: data not utf8)"))
376 })?;
377
378 self.execute_cached(
379 "INSERT INTO breachesL (encryptedPassword) VALUES (:encrypted_password)",
380 named_params! {
381 ":encrypted_password": encrypted_password,
382 },
383 )?;
384 }
385
386 Ok(())
387 }
388
389 pub fn are_potentially_vulnerable_passwords(
399 &self,
400 guids: &[&str],
401 encdec: &dyn EncryptorDecryptor,
402 ) -> Result<Vec<String>> {
403 if guids.is_empty() {
404 return Ok(Vec::new());
405 }
406
407 let all_encrypted_passwords: Vec<String> = self.db.query_rows_and_then_cached(
409 "SELECT encryptedPassword FROM breachesL",
410 [],
411 |row| row.get(0),
412 )?;
413
414 let mut breached_passwords = std::collections::HashSet::new();
415 for ciphertext in &all_encrypted_passwords {
416 let decrypted_bytes = encdec.decrypt(ciphertext.as_bytes().into()).map_err(|e| {
417 Error::DecryptionFailed(format!("Failed to decrypt password from breachesL: {}", e))
418 })?;
419
420 let decrypted_password = std::str::from_utf8(&decrypted_bytes).map_err(|e| {
421 Error::DecryptionFailed(format!(
422 "Decrypted password from breachesL is not valid UTF-8: {}",
423 e
424 ))
425 })?;
426
427 breached_passwords.insert(decrypted_password.to_string());
428 }
429
430 let mut vulnerable_guids = Vec::new();
432 for guid in guids {
433 if let Some(login) = self.get_by_id(guid)? {
434 let decrypted_login = login.decrypt(encdec)?;
435 if breached_passwords.contains(&decrypted_login.password) {
436 vulnerable_guids.push(guid.to_string());
437 }
438 }
439 }
440
441 Ok(vulnerable_guids)
442 }
443
444 pub fn is_potentially_vulnerable_password(
445 &self,
446 guid: &str,
447 encdec: &dyn EncryptorDecryptor,
448 ) -> Result<bool> {
449 let vulnerable = self.are_potentially_vulnerable_passwords(&[guid], encdec)?;
451 Ok(!vulnerable.is_empty())
452 }
453
454 pub fn reset_all_breaches(&self) -> Result<()> {
455 let tx = self.unchecked_transaction()?;
456 self.execute_cached("DELETE FROM breachesL", [])?;
457 tx.commit()?;
458 Ok(())
459 }
460
461 pub fn record_breach_alert_dismissal(&self, id: &str) -> Result<()> {
466 let timestamp = util::system_time_ms_i64(SystemTime::now());
467 self.record_breach_alert_dismissal_time(id, timestamp)
468 }
469
470 pub fn record_breach_alert_dismissal_time(&self, id: &str, timestamp: i64) -> Result<()> {
476 let tx = self.unchecked_transaction()?;
477 self.ensure_local_overlay_exists(id)?;
478 self.mark_mirror_overridden(id)?;
479 self.execute_cached(
480 "UPDATE loginsL
481 SET timeLastBreachAlertDismissed = :now_millis
482 WHERE guid = :guid",
483 named_params! {
484 ":now_millis": timestamp,
485 ":guid": id,
486 },
487 )?;
488 tx.commit()?;
489 Ok(())
490 }
491
492 fn insert_new_login(&self, login: &EncryptedLogin) -> Result<()> {
495 let sql = format!(
496 "INSERT OR REPLACE INTO loginsL (
497 origin,
498 httpRealm,
499 formActionOrigin,
500 usernameField,
501 passwordField,
502 timesUsed,
503 secFields,
504 guid,
505 timeCreated,
506 timeLastUsed,
507 timePasswordChanged,
508 timeLastBreachAlertDismissed,
509 local_modified,
510 is_deleted,
511 sync_status
512 ) VALUES (
513 :origin,
514 :http_realm,
515 :form_action_origin,
516 :username_field,
517 :password_field,
518 :times_used,
519 :sec_fields,
520 :guid,
521 :time_created,
522 :time_last_used,
523 :time_password_changed,
524 :time_last_breach_alert_dismissed,
525 :local_modified,
526 0, -- is_deleted
527 {new} -- sync_status
528 )",
529 new = SyncStatus::New as u8
530 );
531
532 self.execute(
533 &sql,
534 named_params! {
535 ":origin": login.fields.origin,
536 ":http_realm": login.fields.http_realm,
537 ":form_action_origin": login.fields.form_action_origin,
538 ":username_field": login.fields.username_field,
539 ":password_field": login.fields.password_field,
540 ":time_created": login.meta.time_created,
541 ":times_used": login.meta.times_used,
542 ":time_last_used": login.meta.time_last_used,
543 ":time_password_changed": login.meta.time_password_changed,
544 ":local_modified": login.meta.time_created,
545 ":time_last_breach_alert_dismissed": login.meta.time_last_breach_alert_dismissed,
546 ":sec_fields": login.sec_fields,
547 ":guid": login.guid(),
548 },
549 )?;
550 Ok(())
551 }
552
553 fn update_existing_login(&self, login: &EncryptedLogin) -> Result<()> {
554 let sql = format!(
556 "UPDATE loginsL
557 SET local_modified = :now_millis,
558 timeLastUsed = :time_last_used,
559 timePasswordChanged = :time_password_changed,
560 httpRealm = :http_realm,
561 formActionOrigin = :form_action_origin,
562 usernameField = :username_field,
563 passwordField = :password_field,
564 timesUsed = :times_used,
565 secFields = :sec_fields,
566 origin = :origin,
567 -- leave New records as they are, otherwise update them to `changed`
568 sync_status = max(sync_status, {changed})
569 WHERE guid = :guid",
570 changed = SyncStatus::Changed as u8
571 );
572
573 self.db.execute(
574 &sql,
575 named_params! {
576 ":origin": login.fields.origin,
577 ":http_realm": login.fields.http_realm,
578 ":form_action_origin": login.fields.form_action_origin,
579 ":username_field": login.fields.username_field,
580 ":password_field": login.fields.password_field,
581 ":time_last_used": login.meta.time_last_used,
582 ":times_used": login.meta.times_used,
583 ":time_password_changed": login.meta.time_password_changed,
584 ":sec_fields": login.sec_fields,
585 ":guid": &login.meta.id,
586 ":now_millis": login.meta.time_last_used,
588 },
589 )?;
590 Ok(())
591 }
592
593 pub fn add_many(
595 &self,
596 entries: Vec<LoginEntry>,
597 encdec: &dyn EncryptorDecryptor,
598 ) -> Result<Vec<Result<EncryptedLogin>>> {
599 let now_ms = util::system_time_ms_i64(SystemTime::now());
600
601 let entries_with_meta = entries
602 .into_iter()
603 .map(|entry| {
604 let guid = Guid::random();
605 LoginEntryWithMeta {
606 entry,
607 meta: LoginMeta {
608 id: guid.to_string(),
609 time_created: now_ms,
610 time_password_changed: now_ms,
611 time_last_used: now_ms,
612 times_used: 1,
613 time_last_breach_alert_dismissed: None,
614 },
615 }
616 })
617 .collect();
618
619 self.add_many_with_meta(entries_with_meta, encdec)
620 }
621
622 pub fn add_many_with_meta(
627 &self,
628 entries_with_meta: Vec<LoginEntryWithMeta>,
629 encdec: &dyn EncryptorDecryptor,
630 ) -> Result<Vec<Result<EncryptedLogin>>> {
631 let tx = self.unchecked_transaction()?;
632 let mut results = vec![];
633 for entry_with_meta in entries_with_meta {
634 let guid = Guid::from_string(entry_with_meta.meta.id.clone());
635 match self.fixup_and_check_for_dupes(&guid, entry_with_meta.entry, encdec) {
636 Ok(new_entry) => {
637 let sec_fields = SecureLoginFields {
638 username: new_entry.username,
639 password: new_entry.password,
640 }
641 .encrypt(encdec, &entry_with_meta.meta.id)?;
642 let encrypted_login = EncryptedLogin {
643 meta: entry_with_meta.meta,
644 fields: LoginFields {
645 origin: new_entry.origin,
646 form_action_origin: new_entry.form_action_origin,
647 http_realm: new_entry.http_realm,
648 username_field: new_entry.username_field,
649 password_field: new_entry.password_field,
650 },
651 sec_fields,
652 };
653 let result = self
654 .insert_new_login(&encrypted_login)
655 .map(|_| encrypted_login);
656 results.push(result);
657 }
658
659 Err(error) => results.push(Err(error)),
660 }
661 }
662
663 tx.commit()?;
664
665 Ok(results)
666 }
667
668 pub fn add(
669 &self,
670 entry: LoginEntry,
671 encdec: &dyn EncryptorDecryptor,
672 ) -> Result<EncryptedLogin> {
673 let guid = Guid::random();
674 let now_ms = util::system_time_ms_i64(SystemTime::now());
675
676 let entry_with_meta = LoginEntryWithMeta {
677 entry,
678 meta: LoginMeta {
679 id: guid.to_string(),
680 time_created: now_ms,
681 time_password_changed: now_ms,
682 time_last_used: now_ms,
683 times_used: 1,
684 time_last_breach_alert_dismissed: None,
685 },
686 };
687
688 self.add_with_meta(entry_with_meta, encdec)
689 }
690
691 pub fn add_with_meta(
695 &self,
696 entry_with_meta: LoginEntryWithMeta,
697 encdec: &dyn EncryptorDecryptor,
698 ) -> Result<EncryptedLogin> {
699 let mut results = self.add_many_with_meta(vec![entry_with_meta], encdec)?;
700 results.pop().expect("there should be a single result")
701 }
702
703 pub fn update(
704 &self,
705 sguid: &str,
706 entry: LoginEntry,
707 encdec: &dyn EncryptorDecryptor,
708 ) -> Result<EncryptedLogin> {
709 let guid = Guid::new(sguid);
710 let now_ms = util::system_time_ms_i64(SystemTime::now());
711 let tx = self.unchecked_transaction()?;
712
713 let entry = entry.fixup()?;
714
715 if self.check_for_dupes(&guid, &entry, encdec).is_err() {
721 let has_mirror_row: bool = self
723 .db
724 .conn_ext_query_one("SELECT EXISTS (SELECT 1 FROM loginsM)")?;
725 let has_http_realm = entry.http_realm.is_some();
726 let has_form_action_origin = entry.form_action_origin.is_some();
727 report_error!(
728 "logins-duplicate-in-update",
729 "(mirror: {has_mirror_row}, realm: {has_http_realm}, form_origin: {has_form_action_origin})");
730 }
731
732 self.ensure_local_overlay_exists(&guid)?;
734 self.mark_mirror_overridden(&guid)?;
735
736 let existing = match self.get_by_id(sguid)? {
738 Some(e) => e.decrypt(encdec)?,
739 None => return Err(Error::NoSuchRecord(sguid.to_owned())),
740 };
741 let time_password_changed = if existing.password == entry.password {
742 existing.time_password_changed
743 } else {
744 now_ms
745 };
746
747 let sec_fields = SecureLoginFields {
749 username: entry.username,
750 password: entry.password,
751 }
752 .encrypt(encdec, &existing.id)?;
753 let result = EncryptedLogin {
754 meta: LoginMeta {
755 id: existing.id,
756 time_created: existing.time_created,
757 time_password_changed,
758 time_last_used: now_ms,
759 times_used: existing.times_used + 1,
760 time_last_breach_alert_dismissed: None,
761 },
762 fields: LoginFields {
763 origin: entry.origin,
764 form_action_origin: entry.form_action_origin,
765 http_realm: entry.http_realm,
766 username_field: entry.username_field,
767 password_field: entry.password_field,
768 },
769 sec_fields,
770 };
771
772 self.update_existing_login(&result)?;
773 tx.commit()?;
774 Ok(result)
775 }
776
777 pub fn add_or_update(
778 &self,
779 entry: LoginEntry,
780 encdec: &dyn EncryptorDecryptor,
781 ) -> Result<EncryptedLogin> {
782 let entry = entry.fixup()?;
784 match self.find_login_to_update(entry.clone(), encdec)? {
785 Some(login) => self.update(&login.id, entry, encdec),
786 None => self.add(entry, encdec),
787 }
788 }
789
790 pub fn fixup_and_check_for_dupes(
791 &self,
792 guid: &Guid,
793 entry: LoginEntry,
794 encdec: &dyn EncryptorDecryptor,
795 ) -> Result<LoginEntry> {
796 let entry = entry.fixup()?;
797 self.check_for_dupes(guid, &entry, encdec)?;
798 Ok(entry)
799 }
800
801 pub fn check_for_dupes(
802 &self,
803 guid: &Guid,
804 entry: &LoginEntry,
805 encdec: &dyn EncryptorDecryptor,
806 ) -> Result<()> {
807 if self.dupe_exists(guid, entry, encdec)? {
808 return Err(InvalidLogin::DuplicateLogin.into());
809 }
810 Ok(())
811 }
812
813 pub fn dupe_exists(
814 &self,
815 guid: &Guid,
816 entry: &LoginEntry,
817 encdec: &dyn EncryptorDecryptor,
818 ) -> Result<bool> {
819 Ok(self.find_dupe(guid, entry, encdec)?.is_some())
820 }
821
822 pub fn find_dupe(
823 &self,
824 guid: &Guid,
825 entry: &LoginEntry,
826 encdec: &dyn EncryptorDecryptor,
827 ) -> Result<Option<Guid>> {
828 for possible in self.get_by_entry_target(entry)? {
829 if possible.guid() != *guid {
830 let pos_sec_fields = possible.decrypt_fields(encdec)?;
831 if pos_sec_fields.username == entry.username {
832 return Ok(Some(possible.guid()));
833 }
834 }
835 }
836 Ok(None)
837 }
838
839 fn get_by_entry_target(&self, entry: &LoginEntry) -> Result<Vec<EncryptedLogin>> {
849 lazy_static::lazy_static! {
851 static ref GET_BY_FORM_ACTION_ORIGIN: String = format!(
852 "SELECT {common_cols} FROM loginsL
853 WHERE is_deleted = 0
854 AND origin = :origin
855 AND formActionOrigin = :form_action_origin
856
857 UNION ALL
858
859 SELECT {common_cols} FROM loginsM
860 WHERE is_overridden = 0
861 AND origin = :origin
862 AND formActionOrigin = :form_action_origin
863 ",
864 common_cols = schema::COMMON_COLS
865 );
866 static ref GET_BY_HTTP_REALM: String = format!(
867 "SELECT {common_cols} FROM loginsL
868 WHERE is_deleted = 0
869 AND origin = :origin
870 AND httpRealm = :http_realm
871
872 UNION ALL
873
874 SELECT {common_cols} FROM loginsM
875 WHERE is_overridden = 0
876 AND origin = :origin
877 AND httpRealm = :http_realm
878 ",
879 common_cols = schema::COMMON_COLS
880 );
881 }
882 match (entry.form_action_origin.as_ref(), entry.http_realm.as_ref()) {
883 (Some(form_action_origin), None) => {
884 let params = named_params! {
885 ":origin": &entry.origin,
886 ":form_action_origin": form_action_origin,
887 };
888 self.db
889 .prepare_cached(&GET_BY_FORM_ACTION_ORIGIN)?
890 .query_and_then(params, EncryptedLogin::from_row)?
891 .collect()
892 }
893 (None, Some(http_realm)) => {
894 let params = named_params! {
895 ":origin": &entry.origin,
896 ":http_realm": http_realm,
897 };
898 self.db
899 .prepare_cached(&GET_BY_HTTP_REALM)?
900 .query_and_then(params, EncryptedLogin::from_row)?
901 .collect()
902 }
903 (Some(_), Some(_)) => Err(InvalidLogin::BothTargets.into()),
904 (None, None) => Err(InvalidLogin::NoTarget.into()),
905 }
906 }
907
908 pub fn exists(&self, id: &str) -> Result<bool> {
909 Ok(self.db.query_row(
910 "SELECT EXISTS(
911 SELECT 1 FROM loginsL
912 WHERE guid = :guid AND is_deleted = 0
913 UNION ALL
914 SELECT 1 FROM loginsM
915 WHERE guid = :guid AND is_overridden IS NOT 1
916 )",
917 named_params! { ":guid": id },
918 |row| row.get(0),
919 )?)
920 }
921
922 pub fn delete(&self, id: &str) -> Result<bool> {
925 let mut results = self.delete_many(vec![id])?;
926 Ok(results.pop().expect("there should be a single result"))
927 }
928
929 pub fn delete_many(&self, ids: Vec<&str>) -> Result<Vec<bool>> {
932 let tx = self.unchecked_transaction_imm()?;
933 let sql = format!(
934 "
935 UPDATE loginsL
936 SET local_modified = :now_ms,
937 sync_status = {status_changed},
938 is_deleted = 1,
939 secFields = '',
940 origin = '',
941 httpRealm = NULL,
942 formActionOrigin = NULL
943 WHERE guid = :guid AND is_deleted IS FALSE
944 ",
945 status_changed = SyncStatus::Changed as u8
946 );
947 let mut stmt = self.db.prepare_cached(&sql)?;
948
949 let mut result = vec![];
950
951 for id in ids {
952 let now_ms = util::system_time_ms_i64(SystemTime::now());
953
954 let update_result = stmt.execute(named_params! { ":now_ms": now_ms, ":guid": id })?;
956
957 let exists = update_result == 1;
958
959 self.execute(
961 "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
962 named_params! { ":guid": id },
963 )?;
964
965 self.execute(&format!("
968 INSERT OR IGNORE INTO loginsL
969 (guid, local_modified, is_deleted, sync_status, origin, timeCreated, timePasswordChanged, secFields)
970 SELECT guid, :now_ms, 1, {changed}, '', timeCreated, :now_ms, ''
971 FROM loginsM
972 WHERE guid = :guid",
973 changed = SyncStatus::Changed as u8),
974 named_params! { ":now_ms": now_ms, ":guid": id })?;
975
976 result.push(exists);
977 }
978
979 tx.commit()?;
980
981 Ok(result)
982 }
983
984 pub fn delete_undecryptable_records_for_remote_replacement(
985 &self,
986 encdec: &dyn EncryptorDecryptor,
987 ) -> Result<LoginsDeletionMetrics> {
988 let corrupted_logins = self
990 .get_all()?
991 .into_iter()
992 .filter(|login| login.clone().decrypt(encdec).is_err())
993 .collect::<Vec<_>>();
994 let ids = corrupted_logins
995 .iter()
996 .map(|login| login.guid_str())
997 .collect::<Vec<_>>();
998
999 self.delete_local_records_for_remote_replacement(ids)
1000 }
1001
1002 pub fn delete_local_records_for_remote_replacement(
1003 &self,
1004 ids: Vec<&str>,
1005 ) -> Result<LoginsDeletionMetrics> {
1006 let tx = self.unchecked_transaction_imm()?;
1007 let mut local_deleted = 0;
1008 let mut mirror_deleted = 0;
1009
1010 sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
1011 let deleted = self.execute(
1012 &format!(
1013 "DELETE FROM loginsL WHERE guid IN ({})",
1014 sql_support::repeat_sql_values(chunk.len())
1015 ),
1016 rusqlite::params_from_iter(chunk),
1017 )?;
1018 local_deleted += deleted;
1019 Ok(())
1020 })?;
1021
1022 sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
1023 let deleted = self.execute(
1024 &format!(
1025 "DELETE FROM loginsM WHERE guid IN ({})",
1026 sql_support::repeat_sql_values(chunk.len())
1027 ),
1028 rusqlite::params_from_iter(chunk),
1029 )?;
1030 mirror_deleted += deleted;
1031 Ok(())
1032 })?;
1033
1034 tx.commit()?;
1035 Ok(LoginsDeletionMetrics {
1036 local_deleted: local_deleted as u64,
1037 mirror_deleted: mirror_deleted as u64,
1038 })
1039 }
1040
1041 fn mark_mirror_overridden(&self, guid: &str) -> Result<()> {
1042 self.execute_cached(
1043 "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
1044 named_params! { ":guid": guid },
1045 )?;
1046 Ok(())
1047 }
1048
1049 fn ensure_local_overlay_exists(&self, guid: &str) -> Result<()> {
1050 let already_have_local: bool = self.db.query_row(
1051 "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid)",
1052 named_params! { ":guid": guid },
1053 |row| row.get(0),
1054 )?;
1055
1056 if already_have_local {
1057 return Ok(());
1058 }
1059
1060 debug!("No overlay; cloning one for {:?}.", guid);
1061 let changed = self.clone_mirror_to_overlay(guid)?;
1062 if changed == 0 {
1063 report_error!(
1064 "logins-local-overlay-error",
1065 "Failed to create local overlay for GUID {guid:?}."
1066 );
1067 return Err(Error::NoSuchRecord(guid.to_owned()));
1068 }
1069 Ok(())
1070 }
1071
1072 fn clone_mirror_to_overlay(&self, guid: &str) -> Result<usize> {
1073 Ok(self.execute_cached(&CLONE_SINGLE_MIRROR_SQL, &[(":guid", &guid as &dyn ToSql)])?)
1074 }
1075
1076 pub fn wipe_local(&self) -> Result<usize> {
1078 info!("Executing wipe_local on password engine!");
1079 let tx = self.unchecked_transaction()?;
1080 let mut row_count = 0;
1081 row_count += self.execute("DELETE FROM loginsL", [])?;
1082 row_count += self.execute("DELETE FROM loginsM", [])?;
1083 row_count += self.execute("DELETE FROM loginsSyncMeta", [])?;
1084 row_count += self.execute("DELETE FROM breachesL", [])?;
1085 tx.commit()?;
1086 Ok(row_count)
1087 }
1088
1089 pub fn shutdown(self) -> Result<()> {
1090 self.db.close().map_err(|(_, e)| Error::SqlError(e))
1091 }
1092}
1093
1094lazy_static! {
1095 static ref GET_ALL_SQL: String = format!(
1096 "SELECT {common_cols} FROM loginsL WHERE is_deleted = 0
1097 UNION ALL
1098 SELECT {common_cols} FROM loginsM WHERE is_overridden = 0",
1099 common_cols = schema::COMMON_COLS,
1100 );
1101 static ref COUNT_ALL_SQL: String = format!(
1102 "SELECT COUNT(*) FROM (
1103 SELECT guid FROM loginsL WHERE is_deleted = 0
1104 UNION ALL
1105 SELECT guid FROM loginsM WHERE is_overridden = 0
1106 )"
1107 );
1108 static ref COUNT_BY_ORIGIN_SQL: String = format!(
1109 "SELECT COUNT(*) FROM (
1110 SELECT guid FROM loginsL WHERE is_deleted = 0 AND origin = :origin
1111 UNION ALL
1112 SELECT guid FROM loginsM WHERE is_overridden = 0 AND origin = :origin
1113 )"
1114 );
1115 static ref COUNT_BY_FORM_ACTION_ORIGIN_SQL: String = format!(
1116 "SELECT COUNT(*) FROM (
1117 SELECT guid FROM loginsL WHERE is_deleted = 0 AND formActionOrigin = :form_action_origin
1118 UNION ALL
1119 SELECT guid FROM loginsM WHERE is_overridden = 0 AND formActionOrigin = :form_action_origin
1120 )"
1121 );
1122 static ref GET_BY_GUID_SQL: String = format!(
1123 "SELECT {common_cols}
1124 FROM loginsL
1125 WHERE is_deleted = 0
1126 AND guid = :guid
1127
1128 UNION ALL
1129
1130 SELECT {common_cols}
1131 FROM loginsM
1132 WHERE is_overridden IS NOT 1
1133 AND guid = :guid
1134 ORDER BY origin ASC
1135
1136 LIMIT 1",
1137 common_cols = schema::COMMON_COLS,
1138 );
1139 pub static ref CLONE_ENTIRE_MIRROR_SQL: String = format!(
1140 "INSERT OR IGNORE INTO loginsL ({common_cols}, local_modified, is_deleted, sync_status)
1141 SELECT {common_cols}, NULL AS local_modified, 0 AS is_deleted, 0 AS sync_status
1142 FROM loginsM",
1143 common_cols = schema::COMMON_COLS,
1144 );
1145 static ref CLONE_SINGLE_MIRROR_SQL: String =
1146 format!("{} WHERE guid = :guid", &*CLONE_ENTIRE_MIRROR_SQL,);
1147}
1148
1149#[cfg(not(feature = "keydb"))]
1150#[cfg(test)]
1151pub mod test_utils {
1152 use super::*;
1153 use crate::encryption::test_utils::decrypt_struct;
1154 use crate::login::test_utils::enc_login;
1155 use crate::SecureLoginFields;
1156 use sync15::ServerTimestamp;
1157
1158 pub fn insert_login(
1162 db: &LoginDb,
1163 guid: &str,
1164 local_login: Option<&str>,
1165 mirror_login: Option<&str>,
1166 ) {
1167 if let Some(password) = mirror_login {
1168 add_mirror(
1169 db,
1170 &enc_login(guid, password),
1171 &ServerTimestamp(util::system_time_ms_i64(std::time::SystemTime::now())),
1172 local_login.is_some(),
1173 )
1174 .unwrap();
1175 }
1176 if let Some(password) = local_login {
1177 db.insert_new_login(&enc_login(guid, password)).unwrap();
1178 }
1179 }
1180
1181 pub fn insert_encrypted_login(
1182 db: &LoginDb,
1183 local: &EncryptedLogin,
1184 mirror: &EncryptedLogin,
1185 server_modified: &ServerTimestamp,
1186 ) {
1187 db.insert_new_login(local).unwrap();
1188 add_mirror(db, mirror, server_modified, true).unwrap();
1189 }
1190
1191 pub fn add_mirror(
1192 db: &LoginDb,
1193 login: &EncryptedLogin,
1194 server_modified: &ServerTimestamp,
1195 is_overridden: bool,
1196 ) -> Result<()> {
1197 let sql = "
1198 INSERT OR IGNORE INTO loginsM (
1199 is_overridden,
1200 server_modified,
1201
1202 httpRealm,
1203 formActionOrigin,
1204 usernameField,
1205 passwordField,
1206 secFields,
1207 origin,
1208
1209 timesUsed,
1210 timeLastUsed,
1211 timePasswordChanged,
1212 timeCreated,
1213
1214 timeLastBreachAlertDismissed,
1215
1216 guid
1217 ) VALUES (
1218 :is_overridden,
1219 :server_modified,
1220
1221 :http_realm,
1222 :form_action_origin,
1223 :username_field,
1224 :password_field,
1225 :sec_fields,
1226 :origin,
1227
1228 :times_used,
1229 :time_last_used,
1230 :time_password_changed,
1231 :time_created,
1232
1233 :time_last_breach_alert_dismissed,
1234
1235 :guid
1236 )";
1237 let mut stmt = db.prepare_cached(sql)?;
1238
1239 stmt.execute(named_params! {
1240 ":is_overridden": is_overridden,
1241 ":server_modified": server_modified.as_millis(),
1242 ":http_realm": login.fields.http_realm,
1243 ":form_action_origin": login.fields.form_action_origin,
1244 ":username_field": login.fields.username_field,
1245 ":password_field": login.fields.password_field,
1246 ":origin": login.fields.origin,
1247 ":sec_fields": login.sec_fields,
1248 ":times_used": login.meta.times_used,
1249 ":time_last_used": login.meta.time_last_used,
1250 ":time_password_changed": login.meta.time_password_changed,
1251 ":time_created": login.meta.time_created,
1252 ":time_last_breach_alert_dismissed": login.meta.time_last_breach_alert_dismissed,
1253 ":guid": login.guid_str(),
1254 })?;
1255 Ok(())
1256 }
1257
1258 pub fn get_local_guids(db: &LoginDb) -> Vec<String> {
1259 get_guids(db, "SELECT guid FROM loginsL")
1260 }
1261
1262 pub fn get_mirror_guids(db: &LoginDb) -> Vec<String> {
1263 get_guids(db, "SELECT guid FROM loginsM")
1264 }
1265
1266 fn get_guids(db: &LoginDb, sql: &str) -> Vec<String> {
1267 let mut stmt = db.prepare_cached(sql).unwrap();
1268 let mut res: Vec<String> = stmt
1269 .query_map([], |r| r.get(0))
1270 .unwrap()
1271 .map(|r| r.unwrap())
1272 .collect();
1273 res.sort();
1274 res
1275 }
1276
1277 pub fn get_server_modified(db: &LoginDb, guid: &str) -> i64 {
1278 db.conn_ext_query_one(&format!(
1279 "SELECT server_modified FROM loginsM WHERE guid='{}'",
1280 guid
1281 ))
1282 .unwrap()
1283 }
1284
1285 pub fn check_local_login(db: &LoginDb, guid: &str, password: &str, local_modified_gte: i64) {
1286 let row: (String, i64, bool) = db
1287 .query_row(
1288 "SELECT secFields, local_modified, is_deleted FROM loginsL WHERE guid=?",
1289 [guid],
1290 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1291 )
1292 .unwrap();
1293 let enc: SecureLoginFields = decrypt_struct(row.0);
1294 assert_eq!(enc.password, password);
1295 assert!(row.1 >= local_modified_gte);
1296 assert!(!row.2);
1297 }
1298
1299 pub fn check_mirror_login(
1300 db: &LoginDb,
1301 guid: &str,
1302 password: &str,
1303 server_modified: i64,
1304 is_overridden: bool,
1305 ) {
1306 let row: (String, i64, bool) = db
1307 .query_row(
1308 "SELECT secFields, server_modified, is_overridden FROM loginsM WHERE guid=?",
1309 [guid],
1310 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1311 )
1312 .unwrap();
1313 let enc: SecureLoginFields = decrypt_struct(row.0);
1314 assert_eq!(enc.password, password);
1315 assert_eq!(row.1, server_modified);
1316 assert_eq!(row.2, is_overridden);
1317 }
1318}
1319
1320#[cfg(not(feature = "keydb"))]
1321#[cfg(test)]
1322mod tests {
1323 use super::*;
1324 use crate::db::test_utils::{get_local_guids, get_mirror_guids};
1325 use crate::encryption::test_utils::TEST_ENCDEC;
1326 use crate::sync::merge::LocalLogin;
1327 use nss::ensure_initialized;
1328 use std::{thread, time};
1329
1330 #[test]
1331 fn test_username_dupe_semantics() {
1332 ensure_initialized();
1333 let mut login = LoginEntry {
1334 origin: "https://www.example.com".into(),
1335 http_realm: Some("https://www.example.com".into()),
1336 username: "test".into(),
1337 password: "sekret".into(),
1338 ..LoginEntry::default()
1339 };
1340
1341 let db = LoginDb::open_in_memory();
1342 db.add(login.clone(), &*TEST_ENCDEC)
1343 .expect("should be able to add first login");
1344
1345 let exp_err = "Invalid login: Login already exists";
1347 assert_eq!(
1348 db.add(login.clone(), &*TEST_ENCDEC)
1349 .unwrap_err()
1350 .to_string(),
1351 exp_err
1352 );
1353
1354 login.username = "".to_string();
1356 db.add(login.clone(), &*TEST_ENCDEC)
1357 .expect("empty login isn't a dupe");
1358
1359 assert_eq!(
1360 db.add(login, &*TEST_ENCDEC).unwrap_err().to_string(),
1361 exp_err
1362 );
1363
1364 assert_eq!(db.get_all().unwrap().len(), 2);
1366 }
1367
1368 #[test]
1369 fn test_add_many() {
1370 ensure_initialized();
1371
1372 let login_a = LoginEntry {
1373 origin: "https://a.example.com".into(),
1374 http_realm: Some("https://www.example.com".into()),
1375 username: "test".into(),
1376 password: "sekret".into(),
1377 ..LoginEntry::default()
1378 };
1379
1380 let login_b = LoginEntry {
1381 origin: "https://b.example.com".into(),
1382 http_realm: Some("https://www.example.com".into()),
1383 username: "test".into(),
1384 password: "sekret".into(),
1385 ..LoginEntry::default()
1386 };
1387
1388 let db = LoginDb::open_in_memory();
1389 let added = db
1390 .add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1391 .expect("should be able to add logins");
1392
1393 let [added_a, added_b] = added.as_slice() else {
1394 panic!("there should really be 2")
1395 };
1396
1397 let fetched_a = db
1398 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1399 .expect("should work")
1400 .expect("should get a record");
1401
1402 assert_eq!(fetched_a.fields.origin, login_a.origin);
1403
1404 let fetched_b = db
1405 .get_by_id(&added_b.as_ref().unwrap().meta.id)
1406 .expect("should work")
1407 .expect("should get a record");
1408
1409 assert_eq!(fetched_b.fields.origin, login_b.origin);
1410
1411 assert_eq!(db.count_all().unwrap(), 2);
1412 }
1413
1414 #[test]
1415 fn test_count_by_origin() {
1416 ensure_initialized();
1417
1418 let origin_a = "https://a.example.com";
1419 let login_a = LoginEntry {
1420 origin: origin_a.into(),
1421 http_realm: Some("https://www.example.com".into()),
1422 username: "test".into(),
1423 password: "sekret".into(),
1424 ..LoginEntry::default()
1425 };
1426
1427 let login_b = LoginEntry {
1428 origin: "https://b.example.com".into(),
1429 http_realm: Some("https://www.example.com".into()),
1430 username: "test".into(),
1431 password: "sekret".into(),
1432 ..LoginEntry::default()
1433 };
1434
1435 let origin_umlaut = "https://bücher.example.com";
1436 let login_umlaut = LoginEntry {
1437 origin: origin_umlaut.into(),
1438 http_realm: Some("https://www.example.com".into()),
1439 username: "test".into(),
1440 password: "sekret".into(),
1441 ..LoginEntry::default()
1442 };
1443
1444 let db = LoginDb::open_in_memory();
1445 db.add_many(
1446 vec![login_a.clone(), login_b.clone(), login_umlaut.clone()],
1447 &*TEST_ENCDEC,
1448 )
1449 .expect("should be able to add logins");
1450
1451 assert_eq!(db.count_by_origin(origin_a).unwrap(), 1);
1452 assert_eq!(db.count_by_origin(origin_umlaut).unwrap(), 1);
1453 }
1454
1455 #[test]
1456 fn test_count_by_form_action_origin() {
1457 ensure_initialized();
1458
1459 let origin_a = "https://a.example.com";
1460 let login_a = LoginEntry {
1461 origin: origin_a.into(),
1462 form_action_origin: Some(origin_a.into()),
1463 http_realm: Some("https://www.example.com".into()),
1464 username: "test".into(),
1465 password: "sekret".into(),
1466 ..LoginEntry::default()
1467 };
1468
1469 let login_b = LoginEntry {
1470 origin: "https://b.example.com".into(),
1471 form_action_origin: Some("https://b.example.com".into()),
1472 http_realm: Some("https://www.example.com".into()),
1473 username: "test".into(),
1474 password: "sekret".into(),
1475 ..LoginEntry::default()
1476 };
1477
1478 let origin_umlaut = "https://bücher.example.com";
1479 let login_umlaut = LoginEntry {
1480 origin: origin_umlaut.into(),
1481 form_action_origin: Some(origin_umlaut.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 db = LoginDb::open_in_memory();
1489 db.add_many(
1490 vec![login_a.clone(), login_b.clone(), login_umlaut.clone()],
1491 &*TEST_ENCDEC,
1492 )
1493 .expect("should be able to add logins");
1494
1495 assert_eq!(db.count_by_form_action_origin(origin_a).unwrap(), 1);
1496 assert_eq!(db.count_by_form_action_origin(origin_umlaut).unwrap(), 1);
1497 }
1498
1499 #[test]
1500 #[cfg(feature = "ignore_form_action_origin_validation_errors")]
1501 fn test_count_by_invalid_form_action_origin() {
1502 ensure_initialized();
1503
1504 let login = LoginEntry {
1505 origin: "https://example.com".into(),
1506 form_action_origin: Some("email".into()),
1507 username: "test".into(),
1508 password: "sekret".into(),
1509 ..LoginEntry::default()
1510 };
1511
1512 let db = LoginDb::open_in_memory();
1513 db.add(login, &*TEST_ENCDEC)
1514 .expect("should be able to add login with invalid form_action_origin");
1515 assert_eq!(db.count_by_form_action_origin("email").unwrap(), 1);
1516 }
1517
1518 #[test]
1519 fn test_add_many_with_failed_constraint() {
1520 ensure_initialized();
1521
1522 let login_a = LoginEntry {
1523 origin: "https://example.com".into(),
1524 http_realm: Some("https://www.example.com".into()),
1525 username: "test".into(),
1526 password: "sekret".into(),
1527 ..LoginEntry::default()
1528 };
1529
1530 let login_b = LoginEntry {
1531 origin: "https://example.com".into(),
1533 http_realm: Some("https://www.example.com".into()),
1534 username: "test".into(),
1535 password: "sekret".into(),
1536 ..LoginEntry::default()
1537 };
1538
1539 let db = LoginDb::open_in_memory();
1540 let added = db
1541 .add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1542 .expect("should be able to add logins");
1543
1544 let [added_a, added_b] = added.as_slice() else {
1545 panic!("there should really be 2")
1546 };
1547
1548 let fetched_a = db
1550 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1551 .expect("should work")
1552 .expect("should get a record");
1553
1554 assert_eq!(fetched_a.fields.origin, login_a.origin);
1555
1556 assert!(!added_b.is_ok());
1558 }
1559
1560 #[test]
1561 fn test_add_with_meta() {
1562 ensure_initialized();
1563
1564 let guid = Guid::random();
1565 let now_ms = util::system_time_ms_i64(SystemTime::now());
1566 let login = LoginEntry {
1567 origin: "https://www.example.com".into(),
1568 http_realm: Some("https://www.example.com".into()),
1569 username: "test".into(),
1570 password: "sekret".into(),
1571 ..LoginEntry::default()
1572 };
1573 let meta = LoginMeta {
1574 id: guid.to_string(),
1575 time_created: now_ms,
1576 time_password_changed: now_ms + 100,
1577 time_last_used: now_ms + 10,
1578 times_used: 42,
1579 time_last_breach_alert_dismissed: None,
1580 };
1581
1582 let db = LoginDb::open_in_memory();
1583 let entry_with_meta = LoginEntryWithMeta {
1584 entry: login.clone(),
1585 meta: meta.clone(),
1586 };
1587
1588 db.add_with_meta(entry_with_meta, &*TEST_ENCDEC)
1589 .expect("should be able to add login with record");
1590
1591 let fetched = db
1592 .get_by_id(&guid)
1593 .expect("should work")
1594 .expect("should get a record");
1595
1596 assert_eq!(fetched.meta, meta);
1597 }
1598
1599 #[test]
1600 fn test_record_potentially_vulnerable_passwords() {
1601 ensure_initialized();
1602 let db = LoginDb::open_in_memory();
1603
1604 let count: i64 = db
1606 .db
1607 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1608 .unwrap();
1609 assert_eq!(count, 0);
1610
1611 db.record_potentially_vulnerable_passwords(
1613 vec!["password1".into(), "password2".into(), "password3".into()],
1614 &*TEST_ENCDEC,
1615 )
1616 .unwrap();
1617
1618 let count: i64 = db
1620 .db
1621 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1622 .unwrap();
1623 assert_eq!(count, 3);
1624
1625 db.record_potentially_vulnerable_passwords(
1627 vec!["password1".into(), "password4".into()],
1628 &*TEST_ENCDEC,
1629 )
1630 .unwrap();
1631
1632 let count: i64 = db
1634 .db
1635 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1636 .unwrap();
1637 assert_eq!(count, 4);
1638
1639 db.record_potentially_vulnerable_passwords(
1641 vec!["password1".into(), "password2".into()],
1642 &*TEST_ENCDEC,
1643 )
1644 .unwrap();
1645
1646 let count: i64 = db
1647 .db
1648 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1649 .unwrap();
1650 assert_eq!(count, 4);
1651 }
1652
1653 #[test]
1654 fn test_add_with_meta_deleted() {
1655 ensure_initialized();
1656
1657 let guid = Guid::random();
1658 let now_ms = util::system_time_ms_i64(SystemTime::now());
1659 let login = LoginEntry {
1660 origin: "https://www.example.com".into(),
1661 http_realm: Some("https://www.example.com".into()),
1662 username: "test".into(),
1663 password: "sekret".into(),
1664 ..LoginEntry::default()
1665 };
1666 let meta = LoginMeta {
1667 id: guid.to_string(),
1668 time_created: now_ms,
1669 time_password_changed: now_ms + 100,
1670 time_last_used: now_ms + 10,
1671 times_used: 42,
1672 time_last_breach_alert_dismissed: None,
1673 };
1674
1675 let db = LoginDb::open_in_memory();
1676 let entry_with_meta = LoginEntryWithMeta {
1677 entry: login.clone(),
1678 meta: meta.clone(),
1679 };
1680
1681 db.add_with_meta(entry_with_meta, &*TEST_ENCDEC)
1682 .expect("should be able to add login with record");
1683
1684 db.delete(&guid).expect("should be able to delete login");
1685
1686 let entry_with_meta2 = LoginEntryWithMeta {
1687 entry: login.clone(),
1688 meta: meta.clone(),
1689 };
1690
1691 db.add_with_meta(entry_with_meta2, &*TEST_ENCDEC)
1692 .expect("should be able to re-add login with record");
1693
1694 let fetched = db
1695 .get_by_id(&guid)
1696 .expect("should work")
1697 .expect("should get a record");
1698
1699 assert_eq!(fetched.meta, meta);
1700 }
1701
1702 #[test]
1703 fn test_unicode_submit() {
1704 ensure_initialized();
1705 let db = LoginDb::open_in_memory();
1706 let added = db
1707 .add(
1708 LoginEntry {
1709 form_action_origin: Some("http://😍.com".into()),
1710 origin: "http://😍.com".into(),
1711 http_realm: None,
1712 username_field: "😍".into(),
1713 password_field: "😍".into(),
1714 username: "😍".into(),
1715 password: "😍".into(),
1716 },
1717 &*TEST_ENCDEC,
1718 )
1719 .unwrap();
1720 let fetched = db
1721 .get_by_id(&added.meta.id)
1722 .expect("should work")
1723 .expect("should get a record");
1724 assert_eq!(added, fetched);
1725 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1726 assert_eq!(
1727 fetched.fields.form_action_origin,
1728 Some("http://xn--r28h.com".to_string())
1729 );
1730 assert_eq!(fetched.fields.username_field, "😍");
1731 assert_eq!(fetched.fields.password_field, "😍");
1732 let sec_fields = fetched.decrypt_fields(&*TEST_ENCDEC).unwrap();
1733 assert_eq!(sec_fields.username, "😍");
1734 assert_eq!(sec_fields.password, "😍");
1735 }
1736
1737 #[test]
1738 fn test_unicode_realm() {
1739 ensure_initialized();
1740 let db = LoginDb::open_in_memory();
1741 let added = db
1742 .add(
1743 LoginEntry {
1744 form_action_origin: None,
1745 origin: "http://😍.com".into(),
1746 http_realm: Some("😍😍".into()),
1747 username: "😍".into(),
1748 password: "😍".into(),
1749 ..Default::default()
1750 },
1751 &*TEST_ENCDEC,
1752 )
1753 .unwrap();
1754 let fetched = db
1755 .get_by_id(&added.meta.id)
1756 .expect("should work")
1757 .expect("should get a record");
1758 assert_eq!(added, fetched);
1759 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1760 assert_eq!(fetched.fields.http_realm.unwrap(), "😍😍");
1761 }
1762
1763 fn check_matches(db: &LoginDb, query: &str, expected: &[&str]) {
1764 let mut results = db
1765 .get_by_base_domain(query)
1766 .unwrap()
1767 .into_iter()
1768 .map(|l| l.fields.origin)
1769 .collect::<Vec<String>>();
1770 results.sort_unstable();
1771 let mut sorted = expected.to_owned();
1772 sorted.sort_unstable();
1773 assert_eq!(sorted, results);
1774 }
1775
1776 fn check_good_bad(
1777 good: Vec<&str>,
1778 bad: Vec<&str>,
1779 good_queries: Vec<&str>,
1780 zero_queries: Vec<&str>,
1781 ) {
1782 let db = LoginDb::open_in_memory();
1783 for h in good.iter().chain(bad.iter()) {
1784 db.add(
1785 LoginEntry {
1786 origin: (*h).into(),
1787 http_realm: Some((*h).into()),
1788 password: "test".into(),
1789 ..Default::default()
1790 },
1791 &*TEST_ENCDEC,
1792 )
1793 .unwrap();
1794 }
1795 for query in good_queries {
1796 check_matches(&db, query, &good);
1797 }
1798 for query in zero_queries {
1799 check_matches(&db, query, &[]);
1800 }
1801 }
1802
1803 #[test]
1804 fn test_get_by_base_domain_invalid() {
1805 ensure_initialized();
1806 check_good_bad(
1807 vec!["https://example.com"],
1808 vec![],
1809 vec![],
1810 vec!["invalid query"],
1811 );
1812 }
1813
1814 #[test]
1815 fn test_get_by_base_domain() {
1816 ensure_initialized();
1817 check_good_bad(
1818 vec![
1819 "https://example.com",
1820 "https://www.example.com",
1821 "http://www.example.com",
1822 "http://www.example.com:8080",
1823 "http://sub.example.com:8080",
1824 "https://sub.example.com:8080",
1825 "https://sub.sub.example.com",
1826 "ftp://sub.example.com",
1827 ],
1828 vec![
1829 "https://badexample.com",
1830 "https://example.co",
1831 "https://example.com.au",
1832 ],
1833 vec!["example.com"],
1834 vec!["foo.com"],
1835 );
1836 }
1837
1838 #[test]
1839 fn test_get_by_base_domain_punicode() {
1840 ensure_initialized();
1841 check_good_bad(
1844 vec![
1845 "http://xn--r28h.com", ],
1847 vec!["http://💖.com"],
1848 vec!["😍.com", "xn--r28h.com"],
1849 vec![],
1850 );
1851 }
1852
1853 #[test]
1854 fn test_get_by_base_domain_ipv4() {
1855 ensure_initialized();
1856 check_good_bad(
1857 vec!["http://127.0.0.1", "https://127.0.0.1:8000"],
1858 vec!["https://127.0.0.0", "https://example.com"],
1859 vec!["127.0.0.1"],
1860 vec!["127.0.0.2"],
1861 );
1862 }
1863
1864 #[test]
1865 fn test_get_by_base_domain_ipv6() {
1866 ensure_initialized();
1867 check_good_bad(
1868 vec!["http://[::1]", "https://[::1]:8000"],
1869 vec!["https://[0:0:0:0:0:0:1:1]", "https://example.com"],
1870 vec!["[::1]", "[0:0:0:0:0:0:0:1]"],
1871 vec!["[0:0:0:0:0:0:1:2]"],
1872 );
1873 }
1874
1875 #[test]
1876 fn test_add() {
1877 ensure_initialized();
1878 let db = LoginDb::open_in_memory();
1879 let to_add = LoginEntry {
1880 origin: "https://www.example.com".into(),
1881 http_realm: Some("https://www.example.com".into()),
1882 username: "test_user".into(),
1883 password: "test_password".into(),
1884 ..Default::default()
1885 };
1886 let login = db.add(to_add, &*TEST_ENCDEC).unwrap();
1887 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1888
1889 assert_eq!(login.fields.origin, login2.fields.origin);
1890 assert_eq!(login.fields.http_realm, login2.fields.http_realm);
1891 assert_eq!(login.sec_fields, login2.sec_fields);
1892 }
1893
1894 #[test]
1895 fn test_update() {
1896 ensure_initialized();
1897 let db = LoginDb::open_in_memory();
1898 let login = db
1899 .add(
1900 LoginEntry {
1901 origin: "https://www.example.com".into(),
1902 http_realm: Some("https://www.example.com".into()),
1903 username: "user1".into(),
1904 password: "password1".into(),
1905 ..Default::default()
1906 },
1907 &*TEST_ENCDEC,
1908 )
1909 .unwrap();
1910 db.update(
1911 &login.meta.id,
1912 LoginEntry {
1913 origin: "https://www.example2.com".into(),
1914 http_realm: Some("https://www.example2.com".into()),
1915 username: "user2".into(),
1916 password: "password2".into(),
1917 ..Default::default() },
1919 &*TEST_ENCDEC,
1920 )
1921 .unwrap();
1922
1923 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1924
1925 assert_eq!(login2.fields.origin, "https://www.example2.com");
1926 assert_eq!(
1927 login2.fields.http_realm,
1928 Some("https://www.example2.com".into())
1929 );
1930 let sec_fields = login2.decrypt_fields(&*TEST_ENCDEC).unwrap();
1931 assert_eq!(sec_fields.username, "user2");
1932 assert_eq!(sec_fields.password, "password2");
1933 }
1934
1935 #[test]
1936 fn test_touch() {
1937 ensure_initialized();
1938 let db = LoginDb::open_in_memory();
1939 let login = db
1940 .add(
1941 LoginEntry {
1942 origin: "https://www.example.com".into(),
1943 http_realm: Some("https://www.example.com".into()),
1944 username: "user1".into(),
1945 password: "password1".into(),
1946 ..Default::default()
1947 },
1948 &*TEST_ENCDEC,
1949 )
1950 .unwrap();
1951 thread::sleep(time::Duration::from_millis(50));
1953 db.touch(&login.meta.id).unwrap();
1954 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1955 assert!(login2.meta.time_last_used > login.meta.time_last_used);
1956 assert_eq!(login2.meta.times_used, login.meta.times_used + 1);
1957 }
1958
1959 #[test]
1960 fn test_breach_alert_dismissal() {
1961 ensure_initialized();
1962 let db = LoginDb::open_in_memory();
1963 let login = db
1964 .add(
1965 LoginEntry {
1966 origin: "https://www.example.com".into(),
1967 http_realm: Some("https://www.example.com".into()),
1968 username: "user1".into(),
1969 password: "password1".into(),
1970 ..Default::default()
1971 },
1972 &*TEST_ENCDEC,
1973 )
1974 .unwrap();
1975 assert!(login.meta.time_last_breach_alert_dismissed.is_none());
1977
1978 db.record_breach_alert_dismissal(&login.meta.id).unwrap();
1980 let login1 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1981 assert!(login1.meta.time_last_breach_alert_dismissed.is_some());
1982 }
1983
1984 #[test]
1985 fn test_breach_alert_dismissal_with_specific_timestamp() {
1986 ensure_initialized();
1987 let db = LoginDb::open_in_memory();
1988 let login = db
1989 .add(
1990 LoginEntry {
1991 origin: "https://www.example.com".into(),
1992 http_realm: Some("https://www.example.com".into()),
1993 username: "user1".into(),
1994 password: "password1".into(),
1995 ..Default::default()
1996 },
1997 &*TEST_ENCDEC,
1998 )
1999 .unwrap();
2000
2001 let dismiss_time = login.meta.time_password_changed + 1000;
2002 db.record_breach_alert_dismissal_time(&login.meta.id, dismiss_time)
2003 .unwrap();
2004
2005 let retrieved = db
2006 .get_by_id(&login.meta.id)
2007 .unwrap()
2008 .unwrap()
2009 .decrypt(&*TEST_ENCDEC)
2010 .unwrap();
2011 assert_eq!(
2012 retrieved.time_last_breach_alert_dismissed,
2013 Some(dismiss_time)
2014 );
2015 }
2016
2017 #[test]
2018 fn test_delete() {
2019 ensure_initialized();
2020 let db = LoginDb::open_in_memory();
2021 let login = db
2022 .add(
2023 LoginEntry {
2024 origin: "https://www.example.com".into(),
2025 http_realm: Some("https://www.example.com".into()),
2026 username: "test_user".into(),
2027 password: "test_password".into(),
2028 ..Default::default()
2029 },
2030 &*TEST_ENCDEC,
2031 )
2032 .unwrap();
2033
2034 assert!(db.delete(login.guid_str()).unwrap());
2035
2036 let local_login = db
2037 .query_row(
2038 "SELECT * FROM loginsL WHERE guid = :guid",
2039 named_params! { ":guid": login.guid_str() },
2040 |row| Ok(LocalLogin::test_raw_from_row(row).unwrap()),
2041 )
2042 .unwrap();
2043 assert_eq!(local_login.fields.http_realm, None);
2044 assert_eq!(local_login.fields.form_action_origin, None);
2045
2046 assert!(!db.exists(login.guid_str()).unwrap());
2047 }
2048
2049 #[test]
2050 fn test_delete_many() {
2051 ensure_initialized();
2052 let db = LoginDb::open_in_memory();
2053
2054 let login_a = db
2055 .add(
2056 LoginEntry {
2057 origin: "https://a.example.com".into(),
2058 http_realm: Some("https://www.example.com".into()),
2059 username: "test_user".into(),
2060 password: "test_password".into(),
2061 ..Default::default()
2062 },
2063 &*TEST_ENCDEC,
2064 )
2065 .unwrap();
2066
2067 let login_b = db
2068 .add(
2069 LoginEntry {
2070 origin: "https://b.example.com".into(),
2071 http_realm: Some("https://www.example.com".into()),
2072 username: "test_user".into(),
2073 password: "test_password".into(),
2074 ..Default::default()
2075 },
2076 &*TEST_ENCDEC,
2077 )
2078 .unwrap();
2079
2080 let result = db
2081 .delete_many(vec![login_a.guid_str(), login_b.guid_str()])
2082 .unwrap();
2083 assert!(result[0]);
2084 assert!(result[1]);
2085 assert!(!db.exists(login_a.guid_str()).unwrap());
2086 assert!(!db.exists(login_b.guid_str()).unwrap());
2087 }
2088
2089 #[test]
2090 fn test_subsequent_delete_many() {
2091 ensure_initialized();
2092 let db = LoginDb::open_in_memory();
2093
2094 let login = db
2095 .add(
2096 LoginEntry {
2097 origin: "https://a.example.com".into(),
2098 http_realm: Some("https://www.example.com".into()),
2099 username: "test_user".into(),
2100 password: "test_password".into(),
2101 ..Default::default()
2102 },
2103 &*TEST_ENCDEC,
2104 )
2105 .unwrap();
2106
2107 let result = db.delete_many(vec![login.guid_str()]).unwrap();
2108 assert!(result[0]);
2109 assert!(!db.exists(login.guid_str()).unwrap());
2110
2111 let result = db.delete_many(vec![login.guid_str()]).unwrap();
2112 assert!(!result[0]);
2113 }
2114
2115 #[test]
2116 fn test_delete_many_with_non_existent_id() {
2117 ensure_initialized();
2118 let db = LoginDb::open_in_memory();
2119
2120 let result = db.delete_many(vec![&Guid::random()]).unwrap();
2121 assert!(!result[0]);
2122 }
2123
2124 #[test]
2125 fn test_delete_local_for_remote_replacement() {
2126 ensure_initialized();
2127 let db = LoginDb::open_in_memory();
2128 let login = db
2129 .add(
2130 LoginEntry {
2131 origin: "https://www.example.com".into(),
2132 http_realm: Some("https://www.example.com".into()),
2133 username: "test_user".into(),
2134 password: "test_password".into(),
2135 ..Default::default()
2136 },
2137 &*TEST_ENCDEC,
2138 )
2139 .unwrap();
2140
2141 let result = db
2142 .delete_local_records_for_remote_replacement(vec![login.guid_str()])
2143 .unwrap();
2144
2145 let local_guids = get_local_guids(&db);
2146 assert_eq!(local_guids.len(), 0);
2147
2148 let mirror_guids = get_mirror_guids(&db);
2149 assert_eq!(mirror_guids.len(), 0);
2150
2151 assert_eq!(result.local_deleted, 1);
2152 }
2153
2154 mod test_find_login_to_update {
2155 use super::*;
2156
2157 fn make_entry(username: &str, password: &str) -> LoginEntry {
2158 LoginEntry {
2159 origin: "https://www.example.com".into(),
2160 http_realm: Some("the website".into()),
2161 username: username.into(),
2162 password: password.into(),
2163 ..Default::default()
2164 }
2165 }
2166
2167 fn make_saved_login(db: &LoginDb, username: &str, password: &str) -> Login {
2168 db.add(make_entry(username, password), &*TEST_ENCDEC)
2169 .unwrap()
2170 .decrypt(&*TEST_ENCDEC)
2171 .unwrap()
2172 }
2173
2174 #[test]
2175 fn test_match() {
2176 ensure_initialized();
2177 let db = LoginDb::open_in_memory();
2178 let login = make_saved_login(&db, "user", "pass");
2179 assert_eq!(
2180 Some(login),
2181 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2182 .unwrap(),
2183 );
2184 }
2185
2186 #[test]
2187 fn test_non_matches() {
2188 ensure_initialized();
2189 let db = LoginDb::open_in_memory();
2190 make_saved_login(&db, "other-user", "pass");
2192 db.add(
2194 LoginEntry {
2195 origin: "https://www.example.com".into(),
2196 http_realm: Some("the other website".into()),
2197 username: "user".into(),
2198 password: "pass".into(),
2199 ..Default::default()
2200 },
2201 &*TEST_ENCDEC,
2202 )
2203 .unwrap();
2204 db.add(
2206 LoginEntry {
2207 origin: "https://www.example.com".into(),
2208 form_action_origin: Some("https://www.example.com/".into()),
2209 username: "user".into(),
2210 password: "pass".into(),
2211 ..Default::default()
2212 },
2213 &*TEST_ENCDEC,
2214 )
2215 .unwrap();
2216 assert_eq!(
2217 None,
2218 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2219 .unwrap(),
2220 );
2221 }
2222
2223 #[test]
2224 fn test_match_blank_password() {
2225 ensure_initialized();
2226 let db = LoginDb::open_in_memory();
2227 let login = make_saved_login(&db, "", "pass");
2228 assert_eq!(
2229 Some(login),
2230 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2231 .unwrap(),
2232 );
2233 }
2234
2235 #[test]
2236 fn test_username_match_takes_precedence_over_blank_username() {
2237 ensure_initialized();
2238 let db = LoginDb::open_in_memory();
2239 make_saved_login(&db, "", "pass");
2240 let username_match = make_saved_login(&db, "user", "pass");
2241 assert_eq!(
2242 Some(username_match),
2243 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2244 .unwrap(),
2245 );
2246 }
2247
2248 #[test]
2249 fn test_invalid_login() {
2250 ensure_initialized();
2251 let db = LoginDb::open_in_memory();
2252 assert!(db
2253 .find_login_to_update(
2254 LoginEntry {
2255 http_realm: None,
2256 form_action_origin: None,
2257 ..LoginEntry::default()
2258 },
2259 &*TEST_ENCDEC
2260 )
2261 .is_err());
2262 }
2263
2264 #[test]
2265 fn test_update_with_duplicate_login() {
2266 ensure_initialized();
2267 let db = LoginDb::open_in_memory();
2270 let login = make_saved_login(&db, "user", "pass");
2271 let mut dupe = login.clone().encrypt(&*TEST_ENCDEC).unwrap();
2272 dupe.meta.id = "different-guid".to_string();
2273 db.insert_new_login(&dupe).unwrap();
2274
2275 let mut entry = login.entry();
2276 entry.password = "pass2".to_string();
2277 db.update(&login.id, entry, &*TEST_ENCDEC).unwrap();
2278
2279 let mut entry = login.entry();
2280 entry.password = "pass3".to_string();
2281 db.add_or_update(entry, &*TEST_ENCDEC).unwrap();
2282 }
2283
2284 #[test]
2285 fn test_password_reuse_detection() {
2286 ensure_initialized();
2287 let db = LoginDb::open_in_memory();
2288
2289 let login1 = db
2291 .add(
2292 LoginEntry {
2293 origin: "https://site1.com".into(),
2294 http_realm: Some("realm".into()),
2295 username: "user1".into(),
2296 password: "shared_password".into(),
2297 ..Default::default()
2298 },
2299 &*TEST_ENCDEC,
2300 )
2301 .unwrap();
2302
2303 let login2 = db
2304 .add(
2305 LoginEntry {
2306 origin: "https://site2.com".into(),
2307 http_realm: Some("realm".into()),
2308 username: "user2".into(),
2309 password: "shared_password".into(),
2310 ..Default::default()
2311 },
2312 &*TEST_ENCDEC,
2313 )
2314 .unwrap();
2315
2316 assert!(!db
2318 .is_potentially_vulnerable_password(&login1.meta.id, &*TEST_ENCDEC)
2319 .unwrap());
2320 assert!(!db
2321 .is_potentially_vulnerable_password(&login2.meta.id, &*TEST_ENCDEC)
2322 .unwrap());
2323 let vulnerable = db
2325 .are_potentially_vulnerable_passwords(
2326 &[&login1.meta.id, &login2.meta.id],
2327 &*TEST_ENCDEC,
2328 )
2329 .unwrap();
2330 assert_eq!(vulnerable.len(), 0);
2331
2332 db.record_potentially_vulnerable_passwords(
2334 vec!["shared_password".into()],
2335 &*TEST_ENCDEC,
2336 )
2337 .unwrap();
2338
2339 assert!(db
2341 .is_potentially_vulnerable_password(&login2.meta.id, &*TEST_ENCDEC)
2342 .unwrap());
2343 let vulnerable = db
2345 .are_potentially_vulnerable_passwords(
2346 &[&login1.meta.id, &login2.meta.id],
2347 &*TEST_ENCDEC,
2348 )
2349 .unwrap();
2350 assert_eq!(vulnerable.len(), 2);
2351 assert!(vulnerable.contains(&login1.meta.id));
2352 assert!(vulnerable.contains(&login2.meta.id));
2353
2354 db.update(
2356 &login2.meta.id,
2357 LoginEntry {
2358 origin: "https://site2.com".into(),
2359 http_realm: Some("realm".into()),
2360 username: "user2".into(),
2361 password: "different_password".into(),
2362 ..Default::default()
2363 },
2364 &*TEST_ENCDEC,
2365 )
2366 .unwrap();
2367
2368 assert!(!db
2369 .is_potentially_vulnerable_password(&login2.meta.id, &*TEST_ENCDEC)
2370 .unwrap());
2371 }
2372
2373 #[test]
2374 fn test_reset_all_breaches_clears_breach_table() {
2375 ensure_initialized();
2376 let db = LoginDb::open_in_memory();
2377
2378 let login = db
2379 .add(
2380 LoginEntry {
2381 origin: "https://example.com".into(),
2382 http_realm: Some("realm".into()),
2383 username: "user".into(),
2384 password: "password123".into(),
2385 ..Default::default()
2386 },
2387 &*TEST_ENCDEC,
2388 )
2389 .unwrap();
2390
2391 db.record_potentially_vulnerable_passwords(vec!["password123".into()], &*TEST_ENCDEC)
2392 .unwrap();
2393
2394 let count: i64 = db
2396 .db
2397 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
2398 .unwrap();
2399 assert_eq!(count, 1);
2400 let vulnerable = db
2402 .are_potentially_vulnerable_passwords(&[&login.meta.id], &*TEST_ENCDEC)
2403 .unwrap();
2404 assert_eq!(vulnerable.len(), 1);
2405 assert_eq!(vulnerable[0], login.meta.id);
2406
2407 db.reset_all_breaches().unwrap();
2409
2410 let count: i64 = db
2412 .db
2413 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
2414 .unwrap();
2415 assert_eq!(count, 0);
2416 let vulnerable = db
2418 .are_potentially_vulnerable_passwords(&[&login.meta.id], &*TEST_ENCDEC)
2419 .unwrap();
2420 assert_eq!(vulnerable.len(), 0);
2421 }
2422
2423 #[test]
2424 fn test_different_passwords_not_vulnerable() {
2425 ensure_initialized();
2426 let db = LoginDb::open_in_memory();
2427
2428 let login1 = db
2429 .add(
2430 LoginEntry {
2431 origin: "https://site1.com".into(),
2432 http_realm: Some("realm".into()),
2433 username: "user".into(),
2434 password: "password_A".into(),
2435 ..Default::default()
2436 },
2437 &*TEST_ENCDEC,
2438 )
2439 .unwrap();
2440
2441 let login2 = db
2442 .add(
2443 LoginEntry {
2444 origin: "https://site2.com".into(),
2445 http_realm: Some("realm".into()),
2446 username: "user".into(),
2447 password: "password_B".into(),
2448 ..Default::default()
2449 },
2450 &*TEST_ENCDEC,
2451 )
2452 .unwrap();
2453
2454 db.record_potentially_vulnerable_passwords(vec!["password_A".into()], &*TEST_ENCDEC)
2455 .unwrap();
2456
2457 assert!(!db
2459 .is_potentially_vulnerable_password(&login2.meta.id, &*TEST_ENCDEC)
2460 .unwrap());
2461 let vulnerable = db
2464 .are_potentially_vulnerable_passwords(
2465 &[&login1.meta.id, &login2.meta.id],
2466 &*TEST_ENCDEC,
2467 )
2468 .unwrap();
2469 assert_eq!(vulnerable.len(), 1);
2470 assert!(vulnerable.contains(&login1.meta.id));
2471 }
2472 }
2473}