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