1use crate::encryption::{EncryptorDecryptor, NoopEncryptorDecryptor};
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(mut self) -> Result<()> {
1083 self.encdec = Arc::new(NoopEncryptorDecryptor);
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_as::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())
1340 .expect("should be able to add first login");
1341
1342 let exp_err = "Invalid login: Login already exists";
1344 assert_eq!(db.add(login.clone()).unwrap_err().to_string(), exp_err);
1345
1346 login.username = "".to_string();
1348 db.add(login.clone()).expect("empty login isn't a dupe");
1349
1350 assert_eq!(db.add(login).unwrap_err().to_string(), exp_err);
1351
1352 assert_eq!(db.get_all().unwrap().len(), 2);
1354 }
1355
1356 #[test]
1357 fn test_add_many() {
1358 ensure_initialized();
1359
1360 let login_a = LoginEntry {
1361 origin: "https://a.example.com".into(),
1362 http_realm: Some("https://www.example.com".into()),
1363 username: "test".into(),
1364 password: "sekret".into(),
1365 ..LoginEntry::default()
1366 };
1367
1368 let login_b = LoginEntry {
1369 origin: "https://b.example.com".into(),
1370 http_realm: Some("https://www.example.com".into()),
1371 username: "test".into(),
1372 password: "sekret".into(),
1373 ..LoginEntry::default()
1374 };
1375
1376 let db = LoginDb::open_in_memory();
1377 let added = db
1378 .add_many(vec![login_a.clone(), login_b.clone()])
1379 .expect("should be able to add logins");
1380
1381 let [added_a, added_b] = added.as_slice() else {
1382 panic!("there should really be 2")
1383 };
1384
1385 let fetched_a = db
1386 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1387 .expect("should work")
1388 .expect("should get a record");
1389
1390 assert_eq!(fetched_a.fields.origin, login_a.origin);
1391
1392 let fetched_b = db
1393 .get_by_id(&added_b.as_ref().unwrap().meta.id)
1394 .expect("should work")
1395 .expect("should get a record");
1396
1397 assert_eq!(fetched_b.fields.origin, login_b.origin);
1398
1399 assert_eq!(db.count_all().unwrap(), 2);
1400 }
1401
1402 #[test]
1403 fn test_count_by_origin() {
1404 ensure_initialized();
1405
1406 let origin_a = "https://a.example.com";
1407 let login_a = LoginEntry {
1408 origin: origin_a.into(),
1409 http_realm: Some("https://www.example.com".into()),
1410 username: "test".into(),
1411 password: "sekret".into(),
1412 ..LoginEntry::default()
1413 };
1414
1415 let login_b = LoginEntry {
1416 origin: "https://b.example.com".into(),
1417 http_realm: Some("https://www.example.com".into()),
1418 username: "test".into(),
1419 password: "sekret".into(),
1420 ..LoginEntry::default()
1421 };
1422
1423 let origin_umlaut = "https://bücher.example.com";
1424 let login_umlaut = LoginEntry {
1425 origin: origin_umlaut.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 db = LoginDb::open_in_memory();
1433 db.add_many(vec![login_a.clone(), login_b.clone(), login_umlaut.clone()])
1434 .expect("should be able to add logins");
1435
1436 assert_eq!(db.count_by_origin(origin_a).unwrap(), 1);
1437 assert_eq!(db.count_by_origin(origin_umlaut).unwrap(), 1);
1438 }
1439
1440 #[test]
1441 fn test_count_by_form_action_origin() {
1442 ensure_initialized();
1443
1444 let origin_a = "https://a.example.com";
1445 let login_a = LoginEntry {
1446 origin: origin_a.into(),
1447 form_action_origin: Some(origin_a.into()),
1448 http_realm: Some("https://www.example.com".into()),
1449 username: "test".into(),
1450 password: "sekret".into(),
1451 ..LoginEntry::default()
1452 };
1453
1454 let login_b = LoginEntry {
1455 origin: "https://b.example.com".into(),
1456 form_action_origin: Some("https://b.example.com".into()),
1457 http_realm: Some("https://www.example.com".into()),
1458 username: "test".into(),
1459 password: "sekret".into(),
1460 ..LoginEntry::default()
1461 };
1462
1463 let origin_umlaut = "https://bücher.example.com";
1464 let login_umlaut = LoginEntry {
1465 origin: origin_umlaut.into(),
1466 form_action_origin: Some(origin_umlaut.into()),
1467 http_realm: Some("https://www.example.com".into()),
1468 username: "test".into(),
1469 password: "sekret".into(),
1470 ..LoginEntry::default()
1471 };
1472
1473 let db = LoginDb::open_in_memory();
1474 db.add_many(vec![login_a.clone(), login_b.clone(), login_umlaut.clone()])
1475 .expect("should be able to add logins");
1476
1477 assert_eq!(db.count_by_form_action_origin(origin_a).unwrap(), 1);
1478 assert_eq!(db.count_by_form_action_origin(origin_umlaut).unwrap(), 1);
1479 }
1480
1481 #[test]
1482 #[cfg(feature = "ignore_form_action_origin_validation_errors")]
1483 fn test_count_by_invalid_form_action_origin() {
1484 ensure_initialized();
1485
1486 let login = LoginEntry {
1487 origin: "https://example.com".into(),
1488 form_action_origin: Some("email".into()),
1489 username: "test".into(),
1490 password: "sekret".into(),
1491 ..LoginEntry::default()
1492 };
1493
1494 let db = LoginDb::open_in_memory();
1495 db.add(login)
1496 .expect("should be able to add login with invalid form_action_origin");
1497 assert_eq!(db.count_by_form_action_origin("email").unwrap(), 1);
1498 }
1499
1500 #[test]
1501 fn test_add_many_with_failed_constraint() {
1502 ensure_initialized();
1503
1504 let login_a = LoginEntry {
1505 origin: "https://example.com".into(),
1506 http_realm: Some("https://www.example.com".into()),
1507 username: "test".into(),
1508 password: "sekret".into(),
1509 ..LoginEntry::default()
1510 };
1511
1512 let login_b = LoginEntry {
1513 origin: "https://example.com".into(),
1515 http_realm: Some("https://www.example.com".into()),
1516 username: "test".into(),
1517 password: "sekret".into(),
1518 ..LoginEntry::default()
1519 };
1520
1521 let db = LoginDb::open_in_memory();
1522 let added = db
1523 .add_many(vec![login_a.clone(), login_b.clone()])
1524 .expect("should be able to add logins");
1525
1526 let [added_a, added_b] = added.as_slice() else {
1527 panic!("there should really be 2")
1528 };
1529
1530 let fetched_a = db
1532 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1533 .expect("should work")
1534 .expect("should get a record");
1535
1536 assert_eq!(fetched_a.fields.origin, login_a.origin);
1537
1538 assert!(!added_b.is_ok());
1540 }
1541
1542 #[test]
1543 fn test_add_with_meta() {
1544 ensure_initialized();
1545
1546 let guid = Guid::random();
1547 let now_ms = util::system_time_ms_i64(SystemTime::now());
1548 let login = LoginEntry {
1549 origin: "https://www.example.com".into(),
1550 http_realm: Some("https://www.example.com".into()),
1551 username: "test".into(),
1552 password: "sekret".into(),
1553 ..LoginEntry::default()
1554 };
1555 let meta = LoginMeta {
1556 id: guid.to_string(),
1557 time_created: now_ms,
1558 time_password_changed: now_ms + 100,
1559 time_last_used: now_ms + 10,
1560 times_used: 42,
1561 time_last_breach_alert_dismissed: None,
1562 };
1563
1564 let db = LoginDb::open_in_memory();
1565 let entry_with_meta = LoginEntryWithMeta {
1566 entry: login.clone(),
1567 meta: meta.clone(),
1568 };
1569
1570 db.add_with_meta(entry_with_meta)
1571 .expect("should be able to add login with record");
1572
1573 let fetched = db
1574 .get_by_id(&guid)
1575 .expect("should work")
1576 .expect("should get a record");
1577
1578 assert_eq!(fetched.meta, meta);
1579 }
1580
1581 #[test]
1582 fn test_add_with_meta_duplicate_id() {
1583 ensure_initialized();
1584
1585 let guid = Guid::random();
1586 let now_ms = util::system_time_ms_i64(SystemTime::now());
1587 let meta = LoginMeta {
1588 id: guid.to_string(),
1589 time_created: now_ms,
1590 time_password_changed: now_ms,
1591 time_last_used: now_ms,
1592 times_used: 1,
1593 time_last_breach_alert_dismissed: None,
1594 };
1595
1596 let db = LoginDb::open_in_memory();
1597 db.add_with_meta(LoginEntryWithMeta {
1598 entry: LoginEntry {
1599 origin: "https://www.example.com".into(),
1600 http_realm: Some("https://www.example.com".into()),
1601 username: "test".into(),
1602 password: "sekret".into(),
1603 ..LoginEntry::default()
1604 },
1605 meta: meta.clone(),
1606 })
1607 .expect("should be able to add login with record");
1608
1609 db.add_with_meta(LoginEntryWithMeta {
1612 entry: LoginEntry {
1613 origin: "https://www.other.com".into(),
1614 http_realm: Some("https://www.other.com".into()),
1615 username: "test".into(),
1616 password: "sekret".into(),
1617 ..LoginEntry::default()
1618 },
1619 meta,
1620 })
1621 .expect("should be able to re-add a login with the same id");
1622
1623 let fetched = db
1624 .get_by_id(&guid)
1625 .expect("should work")
1626 .expect("should get a record");
1627 assert_eq!(fetched.fields.origin, "https://www.other.com");
1628 }
1629
1630 #[test]
1631 fn test_record_potentially_vulnerable_passwords() {
1632 ensure_initialized();
1633 let db = LoginDb::open_in_memory();
1634
1635 let count: i64 = db
1637 .db
1638 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1639 .unwrap();
1640 assert_eq!(count, 0);
1641
1642 db.record_potentially_vulnerable_passwords(vec![
1644 "password1".into(),
1645 "password2".into(),
1646 "password3".into(),
1647 ])
1648 .unwrap();
1649
1650 let count: i64 = db
1652 .db
1653 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1654 .unwrap();
1655 assert_eq!(count, 3);
1656
1657 db.record_potentially_vulnerable_passwords(vec!["password1".into(), "password4".into()])
1659 .unwrap();
1660
1661 let count: i64 = db
1663 .db
1664 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1665 .unwrap();
1666 assert_eq!(count, 4);
1667
1668 db.record_potentially_vulnerable_passwords(vec!["password1".into(), "password2".into()])
1670 .unwrap();
1671
1672 let count: i64 = db
1673 .db
1674 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1675 .unwrap();
1676 assert_eq!(count, 4);
1677 }
1678
1679 #[test]
1680 fn test_add_with_meta_deleted() {
1681 ensure_initialized();
1682
1683 let guid = Guid::random();
1684 let now_ms = util::system_time_ms_i64(SystemTime::now());
1685 let login = LoginEntry {
1686 origin: "https://www.example.com".into(),
1687 http_realm: Some("https://www.example.com".into()),
1688 username: "test".into(),
1689 password: "sekret".into(),
1690 ..LoginEntry::default()
1691 };
1692 let meta = LoginMeta {
1693 id: guid.to_string(),
1694 time_created: now_ms,
1695 time_password_changed: now_ms + 100,
1696 time_last_used: now_ms + 10,
1697 times_used: 42,
1698 time_last_breach_alert_dismissed: None,
1699 };
1700
1701 let db = LoginDb::open_in_memory();
1702 let entry_with_meta = LoginEntryWithMeta {
1703 entry: login.clone(),
1704 meta: meta.clone(),
1705 };
1706
1707 db.add_with_meta(entry_with_meta)
1708 .expect("should be able to add login with record");
1709
1710 db.delete(&guid).expect("should be able to delete login");
1711
1712 let entry_with_meta2 = LoginEntryWithMeta {
1713 entry: login.clone(),
1714 meta: meta.clone(),
1715 };
1716
1717 db.add_with_meta(entry_with_meta2)
1718 .expect("should be able to re-add login with record");
1719
1720 let fetched = db
1721 .get_by_id(&guid)
1722 .expect("should work")
1723 .expect("should get a record");
1724
1725 assert_eq!(fetched.meta, meta);
1726 }
1727
1728 #[test]
1729 fn test_unicode_submit() {
1730 ensure_initialized();
1731 let db = LoginDb::open_in_memory();
1732 let added = db
1733 .add(LoginEntry {
1734 form_action_origin: Some("http://😍.com".into()),
1735 origin: "http://😍.com".into(),
1736 http_realm: None,
1737 username_field: "😍".into(),
1738 password_field: "😍".into(),
1739 username: "😍".into(),
1740 password: "😍".into(),
1741 })
1742 .unwrap();
1743 let fetched = db
1744 .get_by_id(&added.meta.id)
1745 .expect("should work")
1746 .expect("should get a record");
1747 assert_eq!(added, fetched);
1748 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1749 assert_eq!(
1750 fetched.fields.form_action_origin,
1751 Some("http://xn--r28h.com".to_string())
1752 );
1753 assert_eq!(fetched.fields.username_field, "😍");
1754 assert_eq!(fetched.fields.password_field, "😍");
1755 let sec_fields = fetched.decrypt_fields(db.encdec.as_ref()).unwrap();
1756 assert_eq!(sec_fields.username, "😍");
1757 assert_eq!(sec_fields.password, "😍");
1758 }
1759
1760 #[test]
1761 fn test_unicode_realm() {
1762 ensure_initialized();
1763 let db = LoginDb::open_in_memory();
1764 let added = db
1765 .add(LoginEntry {
1766 form_action_origin: None,
1767 origin: "http://😍.com".into(),
1768 http_realm: Some("😍😍".into()),
1769 username: "😍".into(),
1770 password: "😍".into(),
1771 ..Default::default()
1772 })
1773 .unwrap();
1774 let fetched = db
1775 .get_by_id(&added.meta.id)
1776 .expect("should work")
1777 .expect("should get a record");
1778 assert_eq!(added, fetched);
1779 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1780 assert_eq!(fetched.fields.http_realm.unwrap(), "😍😍");
1781 }
1782
1783 fn check_matches(db: &LoginDb, query: &str, expected: &[&str]) {
1784 let mut results = db
1785 .get_by_base_domain(query)
1786 .unwrap()
1787 .into_iter()
1788 .map(|l| l.fields.origin)
1789 .collect::<Vec<String>>();
1790 results.sort_unstable();
1791 let mut sorted = expected.to_owned();
1792 sorted.sort_unstable();
1793 assert_eq!(sorted, results);
1794 }
1795
1796 fn check_good_bad(
1797 good: Vec<&str>,
1798 bad: Vec<&str>,
1799 good_queries: Vec<&str>,
1800 zero_queries: Vec<&str>,
1801 ) {
1802 let db = LoginDb::open_in_memory();
1803 for h in good.iter().chain(bad.iter()) {
1804 db.add(LoginEntry {
1805 origin: (*h).into(),
1806 http_realm: Some((*h).into()),
1807 password: "test".into(),
1808 ..Default::default()
1809 })
1810 .unwrap();
1811 }
1812 for query in good_queries {
1813 check_matches(&db, query, &good);
1814 }
1815 for query in zero_queries {
1816 check_matches(&db, query, &[]);
1817 }
1818 }
1819
1820 #[test]
1821 fn test_get_by_base_domain_invalid() {
1822 ensure_initialized();
1823 check_good_bad(
1824 vec!["https://example.com"],
1825 vec![],
1826 vec![],
1827 vec!["invalid query"],
1828 );
1829 }
1830
1831 #[test]
1832 fn test_get_by_base_domain() {
1833 ensure_initialized();
1834 check_good_bad(
1835 vec![
1836 "https://example.com",
1837 "https://www.example.com",
1838 "http://www.example.com",
1839 "http://www.example.com:8080",
1840 "http://sub.example.com:8080",
1841 "https://sub.example.com:8080",
1842 "https://sub.sub.example.com",
1843 "ftp://sub.example.com",
1844 ],
1845 vec![
1846 "https://badexample.com",
1847 "https://example.co",
1848 "https://example.com.au",
1849 ],
1850 vec!["example.com"],
1851 vec!["foo.com"],
1852 );
1853 }
1854
1855 #[test]
1856 fn test_get_by_base_domain_punicode() {
1857 ensure_initialized();
1858 check_good_bad(
1861 vec![
1862 "http://xn--r28h.com", ],
1864 vec!["http://💖.com"],
1865 vec!["😍.com", "xn--r28h.com"],
1866 vec![],
1867 );
1868 }
1869
1870 #[test]
1871 fn test_get_by_base_domain_ipv4() {
1872 ensure_initialized();
1873 check_good_bad(
1874 vec!["http://127.0.0.1", "https://127.0.0.1:8000"],
1875 vec!["https://127.0.0.0", "https://example.com"],
1876 vec!["127.0.0.1"],
1877 vec!["127.0.0.2"],
1878 );
1879 }
1880
1881 #[test]
1882 fn test_get_by_base_domain_ipv6() {
1883 ensure_initialized();
1884 check_good_bad(
1885 vec!["http://[::1]", "https://[::1]:8000"],
1886 vec!["https://[0:0:0:0:0:0:1:1]", "https://example.com"],
1887 vec!["[::1]", "[0:0:0:0:0:0:0:1]"],
1888 vec!["[0:0:0:0:0:0:1:2]"],
1889 );
1890 }
1891
1892 #[test]
1893 fn test_add() {
1894 ensure_initialized();
1895 let db = LoginDb::open_in_memory();
1896 let to_add = LoginEntry {
1897 origin: "https://www.example.com".into(),
1898 http_realm: Some("https://www.example.com".into()),
1899 username: "test_user".into(),
1900 password: "test_password".into(),
1901 ..Default::default()
1902 };
1903 let login = db.add(to_add).unwrap();
1904 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1905
1906 assert_eq!(login.fields.origin, login2.fields.origin);
1907 assert_eq!(login.fields.http_realm, login2.fields.http_realm);
1908 assert_eq!(login.sec_fields, login2.sec_fields);
1909 }
1910
1911 #[test]
1912 fn test_update() {
1913 ensure_initialized();
1914 let db = LoginDb::open_in_memory();
1915 let login = db
1916 .add(LoginEntry {
1917 origin: "https://www.example.com".into(),
1918 http_realm: Some("https://www.example.com".into()),
1919 username: "user1".into(),
1920 password: "password1".into(),
1921 ..Default::default()
1922 })
1923 .unwrap();
1924 db.update(
1925 &login.meta.id,
1926 LoginEntry {
1927 origin: "https://www.example2.com".into(),
1928 http_realm: Some("https://www.example2.com".into()),
1929 username: "user2".into(),
1930 password: "password2".into(),
1931 ..Default::default() },
1933 )
1934 .unwrap();
1935
1936 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1937
1938 assert_eq!(login2.fields.origin, "https://www.example2.com");
1939 assert_eq!(
1940 login2.fields.http_realm,
1941 Some("https://www.example2.com".into())
1942 );
1943 let sec_fields = login2.decrypt_fields(db.encdec.as_ref()).unwrap();
1944 assert_eq!(sec_fields.username, "user2");
1945 assert_eq!(sec_fields.password, "password2");
1946 }
1947
1948 #[test]
1949 fn test_touch() {
1950 ensure_initialized();
1951 let db = LoginDb::open_in_memory();
1952 let login = db
1953 .add(LoginEntry {
1954 origin: "https://www.example.com".into(),
1955 http_realm: Some("https://www.example.com".into()),
1956 username: "user1".into(),
1957 password: "password1".into(),
1958 ..Default::default()
1959 })
1960 .unwrap();
1961 thread::sleep(time::Duration::from_millis(50));
1963 db.touch(&login.meta.id).unwrap();
1964 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1965 assert!(login2.meta.time_last_used > login.meta.time_last_used);
1966 assert_eq!(login2.meta.times_used, login.meta.times_used + 1);
1967 }
1968
1969 #[test]
1970 fn test_update_does_not_count_as_use() {
1971 ensure_initialized();
1975 let db = LoginDb::open_in_memory();
1976 let login = db
1977 .add(LoginEntry {
1978 origin: "https://www.example.com".into(),
1979 http_realm: Some("https://www.example.com".into()),
1980 username: "user1".into(),
1981 password: "password1".into(),
1982 ..Default::default()
1983 })
1984 .unwrap();
1985 thread::sleep(time::Duration::from_millis(50));
1987 db.update(
1988 &login.meta.id,
1989 LoginEntry {
1990 origin: "https://www.example.com".into(),
1991 http_realm: Some("https://www.example.com".into()),
1992 username: "user1".into(),
1993 password: "password2".into(),
1994 ..Default::default()
1995 },
1996 )
1997 .unwrap();
1998 let updated = db.get_by_id(&login.meta.id).unwrap().unwrap();
1999 assert_eq!(updated.meta.times_used, login.meta.times_used);
2001 assert_eq!(updated.meta.time_last_used, login.meta.time_last_used);
2003 }
2004
2005 #[test]
2006 fn test_breach_alert_dismissal() {
2007 ensure_initialized();
2008 let db = LoginDb::open_in_memory();
2009 let login = db
2010 .add(LoginEntry {
2011 origin: "https://www.example.com".into(),
2012 http_realm: Some("https://www.example.com".into()),
2013 username: "user1".into(),
2014 password: "password1".into(),
2015 ..Default::default()
2016 })
2017 .unwrap();
2018 assert!(login.meta.time_last_breach_alert_dismissed.is_none());
2020
2021 db.record_breach_alert_dismissal(&login.meta.id).unwrap();
2023 let login1 = db.get_by_id(&login.meta.id).unwrap().unwrap();
2024 assert!(login1.meta.time_last_breach_alert_dismissed.is_some());
2025 }
2026
2027 #[test]
2028 fn test_breach_alert_dismissal_with_specific_timestamp() {
2029 ensure_initialized();
2030 let db = LoginDb::open_in_memory();
2031 let login = db
2032 .add(LoginEntry {
2033 origin: "https://www.example.com".into(),
2034 http_realm: Some("https://www.example.com".into()),
2035 username: "user1".into(),
2036 password: "password1".into(),
2037 ..Default::default()
2038 })
2039 .unwrap();
2040
2041 let dismiss_time = login.meta.time_password_changed + 1000;
2042 db.record_breach_alert_dismissal_time(&login.meta.id, dismiss_time)
2043 .unwrap();
2044
2045 let retrieved = db
2046 .get_by_id(&login.meta.id)
2047 .unwrap()
2048 .unwrap()
2049 .decrypt(db.encdec.as_ref())
2050 .unwrap();
2051 assert_eq!(
2052 retrieved.time_last_breach_alert_dismissed,
2053 Some(dismiss_time)
2054 );
2055 }
2056
2057 #[test]
2058 fn test_delete() {
2059 ensure_initialized();
2060 let db = LoginDb::open_in_memory();
2061 let login = db
2062 .add(LoginEntry {
2063 origin: "https://www.example.com".into(),
2064 http_realm: Some("https://www.example.com".into()),
2065 username: "test_user".into(),
2066 password: "test_password".into(),
2067 ..Default::default()
2068 })
2069 .unwrap();
2070
2071 assert!(db.delete(login.guid_str()).unwrap());
2072
2073 let local_login = db
2074 .query_row(
2075 "SELECT * FROM loginsL WHERE guid = :guid",
2076 named_params! { ":guid": login.guid_str() },
2077 |row| Ok(LocalLogin::test_raw_from_row(row).unwrap()),
2078 )
2079 .unwrap();
2080 assert_eq!(local_login.fields.http_realm, None);
2081 assert_eq!(local_login.fields.form_action_origin, None);
2082
2083 assert!(!db.exists(login.guid_str()).unwrap());
2084 }
2085
2086 #[test]
2087 fn test_delete_many() {
2088 ensure_initialized();
2089 let db = LoginDb::open_in_memory();
2090
2091 let login_a = db
2092 .add(LoginEntry {
2093 origin: "https://a.example.com".into(),
2094 http_realm: Some("https://www.example.com".into()),
2095 username: "test_user".into(),
2096 password: "test_password".into(),
2097 ..Default::default()
2098 })
2099 .unwrap();
2100
2101 let login_b = db
2102 .add(LoginEntry {
2103 origin: "https://b.example.com".into(),
2104 http_realm: Some("https://www.example.com".into()),
2105 username: "test_user".into(),
2106 password: "test_password".into(),
2107 ..Default::default()
2108 })
2109 .unwrap();
2110
2111 let result = db
2112 .delete_many(vec![login_a.guid_str(), login_b.guid_str()])
2113 .unwrap();
2114 assert!(result[0]);
2115 assert!(result[1]);
2116 assert!(!db.exists(login_a.guid_str()).unwrap());
2117 assert!(!db.exists(login_b.guid_str()).unwrap());
2118 }
2119
2120 #[test]
2121 fn test_subsequent_delete_many() {
2122 ensure_initialized();
2123 let db = LoginDb::open_in_memory();
2124
2125 let login = db
2126 .add(LoginEntry {
2127 origin: "https://a.example.com".into(),
2128 http_realm: Some("https://www.example.com".into()),
2129 username: "test_user".into(),
2130 password: "test_password".into(),
2131 ..Default::default()
2132 })
2133 .unwrap();
2134
2135 let result = db.delete_many(vec![login.guid_str()]).unwrap();
2136 assert!(result[0]);
2137 assert!(!db.exists(login.guid_str()).unwrap());
2138
2139 let result = db.delete_many(vec![login.guid_str()]).unwrap();
2140 assert!(!result[0]);
2141 }
2142
2143 #[test]
2144 fn test_delete_many_with_non_existent_id() {
2145 ensure_initialized();
2146 let db = LoginDb::open_in_memory();
2147
2148 let result = db.delete_many(vec![&Guid::random()]).unwrap();
2149 assert!(!result[0]);
2150 }
2151
2152 #[test]
2153 fn test_delete_all() {
2154 ensure_initialized();
2155 let db = LoginDb::open_in_memory();
2156 let login_a = db
2157 .add(LoginEntry {
2158 origin: "https://a.example.com".into(),
2159 http_realm: Some("https://www.example.com".into()),
2160 username: "test_user".into(),
2161 password: "test_password".into(),
2162 ..Default::default()
2163 })
2164 .unwrap();
2165 let login_b = db
2166 .add(LoginEntry {
2167 origin: "https://b.example.com".into(),
2168 http_realm: Some("https://www.example.com".into()),
2169 username: "test_user".into(),
2170 password: "test_password".into(),
2171 ..Default::default()
2172 })
2173 .unwrap();
2174
2175 let mut deleted = db.delete_all().unwrap();
2176 deleted.sort();
2177 let mut expected = vec![login_a.meta.id.clone(), login_b.meta.id.clone()];
2178 expected.sort();
2179 assert_eq!(deleted, expected);
2180 assert!(!db.exists(login_a.guid_str()).unwrap());
2181 assert!(!db.exists(login_b.guid_str()).unwrap());
2182
2183 assert_eq!(db.delete_all().unwrap(), Vec::<String>::new());
2185 }
2186
2187 #[test]
2188 fn test_delete_all_except_fxa() {
2189 ensure_initialized();
2190 let db = LoginDb::open_in_memory();
2191 let login = db
2192 .add(LoginEntry {
2193 origin: "https://a.example.com".into(),
2194 http_realm: Some("https://www.example.com".into()),
2195 username: "test_user".into(),
2196 password: "test_password".into(),
2197 ..Default::default()
2198 })
2199 .unwrap();
2200 let fxa_login = db
2201 .add(LoginEntry {
2202 origin: FXA_CREDENTIALS_ORIGIN.into(),
2203 http_realm: Some("https://www.example.com".into()),
2204 username: "test_user".into(),
2205 password: "test_password".into(),
2206 ..Default::default()
2207 })
2208 .unwrap();
2209
2210 let deleted = db.delete_all_except_fxa().unwrap();
2211 assert_eq!(deleted, vec![login.meta.id.clone()]);
2212
2213 assert!(!db.exists(login.guid_str()).unwrap());
2215 assert!(db.exists(fxa_login.guid_str()).unwrap());
2216 }
2217
2218 #[test]
2219 fn test_wipe_local_except_fxa() {
2220 ensure_initialized();
2221 let db = LoginDb::open_in_memory();
2222 let login = db
2223 .add(LoginEntry {
2224 origin: "https://a.example.com".into(),
2225 http_realm: Some("https://www.example.com".into()),
2226 username: "test_user".into(),
2227 password: "test_password".into(),
2228 ..Default::default()
2229 })
2230 .unwrap();
2231 let fxa_login = db
2232 .add(LoginEntry {
2233 origin: FXA_CREDENTIALS_ORIGIN.into(),
2234 http_realm: Some("https://www.example.com".into()),
2235 username: "test_user".into(),
2236 password: "test_password".into(),
2237 ..Default::default()
2238 })
2239 .unwrap();
2240
2241 db.wipe_local_except_fxa().unwrap();
2242
2243 assert!(!db.exists(login.guid_str()).unwrap());
2245 assert!(db.exists(fxa_login.guid_str()).unwrap());
2246 }
2247
2248 #[test]
2249 fn test_delete_local_for_remote_replacement() {
2250 ensure_initialized();
2251 let db = LoginDb::open_in_memory();
2252 let login = db
2253 .add(LoginEntry {
2254 origin: "https://www.example.com".into(),
2255 http_realm: Some("https://www.example.com".into()),
2256 username: "test_user".into(),
2257 password: "test_password".into(),
2258 ..Default::default()
2259 })
2260 .unwrap();
2261
2262 let result = db
2263 .delete_local_records_for_remote_replacement(vec![login.guid_str()])
2264 .unwrap();
2265
2266 let local_guids = get_local_guids(&db);
2267 assert_eq!(local_guids.len(), 0);
2268
2269 let mirror_guids = get_mirror_guids(&db);
2270 assert_eq!(mirror_guids.len(), 0);
2271
2272 assert_eq!(result.local_deleted, 1);
2273 }
2274
2275 mod test_find_login_to_update {
2276 use super::*;
2277
2278 fn make_entry(username: &str, password: &str) -> LoginEntry {
2279 LoginEntry {
2280 origin: "https://www.example.com".into(),
2281 http_realm: Some("the website".into()),
2282 username: username.into(),
2283 password: password.into(),
2284 ..Default::default()
2285 }
2286 }
2287
2288 fn make_saved_login(db: &LoginDb, username: &str, password: &str) -> Login {
2289 db.add(make_entry(username, password))
2290 .unwrap()
2291 .decrypt(db.encdec.as_ref())
2292 .unwrap()
2293 }
2294
2295 #[test]
2296 fn test_match() {
2297 ensure_initialized();
2298 let db = LoginDb::open_in_memory();
2299 let login = make_saved_login(&db, "user", "pass");
2300 assert_eq!(
2301 Some(login),
2302 db.find_login_to_update(make_entry("user", "pass")).unwrap(),
2303 );
2304 }
2305
2306 #[test]
2307 fn test_non_matches() {
2308 ensure_initialized();
2309 let db = LoginDb::open_in_memory();
2310 make_saved_login(&db, "other-user", "pass");
2312 db.add(LoginEntry {
2314 origin: "https://www.example.com".into(),
2315 http_realm: Some("the other website".into()),
2316 username: "user".into(),
2317 password: "pass".into(),
2318 ..Default::default()
2319 })
2320 .unwrap();
2321 db.add(LoginEntry {
2323 origin: "https://www.example.com".into(),
2324 form_action_origin: Some("https://www.example.com/".into()),
2325 username: "user".into(),
2326 password: "pass".into(),
2327 ..Default::default()
2328 })
2329 .unwrap();
2330 assert_eq!(
2331 None,
2332 db.find_login_to_update(make_entry("user", "pass")).unwrap(),
2333 );
2334 }
2335
2336 #[test]
2337 fn test_match_blank_password() {
2338 ensure_initialized();
2339 let db = LoginDb::open_in_memory();
2340 let login = make_saved_login(&db, "", "pass");
2341 assert_eq!(
2342 Some(login),
2343 db.find_login_to_update(make_entry("user", "pass")).unwrap(),
2344 );
2345 }
2346
2347 #[test]
2348 fn test_username_match_takes_precedence_over_blank_username() {
2349 ensure_initialized();
2350 let db = LoginDb::open_in_memory();
2351 make_saved_login(&db, "", "pass");
2352 let username_match = make_saved_login(&db, "user", "pass");
2353 assert_eq!(
2354 Some(username_match),
2355 db.find_login_to_update(make_entry("user", "pass")).unwrap(),
2356 );
2357 }
2358
2359 #[test]
2360 fn test_invalid_login() {
2361 ensure_initialized();
2362 let db = LoginDb::open_in_memory();
2363 assert!(db
2364 .find_login_to_update(LoginEntry {
2365 http_realm: None,
2366 form_action_origin: None,
2367 ..LoginEntry::default()
2368 })
2369 .is_err());
2370 }
2371
2372 #[test]
2373 fn test_update_with_duplicate_login() {
2374 ensure_initialized();
2375 let db = LoginDb::open_in_memory();
2378 let login = make_saved_login(&db, "user", "pass");
2379 let mut dupe = login.clone().encrypt(&*TEST_ENCDEC).unwrap();
2380 dupe.meta.id = "different-guid".to_string();
2381 db.insert_new_login(&dupe).unwrap();
2382
2383 let mut entry = login.entry();
2384 entry.password = "pass2".to_string();
2385 db.update(&login.id, entry).unwrap();
2386
2387 let mut entry = login.entry();
2388 entry.password = "pass3".to_string();
2389 db.add_or_update(entry).unwrap();
2390 }
2391
2392 #[test]
2393 fn test_password_reuse_detection() {
2394 ensure_initialized();
2395 let db = LoginDb::open_in_memory();
2396
2397 let login1 = db
2399 .add(LoginEntry {
2400 origin: "https://site1.com".into(),
2401 http_realm: Some("realm".into()),
2402 username: "user1".into(),
2403 password: "shared_password".into(),
2404 ..Default::default()
2405 })
2406 .unwrap();
2407
2408 let login2 = db
2409 .add(LoginEntry {
2410 origin: "https://site2.com".into(),
2411 http_realm: Some("realm".into()),
2412 username: "user2".into(),
2413 password: "shared_password".into(),
2414 ..Default::default()
2415 })
2416 .unwrap();
2417
2418 assert!(!db
2420 .is_potentially_vulnerable_password(&login1.meta.id)
2421 .unwrap());
2422 assert!(!db
2423 .is_potentially_vulnerable_password(&login2.meta.id)
2424 .unwrap());
2425 let vulnerable = db
2427 .are_potentially_vulnerable_passwords(&[&login1.meta.id, &login2.meta.id])
2428 .unwrap();
2429 assert_eq!(vulnerable.len(), 0);
2430
2431 db.record_potentially_vulnerable_passwords(vec!["shared_password".into()])
2433 .unwrap();
2434
2435 assert!(db
2437 .is_potentially_vulnerable_password(&login2.meta.id)
2438 .unwrap());
2439 let vulnerable = db
2441 .are_potentially_vulnerable_passwords(&[&login1.meta.id, &login2.meta.id])
2442 .unwrap();
2443 assert_eq!(vulnerable.len(), 2);
2444 assert!(vulnerable.contains(&login1.meta.id));
2445 assert!(vulnerable.contains(&login2.meta.id));
2446
2447 db.update(
2449 &login2.meta.id,
2450 LoginEntry {
2451 origin: "https://site2.com".into(),
2452 http_realm: Some("realm".into()),
2453 username: "user2".into(),
2454 password: "different_password".into(),
2455 ..Default::default()
2456 },
2457 )
2458 .unwrap();
2459
2460 assert!(!db
2461 .is_potentially_vulnerable_password(&login2.meta.id)
2462 .unwrap());
2463 }
2464
2465 #[test]
2466 fn test_reset_all_breaches_clears_breach_table() {
2467 ensure_initialized();
2468 let db = LoginDb::open_in_memory();
2469
2470 let login = db
2471 .add(LoginEntry {
2472 origin: "https://example.com".into(),
2473 http_realm: Some("realm".into()),
2474 username: "user".into(),
2475 password: "password123".into(),
2476 ..Default::default()
2477 })
2478 .unwrap();
2479
2480 db.record_potentially_vulnerable_passwords(vec!["password123".into()])
2481 .unwrap();
2482
2483 let count: i64 = db
2485 .db
2486 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
2487 .unwrap();
2488 assert_eq!(count, 1);
2489 let vulnerable = db
2491 .are_potentially_vulnerable_passwords(&[&login.meta.id])
2492 .unwrap();
2493 assert_eq!(vulnerable.len(), 1);
2494 assert_eq!(vulnerable[0], login.meta.id);
2495
2496 db.reset_all_breaches().unwrap();
2498
2499 let count: i64 = db
2501 .db
2502 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
2503 .unwrap();
2504 assert_eq!(count, 0);
2505 let vulnerable = db
2507 .are_potentially_vulnerable_passwords(&[&login.meta.id])
2508 .unwrap();
2509 assert_eq!(vulnerable.len(), 0);
2510 }
2511
2512 #[test]
2513 fn test_different_passwords_not_vulnerable() {
2514 ensure_initialized();
2515 let db = LoginDb::open_in_memory();
2516
2517 let login1 = db
2518 .add(LoginEntry {
2519 origin: "https://site1.com".into(),
2520 http_realm: Some("realm".into()),
2521 username: "user".into(),
2522 password: "password_A".into(),
2523 ..Default::default()
2524 })
2525 .unwrap();
2526
2527 let login2 = db
2528 .add(LoginEntry {
2529 origin: "https://site2.com".into(),
2530 http_realm: Some("realm".into()),
2531 username: "user".into(),
2532 password: "password_B".into(),
2533 ..Default::default()
2534 })
2535 .unwrap();
2536
2537 db.record_potentially_vulnerable_passwords(vec!["password_A".into()])
2538 .unwrap();
2539
2540 assert!(!db
2542 .is_potentially_vulnerable_password(&login2.meta.id)
2543 .unwrap());
2544 let vulnerable = db
2547 .are_potentially_vulnerable_passwords(&[&login1.meta.id, &login2.meta.id])
2548 .unwrap();
2549 assert_eq!(vulnerable.len(), 1);
2550 assert!(vulnerable.contains(&login1.meta.id));
2551 }
2552 }
2553}