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