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 now_ms = util::system_time_ms_i64(SystemTime::now());
556 let sql = format!(
557 "UPDATE loginsL
558 SET local_modified = :now_millis,
559 timeLastUsed = :time_last_used,
560 timePasswordChanged = :time_password_changed,
561 httpRealm = :http_realm,
562 formActionOrigin = :form_action_origin,
563 usernameField = :username_field,
564 passwordField = :password_field,
565 timesUsed = :times_used,
566 secFields = :sec_fields,
567 origin = :origin,
568 -- leave New records as they are, otherwise update them to `changed`
569 sync_status = max(sync_status, {changed})
570 WHERE guid = :guid",
571 changed = SyncStatus::Changed as u8
572 );
573
574 self.db.execute(
575 &sql,
576 named_params! {
577 ":origin": login.fields.origin,
578 ":http_realm": login.fields.http_realm,
579 ":form_action_origin": login.fields.form_action_origin,
580 ":username_field": login.fields.username_field,
581 ":password_field": login.fields.password_field,
582 ":time_last_used": login.meta.time_last_used,
583 ":times_used": login.meta.times_used,
584 ":time_password_changed": login.meta.time_password_changed,
585 ":sec_fields": login.sec_fields,
586 ":guid": &login.meta.id,
587 ":now_millis": now_ms,
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: existing.time_last_used,
760 times_used: existing.times_used,
761 time_last_breach_alert_dismissed: None,
762 },
763 fields: LoginFields {
764 origin: entry.origin,
765 form_action_origin: entry.form_action_origin,
766 http_realm: entry.http_realm,
767 username_field: entry.username_field,
768 password_field: entry.password_field,
769 },
770 sec_fields,
771 };
772
773 self.update_existing_login(&result)?;
774 tx.commit()?;
775 Ok(result)
776 }
777
778 pub fn add_or_update(
779 &self,
780 entry: LoginEntry,
781 encdec: &dyn EncryptorDecryptor,
782 ) -> Result<EncryptedLogin> {
783 let entry = entry.fixup()?;
785 match self.find_login_to_update(entry.clone(), encdec)? {
786 Some(login) => self.update(&login.id, entry, encdec),
787 None => self.add(entry, encdec),
788 }
789 }
790
791 pub fn fixup_and_check_for_dupes(
792 &self,
793 guid: &Guid,
794 entry: LoginEntry,
795 encdec: &dyn EncryptorDecryptor,
796 ) -> Result<LoginEntry> {
797 let entry = entry.fixup()?;
798 self.check_for_dupes(guid, &entry, encdec)?;
799 Ok(entry)
800 }
801
802 pub fn check_for_dupes(
803 &self,
804 guid: &Guid,
805 entry: &LoginEntry,
806 encdec: &dyn EncryptorDecryptor,
807 ) -> Result<()> {
808 if self.dupe_exists(guid, entry, encdec)? {
809 return Err(InvalidLogin::DuplicateLogin.into());
810 }
811 Ok(())
812 }
813
814 pub fn dupe_exists(
815 &self,
816 guid: &Guid,
817 entry: &LoginEntry,
818 encdec: &dyn EncryptorDecryptor,
819 ) -> Result<bool> {
820 Ok(self.find_dupe(guid, entry, encdec)?.is_some())
821 }
822
823 pub fn find_dupe(
824 &self,
825 guid: &Guid,
826 entry: &LoginEntry,
827 encdec: &dyn EncryptorDecryptor,
828 ) -> Result<Option<Guid>> {
829 for possible in self.get_by_entry_target(entry)? {
830 if possible.guid() != *guid {
831 let pos_sec_fields = possible.decrypt_fields(encdec)?;
832 if pos_sec_fields.username == entry.username {
833 return Ok(Some(possible.guid()));
834 }
835 }
836 }
837 Ok(None)
838 }
839
840 fn get_by_entry_target(&self, entry: &LoginEntry) -> Result<Vec<EncryptedLogin>> {
850 lazy_static::lazy_static! {
852 static ref GET_BY_FORM_ACTION_ORIGIN: String = format!(
853 "SELECT {common_cols} FROM loginsL
854 WHERE is_deleted = 0
855 AND origin = :origin
856 AND formActionOrigin = :form_action_origin
857
858 UNION ALL
859
860 SELECT {common_cols} FROM loginsM
861 WHERE is_overridden = 0
862 AND origin = :origin
863 AND formActionOrigin = :form_action_origin
864 ",
865 common_cols = schema::COMMON_COLS
866 );
867 static ref GET_BY_HTTP_REALM: String = format!(
868 "SELECT {common_cols} FROM loginsL
869 WHERE is_deleted = 0
870 AND origin = :origin
871 AND httpRealm = :http_realm
872
873 UNION ALL
874
875 SELECT {common_cols} FROM loginsM
876 WHERE is_overridden = 0
877 AND origin = :origin
878 AND httpRealm = :http_realm
879 ",
880 common_cols = schema::COMMON_COLS
881 );
882 }
883 match (entry.form_action_origin.as_ref(), entry.http_realm.as_ref()) {
884 (Some(form_action_origin), None) => {
885 let params = named_params! {
886 ":origin": &entry.origin,
887 ":form_action_origin": form_action_origin,
888 };
889 self.db
890 .prepare_cached(&GET_BY_FORM_ACTION_ORIGIN)?
891 .query_and_then(params, EncryptedLogin::from_row)?
892 .collect()
893 }
894 (None, Some(http_realm)) => {
895 let params = named_params! {
896 ":origin": &entry.origin,
897 ":http_realm": http_realm,
898 };
899 self.db
900 .prepare_cached(&GET_BY_HTTP_REALM)?
901 .query_and_then(params, EncryptedLogin::from_row)?
902 .collect()
903 }
904 (Some(_), Some(_)) => Err(InvalidLogin::BothTargets.into()),
905 (None, None) => Err(InvalidLogin::NoTarget.into()),
906 }
907 }
908
909 pub fn exists(&self, id: &str) -> Result<bool> {
910 Ok(self.db.query_row(
911 "SELECT EXISTS(
912 SELECT 1 FROM loginsL
913 WHERE guid = :guid AND is_deleted = 0
914 UNION ALL
915 SELECT 1 FROM loginsM
916 WHERE guid = :guid AND is_overridden IS NOT 1
917 )",
918 named_params! { ":guid": id },
919 |row| row.get(0),
920 )?)
921 }
922
923 pub fn delete(&self, id: &str) -> Result<bool> {
926 let mut results = self.delete_many(vec![id])?;
927 Ok(results.pop().expect("there should be a single result"))
928 }
929
930 pub fn delete_many(&self, ids: Vec<&str>) -> Result<Vec<bool>> {
933 let tx = self.unchecked_transaction_imm()?;
934 let sql = format!(
935 "
936 UPDATE loginsL
937 SET local_modified = :now_ms,
938 sync_status = {status_changed},
939 is_deleted = 1,
940 secFields = '',
941 origin = '',
942 httpRealm = NULL,
943 formActionOrigin = NULL
944 WHERE guid = :guid AND is_deleted IS FALSE
945 ",
946 status_changed = SyncStatus::Changed as u8
947 );
948 let mut stmt = self.db.prepare_cached(&sql)?;
949
950 let mut result = vec![];
951
952 for id in ids {
953 let now_ms = util::system_time_ms_i64(SystemTime::now());
954
955 let update_result = stmt.execute(named_params! { ":now_ms": now_ms, ":guid": id })?;
957
958 let exists = update_result == 1;
959
960 self.execute(
962 "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
963 named_params! { ":guid": id },
964 )?;
965
966 self.execute(&format!("
969 INSERT OR IGNORE INTO loginsL
970 (guid, local_modified, is_deleted, sync_status, origin, timeCreated, timePasswordChanged, secFields)
971 SELECT guid, :now_ms, 1, {changed}, '', timeCreated, :now_ms, ''
972 FROM loginsM
973 WHERE guid = :guid",
974 changed = SyncStatus::Changed as u8),
975 named_params! { ":now_ms": now_ms, ":guid": id })?;
976
977 result.push(exists);
978 }
979
980 tx.commit()?;
981
982 Ok(result)
983 }
984
985 pub fn delete_undecryptable_records_for_remote_replacement(
986 &self,
987 encdec: &dyn EncryptorDecryptor,
988 ) -> Result<LoginsDeletionMetrics> {
989 let corrupted_logins = self
991 .get_all()?
992 .into_iter()
993 .filter(|login| login.clone().decrypt(encdec).is_err())
994 .collect::<Vec<_>>();
995 let ids = corrupted_logins
996 .iter()
997 .map(|login| login.guid_str())
998 .collect::<Vec<_>>();
999
1000 self.delete_local_records_for_remote_replacement(ids)
1001 }
1002
1003 pub fn delete_local_records_for_remote_replacement(
1004 &self,
1005 ids: Vec<&str>,
1006 ) -> Result<LoginsDeletionMetrics> {
1007 let tx = self.unchecked_transaction_imm()?;
1008 let mut local_deleted = 0;
1009 let mut mirror_deleted = 0;
1010
1011 sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
1012 let deleted = self.execute(
1013 &format!(
1014 "DELETE FROM loginsL WHERE guid IN ({})",
1015 sql_support::repeat_sql_values(chunk.len())
1016 ),
1017 rusqlite::params_from_iter(chunk),
1018 )?;
1019 local_deleted += deleted;
1020 Ok(())
1021 })?;
1022
1023 sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
1024 let deleted = self.execute(
1025 &format!(
1026 "DELETE FROM loginsM WHERE guid IN ({})",
1027 sql_support::repeat_sql_values(chunk.len())
1028 ),
1029 rusqlite::params_from_iter(chunk),
1030 )?;
1031 mirror_deleted += deleted;
1032 Ok(())
1033 })?;
1034
1035 tx.commit()?;
1036 Ok(LoginsDeletionMetrics {
1037 local_deleted: local_deleted as u64,
1038 mirror_deleted: mirror_deleted as u64,
1039 })
1040 }
1041
1042 fn mark_mirror_overridden(&self, guid: &str) -> Result<()> {
1043 self.execute_cached(
1044 "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
1045 named_params! { ":guid": guid },
1046 )?;
1047 Ok(())
1048 }
1049
1050 fn ensure_local_overlay_exists(&self, guid: &str) -> Result<()> {
1051 let already_have_local: bool = self.db.query_row(
1052 "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid)",
1053 named_params! { ":guid": guid },
1054 |row| row.get(0),
1055 )?;
1056
1057 if already_have_local {
1058 return Ok(());
1059 }
1060
1061 debug!("No overlay; cloning one for {:?}.", guid);
1062 let changed = self.clone_mirror_to_overlay(guid)?;
1063 if changed == 0 {
1064 report_error!(
1065 "logins-local-overlay-error",
1066 "Failed to create local overlay for GUID {guid:?}."
1067 );
1068 return Err(Error::NoSuchRecord(guid.to_owned()));
1069 }
1070 Ok(())
1071 }
1072
1073 fn clone_mirror_to_overlay(&self, guid: &str) -> Result<usize> {
1074 Ok(self.execute_cached(&CLONE_SINGLE_MIRROR_SQL, &[(":guid", &guid as &dyn ToSql)])?)
1075 }
1076
1077 pub fn wipe_local(&self) -> Result<usize> {
1079 info!("Executing wipe_local on password engine!");
1080 let tx = self.unchecked_transaction()?;
1081 let mut row_count = 0;
1082 row_count += self.execute("DELETE FROM loginsL", [])?;
1083 row_count += self.execute("DELETE FROM loginsM", [])?;
1084 row_count += self.execute("DELETE FROM loginsSyncMeta", [])?;
1085 row_count += self.execute("DELETE FROM breachesL", [])?;
1086 tx.commit()?;
1087 Ok(row_count)
1088 }
1089
1090 pub fn shutdown(self) -> Result<()> {
1091 self.db.close().map_err(|(_, e)| Error::SqlError(e))
1092 }
1093}
1094
1095lazy_static! {
1096 static ref GET_ALL_SQL: String = format!(
1097 "SELECT {common_cols} FROM loginsL WHERE is_deleted = 0
1098 UNION ALL
1099 SELECT {common_cols} FROM loginsM WHERE is_overridden = 0",
1100 common_cols = schema::COMMON_COLS,
1101 );
1102 static ref COUNT_ALL_SQL: String = format!(
1103 "SELECT COUNT(*) FROM (
1104 SELECT guid FROM loginsL WHERE is_deleted = 0
1105 UNION ALL
1106 SELECT guid FROM loginsM WHERE is_overridden = 0
1107 )"
1108 );
1109 static ref COUNT_BY_ORIGIN_SQL: String = format!(
1110 "SELECT COUNT(*) FROM (
1111 SELECT guid FROM loginsL WHERE is_deleted = 0 AND origin = :origin
1112 UNION ALL
1113 SELECT guid FROM loginsM WHERE is_overridden = 0 AND origin = :origin
1114 )"
1115 );
1116 static ref COUNT_BY_FORM_ACTION_ORIGIN_SQL: String = format!(
1117 "SELECT COUNT(*) FROM (
1118 SELECT guid FROM loginsL WHERE is_deleted = 0 AND formActionOrigin = :form_action_origin
1119 UNION ALL
1120 SELECT guid FROM loginsM WHERE is_overridden = 0 AND formActionOrigin = :form_action_origin
1121 )"
1122 );
1123 static ref GET_BY_GUID_SQL: String = format!(
1124 "SELECT {common_cols}
1125 FROM loginsL
1126 WHERE is_deleted = 0
1127 AND guid = :guid
1128
1129 UNION ALL
1130
1131 SELECT {common_cols}
1132 FROM loginsM
1133 WHERE is_overridden IS NOT 1
1134 AND guid = :guid
1135 ORDER BY origin ASC
1136
1137 LIMIT 1",
1138 common_cols = schema::COMMON_COLS,
1139 );
1140 pub static ref CLONE_ENTIRE_MIRROR_SQL: String = format!(
1141 "INSERT OR IGNORE INTO loginsL ({common_cols}, local_modified, is_deleted, sync_status)
1142 SELECT {common_cols}, NULL AS local_modified, 0 AS is_deleted, 0 AS sync_status
1143 FROM loginsM",
1144 common_cols = schema::COMMON_COLS,
1145 );
1146 static ref CLONE_SINGLE_MIRROR_SQL: String =
1147 format!("{} WHERE guid = :guid", &*CLONE_ENTIRE_MIRROR_SQL,);
1148}
1149
1150#[cfg(not(feature = "keydb"))]
1151#[cfg(test)]
1152pub mod test_utils {
1153 use super::*;
1154 use crate::encryption::test_utils::decrypt_struct;
1155 use crate::login::test_utils::enc_login;
1156 use crate::SecureLoginFields;
1157 use sync15::ServerTimestamp;
1158
1159 pub fn insert_login(
1163 db: &LoginDb,
1164 guid: &str,
1165 local_login: Option<&str>,
1166 mirror_login: Option<&str>,
1167 ) {
1168 if let Some(password) = mirror_login {
1169 add_mirror(
1170 db,
1171 &enc_login(guid, password),
1172 &ServerTimestamp(util::system_time_ms_i64(std::time::SystemTime::now())),
1173 local_login.is_some(),
1174 )
1175 .unwrap();
1176 }
1177 if let Some(password) = local_login {
1178 db.insert_new_login(&enc_login(guid, password)).unwrap();
1179 }
1180 }
1181
1182 pub fn insert_encrypted_login(
1183 db: &LoginDb,
1184 local: &EncryptedLogin,
1185 mirror: &EncryptedLogin,
1186 server_modified: &ServerTimestamp,
1187 ) {
1188 db.insert_new_login(local).unwrap();
1189 add_mirror(db, mirror, server_modified, true).unwrap();
1190 }
1191
1192 pub fn add_mirror(
1193 db: &LoginDb,
1194 login: &EncryptedLogin,
1195 server_modified: &ServerTimestamp,
1196 is_overridden: bool,
1197 ) -> Result<()> {
1198 let sql = "
1199 INSERT OR IGNORE INTO loginsM (
1200 is_overridden,
1201 server_modified,
1202
1203 httpRealm,
1204 formActionOrigin,
1205 usernameField,
1206 passwordField,
1207 secFields,
1208 origin,
1209
1210 timesUsed,
1211 timeLastUsed,
1212 timePasswordChanged,
1213 timeCreated,
1214
1215 timeLastBreachAlertDismissed,
1216
1217 guid
1218 ) VALUES (
1219 :is_overridden,
1220 :server_modified,
1221
1222 :http_realm,
1223 :form_action_origin,
1224 :username_field,
1225 :password_field,
1226 :sec_fields,
1227 :origin,
1228
1229 :times_used,
1230 :time_last_used,
1231 :time_password_changed,
1232 :time_created,
1233
1234 :time_last_breach_alert_dismissed,
1235
1236 :guid
1237 )";
1238 let mut stmt = db.prepare_cached(sql)?;
1239
1240 stmt.execute(named_params! {
1241 ":is_overridden": is_overridden,
1242 ":server_modified": server_modified.as_millis(),
1243 ":http_realm": login.fields.http_realm,
1244 ":form_action_origin": login.fields.form_action_origin,
1245 ":username_field": login.fields.username_field,
1246 ":password_field": login.fields.password_field,
1247 ":origin": login.fields.origin,
1248 ":sec_fields": login.sec_fields,
1249 ":times_used": login.meta.times_used,
1250 ":time_last_used": login.meta.time_last_used,
1251 ":time_password_changed": login.meta.time_password_changed,
1252 ":time_created": login.meta.time_created,
1253 ":time_last_breach_alert_dismissed": login.meta.time_last_breach_alert_dismissed,
1254 ":guid": login.guid_str(),
1255 })?;
1256 Ok(())
1257 }
1258
1259 pub fn get_local_guids(db: &LoginDb) -> Vec<String> {
1260 get_guids(db, "SELECT guid FROM loginsL")
1261 }
1262
1263 pub fn get_mirror_guids(db: &LoginDb) -> Vec<String> {
1264 get_guids(db, "SELECT guid FROM loginsM")
1265 }
1266
1267 fn get_guids(db: &LoginDb, sql: &str) -> Vec<String> {
1268 let mut stmt = db.prepare_cached(sql).unwrap();
1269 let mut res: Vec<String> = stmt
1270 .query_map([], |r| r.get(0))
1271 .unwrap()
1272 .map(|r| r.unwrap())
1273 .collect();
1274 res.sort();
1275 res
1276 }
1277
1278 pub fn get_server_modified(db: &LoginDb, guid: &str) -> i64 {
1279 db.conn_ext_query_one(&format!(
1280 "SELECT server_modified FROM loginsM WHERE guid='{}'",
1281 guid
1282 ))
1283 .unwrap()
1284 }
1285
1286 pub fn check_local_login(db: &LoginDb, guid: &str, password: &str, local_modified_gte: i64) {
1287 let row: (String, i64, bool) = db
1288 .query_row(
1289 "SELECT secFields, local_modified, is_deleted FROM loginsL WHERE guid=?",
1290 [guid],
1291 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1292 )
1293 .unwrap();
1294 let enc: SecureLoginFields = decrypt_struct(row.0);
1295 assert_eq!(enc.password, password);
1296 assert!(row.1 >= local_modified_gte);
1297 assert!(!row.2);
1298 }
1299
1300 pub fn check_mirror_login(
1301 db: &LoginDb,
1302 guid: &str,
1303 password: &str,
1304 server_modified: i64,
1305 is_overridden: bool,
1306 ) {
1307 let row: (String, i64, bool) = db
1308 .query_row(
1309 "SELECT secFields, server_modified, is_overridden FROM loginsM WHERE guid=?",
1310 [guid],
1311 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1312 )
1313 .unwrap();
1314 let enc: SecureLoginFields = decrypt_struct(row.0);
1315 assert_eq!(enc.password, password);
1316 assert_eq!(row.1, server_modified);
1317 assert_eq!(row.2, is_overridden);
1318 }
1319}
1320
1321#[cfg(not(feature = "keydb"))]
1322#[cfg(test)]
1323mod tests {
1324 use super::*;
1325 use crate::db::test_utils::{get_local_guids, get_mirror_guids};
1326 use crate::encryption::test_utils::TEST_ENCDEC;
1327 use crate::sync::merge::LocalLogin;
1328 use nss_as::ensure_initialized;
1329 use std::{thread, time};
1330
1331 #[test]
1332 fn test_username_dupe_semantics() {
1333 ensure_initialized();
1334 let mut login = LoginEntry {
1335 origin: "https://www.example.com".into(),
1336 http_realm: Some("https://www.example.com".into()),
1337 username: "test".into(),
1338 password: "sekret".into(),
1339 ..LoginEntry::default()
1340 };
1341
1342 let db = LoginDb::open_in_memory();
1343 db.add(login.clone(), &*TEST_ENCDEC)
1344 .expect("should be able to add first login");
1345
1346 let exp_err = "Invalid login: Login already exists";
1348 assert_eq!(
1349 db.add(login.clone(), &*TEST_ENCDEC)
1350 .unwrap_err()
1351 .to_string(),
1352 exp_err
1353 );
1354
1355 login.username = "".to_string();
1357 db.add(login.clone(), &*TEST_ENCDEC)
1358 .expect("empty login isn't a dupe");
1359
1360 assert_eq!(
1361 db.add(login, &*TEST_ENCDEC).unwrap_err().to_string(),
1362 exp_err
1363 );
1364
1365 assert_eq!(db.get_all().unwrap().len(), 2);
1367 }
1368
1369 #[test]
1370 fn test_add_many() {
1371 ensure_initialized();
1372
1373 let login_a = LoginEntry {
1374 origin: "https://a.example.com".into(),
1375 http_realm: Some("https://www.example.com".into()),
1376 username: "test".into(),
1377 password: "sekret".into(),
1378 ..LoginEntry::default()
1379 };
1380
1381 let login_b = LoginEntry {
1382 origin: "https://b.example.com".into(),
1383 http_realm: Some("https://www.example.com".into()),
1384 username: "test".into(),
1385 password: "sekret".into(),
1386 ..LoginEntry::default()
1387 };
1388
1389 let db = LoginDb::open_in_memory();
1390 let added = db
1391 .add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1392 .expect("should be able to add logins");
1393
1394 let [added_a, added_b] = added.as_slice() else {
1395 panic!("there should really be 2")
1396 };
1397
1398 let fetched_a = db
1399 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1400 .expect("should work")
1401 .expect("should get a record");
1402
1403 assert_eq!(fetched_a.fields.origin, login_a.origin);
1404
1405 let fetched_b = db
1406 .get_by_id(&added_b.as_ref().unwrap().meta.id)
1407 .expect("should work")
1408 .expect("should get a record");
1409
1410 assert_eq!(fetched_b.fields.origin, login_b.origin);
1411
1412 assert_eq!(db.count_all().unwrap(), 2);
1413 }
1414
1415 #[test]
1416 fn test_count_by_origin() {
1417 ensure_initialized();
1418
1419 let origin_a = "https://a.example.com";
1420 let login_a = LoginEntry {
1421 origin: origin_a.into(),
1422 http_realm: Some("https://www.example.com".into()),
1423 username: "test".into(),
1424 password: "sekret".into(),
1425 ..LoginEntry::default()
1426 };
1427
1428 let login_b = LoginEntry {
1429 origin: "https://b.example.com".into(),
1430 http_realm: Some("https://www.example.com".into()),
1431 username: "test".into(),
1432 password: "sekret".into(),
1433 ..LoginEntry::default()
1434 };
1435
1436 let origin_umlaut = "https://bücher.example.com";
1437 let login_umlaut = LoginEntry {
1438 origin: origin_umlaut.into(),
1439 http_realm: Some("https://www.example.com".into()),
1440 username: "test".into(),
1441 password: "sekret".into(),
1442 ..LoginEntry::default()
1443 };
1444
1445 let db = LoginDb::open_in_memory();
1446 db.add_many(
1447 vec![login_a.clone(), login_b.clone(), login_umlaut.clone()],
1448 &*TEST_ENCDEC,
1449 )
1450 .expect("should be able to add logins");
1451
1452 assert_eq!(db.count_by_origin(origin_a).unwrap(), 1);
1453 assert_eq!(db.count_by_origin(origin_umlaut).unwrap(), 1);
1454 }
1455
1456 #[test]
1457 fn test_count_by_form_action_origin() {
1458 ensure_initialized();
1459
1460 let origin_a = "https://a.example.com";
1461 let login_a = LoginEntry {
1462 origin: origin_a.into(),
1463 form_action_origin: Some(origin_a.into()),
1464 http_realm: Some("https://www.example.com".into()),
1465 username: "test".into(),
1466 password: "sekret".into(),
1467 ..LoginEntry::default()
1468 };
1469
1470 let login_b = LoginEntry {
1471 origin: "https://b.example.com".into(),
1472 form_action_origin: Some("https://b.example.com".into()),
1473 http_realm: Some("https://www.example.com".into()),
1474 username: "test".into(),
1475 password: "sekret".into(),
1476 ..LoginEntry::default()
1477 };
1478
1479 let origin_umlaut = "https://bücher.example.com";
1480 let login_umlaut = LoginEntry {
1481 origin: origin_umlaut.into(),
1482 form_action_origin: Some(origin_umlaut.into()),
1483 http_realm: Some("https://www.example.com".into()),
1484 username: "test".into(),
1485 password: "sekret".into(),
1486 ..LoginEntry::default()
1487 };
1488
1489 let db = LoginDb::open_in_memory();
1490 db.add_many(
1491 vec![login_a.clone(), login_b.clone(), login_umlaut.clone()],
1492 &*TEST_ENCDEC,
1493 )
1494 .expect("should be able to add logins");
1495
1496 assert_eq!(db.count_by_form_action_origin(origin_a).unwrap(), 1);
1497 assert_eq!(db.count_by_form_action_origin(origin_umlaut).unwrap(), 1);
1498 }
1499
1500 #[test]
1501 #[cfg(feature = "ignore_form_action_origin_validation_errors")]
1502 fn test_count_by_invalid_form_action_origin() {
1503 ensure_initialized();
1504
1505 let login = LoginEntry {
1506 origin: "https://example.com".into(),
1507 form_action_origin: Some("email".into()),
1508 username: "test".into(),
1509 password: "sekret".into(),
1510 ..LoginEntry::default()
1511 };
1512
1513 let db = LoginDb::open_in_memory();
1514 db.add(login, &*TEST_ENCDEC)
1515 .expect("should be able to add login with invalid form_action_origin");
1516 assert_eq!(db.count_by_form_action_origin("email").unwrap(), 1);
1517 }
1518
1519 #[test]
1520 fn test_add_many_with_failed_constraint() {
1521 ensure_initialized();
1522
1523 let login_a = LoginEntry {
1524 origin: "https://example.com".into(),
1525 http_realm: Some("https://www.example.com".into()),
1526 username: "test".into(),
1527 password: "sekret".into(),
1528 ..LoginEntry::default()
1529 };
1530
1531 let login_b = LoginEntry {
1532 origin: "https://example.com".into(),
1534 http_realm: Some("https://www.example.com".into()),
1535 username: "test".into(),
1536 password: "sekret".into(),
1537 ..LoginEntry::default()
1538 };
1539
1540 let db = LoginDb::open_in_memory();
1541 let added = db
1542 .add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1543 .expect("should be able to add logins");
1544
1545 let [added_a, added_b] = added.as_slice() else {
1546 panic!("there should really be 2")
1547 };
1548
1549 let fetched_a = db
1551 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1552 .expect("should work")
1553 .expect("should get a record");
1554
1555 assert_eq!(fetched_a.fields.origin, login_a.origin);
1556
1557 assert!(!added_b.is_ok());
1559 }
1560
1561 #[test]
1562 fn test_add_with_meta() {
1563 ensure_initialized();
1564
1565 let guid = Guid::random();
1566 let now_ms = util::system_time_ms_i64(SystemTime::now());
1567 let login = LoginEntry {
1568 origin: "https://www.example.com".into(),
1569 http_realm: Some("https://www.example.com".into()),
1570 username: "test".into(),
1571 password: "sekret".into(),
1572 ..LoginEntry::default()
1573 };
1574 let meta = LoginMeta {
1575 id: guid.to_string(),
1576 time_created: now_ms,
1577 time_password_changed: now_ms + 100,
1578 time_last_used: now_ms + 10,
1579 times_used: 42,
1580 time_last_breach_alert_dismissed: None,
1581 };
1582
1583 let db = LoginDb::open_in_memory();
1584 let entry_with_meta = LoginEntryWithMeta {
1585 entry: login.clone(),
1586 meta: meta.clone(),
1587 };
1588
1589 db.add_with_meta(entry_with_meta, &*TEST_ENCDEC)
1590 .expect("should be able to add login with record");
1591
1592 let fetched = db
1593 .get_by_id(&guid)
1594 .expect("should work")
1595 .expect("should get a record");
1596
1597 assert_eq!(fetched.meta, meta);
1598 }
1599
1600 #[test]
1601 fn test_record_potentially_vulnerable_passwords() {
1602 ensure_initialized();
1603 let db = LoginDb::open_in_memory();
1604
1605 let count: i64 = db
1607 .db
1608 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1609 .unwrap();
1610 assert_eq!(count, 0);
1611
1612 db.record_potentially_vulnerable_passwords(
1614 vec!["password1".into(), "password2".into(), "password3".into()],
1615 &*TEST_ENCDEC,
1616 )
1617 .unwrap();
1618
1619 let count: i64 = db
1621 .db
1622 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1623 .unwrap();
1624 assert_eq!(count, 3);
1625
1626 db.record_potentially_vulnerable_passwords(
1628 vec!["password1".into(), "password4".into()],
1629 &*TEST_ENCDEC,
1630 )
1631 .unwrap();
1632
1633 let count: i64 = db
1635 .db
1636 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1637 .unwrap();
1638 assert_eq!(count, 4);
1639
1640 db.record_potentially_vulnerable_passwords(
1642 vec!["password1".into(), "password2".into()],
1643 &*TEST_ENCDEC,
1644 )
1645 .unwrap();
1646
1647 let count: i64 = db
1648 .db
1649 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1650 .unwrap();
1651 assert_eq!(count, 4);
1652 }
1653
1654 #[test]
1655 fn test_add_with_meta_deleted() {
1656 ensure_initialized();
1657
1658 let guid = Guid::random();
1659 let now_ms = util::system_time_ms_i64(SystemTime::now());
1660 let login = LoginEntry {
1661 origin: "https://www.example.com".into(),
1662 http_realm: Some("https://www.example.com".into()),
1663 username: "test".into(),
1664 password: "sekret".into(),
1665 ..LoginEntry::default()
1666 };
1667 let meta = LoginMeta {
1668 id: guid.to_string(),
1669 time_created: now_ms,
1670 time_password_changed: now_ms + 100,
1671 time_last_used: now_ms + 10,
1672 times_used: 42,
1673 time_last_breach_alert_dismissed: None,
1674 };
1675
1676 let db = LoginDb::open_in_memory();
1677 let entry_with_meta = LoginEntryWithMeta {
1678 entry: login.clone(),
1679 meta: meta.clone(),
1680 };
1681
1682 db.add_with_meta(entry_with_meta, &*TEST_ENCDEC)
1683 .expect("should be able to add login with record");
1684
1685 db.delete(&guid).expect("should be able to delete login");
1686
1687 let entry_with_meta2 = LoginEntryWithMeta {
1688 entry: login.clone(),
1689 meta: meta.clone(),
1690 };
1691
1692 db.add_with_meta(entry_with_meta2, &*TEST_ENCDEC)
1693 .expect("should be able to re-add login with record");
1694
1695 let fetched = db
1696 .get_by_id(&guid)
1697 .expect("should work")
1698 .expect("should get a record");
1699
1700 assert_eq!(fetched.meta, meta);
1701 }
1702
1703 #[test]
1704 fn test_unicode_submit() {
1705 ensure_initialized();
1706 let db = LoginDb::open_in_memory();
1707 let added = db
1708 .add(
1709 LoginEntry {
1710 form_action_origin: Some("http://😍.com".into()),
1711 origin: "http://😍.com".into(),
1712 http_realm: None,
1713 username_field: "😍".into(),
1714 password_field: "😍".into(),
1715 username: "😍".into(),
1716 password: "😍".into(),
1717 },
1718 &*TEST_ENCDEC,
1719 )
1720 .unwrap();
1721 let fetched = db
1722 .get_by_id(&added.meta.id)
1723 .expect("should work")
1724 .expect("should get a record");
1725 assert_eq!(added, fetched);
1726 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1727 assert_eq!(
1728 fetched.fields.form_action_origin,
1729 Some("http://xn--r28h.com".to_string())
1730 );
1731 assert_eq!(fetched.fields.username_field, "😍");
1732 assert_eq!(fetched.fields.password_field, "😍");
1733 let sec_fields = fetched.decrypt_fields(&*TEST_ENCDEC).unwrap();
1734 assert_eq!(sec_fields.username, "😍");
1735 assert_eq!(sec_fields.password, "😍");
1736 }
1737
1738 #[test]
1739 fn test_unicode_realm() {
1740 ensure_initialized();
1741 let db = LoginDb::open_in_memory();
1742 let added = db
1743 .add(
1744 LoginEntry {
1745 form_action_origin: None,
1746 origin: "http://😍.com".into(),
1747 http_realm: Some("😍😍".into()),
1748 username: "😍".into(),
1749 password: "😍".into(),
1750 ..Default::default()
1751 },
1752 &*TEST_ENCDEC,
1753 )
1754 .unwrap();
1755 let fetched = db
1756 .get_by_id(&added.meta.id)
1757 .expect("should work")
1758 .expect("should get a record");
1759 assert_eq!(added, fetched);
1760 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1761 assert_eq!(fetched.fields.http_realm.unwrap(), "😍😍");
1762 }
1763
1764 fn check_matches(db: &LoginDb, query: &str, expected: &[&str]) {
1765 let mut results = db
1766 .get_by_base_domain(query)
1767 .unwrap()
1768 .into_iter()
1769 .map(|l| l.fields.origin)
1770 .collect::<Vec<String>>();
1771 results.sort_unstable();
1772 let mut sorted = expected.to_owned();
1773 sorted.sort_unstable();
1774 assert_eq!(sorted, results);
1775 }
1776
1777 fn check_good_bad(
1778 good: Vec<&str>,
1779 bad: Vec<&str>,
1780 good_queries: Vec<&str>,
1781 zero_queries: Vec<&str>,
1782 ) {
1783 let db = LoginDb::open_in_memory();
1784 for h in good.iter().chain(bad.iter()) {
1785 db.add(
1786 LoginEntry {
1787 origin: (*h).into(),
1788 http_realm: Some((*h).into()),
1789 password: "test".into(),
1790 ..Default::default()
1791 },
1792 &*TEST_ENCDEC,
1793 )
1794 .unwrap();
1795 }
1796 for query in good_queries {
1797 check_matches(&db, query, &good);
1798 }
1799 for query in zero_queries {
1800 check_matches(&db, query, &[]);
1801 }
1802 }
1803
1804 #[test]
1805 fn test_get_by_base_domain_invalid() {
1806 ensure_initialized();
1807 check_good_bad(
1808 vec!["https://example.com"],
1809 vec![],
1810 vec![],
1811 vec!["invalid query"],
1812 );
1813 }
1814
1815 #[test]
1816 fn test_get_by_base_domain() {
1817 ensure_initialized();
1818 check_good_bad(
1819 vec![
1820 "https://example.com",
1821 "https://www.example.com",
1822 "http://www.example.com",
1823 "http://www.example.com:8080",
1824 "http://sub.example.com:8080",
1825 "https://sub.example.com:8080",
1826 "https://sub.sub.example.com",
1827 "ftp://sub.example.com",
1828 ],
1829 vec![
1830 "https://badexample.com",
1831 "https://example.co",
1832 "https://example.com.au",
1833 ],
1834 vec!["example.com"],
1835 vec!["foo.com"],
1836 );
1837 }
1838
1839 #[test]
1840 fn test_get_by_base_domain_punicode() {
1841 ensure_initialized();
1842 check_good_bad(
1845 vec![
1846 "http://xn--r28h.com", ],
1848 vec!["http://💖.com"],
1849 vec!["😍.com", "xn--r28h.com"],
1850 vec![],
1851 );
1852 }
1853
1854 #[test]
1855 fn test_get_by_base_domain_ipv4() {
1856 ensure_initialized();
1857 check_good_bad(
1858 vec!["http://127.0.0.1", "https://127.0.0.1:8000"],
1859 vec!["https://127.0.0.0", "https://example.com"],
1860 vec!["127.0.0.1"],
1861 vec!["127.0.0.2"],
1862 );
1863 }
1864
1865 #[test]
1866 fn test_get_by_base_domain_ipv6() {
1867 ensure_initialized();
1868 check_good_bad(
1869 vec!["http://[::1]", "https://[::1]:8000"],
1870 vec!["https://[0:0:0:0:0:0:1:1]", "https://example.com"],
1871 vec!["[::1]", "[0:0:0:0:0:0:0:1]"],
1872 vec!["[0:0:0:0:0:0:1:2]"],
1873 );
1874 }
1875
1876 #[test]
1877 fn test_add() {
1878 ensure_initialized();
1879 let db = LoginDb::open_in_memory();
1880 let to_add = LoginEntry {
1881 origin: "https://www.example.com".into(),
1882 http_realm: Some("https://www.example.com".into()),
1883 username: "test_user".into(),
1884 password: "test_password".into(),
1885 ..Default::default()
1886 };
1887 let login = db.add(to_add, &*TEST_ENCDEC).unwrap();
1888 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1889
1890 assert_eq!(login.fields.origin, login2.fields.origin);
1891 assert_eq!(login.fields.http_realm, login2.fields.http_realm);
1892 assert_eq!(login.sec_fields, login2.sec_fields);
1893 }
1894
1895 #[test]
1896 fn test_update() {
1897 ensure_initialized();
1898 let db = LoginDb::open_in_memory();
1899 let login = db
1900 .add(
1901 LoginEntry {
1902 origin: "https://www.example.com".into(),
1903 http_realm: Some("https://www.example.com".into()),
1904 username: "user1".into(),
1905 password: "password1".into(),
1906 ..Default::default()
1907 },
1908 &*TEST_ENCDEC,
1909 )
1910 .unwrap();
1911 db.update(
1912 &login.meta.id,
1913 LoginEntry {
1914 origin: "https://www.example2.com".into(),
1915 http_realm: Some("https://www.example2.com".into()),
1916 username: "user2".into(),
1917 password: "password2".into(),
1918 ..Default::default() },
1920 &*TEST_ENCDEC,
1921 )
1922 .unwrap();
1923
1924 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1925
1926 assert_eq!(login2.fields.origin, "https://www.example2.com");
1927 assert_eq!(
1928 login2.fields.http_realm,
1929 Some("https://www.example2.com".into())
1930 );
1931 let sec_fields = login2.decrypt_fields(&*TEST_ENCDEC).unwrap();
1932 assert_eq!(sec_fields.username, "user2");
1933 assert_eq!(sec_fields.password, "password2");
1934 }
1935
1936 #[test]
1937 fn test_touch() {
1938 ensure_initialized();
1939 let db = LoginDb::open_in_memory();
1940 let login = db
1941 .add(
1942 LoginEntry {
1943 origin: "https://www.example.com".into(),
1944 http_realm: Some("https://www.example.com".into()),
1945 username: "user1".into(),
1946 password: "password1".into(),
1947 ..Default::default()
1948 },
1949 &*TEST_ENCDEC,
1950 )
1951 .unwrap();
1952 thread::sleep(time::Duration::from_millis(50));
1954 db.touch(&login.meta.id).unwrap();
1955 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1956 assert!(login2.meta.time_last_used > login.meta.time_last_used);
1957 assert_eq!(login2.meta.times_used, login.meta.times_used + 1);
1958 }
1959
1960 #[test]
1961 fn test_update_does_not_count_as_use() {
1962 ensure_initialized();
1966 let db = LoginDb::open_in_memory();
1967 let login = db
1968 .add(
1969 LoginEntry {
1970 origin: "https://www.example.com".into(),
1971 http_realm: Some("https://www.example.com".into()),
1972 username: "user1".into(),
1973 password: "password1".into(),
1974 ..Default::default()
1975 },
1976 &*TEST_ENCDEC,
1977 )
1978 .unwrap();
1979 thread::sleep(time::Duration::from_millis(50));
1981 db.update(
1982 &login.meta.id,
1983 LoginEntry {
1984 origin: "https://www.example.com".into(),
1985 http_realm: Some("https://www.example.com".into()),
1986 username: "user1".into(),
1987 password: "password2".into(),
1988 ..Default::default()
1989 },
1990 &*TEST_ENCDEC,
1991 )
1992 .unwrap();
1993 let updated = db.get_by_id(&login.meta.id).unwrap().unwrap();
1994 assert_eq!(updated.meta.times_used, login.meta.times_used);
1996 assert_eq!(updated.meta.time_last_used, login.meta.time_last_used);
1998 }
1999
2000 #[test]
2001 fn test_breach_alert_dismissal() {
2002 ensure_initialized();
2003 let db = LoginDb::open_in_memory();
2004 let login = db
2005 .add(
2006 LoginEntry {
2007 origin: "https://www.example.com".into(),
2008 http_realm: Some("https://www.example.com".into()),
2009 username: "user1".into(),
2010 password: "password1".into(),
2011 ..Default::default()
2012 },
2013 &*TEST_ENCDEC,
2014 )
2015 .unwrap();
2016 assert!(login.meta.time_last_breach_alert_dismissed.is_none());
2018
2019 db.record_breach_alert_dismissal(&login.meta.id).unwrap();
2021 let login1 = db.get_by_id(&login.meta.id).unwrap().unwrap();
2022 assert!(login1.meta.time_last_breach_alert_dismissed.is_some());
2023 }
2024
2025 #[test]
2026 fn test_breach_alert_dismissal_with_specific_timestamp() {
2027 ensure_initialized();
2028 let db = LoginDb::open_in_memory();
2029 let login = db
2030 .add(
2031 LoginEntry {
2032 origin: "https://www.example.com".into(),
2033 http_realm: Some("https://www.example.com".into()),
2034 username: "user1".into(),
2035 password: "password1".into(),
2036 ..Default::default()
2037 },
2038 &*TEST_ENCDEC,
2039 )
2040 .unwrap();
2041
2042 let dismiss_time = login.meta.time_password_changed + 1000;
2043 db.record_breach_alert_dismissal_time(&login.meta.id, dismiss_time)
2044 .unwrap();
2045
2046 let retrieved = db
2047 .get_by_id(&login.meta.id)
2048 .unwrap()
2049 .unwrap()
2050 .decrypt(&*TEST_ENCDEC)
2051 .unwrap();
2052 assert_eq!(
2053 retrieved.time_last_breach_alert_dismissed,
2054 Some(dismiss_time)
2055 );
2056 }
2057
2058 #[test]
2059 fn test_delete() {
2060 ensure_initialized();
2061 let db = LoginDb::open_in_memory();
2062 let login = db
2063 .add(
2064 LoginEntry {
2065 origin: "https://www.example.com".into(),
2066 http_realm: Some("https://www.example.com".into()),
2067 username: "test_user".into(),
2068 password: "test_password".into(),
2069 ..Default::default()
2070 },
2071 &*TEST_ENCDEC,
2072 )
2073 .unwrap();
2074
2075 assert!(db.delete(login.guid_str()).unwrap());
2076
2077 let local_login = db
2078 .query_row(
2079 "SELECT * FROM loginsL WHERE guid = :guid",
2080 named_params! { ":guid": login.guid_str() },
2081 |row| Ok(LocalLogin::test_raw_from_row(row).unwrap()),
2082 )
2083 .unwrap();
2084 assert_eq!(local_login.fields.http_realm, None);
2085 assert_eq!(local_login.fields.form_action_origin, None);
2086
2087 assert!(!db.exists(login.guid_str()).unwrap());
2088 }
2089
2090 #[test]
2091 fn test_delete_many() {
2092 ensure_initialized();
2093 let db = LoginDb::open_in_memory();
2094
2095 let login_a = db
2096 .add(
2097 LoginEntry {
2098 origin: "https://a.example.com".into(),
2099 http_realm: Some("https://www.example.com".into()),
2100 username: "test_user".into(),
2101 password: "test_password".into(),
2102 ..Default::default()
2103 },
2104 &*TEST_ENCDEC,
2105 )
2106 .unwrap();
2107
2108 let login_b = db
2109 .add(
2110 LoginEntry {
2111 origin: "https://b.example.com".into(),
2112 http_realm: Some("https://www.example.com".into()),
2113 username: "test_user".into(),
2114 password: "test_password".into(),
2115 ..Default::default()
2116 },
2117 &*TEST_ENCDEC,
2118 )
2119 .unwrap();
2120
2121 let result = db
2122 .delete_many(vec![login_a.guid_str(), login_b.guid_str()])
2123 .unwrap();
2124 assert!(result[0]);
2125 assert!(result[1]);
2126 assert!(!db.exists(login_a.guid_str()).unwrap());
2127 assert!(!db.exists(login_b.guid_str()).unwrap());
2128 }
2129
2130 #[test]
2131 fn test_subsequent_delete_many() {
2132 ensure_initialized();
2133 let db = LoginDb::open_in_memory();
2134
2135 let login = db
2136 .add(
2137 LoginEntry {
2138 origin: "https://a.example.com".into(),
2139 http_realm: Some("https://www.example.com".into()),
2140 username: "test_user".into(),
2141 password: "test_password".into(),
2142 ..Default::default()
2143 },
2144 &*TEST_ENCDEC,
2145 )
2146 .unwrap();
2147
2148 let result = db.delete_many(vec![login.guid_str()]).unwrap();
2149 assert!(result[0]);
2150 assert!(!db.exists(login.guid_str()).unwrap());
2151
2152 let result = db.delete_many(vec![login.guid_str()]).unwrap();
2153 assert!(!result[0]);
2154 }
2155
2156 #[test]
2157 fn test_delete_many_with_non_existent_id() {
2158 ensure_initialized();
2159 let db = LoginDb::open_in_memory();
2160
2161 let result = db.delete_many(vec![&Guid::random()]).unwrap();
2162 assert!(!result[0]);
2163 }
2164
2165 #[test]
2166 fn test_delete_local_for_remote_replacement() {
2167 ensure_initialized();
2168 let db = LoginDb::open_in_memory();
2169 let login = db
2170 .add(
2171 LoginEntry {
2172 origin: "https://www.example.com".into(),
2173 http_realm: Some("https://www.example.com".into()),
2174 username: "test_user".into(),
2175 password: "test_password".into(),
2176 ..Default::default()
2177 },
2178 &*TEST_ENCDEC,
2179 )
2180 .unwrap();
2181
2182 let result = db
2183 .delete_local_records_for_remote_replacement(vec![login.guid_str()])
2184 .unwrap();
2185
2186 let local_guids = get_local_guids(&db);
2187 assert_eq!(local_guids.len(), 0);
2188
2189 let mirror_guids = get_mirror_guids(&db);
2190 assert_eq!(mirror_guids.len(), 0);
2191
2192 assert_eq!(result.local_deleted, 1);
2193 }
2194
2195 mod test_find_login_to_update {
2196 use super::*;
2197
2198 fn make_entry(username: &str, password: &str) -> LoginEntry {
2199 LoginEntry {
2200 origin: "https://www.example.com".into(),
2201 http_realm: Some("the website".into()),
2202 username: username.into(),
2203 password: password.into(),
2204 ..Default::default()
2205 }
2206 }
2207
2208 fn make_saved_login(db: &LoginDb, username: &str, password: &str) -> Login {
2209 db.add(make_entry(username, password), &*TEST_ENCDEC)
2210 .unwrap()
2211 .decrypt(&*TEST_ENCDEC)
2212 .unwrap()
2213 }
2214
2215 #[test]
2216 fn test_match() {
2217 ensure_initialized();
2218 let db = LoginDb::open_in_memory();
2219 let login = make_saved_login(&db, "user", "pass");
2220 assert_eq!(
2221 Some(login),
2222 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2223 .unwrap(),
2224 );
2225 }
2226
2227 #[test]
2228 fn test_non_matches() {
2229 ensure_initialized();
2230 let db = LoginDb::open_in_memory();
2231 make_saved_login(&db, "other-user", "pass");
2233 db.add(
2235 LoginEntry {
2236 origin: "https://www.example.com".into(),
2237 http_realm: Some("the other website".into()),
2238 username: "user".into(),
2239 password: "pass".into(),
2240 ..Default::default()
2241 },
2242 &*TEST_ENCDEC,
2243 )
2244 .unwrap();
2245 db.add(
2247 LoginEntry {
2248 origin: "https://www.example.com".into(),
2249 form_action_origin: Some("https://www.example.com/".into()),
2250 username: "user".into(),
2251 password: "pass".into(),
2252 ..Default::default()
2253 },
2254 &*TEST_ENCDEC,
2255 )
2256 .unwrap();
2257 assert_eq!(
2258 None,
2259 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2260 .unwrap(),
2261 );
2262 }
2263
2264 #[test]
2265 fn test_match_blank_password() {
2266 ensure_initialized();
2267 let db = LoginDb::open_in_memory();
2268 let login = make_saved_login(&db, "", "pass");
2269 assert_eq!(
2270 Some(login),
2271 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2272 .unwrap(),
2273 );
2274 }
2275
2276 #[test]
2277 fn test_username_match_takes_precedence_over_blank_username() {
2278 ensure_initialized();
2279 let db = LoginDb::open_in_memory();
2280 make_saved_login(&db, "", "pass");
2281 let username_match = make_saved_login(&db, "user", "pass");
2282 assert_eq!(
2283 Some(username_match),
2284 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2285 .unwrap(),
2286 );
2287 }
2288
2289 #[test]
2290 fn test_invalid_login() {
2291 ensure_initialized();
2292 let db = LoginDb::open_in_memory();
2293 assert!(db
2294 .find_login_to_update(
2295 LoginEntry {
2296 http_realm: None,
2297 form_action_origin: None,
2298 ..LoginEntry::default()
2299 },
2300 &*TEST_ENCDEC
2301 )
2302 .is_err());
2303 }
2304
2305 #[test]
2306 fn test_update_with_duplicate_login() {
2307 ensure_initialized();
2308 let db = LoginDb::open_in_memory();
2311 let login = make_saved_login(&db, "user", "pass");
2312 let mut dupe = login.clone().encrypt(&*TEST_ENCDEC).unwrap();
2313 dupe.meta.id = "different-guid".to_string();
2314 db.insert_new_login(&dupe).unwrap();
2315
2316 let mut entry = login.entry();
2317 entry.password = "pass2".to_string();
2318 db.update(&login.id, entry, &*TEST_ENCDEC).unwrap();
2319
2320 let mut entry = login.entry();
2321 entry.password = "pass3".to_string();
2322 db.add_or_update(entry, &*TEST_ENCDEC).unwrap();
2323 }
2324
2325 #[test]
2326 fn test_password_reuse_detection() {
2327 ensure_initialized();
2328 let db = LoginDb::open_in_memory();
2329
2330 let login1 = db
2332 .add(
2333 LoginEntry {
2334 origin: "https://site1.com".into(),
2335 http_realm: Some("realm".into()),
2336 username: "user1".into(),
2337 password: "shared_password".into(),
2338 ..Default::default()
2339 },
2340 &*TEST_ENCDEC,
2341 )
2342 .unwrap();
2343
2344 let login2 = db
2345 .add(
2346 LoginEntry {
2347 origin: "https://site2.com".into(),
2348 http_realm: Some("realm".into()),
2349 username: "user2".into(),
2350 password: "shared_password".into(),
2351 ..Default::default()
2352 },
2353 &*TEST_ENCDEC,
2354 )
2355 .unwrap();
2356
2357 assert!(!db
2359 .is_potentially_vulnerable_password(&login1.meta.id, &*TEST_ENCDEC)
2360 .unwrap());
2361 assert!(!db
2362 .is_potentially_vulnerable_password(&login2.meta.id, &*TEST_ENCDEC)
2363 .unwrap());
2364 let vulnerable = db
2366 .are_potentially_vulnerable_passwords(
2367 &[&login1.meta.id, &login2.meta.id],
2368 &*TEST_ENCDEC,
2369 )
2370 .unwrap();
2371 assert_eq!(vulnerable.len(), 0);
2372
2373 db.record_potentially_vulnerable_passwords(
2375 vec!["shared_password".into()],
2376 &*TEST_ENCDEC,
2377 )
2378 .unwrap();
2379
2380 assert!(db
2382 .is_potentially_vulnerable_password(&login2.meta.id, &*TEST_ENCDEC)
2383 .unwrap());
2384 let vulnerable = db
2386 .are_potentially_vulnerable_passwords(
2387 &[&login1.meta.id, &login2.meta.id],
2388 &*TEST_ENCDEC,
2389 )
2390 .unwrap();
2391 assert_eq!(vulnerable.len(), 2);
2392 assert!(vulnerable.contains(&login1.meta.id));
2393 assert!(vulnerable.contains(&login2.meta.id));
2394
2395 db.update(
2397 &login2.meta.id,
2398 LoginEntry {
2399 origin: "https://site2.com".into(),
2400 http_realm: Some("realm".into()),
2401 username: "user2".into(),
2402 password: "different_password".into(),
2403 ..Default::default()
2404 },
2405 &*TEST_ENCDEC,
2406 )
2407 .unwrap();
2408
2409 assert!(!db
2410 .is_potentially_vulnerable_password(&login2.meta.id, &*TEST_ENCDEC)
2411 .unwrap());
2412 }
2413
2414 #[test]
2415 fn test_reset_all_breaches_clears_breach_table() {
2416 ensure_initialized();
2417 let db = LoginDb::open_in_memory();
2418
2419 let login = db
2420 .add(
2421 LoginEntry {
2422 origin: "https://example.com".into(),
2423 http_realm: Some("realm".into()),
2424 username: "user".into(),
2425 password: "password123".into(),
2426 ..Default::default()
2427 },
2428 &*TEST_ENCDEC,
2429 )
2430 .unwrap();
2431
2432 db.record_potentially_vulnerable_passwords(vec!["password123".into()], &*TEST_ENCDEC)
2433 .unwrap();
2434
2435 let count: i64 = db
2437 .db
2438 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
2439 .unwrap();
2440 assert_eq!(count, 1);
2441 let vulnerable = db
2443 .are_potentially_vulnerable_passwords(&[&login.meta.id], &*TEST_ENCDEC)
2444 .unwrap();
2445 assert_eq!(vulnerable.len(), 1);
2446 assert_eq!(vulnerable[0], login.meta.id);
2447
2448 db.reset_all_breaches().unwrap();
2450
2451 let count: i64 = db
2453 .db
2454 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
2455 .unwrap();
2456 assert_eq!(count, 0);
2457 let vulnerable = db
2459 .are_potentially_vulnerable_passwords(&[&login.meta.id], &*TEST_ENCDEC)
2460 .unwrap();
2461 assert_eq!(vulnerable.len(), 0);
2462 }
2463
2464 #[test]
2465 fn test_different_passwords_not_vulnerable() {
2466 ensure_initialized();
2467 let db = LoginDb::open_in_memory();
2468
2469 let login1 = db
2470 .add(
2471 LoginEntry {
2472 origin: "https://site1.com".into(),
2473 http_realm: Some("realm".into()),
2474 username: "user".into(),
2475 password: "password_A".into(),
2476 ..Default::default()
2477 },
2478 &*TEST_ENCDEC,
2479 )
2480 .unwrap();
2481
2482 let login2 = db
2483 .add(
2484 LoginEntry {
2485 origin: "https://site2.com".into(),
2486 http_realm: Some("realm".into()),
2487 username: "user".into(),
2488 password: "password_B".into(),
2489 ..Default::default()
2490 },
2491 &*TEST_ENCDEC,
2492 )
2493 .unwrap();
2494
2495 db.record_potentially_vulnerable_passwords(vec!["password_A".into()], &*TEST_ENCDEC)
2496 .unwrap();
2497
2498 assert!(!db
2500 .is_potentially_vulnerable_password(&login2.meta.id, &*TEST_ENCDEC)
2501 .unwrap());
2502 let vulnerable = db
2505 .are_potentially_vulnerable_passwords(
2506 &[&login1.meta.id, &login2.meta.id],
2507 &*TEST_ENCDEC,
2508 )
2509 .unwrap();
2510 assert_eq!(vulnerable.len(), 1);
2511 assert!(vulnerable.contains(&login1.meta.id));
2512 }
2513 }
2514}