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