1use crate::error::*;
26use crate::login::*;
27use crate::schema;
28use crate::sync::SyncStatus;
29use crate::util;
30use db_crypto::EncryptorDecryptor;
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> = crate::test_utils::TEST_ENCDEC.clone();
88 Self::with_connection(Connection::open_in_memory().unwrap(), encdec).unwrap()
89 }
90
91 pub fn new_interrupt_handle(&self) -> Arc<SqlInterruptHandle> {
92 Arc::clone(&self.interrupt_handle)
93 }
94
95 #[inline]
96 pub fn begin_interrupt_scope(&self) -> Result<SqlInterruptScope> {
97 Ok(self.interrupt_handle.begin_interrupt_scope()?)
98 }
99}
100
101impl ConnExt for LoginDb {
102 #[inline]
103 fn conn(&self) -> &Connection {
104 &self.db
105 }
106}
107
108impl Deref for LoginDb {
109 type Target = Connection;
110 #[inline]
111 fn deref(&self) -> &Connection {
112 &self.db
113 }
114}
115
116impl LoginDb {
119 pub(crate) fn put_meta(&self, key: &str, value: &dyn ToSql) -> Result<()> {
120 self.execute_cached(
121 "REPLACE INTO loginsSyncMeta (key, value) VALUES (:key, :value)",
122 named_params! { ":key": key, ":value": value },
123 )?;
124 Ok(())
125 }
126
127 pub(crate) fn get_meta<T: FromSql>(&self, key: &str) -> Result<Option<T>> {
128 self.try_query_row(
129 "SELECT value FROM loginsSyncMeta WHERE key = :key",
130 named_params! { ":key": key },
131 |row| Ok::<_, Error>(row.get(0)?),
132 true,
133 )
134 }
135
136 pub(crate) fn delete_meta(&self, key: &str) -> Result<()> {
137 self.execute_cached(
138 "DELETE FROM loginsSyncMeta WHERE key = :key",
139 named_params! { ":key": key },
140 )?;
141 Ok(())
142 }
143
144 pub fn count_all(&self) -> Result<i64> {
145 let mut stmt = self.db.prepare_cached(&COUNT_ALL_SQL)?;
146
147 let count: i64 = stmt.query_row([], |row| row.get(0))?;
148 Ok(count)
149 }
150
151 pub fn count_by_origin(&self, origin: &str) -> Result<i64> {
152 match LoginEntry::validate_and_fixup_origin(origin) {
153 Ok(result) => {
154 let origin = result.unwrap_or(origin.to_string());
155 let mut stmt = self.db.prepare_cached(&COUNT_BY_ORIGIN_SQL)?;
156 let count: i64 =
157 stmt.query_row(named_params! { ":origin": origin }, |row| row.get(0))?;
158 Ok(count)
159 }
160 Err(e) => {
161 warn!("count_by_origin was passed an invalid origin: {}", e);
163 Ok(0)
164 }
165 }
166 }
167
168 pub fn count_by_form_action_origin(&self, form_action_origin: &str) -> Result<i64> {
169 match LoginEntry::validate_and_normalize_form_action_origin(form_action_origin) {
170 Ok(result) => {
171 let form_action_origin = result.unwrap_or(form_action_origin.to_string());
172 let mut stmt = self.db.prepare_cached(&COUNT_BY_FORM_ACTION_ORIGIN_SQL)?;
173 let count: i64 = stmt.query_row(
174 named_params! { ":form_action_origin": form_action_origin },
175 |row| row.get(0),
176 )?;
177 Ok(count)
178 }
179 Err(e) => {
180 warn!(
182 "count_by_form_action_origin was passed an invalid origin: {}",
183 e
184 );
185 Ok(0)
186 }
187 }
188 }
189
190 pub fn get_all(&self) -> Result<Vec<EncryptedLogin>> {
191 let mut stmt = self.db.prepare_cached(&GET_ALL_SQL)?;
192 let rows = stmt.query_and_then([], EncryptedLogin::from_row)?;
193 rows.collect::<Result<_>>()
194 }
195
196 pub fn get_many(&self, ids: &[String]) -> Result<Vec<EncryptedLogin>> {
201 let mut logins = Vec::with_capacity(ids.len());
202 sql_support::each_chunk(ids, |chunk, _| -> Result<()> {
203 logins.extend(self.db.query_rows_and_then(
204 &format!(
205 "SELECT * FROM ({}) WHERE guid IN ({})",
206 &*GET_ALL_SQL,
207 sql_support::repeat_sql_values(chunk.len())
208 ),
209 rusqlite::params_from_iter(chunk),
210 EncryptedLogin::from_row,
211 )?);
212 Ok(())
213 })?;
214 Ok(logins)
215 }
216
217 pub fn get_by_base_domain(&self, base_domain: &str) -> Result<Vec<EncryptedLogin>> {
218 let base_host = match Host::parse(base_domain) {
220 Ok(d) => d,
221 Err(e) => {
222 warn!("get_by_base_domain was passed an invalid domain: {}", e);
224 return Ok(vec![]);
225 }
226 };
227 let mut stmt = self.db.prepare_cached(&GET_ALL_SQL)?;
234 let rows = stmt
235 .query_and_then([], EncryptedLogin::from_row)?
236 .filter(|r| {
237 let login = r
238 .as_ref()
239 .ok()
240 .and_then(|login| Url::parse(&login.fields.origin).ok());
241 let this_host = login.as_ref().and_then(|url| url.host());
242 match (&base_host, this_host) {
243 (Host::Domain(base), Some(Host::Domain(look))) => {
244 let mut rev_input = base.chars().rev();
248 let mut rev_host = look.chars().rev();
249 loop {
250 match (rev_input.next(), rev_host.next()) {
251 (Some(ref a), Some(ref b)) if a == b => continue,
252 (None, None) => return true, (None, Some(ref h)) => return *h == '.',
254 _ => return false,
255 }
256 }
257 }
258 (Host::Ipv4(base), Some(Host::Ipv4(look))) => *base == look,
260 (Host::Ipv6(base), Some(Host::Ipv6(look))) => *base == look,
261 _ => false,
263 }
264 });
265 rows.collect::<Result<_>>()
266 }
267
268 pub fn get_by_id(&self, id: &str) -> Result<Option<EncryptedLogin>> {
269 self.try_query_row(
270 &GET_BY_GUID_SQL,
271 &[(":guid", &id as &dyn ToSql)],
272 EncryptedLogin::from_row,
273 true,
274 )
275 }
276
277 pub fn find_login_to_update(&self, look: LoginEntry) -> Result<Option<Login>> {
289 let look = look.fixup()?;
290 let logins = self
291 .get_by_entry_target(&look)?
292 .into_iter()
293 .map(|enc_login| enc_login.decrypt(self.encdec.as_ref()))
294 .collect::<Result<Vec<Login>>>()?;
295 Ok(logins
296 .iter()
298 .find(|login| login.username == look.username)
299 .or_else(|| logins.iter().find(|login| login.username.is_empty()))
301 .cloned())
303 }
304
305 pub fn touch(&self, id: &str) -> Result<()> {
306 let tx = self.unchecked_transaction()?;
307 self.ensure_local_overlay_exists(id)?;
308 self.mark_mirror_overridden(id)?;
309 let now_ms = util::system_time_ms_i64(SystemTime::now());
310 self.execute_cached(
313 "UPDATE loginsL
314 SET timeLastUsed = :now_millis,
315 timesUsed = timesUsed + 1,
316 local_modified = :now_millis
317 WHERE guid = :guid
318 AND is_deleted = 0",
319 named_params! {
320 ":now_millis": now_ms,
321 ":guid": id,
322 },
323 )?;
324 tx.commit()?;
325 Ok(())
326 }
327
328 pub fn record_potentially_vulnerable_passwords(&self, passwords: Vec<String>) -> Result<()> {
333 let tx = self.unchecked_transaction()?;
334 self.insert_potentially_vulnerable_passwords(passwords)?;
335 tx.commit()?;
336 Ok(())
337 }
338
339 fn insert_potentially_vulnerable_passwords(&self, passwords: Vec<String>) -> Result<()> {
340 let encrypted_existing_potentially_vulnerable_passwords: Vec<String> = self
341 .db
342 .query_rows_and_then_cached("SELECT encryptedPassword FROM breachesL", [], |row| {
343 row.get(0)
344 })?;
345 let existing_potentially_vulnerable_passwords: Result<Vec<String>> =
346 encrypted_existing_potentially_vulnerable_passwords
347 .iter()
348 .map(|ciphertext| {
349 let decrypted_bytes = self
350 .encdec
351 .decrypt(ciphertext.as_bytes().into())
352 .map_err(|e| {
353 Error::DecryptionFailed(format!(
354 "Failed to decrypt password from breachesL: {}",
355 e
356 ))
357 })?;
358
359 let password = std::str::from_utf8(&decrypted_bytes).map_err(|e| {
360 Error::DecryptionFailed(format!(
361 "Decrypted password from breachesL is not valid UTF-8: {}",
362 e
363 ))
364 })?;
365
366 Ok(password.into())
367 })
368 .collect();
369
370 let existing: std::collections::HashSet<String> =
371 existing_potentially_vulnerable_passwords?
372 .into_iter()
373 .collect();
374 let difference: Vec<_> = passwords
375 .iter()
376 .filter(|item| !existing.contains(item.as_str()))
377 .collect();
378
379 for password in difference {
380 let encrypted_password_bytes = self
381 .encdec
382 .encrypt(password.as_bytes().into())
383 .map_err(|e| Error::EncryptionFailed(format!("{e} (encrypting password)")))?;
384 let encrypted_password =
385 std::str::from_utf8(&encrypted_password_bytes).map_err(|e| {
386 Error::EncryptionFailed(format!("{e} (encrypting password: data not utf8)"))
387 })?;
388
389 self.execute_cached(
390 "INSERT INTO breachesL (encryptedPassword) VALUES (:encrypted_password)",
391 named_params! {
392 ":encrypted_password": encrypted_password,
393 },
394 )?;
395 }
396
397 Ok(())
398 }
399
400 pub fn are_potentially_vulnerable_passwords(&self, guids: &[&str]) -> Result<Vec<String>> {
410 if guids.is_empty() {
411 return Ok(Vec::new());
412 }
413
414 let all_encrypted_passwords: Vec<String> = self.db.query_rows_and_then_cached(
416 "SELECT encryptedPassword FROM breachesL",
417 [],
418 |row| row.get(0),
419 )?;
420
421 let mut breached_passwords = std::collections::HashSet::new();
422 for ciphertext in &all_encrypted_passwords {
423 let decrypted_bytes =
424 self.encdec
425 .decrypt(ciphertext.as_bytes().into())
426 .map_err(|e| {
427 Error::DecryptionFailed(format!(
428 "Failed to decrypt password from breachesL: {}",
429 e
430 ))
431 })?;
432
433 let decrypted_password = std::str::from_utf8(&decrypted_bytes).map_err(|e| {
434 Error::DecryptionFailed(format!(
435 "Decrypted password from breachesL is not valid UTF-8: {}",
436 e
437 ))
438 })?;
439
440 breached_passwords.insert(decrypted_password.to_string());
441 }
442
443 let mut vulnerable_guids = Vec::new();
445 for guid in guids {
446 if let Some(login) = self.get_by_id(guid)? {
447 let decrypted_login = login.decrypt(self.encdec.as_ref())?;
448 if breached_passwords.contains(&decrypted_login.password) {
449 vulnerable_guids.push(guid.to_string());
450 }
451 }
452 }
453
454 Ok(vulnerable_guids)
455 }
456
457 pub fn is_potentially_vulnerable_password(&self, guid: &str) -> Result<bool> {
458 let vulnerable = self.are_potentially_vulnerable_passwords(&[guid])?;
460 Ok(!vulnerable.is_empty())
461 }
462
463 pub fn reset_all_breaches(&self) -> Result<()> {
464 let tx = self.unchecked_transaction()?;
465 self.execute_cached("DELETE FROM breachesL", [])?;
466 tx.commit()?;
467 Ok(())
468 }
469
470 pub fn record_breach_alert_dismissal(&self, id: &str) -> Result<()> {
475 let timestamp = util::system_time_ms_i64(SystemTime::now());
476 self.record_breach_alert_dismissal_time(id, timestamp)
477 }
478
479 pub fn record_breach_alert_dismissal_time(&self, id: &str, timestamp: i64) -> Result<()> {
485 let tx = self.unchecked_transaction()?;
486 self.ensure_local_overlay_exists(id)?;
487 self.mark_mirror_overridden(id)?;
488 self.execute_cached(
489 "UPDATE loginsL
490 SET timeLastBreachAlertDismissed = :now_millis
491 WHERE guid = :guid",
492 named_params! {
493 ":now_millis": timestamp,
494 ":guid": id,
495 },
496 )?;
497 tx.commit()?;
498 Ok(())
499 }
500
501 fn insert_new_login(&self, login: &EncryptedLogin) -> Result<()> {
504 let sql = format!(
505 "INSERT OR REPLACE INTO loginsL (
506 origin,
507 httpRealm,
508 formActionOrigin,
509 usernameField,
510 passwordField,
511 timesUsed,
512 secFields,
513 guid,
514 timeCreated,
515 timeLastUsed,
516 timePasswordChanged,
517 timeLastBreachAlertDismissed,
518 local_modified,
519 is_deleted,
520 sync_status
521 ) VALUES (
522 :origin,
523 :http_realm,
524 :form_action_origin,
525 :username_field,
526 :password_field,
527 :times_used,
528 :sec_fields,
529 :guid,
530 :time_created,
531 :time_last_used,
532 :time_password_changed,
533 :time_last_breach_alert_dismissed,
534 :local_modified,
535 0, -- is_deleted
536 {new} -- sync_status
537 )",
538 new = SyncStatus::New as u8
539 );
540
541 self.execute(
542 &sql,
543 named_params! {
544 ":origin": login.fields.origin,
545 ":http_realm": login.fields.http_realm,
546 ":form_action_origin": login.fields.form_action_origin,
547 ":username_field": login.fields.username_field,
548 ":password_field": login.fields.password_field,
549 ":time_created": login.meta.time_created,
550 ":times_used": login.meta.times_used,
551 ":time_last_used": login.meta.time_last_used,
552 ":time_password_changed": login.meta.time_password_changed,
553 ":local_modified": login.meta.time_created,
554 ":time_last_breach_alert_dismissed": login.meta.time_last_breach_alert_dismissed,
555 ":sec_fields": login.sec_fields,
556 ":guid": login.guid(),
557 },
558 )?;
559 Ok(())
560 }
561
562 fn update_existing_login(&self, login: &EncryptedLogin) -> Result<()> {
563 let now_ms = util::system_time_ms_i64(SystemTime::now());
565 let sql = format!(
566 "UPDATE loginsL
567 SET local_modified = :now_millis,
568 timeLastUsed = :time_last_used,
569 timePasswordChanged = :time_password_changed,
570 httpRealm = :http_realm,
571 formActionOrigin = :form_action_origin,
572 usernameField = :username_field,
573 passwordField = :password_field,
574 timesUsed = :times_used,
575 secFields = :sec_fields,
576 origin = :origin,
577 -- leave New records as they are, otherwise update them to `changed`
578 sync_status = max(sync_status, {changed})
579 WHERE guid = :guid",
580 changed = SyncStatus::Changed as u8
581 );
582
583 self.db.execute(
584 &sql,
585 named_params! {
586 ":origin": login.fields.origin,
587 ":http_realm": login.fields.http_realm,
588 ":form_action_origin": login.fields.form_action_origin,
589 ":username_field": login.fields.username_field,
590 ":password_field": login.fields.password_field,
591 ":time_last_used": login.meta.time_last_used,
592 ":times_used": login.meta.times_used,
593 ":time_password_changed": login.meta.time_password_changed,
594 ":sec_fields": login.sec_fields,
595 ":guid": &login.meta.id,
596 ":now_millis": now_ms,
597 },
598 )?;
599 Ok(())
600 }
601
602 pub fn add_many(&self, entries: Vec<LoginEntry>) -> Result<Vec<Result<EncryptedLogin>>> {
604 let now_ms = util::system_time_ms_i64(SystemTime::now());
605
606 let entries_with_meta = entries
607 .into_iter()
608 .map(|entry| {
609 let guid = Guid::random();
610 LoginEntryWithMeta {
611 entry,
612 meta: LoginMeta {
613 id: guid.to_string(),
614 time_created: now_ms,
615 time_password_changed: now_ms,
616 time_last_used: now_ms,
617 times_used: 1,
618 time_last_breach_alert_dismissed: None,
619 },
620 }
621 })
622 .collect();
623
624 self.add_many_with_meta(entries_with_meta)
625 }
626
627 pub fn add_many_with_meta(
632 &self,
633 entries_with_meta: Vec<LoginEntryWithMeta>,
634 ) -> Result<Vec<Result<EncryptedLogin>>> {
635 let tx = self.unchecked_transaction()?;
636 let mut results = vec![];
637 for mut entry_with_meta in entries_with_meta {
638 let guid = match Self::validate_or_fixup_guid(Guid::from_string(
639 entry_with_meta.meta.id.clone(),
640 )) {
641 Ok(guid) => guid,
642 Err(err) => {
643 results.push(Err(err));
644 continue;
645 }
646 };
647 entry_with_meta.meta.id = guid.to_string();
650 entry_with_meta.meta = entry_with_meta.meta.sanitize_timestamps();
653 match self.fixup_and_check_for_dupes(&guid, entry_with_meta.entry) {
654 Ok(new_entry) => {
655 let sec_fields = SecureLoginFields {
656 username: new_entry.username,
657 password: new_entry.password,
658 }
659 .encrypt(self.encdec.as_ref(), &entry_with_meta.meta.id)?;
660 let encrypted_login = EncryptedLogin {
661 meta: entry_with_meta.meta,
662 fields: LoginFields {
663 origin: new_entry.origin,
664 form_action_origin: new_entry.form_action_origin,
665 http_realm: new_entry.http_realm,
666 username_field: new_entry.username_field,
667 password_field: new_entry.password_field,
668 },
669 sec_fields,
670 };
671 let result = self
672 .insert_new_login(&encrypted_login)
673 .map(|_| encrypted_login);
674 results.push(result);
675 }
676
677 Err(error) => results.push(Err(error)),
678 }
679 }
680
681 tx.commit()?;
682
683 Ok(results)
684 }
685
686 fn validate_or_fixup_guid(guid: Guid) -> Result<Guid> {
696 if guid.is_valid_for_sync_server() {
697 return Ok(guid);
698 }
699 #[cfg(feature = "fixup_invalid_guids")]
700 {
701 warn!("regenerating a login guid that is invalid for the sync server");
702 Ok(Guid::random())
703 }
704 #[cfg(not(feature = "fixup_invalid_guids"))]
705 {
706 Err(InvalidLogin::IllegalFieldValue {
707 field_info: "guid is not valid for the sync server".into(),
708 }
709 .into())
710 }
711 }
712
713 pub fn add(&self, entry: LoginEntry) -> Result<EncryptedLogin> {
714 let guid = Guid::random();
715 let now_ms = util::system_time_ms_i64(SystemTime::now());
716
717 let entry_with_meta = LoginEntryWithMeta {
718 entry,
719 meta: LoginMeta {
720 id: guid.to_string(),
721 time_created: now_ms,
722 time_password_changed: now_ms,
723 time_last_used: now_ms,
724 times_used: 1,
725 time_last_breach_alert_dismissed: None,
726 },
727 };
728
729 self.add_with_meta(entry_with_meta)
730 }
731
732 pub fn add_with_meta(&self, entry_with_meta: LoginEntryWithMeta) -> Result<EncryptedLogin> {
736 let mut results = self.add_many_with_meta(vec![entry_with_meta])?;
737 results.pop().expect("there should be a single result")
738 }
739
740 pub fn update(&self, sguid: &str, entry: LoginEntry) -> Result<EncryptedLogin> {
741 let guid = Guid::new(sguid);
742 let now_ms = util::system_time_ms_i64(SystemTime::now());
743 let tx = self.unchecked_transaction()?;
744
745 let entry = entry.fixup()?;
746
747 if self.check_for_dupes(&guid, &entry).is_err() {
753 let has_mirror_row: bool = self
755 .db
756 .conn_ext_query_one("SELECT EXISTS (SELECT 1 FROM loginsM)")?;
757 let has_http_realm = entry.http_realm.is_some();
758 let has_form_action_origin = entry.form_action_origin.is_some();
759 report_error!(
760 "logins-duplicate-in-update",
761 "(mirror: {has_mirror_row}, realm: {has_http_realm}, form_origin: {has_form_action_origin})");
762 }
763
764 self.ensure_local_overlay_exists(&guid)?;
766 self.mark_mirror_overridden(&guid)?;
767
768 let existing = match self.get_by_id(sguid)? {
770 Some(e) => e.decrypt(self.encdec.as_ref())?,
771 None => return Err(Error::NoSuchRecord(sguid.to_owned())),
772 };
773 let time_password_changed = if existing.password == entry.password {
774 existing.time_password_changed
775 } else {
776 now_ms
777 };
778
779 let sec_fields = SecureLoginFields {
781 username: entry.username,
782 password: entry.password,
783 }
784 .encrypt(self.encdec.as_ref(), &existing.id)?;
785 let result = EncryptedLogin {
786 meta: LoginMeta {
787 id: existing.id,
788 time_created: existing.time_created,
789 time_password_changed,
790 time_last_used: existing.time_last_used,
792 times_used: existing.times_used,
793 time_last_breach_alert_dismissed: None,
794 },
795 fields: LoginFields {
796 origin: entry.origin,
797 form_action_origin: entry.form_action_origin,
798 http_realm: entry.http_realm,
799 username_field: entry.username_field,
800 password_field: entry.password_field,
801 },
802 sec_fields,
803 };
804
805 self.update_existing_login(&result)?;
806 tx.commit()?;
807 Ok(result)
808 }
809
810 pub fn add_or_update(&self, entry: LoginEntry) -> Result<EncryptedLogin> {
811 let entry = entry.fixup()?;
813 match self.find_login_to_update(entry.clone())? {
814 Some(login) => self.update(&login.id, entry),
815 None => self.add(entry),
816 }
817 }
818
819 pub fn fixup_and_check_for_dupes(&self, guid: &Guid, entry: LoginEntry) -> Result<LoginEntry> {
820 let entry = entry.fixup()?;
821 self.check_for_dupes(guid, &entry)?;
822 Ok(entry)
823 }
824
825 pub fn check_for_dupes(&self, guid: &Guid, entry: &LoginEntry) -> Result<()> {
826 if self.dupe_exists(guid, entry)? {
827 return Err(InvalidLogin::DuplicateLogin.into());
828 }
829 Ok(())
830 }
831
832 pub fn dupe_exists(&self, guid: &Guid, entry: &LoginEntry) -> Result<bool> {
833 Ok(self.find_dupe(guid, entry)?.is_some())
834 }
835
836 pub fn find_dupe(&self, guid: &Guid, entry: &LoginEntry) -> Result<Option<Guid>> {
837 for possible in self.get_by_entry_target(entry)? {
838 if possible.guid() != *guid {
839 let pos_sec_fields = possible.decrypt_fields(self.encdec.as_ref())?;
840 if pos_sec_fields.username == entry.username {
841 return Ok(Some(possible.guid()));
842 }
843 }
844 }
845 Ok(None)
846 }
847
848 fn get_by_entry_target(&self, entry: &LoginEntry) -> Result<Vec<EncryptedLogin>> {
858 lazy_static::lazy_static! {
860 static ref GET_BY_FORM_ACTION_ORIGIN: String = format!(
861 "SELECT {common_cols} FROM loginsL
862 WHERE is_deleted = 0
863 AND origin = :origin
864 AND formActionOrigin = :form_action_origin
865
866 UNION ALL
867
868 SELECT {common_cols} FROM loginsM
869 WHERE is_overridden = 0
870 AND origin = :origin
871 AND formActionOrigin = :form_action_origin
872 ",
873 common_cols = schema::COMMON_COLS
874 );
875 static ref GET_BY_HTTP_REALM: String = format!(
876 "SELECT {common_cols} FROM loginsL
877 WHERE is_deleted = 0
878 AND origin = :origin
879 AND httpRealm = :http_realm
880
881 UNION ALL
882
883 SELECT {common_cols} FROM loginsM
884 WHERE is_overridden = 0
885 AND origin = :origin
886 AND httpRealm = :http_realm
887 ",
888 common_cols = schema::COMMON_COLS
889 );
890 }
891 match (entry.form_action_origin.as_ref(), entry.http_realm.as_ref()) {
892 (Some(form_action_origin), None) => {
893 let params = named_params! {
894 ":origin": &entry.origin,
895 ":form_action_origin": form_action_origin,
896 };
897 self.db
898 .prepare_cached(&GET_BY_FORM_ACTION_ORIGIN)?
899 .query_and_then(params, EncryptedLogin::from_row)?
900 .collect()
901 }
902 (None, Some(http_realm)) => {
903 let params = named_params! {
904 ":origin": &entry.origin,
905 ":http_realm": http_realm,
906 };
907 self.db
908 .prepare_cached(&GET_BY_HTTP_REALM)?
909 .query_and_then(params, EncryptedLogin::from_row)?
910 .collect()
911 }
912 (Some(_), Some(_)) => Err(InvalidLogin::BothTargets.into()),
913 (None, None) => Err(InvalidLogin::NoTarget.into()),
914 }
915 }
916
917 pub fn exists(&self, id: &str) -> Result<bool> {
918 Ok(self.db.query_row(
919 "SELECT EXISTS(
920 SELECT 1 FROM loginsL
921 WHERE guid = :guid AND is_deleted = 0
922 UNION ALL
923 SELECT 1 FROM loginsM
924 WHERE guid = :guid AND is_overridden IS NOT 1
925 )",
926 named_params! { ":guid": id },
927 |row| row.get(0),
928 )?)
929 }
930
931 pub fn delete(&self, id: &str) -> Result<bool> {
934 let mut results = self.delete_many(vec![id])?;
935 Ok(results.pop().expect("there should be a single result"))
936 }
937
938 pub fn delete_all(&self) -> Result<Vec<String>> {
940 let ids: Vec<String> = self.db.query_rows_and_then_cached(
941 "SELECT guid FROM loginsL WHERE is_deleted = 0
942 UNION ALL
943 SELECT guid FROM loginsM WHERE is_overridden = 0",
944 [],
945 |row| row.get(0),
946 )?;
947 self.delete_many(ids.iter().map(String::as_str).collect())?;
948 Ok(ids)
949 }
950
951 pub fn delete_all_except_fxa(&self) -> Result<Vec<String>> {
954 let ids: Vec<String> = self.db.query_rows_and_then_cached(
955 "SELECT guid FROM loginsL WHERE is_deleted = 0 AND origin != :fxa_origin
956 UNION ALL
957 SELECT guid FROM loginsM WHERE is_overridden = 0 AND origin != :fxa_origin",
958 named_params! { ":fxa_origin": FXA_CREDENTIALS_ORIGIN },
959 |row| row.get(0),
960 )?;
961 self.delete_many(ids.iter().map(String::as_str).collect())?;
962 Ok(ids)
963 }
964
965 pub fn delete_many(&self, ids: Vec<&str>) -> Result<Vec<bool>> {
968 let tx = self.unchecked_transaction_imm()?;
969 let sql = format!(
970 "
971 UPDATE loginsL
972 SET local_modified = :now_ms,
973 sync_status = {status_changed},
974 is_deleted = 1,
975 secFields = '',
976 origin = '',
977 httpRealm = NULL,
978 formActionOrigin = NULL
979 WHERE guid = :guid AND is_deleted IS FALSE
980 ",
981 status_changed = SyncStatus::Changed as u8
982 );
983 let mut stmt = self.db.prepare_cached(&sql)?;
984
985 let mut result = vec![];
986
987 for id in ids {
988 let now_ms = util::system_time_ms_i64(SystemTime::now());
989
990 let update_result = stmt.execute(named_params! { ":now_ms": now_ms, ":guid": id })?;
992
993 let exists = update_result == 1;
994
995 self.execute(
997 "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
998 named_params! { ":guid": id },
999 )?;
1000
1001 self.execute(&format!("
1004 INSERT OR IGNORE INTO loginsL
1005 (guid, local_modified, is_deleted, sync_status, origin, timeCreated, timePasswordChanged, secFields)
1006 SELECT guid, :now_ms, 1, {changed}, '', timeCreated, :now_ms, ''
1007 FROM loginsM
1008 WHERE guid = :guid",
1009 changed = SyncStatus::Changed as u8),
1010 named_params! { ":now_ms": now_ms, ":guid": id })?;
1011
1012 result.push(exists);
1013 }
1014
1015 tx.commit()?;
1016
1017 Ok(result)
1018 }
1019
1020 pub fn delete_undecryptable_records_for_remote_replacement(
1021 &self,
1022 ) -> Result<LoginsDeletionMetrics> {
1023 let corrupted_logins = self
1025 .get_all()?
1026 .into_iter()
1027 .filter(|login| login.clone().decrypt(self.encdec.as_ref()).is_err())
1028 .collect::<Vec<_>>();
1029 let ids = corrupted_logins
1030 .iter()
1031 .map(|login| login.guid_str())
1032 .collect::<Vec<_>>();
1033
1034 self.delete_local_records_for_remote_replacement(ids)
1035 }
1036
1037 pub fn delete_local_records_for_remote_replacement(
1038 &self,
1039 ids: Vec<&str>,
1040 ) -> Result<LoginsDeletionMetrics> {
1041 let tx = self.unchecked_transaction_imm()?;
1042 let mut local_deleted = 0;
1043 let mut mirror_deleted = 0;
1044
1045 sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
1046 let deleted = self.execute(
1047 &format!(
1048 "DELETE FROM loginsL WHERE guid IN ({})",
1049 sql_support::repeat_sql_values(chunk.len())
1050 ),
1051 rusqlite::params_from_iter(chunk),
1052 )?;
1053 local_deleted += deleted;
1054 Ok(())
1055 })?;
1056
1057 sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
1058 let deleted = self.execute(
1059 &format!(
1060 "DELETE FROM loginsM WHERE guid IN ({})",
1061 sql_support::repeat_sql_values(chunk.len())
1062 ),
1063 rusqlite::params_from_iter(chunk),
1064 )?;
1065 mirror_deleted += deleted;
1066 Ok(())
1067 })?;
1068
1069 tx.commit()?;
1070 Ok(LoginsDeletionMetrics {
1071 local_deleted: local_deleted as u64,
1072 mirror_deleted: mirror_deleted as u64,
1073 })
1074 }
1075
1076 fn mark_mirror_overridden(&self, guid: &str) -> Result<()> {
1077 self.execute_cached(
1078 "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
1079 named_params! { ":guid": guid },
1080 )?;
1081 Ok(())
1082 }
1083
1084 fn ensure_local_overlay_exists(&self, guid: &str) -> Result<()> {
1085 let already_have_local: bool = self.db.query_row(
1086 "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid)",
1087 named_params! { ":guid": guid },
1088 |row| row.get(0),
1089 )?;
1090
1091 if already_have_local {
1092 return Ok(());
1093 }
1094
1095 debug!("No overlay; cloning one for {:?}.", guid);
1096 let changed = self.clone_mirror_to_overlay(guid)?;
1097 if changed == 0 {
1098 report_error!(
1099 "logins-local-overlay-error",
1100 "Failed to create local overlay for GUID {guid:?}."
1101 );
1102 return Err(Error::NoSuchRecord(guid.to_owned()));
1103 }
1104 Ok(())
1105 }
1106
1107 fn clone_mirror_to_overlay(&self, guid: &str) -> Result<usize> {
1108 Ok(self.execute_cached(&CLONE_SINGLE_MIRROR_SQL, &[(":guid", &guid as &dyn ToSql)])?)
1109 }
1110
1111 pub fn wipe_local(&self) -> Result<usize> {
1113 info!("Executing wipe_local on password engine!");
1114 let tx = self.unchecked_transaction()?;
1115 let mut row_count = 0;
1116 row_count += self.execute("DELETE FROM loginsL", [])?;
1117 row_count += self.execute("DELETE FROM loginsM", [])?;
1118 row_count += self.execute("DELETE FROM loginsSyncMeta", [])?;
1119 row_count += self.execute("DELETE FROM breachesL", [])?;
1120 tx.commit()?;
1121 Ok(row_count)
1122 }
1123
1124 pub fn wipe_local_except_fxa(&self) -> Result<usize> {
1126 info!("Executing wipe_local_except_fxa on password engine!");
1127 let tx = self.unchecked_transaction()?;
1128 let mut row_count = 0;
1129 row_count += self.execute(
1130 "DELETE FROM loginsL WHERE origin != :fxa_origin",
1131 named_params! { ":fxa_origin": FXA_CREDENTIALS_ORIGIN },
1132 )?;
1133 row_count += self.execute(
1134 "DELETE FROM loginsM WHERE origin != :fxa_origin",
1135 named_params! { ":fxa_origin": FXA_CREDENTIALS_ORIGIN },
1136 )?;
1137 row_count += self.execute("DELETE FROM loginsSyncMeta", [])?;
1138 row_count += self.execute("DELETE FROM breachesL", [])?;
1139 tx.commit()?;
1140 Ok(row_count)
1141 }
1142
1143 pub fn shutdown(self) -> Result<()> {
1144 self.db.close().map_err(|(_, e)| Error::SqlError(e))
1145 }
1146}
1147
1148lazy_static! {
1149 static ref GET_ALL_SQL: String = format!(
1150 "SELECT {common_cols} FROM loginsL WHERE is_deleted = 0
1151 UNION ALL
1152 SELECT {common_cols} FROM loginsM WHERE is_overridden = 0",
1153 common_cols = schema::COMMON_COLS,
1154 );
1155 static ref COUNT_ALL_SQL: String = format!(
1156 "SELECT COUNT(*) FROM (
1157 SELECT guid FROM loginsL WHERE is_deleted = 0
1158 UNION ALL
1159 SELECT guid FROM loginsM WHERE is_overridden = 0
1160 )"
1161 );
1162 static ref COUNT_BY_ORIGIN_SQL: String = format!(
1163 "SELECT COUNT(*) FROM (
1164 SELECT guid FROM loginsL WHERE is_deleted = 0 AND origin = :origin
1165 UNION ALL
1166 SELECT guid FROM loginsM WHERE is_overridden = 0 AND origin = :origin
1167 )"
1168 );
1169 static ref COUNT_BY_FORM_ACTION_ORIGIN_SQL: String = format!(
1170 "SELECT COUNT(*) FROM (
1171 SELECT guid FROM loginsL WHERE is_deleted = 0 AND formActionOrigin = :form_action_origin
1172 UNION ALL
1173 SELECT guid FROM loginsM WHERE is_overridden = 0 AND formActionOrigin = :form_action_origin
1174 )"
1175 );
1176 static ref GET_BY_GUID_SQL: String = format!(
1177 "SELECT {common_cols}
1178 FROM loginsL
1179 WHERE is_deleted = 0
1180 AND guid = :guid
1181
1182 UNION ALL
1183
1184 SELECT {common_cols}
1185 FROM loginsM
1186 WHERE is_overridden IS NOT 1
1187 AND guid = :guid
1188 ORDER BY origin ASC
1189
1190 LIMIT 1",
1191 common_cols = schema::COMMON_COLS,
1192 );
1193 pub static ref CLONE_ENTIRE_MIRROR_SQL: String = format!(
1194 "INSERT OR IGNORE INTO loginsL ({common_cols}, local_modified, is_deleted, sync_status)
1195 SELECT {common_cols}, NULL AS local_modified, 0 AS is_deleted, 0 AS sync_status
1196 FROM loginsM",
1197 common_cols = schema::COMMON_COLS,
1198 );
1199 static ref CLONE_SINGLE_MIRROR_SQL: String =
1200 format!("{} WHERE guid = :guid", &*CLONE_ENTIRE_MIRROR_SQL,);
1201}
1202
1203#[cfg(not(feature = "keydb"))]
1204#[cfg(test)]
1205pub mod test_utils {
1206 use super::*;
1207 use crate::login::test_utils::enc_login;
1208 use crate::test_utils::decrypt_struct;
1209 use crate::SecureLoginFields;
1210 use sync15::ServerTimestamp;
1211
1212 pub fn insert_login(
1216 db: &LoginDb,
1217 guid: &str,
1218 local_login: Option<&str>,
1219 mirror_login: Option<&str>,
1220 ) {
1221 if let Some(password) = mirror_login {
1222 add_mirror(
1223 db,
1224 &enc_login(guid, password),
1225 &ServerTimestamp(util::system_time_ms_i64(std::time::SystemTime::now())),
1226 local_login.is_some(),
1227 )
1228 .unwrap();
1229 }
1230 if let Some(password) = local_login {
1231 db.insert_new_login(&enc_login(guid, password)).unwrap();
1232 }
1233 }
1234
1235 pub fn insert_encrypted_login(
1236 db: &LoginDb,
1237 local: &EncryptedLogin,
1238 mirror: &EncryptedLogin,
1239 server_modified: &ServerTimestamp,
1240 ) {
1241 db.insert_new_login(local).unwrap();
1242 add_mirror(db, mirror, server_modified, true).unwrap();
1243 }
1244
1245 pub fn add_mirror(
1246 db: &LoginDb,
1247 login: &EncryptedLogin,
1248 server_modified: &ServerTimestamp,
1249 is_overridden: bool,
1250 ) -> Result<()> {
1251 let sql = "
1252 INSERT OR IGNORE INTO loginsM (
1253 is_overridden,
1254 server_modified,
1255
1256 httpRealm,
1257 formActionOrigin,
1258 usernameField,
1259 passwordField,
1260 secFields,
1261 origin,
1262
1263 timesUsed,
1264 timeLastUsed,
1265 timePasswordChanged,
1266 timeCreated,
1267
1268 timeLastBreachAlertDismissed,
1269
1270 guid
1271 ) VALUES (
1272 :is_overridden,
1273 :server_modified,
1274
1275 :http_realm,
1276 :form_action_origin,
1277 :username_field,
1278 :password_field,
1279 :sec_fields,
1280 :origin,
1281
1282 :times_used,
1283 :time_last_used,
1284 :time_password_changed,
1285 :time_created,
1286
1287 :time_last_breach_alert_dismissed,
1288
1289 :guid
1290 )";
1291 let mut stmt = db.prepare_cached(sql)?;
1292
1293 stmt.execute(named_params! {
1294 ":is_overridden": is_overridden,
1295 ":server_modified": server_modified.as_millis(),
1296 ":http_realm": login.fields.http_realm,
1297 ":form_action_origin": login.fields.form_action_origin,
1298 ":username_field": login.fields.username_field,
1299 ":password_field": login.fields.password_field,
1300 ":origin": login.fields.origin,
1301 ":sec_fields": login.sec_fields,
1302 ":times_used": login.meta.times_used,
1303 ":time_last_used": login.meta.time_last_used,
1304 ":time_password_changed": login.meta.time_password_changed,
1305 ":time_created": login.meta.time_created,
1306 ":time_last_breach_alert_dismissed": login.meta.time_last_breach_alert_dismissed,
1307 ":guid": login.guid_str(),
1308 })?;
1309 Ok(())
1310 }
1311
1312 pub fn get_local_guids(db: &LoginDb) -> Vec<String> {
1313 get_guids(db, "SELECT guid FROM loginsL")
1314 }
1315
1316 pub fn get_mirror_guids(db: &LoginDb) -> Vec<String> {
1317 get_guids(db, "SELECT guid FROM loginsM")
1318 }
1319
1320 fn get_guids(db: &LoginDb, sql: &str) -> Vec<String> {
1321 let mut stmt = db.prepare_cached(sql).unwrap();
1322 let mut res: Vec<String> = stmt
1323 .query_map([], |r| r.get(0))
1324 .unwrap()
1325 .map(|r| r.unwrap())
1326 .collect();
1327 res.sort();
1328 res
1329 }
1330
1331 pub fn get_server_modified(db: &LoginDb, guid: &str) -> i64 {
1332 db.conn_ext_query_one(&format!(
1333 "SELECT server_modified FROM loginsM WHERE guid='{}'",
1334 guid
1335 ))
1336 .unwrap()
1337 }
1338
1339 pub fn check_local_login(db: &LoginDb, guid: &str, password: &str, local_modified_gte: i64) {
1340 let row: (String, i64, bool) = db
1341 .query_row(
1342 "SELECT secFields, local_modified, is_deleted FROM loginsL WHERE guid=?",
1343 [guid],
1344 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1345 )
1346 .unwrap();
1347 let enc: SecureLoginFields = decrypt_struct(row.0);
1348 assert_eq!(enc.password, password);
1349 assert!(row.1 >= local_modified_gte);
1350 assert!(!row.2);
1351 }
1352
1353 pub fn check_mirror_login(
1354 db: &LoginDb,
1355 guid: &str,
1356 password: &str,
1357 server_modified: i64,
1358 is_overridden: bool,
1359 ) {
1360 let row: (String, i64, bool) = db
1361 .query_row(
1362 "SELECT secFields, server_modified, is_overridden FROM loginsM WHERE guid=?",
1363 [guid],
1364 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1365 )
1366 .unwrap();
1367 let enc: SecureLoginFields = decrypt_struct(row.0);
1368 assert_eq!(enc.password, password);
1369 assert_eq!(row.1, server_modified);
1370 assert_eq!(row.2, is_overridden);
1371 }
1372}
1373
1374#[cfg(not(feature = "keydb"))]
1375#[cfg(test)]
1376mod tests {
1377 use super::*;
1378 use crate::db::test_utils::{get_local_guids, get_mirror_guids};
1379 use crate::sync::merge::LocalLogin;
1380 use crate::test_utils::TEST_ENCDEC;
1381 use nss_as::ensure_initialized;
1382 use std::{thread, time};
1383
1384 #[test]
1385 fn test_username_dupe_semantics() {
1386 ensure_initialized();
1387 let mut login = LoginEntry {
1388 origin: "https://www.example.com".into(),
1389 http_realm: Some("https://www.example.com".into()),
1390 username: "test".into(),
1391 password: "sekret".into(),
1392 ..LoginEntry::default()
1393 };
1394
1395 let db = LoginDb::open_in_memory();
1396 db.add(login.clone())
1397 .expect("should be able to add first login");
1398
1399 let exp_err = "Invalid login: Login already exists";
1401 assert_eq!(db.add(login.clone()).unwrap_err().to_string(), exp_err);
1402
1403 login.username = "".to_string();
1405 db.add(login.clone()).expect("empty login isn't a dupe");
1406
1407 assert_eq!(db.add(login).unwrap_err().to_string(), exp_err);
1408
1409 assert_eq!(db.get_all().unwrap().len(), 2);
1411 }
1412
1413 #[test]
1414 fn test_get_many() {
1415 ensure_initialized();
1416
1417 let db = LoginDb::open_in_memory();
1418 let mut added = Vec::new();
1419 for origin in ["https://a.example.com", "https://b.example.com"] {
1420 added.push(
1421 db.add(LoginEntry {
1422 origin: origin.into(),
1423 http_realm: Some("https://www.example.com".into()),
1424 username: "test".into(),
1425 password: "sekret".into(),
1426 ..LoginEntry::default()
1427 })
1428 .expect("should be able to add login"),
1429 );
1430 }
1431 let ids = added.iter().map(|l| l.meta.id.clone()).collect::<Vec<_>>();
1432
1433 let by_origin = |logins: Vec<EncryptedLogin>| {
1435 let mut logins = logins;
1436 logins.sort_by(|l, r| l.fields.origin.cmp(&r.fields.origin));
1437 logins
1438 };
1439
1440 assert_eq!(
1442 by_origin(db.get_many(&ids).unwrap()),
1443 by_origin(db.get_all().unwrap())
1444 );
1445
1446 assert_eq!(db.get_many(&ids[1..]).unwrap(), added[1..]);
1448
1449 assert_eq!(
1451 db.get_many(&[ids[0].clone(), "no-such-guid".to_string()])
1452 .unwrap(),
1453 added[..1]
1454 );
1455 assert_eq!(db.get_many(&[]).unwrap(), Vec::new());
1456 }
1457
1458 #[test]
1459 fn test_add_many() {
1460 ensure_initialized();
1461
1462 let login_a = LoginEntry {
1463 origin: "https://a.example.com".into(),
1464 http_realm: Some("https://www.example.com".into()),
1465 username: "test".into(),
1466 password: "sekret".into(),
1467 ..LoginEntry::default()
1468 };
1469
1470 let login_b = LoginEntry {
1471 origin: "https://b.example.com".into(),
1472 http_realm: Some("https://www.example.com".into()),
1473 username: "test".into(),
1474 password: "sekret".into(),
1475 ..LoginEntry::default()
1476 };
1477
1478 let db = LoginDb::open_in_memory();
1479 let added = db
1480 .add_many(vec![login_a.clone(), login_b.clone()])
1481 .expect("should be able to add logins");
1482
1483 let [added_a, added_b] = added.as_slice() else {
1484 panic!("there should really be 2")
1485 };
1486
1487 let fetched_a = db
1488 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1489 .expect("should work")
1490 .expect("should get a record");
1491
1492 assert_eq!(fetched_a.fields.origin, login_a.origin);
1493
1494 let fetched_b = db
1495 .get_by_id(&added_b.as_ref().unwrap().meta.id)
1496 .expect("should work")
1497 .expect("should get a record");
1498
1499 assert_eq!(fetched_b.fields.origin, login_b.origin);
1500
1501 assert_eq!(db.count_all().unwrap(), 2);
1502 }
1503
1504 #[test]
1505 fn test_count_by_origin() {
1506 ensure_initialized();
1507
1508 let origin_a = "https://a.example.com";
1509 let login_a = LoginEntry {
1510 origin: origin_a.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 login_b = LoginEntry {
1518 origin: "https://b.example.com".into(),
1519 http_realm: Some("https://www.example.com".into()),
1520 username: "test".into(),
1521 password: "sekret".into(),
1522 ..LoginEntry::default()
1523 };
1524
1525 let origin_umlaut = "https://bücher.example.com";
1526 let login_umlaut = LoginEntry {
1527 origin: origin_umlaut.into(),
1528 http_realm: Some("https://www.example.com".into()),
1529 username: "test".into(),
1530 password: "sekret".into(),
1531 ..LoginEntry::default()
1532 };
1533
1534 let db = LoginDb::open_in_memory();
1535 db.add_many(vec![login_a.clone(), login_b.clone(), login_umlaut.clone()])
1536 .expect("should be able to add logins");
1537
1538 assert_eq!(db.count_by_origin(origin_a).unwrap(), 1);
1539 assert_eq!(db.count_by_origin(origin_umlaut).unwrap(), 1);
1540 }
1541
1542 #[test]
1543 fn test_count_by_form_action_origin() {
1544 ensure_initialized();
1545
1546 let origin_a = "https://a.example.com";
1547 let login_a = LoginEntry {
1548 origin: origin_a.into(),
1549 form_action_origin: Some(origin_a.into()),
1550 http_realm: Some("https://www.example.com".into()),
1551 username: "test".into(),
1552 password: "sekret".into(),
1553 ..LoginEntry::default()
1554 };
1555
1556 let login_b = LoginEntry {
1557 origin: "https://b.example.com".into(),
1558 form_action_origin: Some("https://b.example.com".into()),
1559 http_realm: Some("https://www.example.com".into()),
1560 username: "test".into(),
1561 password: "sekret".into(),
1562 ..LoginEntry::default()
1563 };
1564
1565 let origin_umlaut = "https://bücher.example.com";
1566 let login_umlaut = LoginEntry {
1567 origin: origin_umlaut.into(),
1568 form_action_origin: Some(origin_umlaut.into()),
1569 http_realm: Some("https://www.example.com".into()),
1570 username: "test".into(),
1571 password: "sekret".into(),
1572 ..LoginEntry::default()
1573 };
1574
1575 let db = LoginDb::open_in_memory();
1576 db.add_many(vec![login_a.clone(), login_b.clone(), login_umlaut.clone()])
1577 .expect("should be able to add logins");
1578
1579 assert_eq!(db.count_by_form_action_origin(origin_a).unwrap(), 1);
1580 assert_eq!(db.count_by_form_action_origin(origin_umlaut).unwrap(), 1);
1581 }
1582
1583 #[test]
1584 #[cfg(feature = "ignore_form_action_origin_validation_errors")]
1585 fn test_count_by_invalid_form_action_origin() {
1586 ensure_initialized();
1587
1588 let login = LoginEntry {
1589 origin: "https://example.com".into(),
1590 form_action_origin: Some("email".into()),
1591 username: "test".into(),
1592 password: "sekret".into(),
1593 ..LoginEntry::default()
1594 };
1595
1596 let db = LoginDb::open_in_memory();
1597 db.add(login)
1598 .expect("should be able to add login with invalid form_action_origin");
1599 assert_eq!(db.count_by_form_action_origin("email").unwrap(), 1);
1600 }
1601
1602 #[test]
1603 fn test_add_many_with_failed_constraint() {
1604 ensure_initialized();
1605
1606 let login_a = LoginEntry {
1607 origin: "https://example.com".into(),
1608 http_realm: Some("https://www.example.com".into()),
1609 username: "test".into(),
1610 password: "sekret".into(),
1611 ..LoginEntry::default()
1612 };
1613
1614 let login_b = LoginEntry {
1615 origin: "https://example.com".into(),
1617 http_realm: Some("https://www.example.com".into()),
1618 username: "test".into(),
1619 password: "sekret".into(),
1620 ..LoginEntry::default()
1621 };
1622
1623 let db = LoginDb::open_in_memory();
1624 let added = db
1625 .add_many(vec![login_a.clone(), login_b.clone()])
1626 .expect("should be able to add logins");
1627
1628 let [added_a, added_b] = added.as_slice() else {
1629 panic!("there should really be 2")
1630 };
1631
1632 let fetched_a = db
1634 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1635 .expect("should work")
1636 .expect("should get a record");
1637
1638 assert_eq!(fetched_a.fields.origin, login_a.origin);
1639
1640 assert!(!added_b.is_ok());
1642 }
1643
1644 #[test]
1645 fn test_add_with_meta() {
1646 ensure_initialized();
1647
1648 let guid = Guid::random();
1649 let now_ms = util::system_time_ms_i64(SystemTime::now());
1650 let login = LoginEntry {
1651 origin: "https://www.example.com".into(),
1652 http_realm: Some("https://www.example.com".into()),
1653 username: "test".into(),
1654 password: "sekret".into(),
1655 ..LoginEntry::default()
1656 };
1657 let meta = LoginMeta {
1658 id: guid.to_string(),
1659 time_created: now_ms,
1660 time_password_changed: now_ms + 100,
1661 time_last_used: now_ms + 10,
1662 times_used: 42,
1663 time_last_breach_alert_dismissed: None,
1664 };
1665
1666 let db = LoginDb::open_in_memory();
1667 let entry_with_meta = LoginEntryWithMeta {
1668 entry: login.clone(),
1669 meta: meta.clone(),
1670 };
1671
1672 db.add_with_meta(entry_with_meta)
1673 .expect("should be able to add login with record");
1674
1675 let fetched = db
1676 .get_by_id(&guid)
1677 .expect("should work")
1678 .expect("should get a record");
1679
1680 assert_eq!(fetched.meta, meta);
1681 }
1682
1683 #[test]
1687 fn test_get_heals_corrupt_timestamp_already_in_db() {
1688 ensure_initialized();
1689
1690 let db = LoginDb::open_in_memory();
1691 let login = db
1692 .add(LoginEntry {
1693 origin: "https://www.example.com".into(),
1694 http_realm: Some("https://www.example.com".into()),
1695 username: "user".into(),
1696 password: "password".into(),
1697 ..Default::default()
1698 })
1699 .unwrap();
1700
1701 const CORRUPT: i64 = 18446744071857664;
1704 db.execute(
1705 "UPDATE loginsL
1706 SET timeCreated = :corrupt,
1707 timePasswordChanged = :corrupt,
1708 timeLastUsed = :corrupt,
1709 timeLastBreachAlertDismissed = :corrupt,
1710 local_modified = -1
1711 WHERE guid = :guid",
1712 named_params! { ":corrupt": CORRUPT, ":guid": &login.meta.id },
1713 )
1714 .unwrap();
1715
1716 let fetched = [
1717 db.get_by_id(&login.meta.id).unwrap().unwrap(),
1718 db.get_all().unwrap().pop().unwrap(),
1719 ];
1720 for fetched in fetched {
1721 assert_eq!(fetched.meta.time_created, 0);
1722 assert_eq!(fetched.meta.time_password_changed, 0);
1723 assert_eq!(fetched.meta.time_last_used, 0);
1724 assert_eq!(fetched.meta.time_last_breach_alert_dismissed, Some(0));
1725 }
1726 }
1727
1728 #[test]
1730 fn test_add_with_meta_repairs_absurd_timestamps() {
1731 ensure_initialized();
1732
1733 let db = LoginDb::open_in_memory();
1734 let guid = Guid::random();
1735 let added = db
1736 .add_with_meta(LoginEntryWithMeta {
1737 entry: LoginEntry {
1738 origin: "https://www.example.com".into(),
1739 http_realm: Some("https://www.example.com".into()),
1740 username: "user".into(),
1741 password: "password".into(),
1742 ..Default::default()
1743 },
1744 meta: LoginMeta {
1745 id: guid.to_string(),
1746 time_created: 18446744071857664,
1747 time_password_changed: i64::MAX,
1748 time_last_used: -1,
1749 times_used: 1,
1750 time_last_breach_alert_dismissed: Some(i64::MAX),
1751 },
1752 })
1753 .unwrap();
1754
1755 assert_eq!(added.meta.time_created, 0);
1756 assert_eq!(added.meta.time_password_changed, 0);
1757 assert_eq!(added.meta.time_last_used, 0);
1758 assert_eq!(added.meta.time_last_breach_alert_dismissed, Some(0));
1759 }
1760
1761 #[test]
1762 fn test_add_with_meta_invalid_guid() {
1763 ensure_initialized();
1764
1765 let now_ms = util::system_time_ms_i64(SystemTime::now());
1766 let meta = LoginMeta {
1768 id: "invalid,guid".to_string(),
1769 time_created: now_ms,
1770 time_password_changed: now_ms,
1771 time_last_used: now_ms,
1772 times_used: 1,
1773 time_last_breach_alert_dismissed: None,
1774 };
1775 let db = LoginDb::open_in_memory();
1776 let result = db.add_with_meta(LoginEntryWithMeta {
1777 entry: LoginEntry {
1778 origin: "https://www.example.com".into(),
1779 http_realm: Some("https://www.example.com".into()),
1780 username: "test".into(),
1781 password: "sekret".into(),
1782 ..LoginEntry::default()
1783 },
1784 meta,
1785 });
1786
1787 #[cfg(not(feature = "fixup_invalid_guids"))]
1790 assert!(result.is_err());
1791
1792 #[cfg(feature = "fixup_invalid_guids")]
1793 {
1794 let login = result.expect("invalid guid should be repaired");
1795 assert!(Guid::new(&login.meta.id).is_valid_for_sync_server());
1796 }
1797 }
1798
1799 #[test]
1800 fn test_add_with_meta_duplicate_id() {
1801 ensure_initialized();
1802
1803 let guid = Guid::random();
1804 let now_ms = util::system_time_ms_i64(SystemTime::now());
1805 let meta = LoginMeta {
1806 id: guid.to_string(),
1807 time_created: now_ms,
1808 time_password_changed: now_ms,
1809 time_last_used: now_ms,
1810 times_used: 1,
1811 time_last_breach_alert_dismissed: None,
1812 };
1813
1814 let db = LoginDb::open_in_memory();
1815 db.add_with_meta(LoginEntryWithMeta {
1816 entry: LoginEntry {
1817 origin: "https://www.example.com".into(),
1818 http_realm: Some("https://www.example.com".into()),
1819 username: "test".into(),
1820 password: "sekret".into(),
1821 ..LoginEntry::default()
1822 },
1823 meta: meta.clone(),
1824 })
1825 .expect("should be able to add login with record");
1826
1827 db.add_with_meta(LoginEntryWithMeta {
1830 entry: LoginEntry {
1831 origin: "https://www.other.com".into(),
1832 http_realm: Some("https://www.other.com".into()),
1833 username: "test".into(),
1834 password: "sekret".into(),
1835 ..LoginEntry::default()
1836 },
1837 meta,
1838 })
1839 .expect("should be able to re-add a login with the same id");
1840
1841 let fetched = db
1842 .get_by_id(&guid)
1843 .expect("should work")
1844 .expect("should get a record");
1845 assert_eq!(fetched.fields.origin, "https://www.other.com");
1846 }
1847
1848 #[test]
1849 fn test_record_potentially_vulnerable_passwords() {
1850 ensure_initialized();
1851 let db = LoginDb::open_in_memory();
1852
1853 let count: i64 = db
1855 .db
1856 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1857 .unwrap();
1858 assert_eq!(count, 0);
1859
1860 db.record_potentially_vulnerable_passwords(vec![
1862 "password1".into(),
1863 "password2".into(),
1864 "password3".into(),
1865 ])
1866 .unwrap();
1867
1868 let count: i64 = db
1870 .db
1871 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1872 .unwrap();
1873 assert_eq!(count, 3);
1874
1875 db.record_potentially_vulnerable_passwords(vec!["password1".into(), "password4".into()])
1877 .unwrap();
1878
1879 let count: i64 = db
1881 .db
1882 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1883 .unwrap();
1884 assert_eq!(count, 4);
1885
1886 db.record_potentially_vulnerable_passwords(vec!["password1".into(), "password2".into()])
1888 .unwrap();
1889
1890 let count: i64 = db
1891 .db
1892 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
1893 .unwrap();
1894 assert_eq!(count, 4);
1895 }
1896
1897 #[test]
1898 fn test_add_with_meta_deleted() {
1899 ensure_initialized();
1900
1901 let guid = Guid::random();
1902 let now_ms = util::system_time_ms_i64(SystemTime::now());
1903 let login = LoginEntry {
1904 origin: "https://www.example.com".into(),
1905 http_realm: Some("https://www.example.com".into()),
1906 username: "test".into(),
1907 password: "sekret".into(),
1908 ..LoginEntry::default()
1909 };
1910 let meta = LoginMeta {
1911 id: guid.to_string(),
1912 time_created: now_ms,
1913 time_password_changed: now_ms + 100,
1914 time_last_used: now_ms + 10,
1915 times_used: 42,
1916 time_last_breach_alert_dismissed: None,
1917 };
1918
1919 let db = LoginDb::open_in_memory();
1920 let entry_with_meta = LoginEntryWithMeta {
1921 entry: login.clone(),
1922 meta: meta.clone(),
1923 };
1924
1925 db.add_with_meta(entry_with_meta)
1926 .expect("should be able to add login with record");
1927
1928 db.delete(&guid).expect("should be able to delete login");
1929
1930 let entry_with_meta2 = LoginEntryWithMeta {
1931 entry: login.clone(),
1932 meta: meta.clone(),
1933 };
1934
1935 db.add_with_meta(entry_with_meta2)
1936 .expect("should be able to re-add login with record");
1937
1938 let fetched = db
1939 .get_by_id(&guid)
1940 .expect("should work")
1941 .expect("should get a record");
1942
1943 assert_eq!(fetched.meta, meta);
1944 }
1945
1946 #[test]
1947 fn test_unicode_submit() {
1948 ensure_initialized();
1949 let db = LoginDb::open_in_memory();
1950 let added = db
1951 .add(LoginEntry {
1952 form_action_origin: Some("http://😍.com".into()),
1953 origin: "http://😍.com".into(),
1954 http_realm: None,
1955 username_field: "😍".into(),
1956 password_field: "😍".into(),
1957 username: "😍".into(),
1958 password: "😍".into(),
1959 })
1960 .unwrap();
1961 let fetched = db
1962 .get_by_id(&added.meta.id)
1963 .expect("should work")
1964 .expect("should get a record");
1965 assert_eq!(added, fetched);
1966 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1967 assert_eq!(
1968 fetched.fields.form_action_origin,
1969 Some("http://xn--r28h.com".to_string())
1970 );
1971 assert_eq!(fetched.fields.username_field, "😍");
1972 assert_eq!(fetched.fields.password_field, "😍");
1973 let sec_fields = fetched.decrypt_fields(db.encdec.as_ref()).unwrap();
1974 assert_eq!(sec_fields.username, "😍");
1975 assert_eq!(sec_fields.password, "😍");
1976 }
1977
1978 #[test]
1979 fn test_unicode_realm() {
1980 ensure_initialized();
1981 let db = LoginDb::open_in_memory();
1982 let added = db
1983 .add(LoginEntry {
1984 form_action_origin: None,
1985 origin: "http://😍.com".into(),
1986 http_realm: Some("😍😍".into()),
1987 username: "😍".into(),
1988 password: "😍".into(),
1989 ..Default::default()
1990 })
1991 .unwrap();
1992 let fetched = db
1993 .get_by_id(&added.meta.id)
1994 .expect("should work")
1995 .expect("should get a record");
1996 assert_eq!(added, fetched);
1997 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1998 assert_eq!(fetched.fields.http_realm.unwrap(), "😍😍");
1999 }
2000
2001 fn check_matches(db: &LoginDb, query: &str, expected: &[&str]) {
2002 let mut results = db
2003 .get_by_base_domain(query)
2004 .unwrap()
2005 .into_iter()
2006 .map(|l| l.fields.origin)
2007 .collect::<Vec<String>>();
2008 results.sort_unstable();
2009 let mut sorted = expected.to_owned();
2010 sorted.sort_unstable();
2011 assert_eq!(sorted, results);
2012 }
2013
2014 fn check_good_bad(
2015 good: Vec<&str>,
2016 bad: Vec<&str>,
2017 good_queries: Vec<&str>,
2018 zero_queries: Vec<&str>,
2019 ) {
2020 let db = LoginDb::open_in_memory();
2021 for h in good.iter().chain(bad.iter()) {
2022 db.add(LoginEntry {
2023 origin: (*h).into(),
2024 http_realm: Some((*h).into()),
2025 password: "test".into(),
2026 ..Default::default()
2027 })
2028 .unwrap();
2029 }
2030 for query in good_queries {
2031 check_matches(&db, query, &good);
2032 }
2033 for query in zero_queries {
2034 check_matches(&db, query, &[]);
2035 }
2036 }
2037
2038 #[test]
2039 fn test_get_by_base_domain_invalid() {
2040 ensure_initialized();
2041 check_good_bad(
2042 vec!["https://example.com"],
2043 vec![],
2044 vec![],
2045 vec!["invalid query"],
2046 );
2047 }
2048
2049 #[test]
2050 fn test_get_by_base_domain() {
2051 ensure_initialized();
2052 check_good_bad(
2053 vec![
2054 "https://example.com",
2055 "https://www.example.com",
2056 "http://www.example.com",
2057 "http://www.example.com:8080",
2058 "http://sub.example.com:8080",
2059 "https://sub.example.com:8080",
2060 "https://sub.sub.example.com",
2061 "ftp://sub.example.com",
2062 ],
2063 vec![
2064 "https://badexample.com",
2065 "https://example.co",
2066 "https://example.com.au",
2067 ],
2068 vec!["example.com"],
2069 vec!["foo.com"],
2070 );
2071 }
2072
2073 #[test]
2074 fn test_get_by_base_domain_punicode() {
2075 ensure_initialized();
2076 check_good_bad(
2079 vec![
2080 "http://xn--r28h.com", ],
2082 vec!["http://💖.com"],
2083 vec!["😍.com", "xn--r28h.com"],
2084 vec![],
2085 );
2086 }
2087
2088 #[test]
2089 fn test_get_by_base_domain_ipv4() {
2090 ensure_initialized();
2091 check_good_bad(
2092 vec!["http://127.0.0.1", "https://127.0.0.1:8000"],
2093 vec!["https://127.0.0.0", "https://example.com"],
2094 vec!["127.0.0.1"],
2095 vec!["127.0.0.2"],
2096 );
2097 }
2098
2099 #[test]
2100 fn test_get_by_base_domain_ipv6() {
2101 ensure_initialized();
2102 check_good_bad(
2103 vec!["http://[::1]", "https://[::1]:8000"],
2104 vec!["https://[0:0:0:0:0:0:1:1]", "https://example.com"],
2105 vec!["[::1]", "[0:0:0:0:0:0:0:1]"],
2106 vec!["[0:0:0:0:0:0:1:2]"],
2107 );
2108 }
2109
2110 #[test]
2111 fn test_add() {
2112 ensure_initialized();
2113 let db = LoginDb::open_in_memory();
2114 let to_add = LoginEntry {
2115 origin: "https://www.example.com".into(),
2116 http_realm: Some("https://www.example.com".into()),
2117 username: "test_user".into(),
2118 password: "test_password".into(),
2119 ..Default::default()
2120 };
2121 let login = db.add(to_add).unwrap();
2122 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
2123
2124 assert_eq!(login.fields.origin, login2.fields.origin);
2125 assert_eq!(login.fields.http_realm, login2.fields.http_realm);
2126 assert_eq!(login.sec_fields, login2.sec_fields);
2127 }
2128
2129 #[test]
2130 fn test_update() {
2131 ensure_initialized();
2132 let db = LoginDb::open_in_memory();
2133 let login = db
2134 .add(LoginEntry {
2135 origin: "https://www.example.com".into(),
2136 http_realm: Some("https://www.example.com".into()),
2137 username: "user1".into(),
2138 password: "password1".into(),
2139 ..Default::default()
2140 })
2141 .unwrap();
2142 db.update(
2143 &login.meta.id,
2144 LoginEntry {
2145 origin: "https://www.example2.com".into(),
2146 http_realm: Some("https://www.example2.com".into()),
2147 username: "user2".into(),
2148 password: "password2".into(),
2149 ..Default::default() },
2151 )
2152 .unwrap();
2153
2154 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
2155
2156 assert_eq!(login2.fields.origin, "https://www.example2.com");
2157 assert_eq!(
2158 login2.fields.http_realm,
2159 Some("https://www.example2.com".into())
2160 );
2161 let sec_fields = login2.decrypt_fields(db.encdec.as_ref()).unwrap();
2162 assert_eq!(sec_fields.username, "user2");
2163 assert_eq!(sec_fields.password, "password2");
2164 }
2165
2166 #[test]
2167 fn test_touch() {
2168 ensure_initialized();
2169 let db = LoginDb::open_in_memory();
2170 let login = db
2171 .add(LoginEntry {
2172 origin: "https://www.example.com".into(),
2173 http_realm: Some("https://www.example.com".into()),
2174 username: "user1".into(),
2175 password: "password1".into(),
2176 ..Default::default()
2177 })
2178 .unwrap();
2179 thread::sleep(time::Duration::from_millis(50));
2181 db.touch(&login.meta.id).unwrap();
2182 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
2183 assert!(login2.meta.time_last_used > login.meta.time_last_used);
2184 assert_eq!(login2.meta.times_used, login.meta.times_used + 1);
2185 }
2186
2187 #[test]
2188 fn test_update_does_not_count_as_use() {
2189 ensure_initialized();
2193 let db = LoginDb::open_in_memory();
2194 let login = db
2195 .add(LoginEntry {
2196 origin: "https://www.example.com".into(),
2197 http_realm: Some("https://www.example.com".into()),
2198 username: "user1".into(),
2199 password: "password1".into(),
2200 ..Default::default()
2201 })
2202 .unwrap();
2203 thread::sleep(time::Duration::from_millis(50));
2205 db.update(
2206 &login.meta.id,
2207 LoginEntry {
2208 origin: "https://www.example.com".into(),
2209 http_realm: Some("https://www.example.com".into()),
2210 username: "user1".into(),
2211 password: "password2".into(),
2212 ..Default::default()
2213 },
2214 )
2215 .unwrap();
2216 let updated = db.get_by_id(&login.meta.id).unwrap().unwrap();
2217 assert_eq!(updated.meta.times_used, login.meta.times_used);
2219 assert_eq!(updated.meta.time_last_used, login.meta.time_last_used);
2221 }
2222
2223 #[test]
2224 fn test_breach_alert_dismissal() {
2225 ensure_initialized();
2226 let db = LoginDb::open_in_memory();
2227 let login = db
2228 .add(LoginEntry {
2229 origin: "https://www.example.com".into(),
2230 http_realm: Some("https://www.example.com".into()),
2231 username: "user1".into(),
2232 password: "password1".into(),
2233 ..Default::default()
2234 })
2235 .unwrap();
2236 assert!(login.meta.time_last_breach_alert_dismissed.is_none());
2238
2239 db.record_breach_alert_dismissal(&login.meta.id).unwrap();
2241 let login1 = db.get_by_id(&login.meta.id).unwrap().unwrap();
2242 assert!(login1.meta.time_last_breach_alert_dismissed.is_some());
2243 }
2244
2245 #[test]
2246 fn test_breach_alert_dismissal_with_specific_timestamp() {
2247 ensure_initialized();
2248 let db = LoginDb::open_in_memory();
2249 let login = db
2250 .add(LoginEntry {
2251 origin: "https://www.example.com".into(),
2252 http_realm: Some("https://www.example.com".into()),
2253 username: "user1".into(),
2254 password: "password1".into(),
2255 ..Default::default()
2256 })
2257 .unwrap();
2258
2259 let dismiss_time = login.meta.time_password_changed + 1000;
2260 db.record_breach_alert_dismissal_time(&login.meta.id, dismiss_time)
2261 .unwrap();
2262
2263 let retrieved = db
2264 .get_by_id(&login.meta.id)
2265 .unwrap()
2266 .unwrap()
2267 .decrypt(db.encdec.as_ref())
2268 .unwrap();
2269 assert_eq!(
2270 retrieved.time_last_breach_alert_dismissed,
2271 Some(dismiss_time)
2272 );
2273 }
2274
2275 #[test]
2276 fn test_delete() {
2277 ensure_initialized();
2278 let db = LoginDb::open_in_memory();
2279 let login = db
2280 .add(LoginEntry {
2281 origin: "https://www.example.com".into(),
2282 http_realm: Some("https://www.example.com".into()),
2283 username: "test_user".into(),
2284 password: "test_password".into(),
2285 ..Default::default()
2286 })
2287 .unwrap();
2288
2289 assert!(db.delete(login.guid_str()).unwrap());
2290
2291 let local_login = db
2292 .query_row(
2293 "SELECT * FROM loginsL WHERE guid = :guid",
2294 named_params! { ":guid": login.guid_str() },
2295 |row| Ok(LocalLogin::test_raw_from_row(row).unwrap()),
2296 )
2297 .unwrap();
2298 assert_eq!(local_login.fields.http_realm, None);
2299 assert_eq!(local_login.fields.form_action_origin, None);
2300
2301 assert!(!db.exists(login.guid_str()).unwrap());
2302 }
2303
2304 #[test]
2305 fn test_delete_many() {
2306 ensure_initialized();
2307 let db = LoginDb::open_in_memory();
2308
2309 let login_a = db
2310 .add(LoginEntry {
2311 origin: "https://a.example.com".into(),
2312 http_realm: Some("https://www.example.com".into()),
2313 username: "test_user".into(),
2314 password: "test_password".into(),
2315 ..Default::default()
2316 })
2317 .unwrap();
2318
2319 let login_b = db
2320 .add(LoginEntry {
2321 origin: "https://b.example.com".into(),
2322 http_realm: Some("https://www.example.com".into()),
2323 username: "test_user".into(),
2324 password: "test_password".into(),
2325 ..Default::default()
2326 })
2327 .unwrap();
2328
2329 let result = db
2330 .delete_many(vec![login_a.guid_str(), login_b.guid_str()])
2331 .unwrap();
2332 assert!(result[0]);
2333 assert!(result[1]);
2334 assert!(!db.exists(login_a.guid_str()).unwrap());
2335 assert!(!db.exists(login_b.guid_str()).unwrap());
2336 }
2337
2338 #[test]
2339 fn test_subsequent_delete_many() {
2340 ensure_initialized();
2341 let db = LoginDb::open_in_memory();
2342
2343 let login = db
2344 .add(LoginEntry {
2345 origin: "https://a.example.com".into(),
2346 http_realm: Some("https://www.example.com".into()),
2347 username: "test_user".into(),
2348 password: "test_password".into(),
2349 ..Default::default()
2350 })
2351 .unwrap();
2352
2353 let result = db.delete_many(vec![login.guid_str()]).unwrap();
2354 assert!(result[0]);
2355 assert!(!db.exists(login.guid_str()).unwrap());
2356
2357 let result = db.delete_many(vec![login.guid_str()]).unwrap();
2358 assert!(!result[0]);
2359 }
2360
2361 #[test]
2362 fn test_delete_many_with_non_existent_id() {
2363 ensure_initialized();
2364 let db = LoginDb::open_in_memory();
2365
2366 let result = db.delete_many(vec![&Guid::random()]).unwrap();
2367 assert!(!result[0]);
2368 }
2369
2370 #[test]
2371 fn test_delete_all() {
2372 ensure_initialized();
2373 let db = LoginDb::open_in_memory();
2374 let login_a = db
2375 .add(LoginEntry {
2376 origin: "https://a.example.com".into(),
2377 http_realm: Some("https://www.example.com".into()),
2378 username: "test_user".into(),
2379 password: "test_password".into(),
2380 ..Default::default()
2381 })
2382 .unwrap();
2383 let login_b = db
2384 .add(LoginEntry {
2385 origin: "https://b.example.com".into(),
2386 http_realm: Some("https://www.example.com".into()),
2387 username: "test_user".into(),
2388 password: "test_password".into(),
2389 ..Default::default()
2390 })
2391 .unwrap();
2392
2393 let mut deleted = db.delete_all().unwrap();
2394 deleted.sort();
2395 let mut expected = vec![login_a.meta.id.clone(), login_b.meta.id.clone()];
2396 expected.sort();
2397 assert_eq!(deleted, expected);
2398 assert!(!db.exists(login_a.guid_str()).unwrap());
2399 assert!(!db.exists(login_b.guid_str()).unwrap());
2400
2401 assert_eq!(db.delete_all().unwrap(), Vec::<String>::new());
2403 }
2404
2405 #[test]
2406 fn test_delete_all_except_fxa() {
2407 ensure_initialized();
2408 let db = LoginDb::open_in_memory();
2409 let login = db
2410 .add(LoginEntry {
2411 origin: "https://a.example.com".into(),
2412 http_realm: Some("https://www.example.com".into()),
2413 username: "test_user".into(),
2414 password: "test_password".into(),
2415 ..Default::default()
2416 })
2417 .unwrap();
2418 let fxa_login = db
2419 .add(LoginEntry {
2420 origin: FXA_CREDENTIALS_ORIGIN.into(),
2421 http_realm: Some("https://www.example.com".into()),
2422 username: "test_user".into(),
2423 password: "test_password".into(),
2424 ..Default::default()
2425 })
2426 .unwrap();
2427
2428 let deleted = db.delete_all_except_fxa().unwrap();
2429 assert_eq!(deleted, vec![login.meta.id.clone()]);
2430
2431 assert!(!db.exists(login.guid_str()).unwrap());
2433 assert!(db.exists(fxa_login.guid_str()).unwrap());
2434 }
2435
2436 #[test]
2437 fn test_wipe_local_except_fxa() {
2438 ensure_initialized();
2439 let db = LoginDb::open_in_memory();
2440 let login = db
2441 .add(LoginEntry {
2442 origin: "https://a.example.com".into(),
2443 http_realm: Some("https://www.example.com".into()),
2444 username: "test_user".into(),
2445 password: "test_password".into(),
2446 ..Default::default()
2447 })
2448 .unwrap();
2449 let fxa_login = db
2450 .add(LoginEntry {
2451 origin: FXA_CREDENTIALS_ORIGIN.into(),
2452 http_realm: Some("https://www.example.com".into()),
2453 username: "test_user".into(),
2454 password: "test_password".into(),
2455 ..Default::default()
2456 })
2457 .unwrap();
2458
2459 db.wipe_local_except_fxa().unwrap();
2460
2461 assert!(!db.exists(login.guid_str()).unwrap());
2463 assert!(db.exists(fxa_login.guid_str()).unwrap());
2464 }
2465
2466 #[test]
2467 fn test_delete_local_for_remote_replacement() {
2468 ensure_initialized();
2469 let db = LoginDb::open_in_memory();
2470 let login = db
2471 .add(LoginEntry {
2472 origin: "https://www.example.com".into(),
2473 http_realm: Some("https://www.example.com".into()),
2474 username: "test_user".into(),
2475 password: "test_password".into(),
2476 ..Default::default()
2477 })
2478 .unwrap();
2479
2480 let result = db
2481 .delete_local_records_for_remote_replacement(vec![login.guid_str()])
2482 .unwrap();
2483
2484 let local_guids = get_local_guids(&db);
2485 assert_eq!(local_guids.len(), 0);
2486
2487 let mirror_guids = get_mirror_guids(&db);
2488 assert_eq!(mirror_guids.len(), 0);
2489
2490 assert_eq!(result.local_deleted, 1);
2491 }
2492
2493 mod test_find_login_to_update {
2494 use super::*;
2495
2496 fn make_entry(username: &str, password: &str) -> LoginEntry {
2497 LoginEntry {
2498 origin: "https://www.example.com".into(),
2499 http_realm: Some("the website".into()),
2500 username: username.into(),
2501 password: password.into(),
2502 ..Default::default()
2503 }
2504 }
2505
2506 fn make_saved_login(db: &LoginDb, username: &str, password: &str) -> Login {
2507 db.add(make_entry(username, password))
2508 .unwrap()
2509 .decrypt(db.encdec.as_ref())
2510 .unwrap()
2511 }
2512
2513 #[test]
2514 fn test_match() {
2515 ensure_initialized();
2516 let db = LoginDb::open_in_memory();
2517 let login = make_saved_login(&db, "user", "pass");
2518 assert_eq!(
2519 Some(login),
2520 db.find_login_to_update(make_entry("user", "pass")).unwrap(),
2521 );
2522 }
2523
2524 #[test]
2525 fn test_non_matches() {
2526 ensure_initialized();
2527 let db = LoginDb::open_in_memory();
2528 make_saved_login(&db, "other-user", "pass");
2530 db.add(LoginEntry {
2532 origin: "https://www.example.com".into(),
2533 http_realm: Some("the other website".into()),
2534 username: "user".into(),
2535 password: "pass".into(),
2536 ..Default::default()
2537 })
2538 .unwrap();
2539 db.add(LoginEntry {
2541 origin: "https://www.example.com".into(),
2542 form_action_origin: Some("https://www.example.com/".into()),
2543 username: "user".into(),
2544 password: "pass".into(),
2545 ..Default::default()
2546 })
2547 .unwrap();
2548 assert_eq!(
2549 None,
2550 db.find_login_to_update(make_entry("user", "pass")).unwrap(),
2551 );
2552 }
2553
2554 #[test]
2555 fn test_match_blank_password() {
2556 ensure_initialized();
2557 let db = LoginDb::open_in_memory();
2558 let login = make_saved_login(&db, "", "pass");
2559 assert_eq!(
2560 Some(login),
2561 db.find_login_to_update(make_entry("user", "pass")).unwrap(),
2562 );
2563 }
2564
2565 #[test]
2566 fn test_username_match_takes_precedence_over_blank_username() {
2567 ensure_initialized();
2568 let db = LoginDb::open_in_memory();
2569 make_saved_login(&db, "", "pass");
2570 let username_match = make_saved_login(&db, "user", "pass");
2571 assert_eq!(
2572 Some(username_match),
2573 db.find_login_to_update(make_entry("user", "pass")).unwrap(),
2574 );
2575 }
2576
2577 #[test]
2578 fn test_invalid_login() {
2579 ensure_initialized();
2580 let db = LoginDb::open_in_memory();
2581 assert!(db
2582 .find_login_to_update(LoginEntry {
2583 http_realm: None,
2584 form_action_origin: None,
2585 ..LoginEntry::default()
2586 })
2587 .is_err());
2588 }
2589
2590 #[test]
2591 fn test_update_with_duplicate_login() {
2592 ensure_initialized();
2593 let db = LoginDb::open_in_memory();
2596 let login = make_saved_login(&db, "user", "pass");
2597 let mut dupe = login.clone().encrypt(&*TEST_ENCDEC).unwrap();
2598 dupe.meta.id = "different-guid".to_string();
2599 db.insert_new_login(&dupe).unwrap();
2600
2601 let mut entry = login.entry();
2602 entry.password = "pass2".to_string();
2603 db.update(&login.id, entry).unwrap();
2604
2605 let mut entry = login.entry();
2606 entry.password = "pass3".to_string();
2607 db.add_or_update(entry).unwrap();
2608 }
2609
2610 #[test]
2611 fn test_password_reuse_detection() {
2612 ensure_initialized();
2613 let db = LoginDb::open_in_memory();
2614
2615 let login1 = db
2617 .add(LoginEntry {
2618 origin: "https://site1.com".into(),
2619 http_realm: Some("realm".into()),
2620 username: "user1".into(),
2621 password: "shared_password".into(),
2622 ..Default::default()
2623 })
2624 .unwrap();
2625
2626 let login2 = db
2627 .add(LoginEntry {
2628 origin: "https://site2.com".into(),
2629 http_realm: Some("realm".into()),
2630 username: "user2".into(),
2631 password: "shared_password".into(),
2632 ..Default::default()
2633 })
2634 .unwrap();
2635
2636 assert!(!db
2638 .is_potentially_vulnerable_password(&login1.meta.id)
2639 .unwrap());
2640 assert!(!db
2641 .is_potentially_vulnerable_password(&login2.meta.id)
2642 .unwrap());
2643 let vulnerable = db
2645 .are_potentially_vulnerable_passwords(&[&login1.meta.id, &login2.meta.id])
2646 .unwrap();
2647 assert_eq!(vulnerable.len(), 0);
2648
2649 db.record_potentially_vulnerable_passwords(vec!["shared_password".into()])
2651 .unwrap();
2652
2653 assert!(db
2655 .is_potentially_vulnerable_password(&login2.meta.id)
2656 .unwrap());
2657 let vulnerable = db
2659 .are_potentially_vulnerable_passwords(&[&login1.meta.id, &login2.meta.id])
2660 .unwrap();
2661 assert_eq!(vulnerable.len(), 2);
2662 assert!(vulnerable.contains(&login1.meta.id));
2663 assert!(vulnerable.contains(&login2.meta.id));
2664
2665 db.update(
2667 &login2.meta.id,
2668 LoginEntry {
2669 origin: "https://site2.com".into(),
2670 http_realm: Some("realm".into()),
2671 username: "user2".into(),
2672 password: "different_password".into(),
2673 ..Default::default()
2674 },
2675 )
2676 .unwrap();
2677
2678 assert!(!db
2679 .is_potentially_vulnerable_password(&login2.meta.id)
2680 .unwrap());
2681 }
2682
2683 #[test]
2684 fn test_reset_all_breaches_clears_breach_table() {
2685 ensure_initialized();
2686 let db = LoginDb::open_in_memory();
2687
2688 let login = db
2689 .add(LoginEntry {
2690 origin: "https://example.com".into(),
2691 http_realm: Some("realm".into()),
2692 username: "user".into(),
2693 password: "password123".into(),
2694 ..Default::default()
2695 })
2696 .unwrap();
2697
2698 db.record_potentially_vulnerable_passwords(vec!["password123".into()])
2699 .unwrap();
2700
2701 let count: i64 = db
2703 .db
2704 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
2705 .unwrap();
2706 assert_eq!(count, 1);
2707 let vulnerable = db
2709 .are_potentially_vulnerable_passwords(&[&login.meta.id])
2710 .unwrap();
2711 assert_eq!(vulnerable.len(), 1);
2712 assert_eq!(vulnerable[0], login.meta.id);
2713
2714 db.reset_all_breaches().unwrap();
2716
2717 let count: i64 = db
2719 .db
2720 .query_row("SELECT COUNT(*) FROM breachesL", [], |row| row.get(0))
2721 .unwrap();
2722 assert_eq!(count, 0);
2723 let vulnerable = db
2725 .are_potentially_vulnerable_passwords(&[&login.meta.id])
2726 .unwrap();
2727 assert_eq!(vulnerable.len(), 0);
2728 }
2729
2730 #[test]
2731 fn test_different_passwords_not_vulnerable() {
2732 ensure_initialized();
2733 let db = LoginDb::open_in_memory();
2734
2735 let login1 = db
2736 .add(LoginEntry {
2737 origin: "https://site1.com".into(),
2738 http_realm: Some("realm".into()),
2739 username: "user".into(),
2740 password: "password_A".into(),
2741 ..Default::default()
2742 })
2743 .unwrap();
2744
2745 let login2 = db
2746 .add(LoginEntry {
2747 origin: "https://site2.com".into(),
2748 http_realm: Some("realm".into()),
2749 username: "user".into(),
2750 password: "password_B".into(),
2751 ..Default::default()
2752 })
2753 .unwrap();
2754
2755 db.record_potentially_vulnerable_passwords(vec!["password_A".into()])
2756 .unwrap();
2757
2758 assert!(!db
2760 .is_potentially_vulnerable_password(&login2.meta.id)
2761 .unwrap());
2762 let vulnerable = db
2765 .are_potentially_vulnerable_passwords(&[&login1.meta.id, &login2.meta.id])
2766 .unwrap();
2767 assert_eq!(vulnerable.len(), 1);
2768 assert!(vulnerable.contains(&login1.meta.id));
2769 }
2770 }
2771}