1use crate::encryption::EncryptorDecryptor;
26use crate::error::*;
27use crate::login::*;
28use crate::schema;
29use crate::sync::SyncStatus;
30use crate::util;
31use interrupt_support::{SqlInterruptHandle, SqlInterruptScope};
32use lazy_static::lazy_static;
33use rusqlite::{
34 named_params,
35 types::{FromSql, ToSql},
36 Connection,
37};
38use sql_support::ConnExt;
39use std::ops::Deref;
40use std::path::Path;
41use std::sync::Arc;
42use std::time::SystemTime;
43use sync_guid::Guid;
44use url::{Host, Url};
45
46pub struct LoginDb {
47 pub db: Connection,
48 pub encdec: Arc<dyn EncryptorDecryptor>,
49 interrupt_handle: Arc<SqlInterruptHandle>,
50}
51
52pub struct LoginsDeletionMetrics {
53 pub local_deleted: u64,
54 pub mirror_deleted: u64,
55}
56
57impl LoginDb {
58 pub fn with_connection(db: Connection, encdec: Arc<dyn EncryptorDecryptor>) -> Result<Self> {
59 #[cfg(test)]
60 {
61 util::init_test_logging();
62 }
63
64 db.set_pragma("temp_store", 2)?;
69
70 let mut logins = Self {
71 interrupt_handle: Arc::new(SqlInterruptHandle::new(&db)),
72 encdec,
73 db,
74 };
75 let tx = logins.db.transaction()?;
76 schema::init(&tx)?;
77 tx.commit()?;
78 Ok(logins)
79 }
80
81 pub fn open(path: impl AsRef<Path>, encdec: Arc<dyn EncryptorDecryptor>) -> Result<Self> {
82 Self::with_connection(Connection::open(path)?, encdec)
83 }
84
85 #[cfg(test)]
86 pub fn open_in_memory() -> Self {
87 let encdec: Arc<dyn EncryptorDecryptor> =
88 crate::encryption::test_utils::TEST_ENCDEC.clone();
89 Self::with_connection(Connection::open_in_memory().unwrap(), encdec).unwrap()
90 }
91
92 pub fn new_interrupt_handle(&self) -> Arc<SqlInterruptHandle> {
93 Arc::clone(&self.interrupt_handle)
94 }
95
96 #[inline]
97 pub fn begin_interrupt_scope(&self) -> Result<SqlInterruptScope> {
98 Ok(self.interrupt_handle.begin_interrupt_scope()?)
99 }
100}
101
102impl ConnExt for LoginDb {
103 #[inline]
104 fn conn(&self) -> &Connection {
105 &self.db
106 }
107}
108
109impl Deref for LoginDb {
110 type Target = Connection;
111 #[inline]
112 fn deref(&self) -> &Connection {
113 &self.db
114 }
115}
116
117impl LoginDb {
120 pub(crate) fn put_meta(&self, key: &str, value: &dyn ToSql) -> Result<()> {
121 self.execute_cached(
122 "REPLACE INTO loginsSyncMeta (key, value) VALUES (:key, :value)",
123 named_params! { ":key": key, ":value": value },
124 )?;
125 Ok(())
126 }
127
128 pub(crate) fn get_meta<T: FromSql>(&self, key: &str) -> Result<Option<T>> {
129 self.try_query_row(
130 "SELECT value FROM loginsSyncMeta WHERE key = :key",
131 named_params! { ":key": key },
132 |row| Ok::<_, Error>(row.get(0)?),
133 true,
134 )
135 }
136
137 pub(crate) fn delete_meta(&self, key: &str) -> Result<()> {
138 self.execute_cached(
139 "DELETE FROM loginsSyncMeta WHERE key = :key",
140 named_params! { ":key": key },
141 )?;
142 Ok(())
143 }
144
145 pub fn count_all(&self) -> Result<i64> {
146 let mut stmt = self.db.prepare_cached(&COUNT_ALL_SQL)?;
147
148 let count: i64 = stmt.query_row([], |row| row.get(0))?;
149 Ok(count)
150 }
151
152 pub fn count_by_origin(&self, origin: &str) -> Result<i64> {
153 match LoginEntry::validate_and_fixup_origin(origin) {
154 Ok(result) => {
155 let origin = result.unwrap_or(origin.to_string());
156 let mut stmt = self.db.prepare_cached(&COUNT_BY_ORIGIN_SQL)?;
157 let count: i64 =
158 stmt.query_row(named_params! { ":origin": origin }, |row| row.get(0))?;
159 Ok(count)
160 }
161 Err(e) => {
162 warn!("count_by_origin was passed an invalid origin: {}", e);
164 Ok(0)
165 }
166 }
167 }
168
169 pub fn count_by_form_action_origin(&self, form_action_origin: &str) -> Result<i64> {
170 match LoginEntry::validate_and_fixup_origin(form_action_origin) {
171 Ok(result) => {
172 let form_action_origin = result.unwrap_or(form_action_origin.to_string());
173 let mut stmt = self.db.prepare_cached(&COUNT_BY_FORM_ACTION_ORIGIN_SQL)?;
174 let count: i64 = stmt.query_row(
175 named_params! { ":form_action_origin": form_action_origin },
176 |row| row.get(0),
177 )?;
178 Ok(count)
179 }
180 Err(e) => {
181 warn!("count_by_origin was passed an invalid origin: {}", e);
183 Ok(0)
184 }
185 }
186 }
187
188 pub fn get_all(&self) -> Result<Vec<EncryptedLogin>> {
189 let mut stmt = self.db.prepare_cached(&GET_ALL_SQL)?;
190 let rows = stmt.query_and_then([], EncryptedLogin::from_row)?;
191 rows.collect::<Result<_>>()
192 }
193
194 pub fn get_by_base_domain(&self, base_domain: &str) -> Result<Vec<EncryptedLogin>> {
195 let base_host = match Host::parse(base_domain) {
197 Ok(d) => d,
198 Err(e) => {
199 warn!("get_by_base_domain was passed an invalid domain: {}", e);
201 return Ok(vec![]);
202 }
203 };
204 let mut stmt = self.db.prepare_cached(&GET_ALL_SQL)?;
211 let rows = stmt
212 .query_and_then([], EncryptedLogin::from_row)?
213 .filter(|r| {
214 let login = r
215 .as_ref()
216 .ok()
217 .and_then(|login| Url::parse(&login.fields.origin).ok());
218 let this_host = login.as_ref().and_then(|url| url.host());
219 match (&base_host, this_host) {
220 (Host::Domain(base), Some(Host::Domain(look))) => {
221 let mut rev_input = base.chars().rev();
225 let mut rev_host = look.chars().rev();
226 loop {
227 match (rev_input.next(), rev_host.next()) {
228 (Some(ref a), Some(ref b)) if a == b => continue,
229 (None, None) => return true, (None, Some(ref h)) => return *h == '.',
231 _ => return false,
232 }
233 }
234 }
235 (Host::Ipv4(base), Some(Host::Ipv4(look))) => *base == look,
237 (Host::Ipv6(base), Some(Host::Ipv6(look))) => *base == look,
238 _ => false,
240 }
241 });
242 rows.collect::<Result<_>>()
243 }
244
245 pub fn get_by_id(&self, id: &str) -> Result<Option<EncryptedLogin>> {
246 self.try_query_row(
247 &GET_BY_GUID_SQL,
248 &[(":guid", &id as &dyn ToSql)],
249 EncryptedLogin::from_row,
250 true,
251 )
252 }
253
254 pub fn find_login_to_update(
266 &self,
267 look: LoginEntry,
268 encdec: &dyn EncryptorDecryptor,
269 ) -> Result<Option<Login>> {
270 let look = look.fixup()?;
271 let logins = self
272 .get_by_entry_target(&look)?
273 .into_iter()
274 .map(|enc_login| enc_login.decrypt(encdec))
275 .collect::<Result<Vec<Login>>>()?;
276 Ok(logins
277 .iter()
279 .find(|login| login.username == look.username)
280 .or_else(|| logins.iter().find(|login| login.username.is_empty()))
282 .cloned())
284 }
285
286 pub fn touch(&self, id: &str) -> Result<()> {
287 let tx = self.unchecked_transaction()?;
288 self.ensure_local_overlay_exists(id)?;
289 self.mark_mirror_overridden(id)?;
290 let now_ms = util::system_time_ms_i64(SystemTime::now());
291 self.execute_cached(
294 "UPDATE loginsL
295 SET timeLastUsed = :now_millis,
296 timesUsed = timesUsed + 1,
297 local_modified = :now_millis
298 WHERE guid = :guid
299 AND is_deleted = 0",
300 named_params! {
301 ":now_millis": now_ms,
302 ":guid": id,
303 },
304 )?;
305 tx.commit()?;
306 Ok(())
307 }
308
309 pub fn record_breach(&self, id: &str, timestamp: i64) -> Result<()> {
310 let tx = self.unchecked_transaction()?;
311 self.ensure_local_overlay_exists(id)?;
312 self.mark_mirror_overridden(id)?;
313 self.execute_cached(
314 "UPDATE loginsL
315 SET timeOfLastBreach = :now_millis
316 WHERE guid = :guid",
317 named_params! {
318 ":now_millis": timestamp,
319 ":guid": id,
320 },
321 )?;
322 tx.commit()?;
323 Ok(())
324 }
325
326 pub fn is_potentially_breached(&self, id: &str) -> Result<bool> {
327 let is_potentially_breached: bool = self.db.query_row(
328 "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid AND timeOfLastBreach IS NOT NULL AND timeOfLastBreach > timePasswordChanged)",
329 named_params! { ":guid": id },
330 |row| row.get(0),
331 )?;
332 Ok(is_potentially_breached)
333 }
334
335 pub fn reset_all_breaches(&self) -> Result<()> {
336 let tx = self.unchecked_transaction()?;
337 self.execute_cached(
338 "UPDATE loginsL
339 SET timeOfLastBreach = NULL
340 WHERE timeOfLastBreach IS NOT NULL",
341 [],
342 )?;
343 tx.commit()?;
344 Ok(())
345 }
346
347 pub fn is_breach_alert_dismissed(&self, id: &str) -> Result<bool> {
348 let is_breach_alert_dismissed: bool = self.db.query_row(
349 "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid AND timeOfLastBreach < timeLastBreachAlertDismissed)",
350 named_params! { ":guid": id },
351 |row| row.get(0),
352 )?;
353 Ok(is_breach_alert_dismissed)
354 }
355
356 pub fn record_breach_alert_dismissal(&self, id: &str) -> Result<()> {
361 let timestamp = util::system_time_ms_i64(SystemTime::now());
362 self.record_breach_alert_dismissal_time(id, timestamp)
363 }
364
365 pub fn record_breach_alert_dismissal_time(&self, id: &str, timestamp: i64) -> Result<()> {
371 let tx = self.unchecked_transaction()?;
372 self.ensure_local_overlay_exists(id)?;
373 self.mark_mirror_overridden(id)?;
374 self.execute_cached(
375 "UPDATE loginsL
376 SET timeLastBreachAlertDismissed = :now_millis
377 WHERE guid = :guid",
378 named_params! {
379 ":now_millis": timestamp,
380 ":guid": id,
381 },
382 )?;
383 tx.commit()?;
384 Ok(())
385 }
386
387 fn insert_new_login(&self, login: &EncryptedLogin) -> Result<()> {
390 let sql = format!(
391 "INSERT OR REPLACE INTO loginsL (
392 origin,
393 httpRealm,
394 formActionOrigin,
395 usernameField,
396 passwordField,
397 timesUsed,
398 secFields,
399 guid,
400 timeCreated,
401 timeLastUsed,
402 timePasswordChanged,
403 timeOfLastBreach,
404 timeLastBreachAlertDismissed,
405 local_modified,
406 is_deleted,
407 sync_status
408 ) VALUES (
409 :origin,
410 :http_realm,
411 :form_action_origin,
412 :username_field,
413 :password_field,
414 :times_used,
415 :sec_fields,
416 :guid,
417 :time_created,
418 :time_last_used,
419 :time_password_changed,
420 :time_of_last_breach,
421 :time_last_breach_alert_dismissed,
422 :local_modified,
423 0, -- is_deleted
424 {new} -- sync_status
425 )",
426 new = SyncStatus::New as u8
427 );
428
429 self.execute(
430 &sql,
431 named_params! {
432 ":origin": login.fields.origin,
433 ":http_realm": login.fields.http_realm,
434 ":form_action_origin": login.fields.form_action_origin,
435 ":username_field": login.fields.username_field,
436 ":password_field": login.fields.password_field,
437 ":time_created": login.meta.time_created,
438 ":times_used": login.meta.times_used,
439 ":time_last_used": login.meta.time_last_used,
440 ":time_password_changed": login.meta.time_password_changed,
441 ":time_of_last_breach": login.fields.time_of_last_breach,
442 ":time_last_breach_alert_dismissed": login.fields.time_last_breach_alert_dismissed,
443 ":local_modified": login.meta.time_created,
444 ":sec_fields": login.sec_fields,
445 ":guid": login.guid(),
446 },
447 )?;
448 Ok(())
449 }
450
451 fn update_existing_login(&self, login: &EncryptedLogin) -> Result<()> {
452 let sql = format!(
454 "UPDATE loginsL
455 SET local_modified = :now_millis,
456 timeLastUsed = :time_last_used,
457 timePasswordChanged = :time_password_changed,
458 httpRealm = :http_realm,
459 formActionOrigin = :form_action_origin,
460 usernameField = :username_field,
461 passwordField = :password_field,
462 timesUsed = :times_used,
463 secFields = :sec_fields,
464 origin = :origin,
465 -- leave New records as they are, otherwise update them to `changed`
466 sync_status = max(sync_status, {changed})
467 WHERE guid = :guid",
468 changed = SyncStatus::Changed as u8
469 );
470
471 self.db.execute(
472 &sql,
473 named_params! {
474 ":origin": login.fields.origin,
475 ":http_realm": login.fields.http_realm,
476 ":form_action_origin": login.fields.form_action_origin,
477 ":username_field": login.fields.username_field,
478 ":password_field": login.fields.password_field,
479 ":time_last_used": login.meta.time_last_used,
480 ":times_used": login.meta.times_used,
481 ":time_password_changed": login.meta.time_password_changed,
482 ":sec_fields": login.sec_fields,
483 ":guid": &login.meta.id,
484 ":now_millis": login.meta.time_last_used,
486 },
487 )?;
488 Ok(())
489 }
490
491 pub fn add_many(
493 &self,
494 entries: Vec<LoginEntry>,
495 encdec: &dyn EncryptorDecryptor,
496 ) -> Result<Vec<Result<EncryptedLogin>>> {
497 let now_ms = util::system_time_ms_i64(SystemTime::now());
498
499 let entries_with_meta = entries
500 .into_iter()
501 .map(|entry| {
502 let guid = Guid::random();
503 LoginEntryWithMeta {
504 entry,
505 meta: LoginMeta {
506 id: guid.to_string(),
507 time_created: now_ms,
508 time_password_changed: now_ms,
509 time_last_used: now_ms,
510 times_used: 1,
511 },
512 }
513 })
514 .collect();
515
516 self.add_many_with_meta(entries_with_meta, encdec)
517 }
518
519 pub fn add_many_with_meta(
523 &self,
524 entries_with_meta: Vec<LoginEntryWithMeta>,
525 encdec: &dyn EncryptorDecryptor,
526 ) -> Result<Vec<Result<EncryptedLogin>>> {
527 let tx = self.unchecked_transaction()?;
528 let mut results = vec![];
529 for entry_with_meta in entries_with_meta {
530 let guid = Guid::from_string(entry_with_meta.meta.id.clone());
531 match self.fixup_and_check_for_dupes(&guid, entry_with_meta.entry, encdec) {
532 Ok(new_entry) => {
533 let sec_fields = SecureLoginFields {
534 username: new_entry.username,
535 password: new_entry.password,
536 }
537 .encrypt(encdec, &entry_with_meta.meta.id)?;
538 let encrypted_login = EncryptedLogin {
539 meta: entry_with_meta.meta,
540 fields: LoginFields {
541 origin: new_entry.origin,
542 form_action_origin: new_entry.form_action_origin,
543 http_realm: new_entry.http_realm,
544 username_field: new_entry.username_field,
545 password_field: new_entry.password_field,
546 time_of_last_breach: None,
547 time_last_breach_alert_dismissed: None,
548 },
549 sec_fields,
550 };
551 let result = self
552 .insert_new_login(&encrypted_login)
553 .map(|_| encrypted_login);
554 results.push(result);
555 }
556
557 Err(error) => results.push(Err(error)),
558 }
559 }
560 tx.commit()?;
561 Ok(results)
562 }
563
564 pub fn add(
565 &self,
566 entry: LoginEntry,
567 encdec: &dyn EncryptorDecryptor,
568 ) -> Result<EncryptedLogin> {
569 let guid = Guid::random();
570 let now_ms = util::system_time_ms_i64(SystemTime::now());
571
572 let entry_with_meta = LoginEntryWithMeta {
573 entry,
574 meta: LoginMeta {
575 id: guid.to_string(),
576 time_created: now_ms,
577 time_password_changed: now_ms,
578 time_last_used: now_ms,
579 times_used: 1,
580 },
581 };
582
583 self.add_with_meta(entry_with_meta, encdec)
584 }
585
586 pub fn add_with_meta(
590 &self,
591 entry_with_meta: LoginEntryWithMeta,
592 encdec: &dyn EncryptorDecryptor,
593 ) -> Result<EncryptedLogin> {
594 let mut results = self.add_many_with_meta(vec![entry_with_meta], encdec)?;
595 results.pop().expect("there should be a single result")
596 }
597
598 pub fn update(
599 &self,
600 sguid: &str,
601 entry: LoginEntry,
602 encdec: &dyn EncryptorDecryptor,
603 ) -> Result<EncryptedLogin> {
604 let guid = Guid::new(sguid);
605 let now_ms = util::system_time_ms_i64(SystemTime::now());
606 let tx = self.unchecked_transaction()?;
607
608 let entry = entry.fixup()?;
609
610 if self.check_for_dupes(&guid, &entry, encdec).is_err() {
616 let has_mirror_row: bool = self
618 .db
619 .conn_ext_query_one("SELECT EXISTS (SELECT 1 FROM loginsM)")?;
620 let has_http_realm = entry.http_realm.is_some();
621 let has_form_action_origin = entry.form_action_origin.is_some();
622 report_error!(
623 "logins-duplicate-in-update",
624 "(mirror: {has_mirror_row}, realm: {has_http_realm}, form_origin: {has_form_action_origin})");
625 }
626
627 self.ensure_local_overlay_exists(&guid)?;
629 self.mark_mirror_overridden(&guid)?;
630
631 let existing = match self.get_by_id(sguid)? {
633 Some(e) => e.decrypt(encdec)?,
634 None => return Err(Error::NoSuchRecord(sguid.to_owned())),
635 };
636 let time_password_changed = if existing.password == entry.password {
637 existing.time_password_changed
638 } else {
639 now_ms
640 };
641
642 let sec_fields = SecureLoginFields {
644 username: entry.username,
645 password: entry.password,
646 }
647 .encrypt(encdec, &existing.id)?;
648 let result = EncryptedLogin {
649 meta: LoginMeta {
650 id: existing.id,
651 time_created: existing.time_created,
652 time_password_changed,
653 time_last_used: now_ms,
654 times_used: existing.times_used + 1,
655 },
656 fields: LoginFields {
657 origin: entry.origin,
658 form_action_origin: entry.form_action_origin,
659 http_realm: entry.http_realm,
660 username_field: entry.username_field,
661 password_field: entry.password_field,
662 time_of_last_breach: None,
663 time_last_breach_alert_dismissed: None,
664 },
665 sec_fields,
666 };
667
668 self.update_existing_login(&result)?;
669 tx.commit()?;
670 Ok(result)
671 }
672
673 pub fn add_or_update(
674 &self,
675 entry: LoginEntry,
676 encdec: &dyn EncryptorDecryptor,
677 ) -> Result<EncryptedLogin> {
678 let entry = entry.fixup()?;
680 match self.find_login_to_update(entry.clone(), encdec)? {
681 Some(login) => self.update(&login.id, entry, encdec),
682 None => self.add(entry, encdec),
683 }
684 }
685
686 pub fn fixup_and_check_for_dupes(
687 &self,
688 guid: &Guid,
689 entry: LoginEntry,
690 encdec: &dyn EncryptorDecryptor,
691 ) -> Result<LoginEntry> {
692 let entry = entry.fixup()?;
693 self.check_for_dupes(guid, &entry, encdec)?;
694 Ok(entry)
695 }
696
697 pub fn check_for_dupes(
698 &self,
699 guid: &Guid,
700 entry: &LoginEntry,
701 encdec: &dyn EncryptorDecryptor,
702 ) -> Result<()> {
703 if self.dupe_exists(guid, entry, encdec)? {
704 return Err(InvalidLogin::DuplicateLogin.into());
705 }
706 Ok(())
707 }
708
709 pub fn dupe_exists(
710 &self,
711 guid: &Guid,
712 entry: &LoginEntry,
713 encdec: &dyn EncryptorDecryptor,
714 ) -> Result<bool> {
715 Ok(self.find_dupe(guid, entry, encdec)?.is_some())
716 }
717
718 pub fn find_dupe(
719 &self,
720 guid: &Guid,
721 entry: &LoginEntry,
722 encdec: &dyn EncryptorDecryptor,
723 ) -> Result<Option<Guid>> {
724 for possible in self.get_by_entry_target(entry)? {
725 if possible.guid() != *guid {
726 let pos_sec_fields = possible.decrypt_fields(encdec)?;
727 if pos_sec_fields.username == entry.username {
728 return Ok(Some(possible.guid()));
729 }
730 }
731 }
732 Ok(None)
733 }
734
735 fn get_by_entry_target(&self, entry: &LoginEntry) -> Result<Vec<EncryptedLogin>> {
745 lazy_static::lazy_static! {
747 static ref GET_BY_FORM_ACTION_ORIGIN: String = format!(
748 "SELECT {common_cols} FROM loginsL
749 WHERE is_deleted = 0
750 AND origin = :origin
751 AND formActionOrigin = :form_action_origin
752
753 UNION ALL
754
755 SELECT {common_cols} FROM loginsM
756 WHERE is_overridden = 0
757 AND origin = :origin
758 AND formActionOrigin = :form_action_origin
759 ",
760 common_cols = schema::COMMON_COLS
761 );
762 static ref GET_BY_HTTP_REALM: String = format!(
763 "SELECT {common_cols} FROM loginsL
764 WHERE is_deleted = 0
765 AND origin = :origin
766 AND httpRealm = :http_realm
767
768 UNION ALL
769
770 SELECT {common_cols} FROM loginsM
771 WHERE is_overridden = 0
772 AND origin = :origin
773 AND httpRealm = :http_realm
774 ",
775 common_cols = schema::COMMON_COLS
776 );
777 }
778 match (entry.form_action_origin.as_ref(), entry.http_realm.as_ref()) {
779 (Some(form_action_origin), None) => {
780 let params = named_params! {
781 ":origin": &entry.origin,
782 ":form_action_origin": form_action_origin,
783 };
784 self.db
785 .prepare_cached(&GET_BY_FORM_ACTION_ORIGIN)?
786 .query_and_then(params, EncryptedLogin::from_row)?
787 .collect()
788 }
789 (None, Some(http_realm)) => {
790 let params = named_params! {
791 ":origin": &entry.origin,
792 ":http_realm": http_realm,
793 };
794 self.db
795 .prepare_cached(&GET_BY_HTTP_REALM)?
796 .query_and_then(params, EncryptedLogin::from_row)?
797 .collect()
798 }
799 (Some(_), Some(_)) => Err(InvalidLogin::BothTargets.into()),
800 (None, None) => Err(InvalidLogin::NoTarget.into()),
801 }
802 }
803
804 pub fn exists(&self, id: &str) -> Result<bool> {
805 Ok(self.db.query_row(
806 "SELECT EXISTS(
807 SELECT 1 FROM loginsL
808 WHERE guid = :guid AND is_deleted = 0
809 UNION ALL
810 SELECT 1 FROM loginsM
811 WHERE guid = :guid AND is_overridden IS NOT 1
812 )",
813 named_params! { ":guid": id },
814 |row| row.get(0),
815 )?)
816 }
817
818 pub fn delete(&self, id: &str) -> Result<bool> {
821 let mut results = self.delete_many(vec![id])?;
822 Ok(results.pop().expect("there should be a single result"))
823 }
824
825 pub fn delete_many(&self, ids: Vec<&str>) -> Result<Vec<bool>> {
828 let tx = self.unchecked_transaction_imm()?;
829 let sql = format!(
830 "
831 UPDATE loginsL
832 SET local_modified = :now_ms,
833 sync_status = {status_changed},
834 is_deleted = 1,
835 secFields = '',
836 origin = '',
837 httpRealm = NULL,
838 formActionOrigin = NULL
839 WHERE guid = :guid AND is_deleted IS FALSE
840 ",
841 status_changed = SyncStatus::Changed as u8
842 );
843 let mut stmt = self.db.prepare_cached(&sql)?;
844
845 let mut result = vec![];
846
847 for id in ids {
848 let now_ms = util::system_time_ms_i64(SystemTime::now());
849
850 let update_result = stmt.execute(named_params! { ":now_ms": now_ms, ":guid": id })?;
852
853 let exists = update_result == 1;
854
855 self.execute(
857 "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
858 named_params! { ":guid": id },
859 )?;
860
861 self.execute(&format!("
864 INSERT OR IGNORE INTO loginsL
865 (guid, local_modified, is_deleted, sync_status, origin, timeCreated, timePasswordChanged, secFields)
866 SELECT guid, :now_ms, 1, {changed}, '', timeCreated, :now_ms, ''
867 FROM loginsM
868 WHERE guid = :guid",
869 changed = SyncStatus::Changed as u8),
870 named_params! { ":now_ms": now_ms, ":guid": id })?;
871
872 result.push(exists);
873 }
874
875 tx.commit()?;
876
877 Ok(result)
878 }
879
880 pub fn delete_undecryptable_records_for_remote_replacement(
881 &self,
882 encdec: &dyn EncryptorDecryptor,
883 ) -> Result<LoginsDeletionMetrics> {
884 let corrupted_logins = self
886 .get_all()?
887 .into_iter()
888 .filter(|login| login.clone().decrypt(encdec).is_err())
889 .collect::<Vec<_>>();
890 let ids = corrupted_logins
891 .iter()
892 .map(|login| login.guid_str())
893 .collect::<Vec<_>>();
894
895 self.delete_local_records_for_remote_replacement(ids)
896 }
897
898 pub fn delete_local_records_for_remote_replacement(
899 &self,
900 ids: Vec<&str>,
901 ) -> Result<LoginsDeletionMetrics> {
902 let tx = self.unchecked_transaction_imm()?;
903 let mut local_deleted = 0;
904 let mut mirror_deleted = 0;
905
906 sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
907 let deleted = self.execute(
908 &format!(
909 "DELETE FROM loginsL WHERE guid IN ({})",
910 sql_support::repeat_sql_values(chunk.len())
911 ),
912 rusqlite::params_from_iter(chunk),
913 )?;
914 local_deleted += deleted;
915 Ok(())
916 })?;
917
918 sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
919 let deleted = self.execute(
920 &format!(
921 "DELETE FROM loginsM WHERE guid IN ({})",
922 sql_support::repeat_sql_values(chunk.len())
923 ),
924 rusqlite::params_from_iter(chunk),
925 )?;
926 mirror_deleted += deleted;
927 Ok(())
928 })?;
929
930 tx.commit()?;
931 Ok(LoginsDeletionMetrics {
932 local_deleted: local_deleted as u64,
933 mirror_deleted: mirror_deleted as u64,
934 })
935 }
936
937 fn mark_mirror_overridden(&self, guid: &str) -> Result<()> {
938 self.execute_cached(
939 "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
940 named_params! { ":guid": guid },
941 )?;
942 Ok(())
943 }
944
945 fn ensure_local_overlay_exists(&self, guid: &str) -> Result<()> {
946 let already_have_local: bool = self.db.query_row(
947 "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid)",
948 named_params! { ":guid": guid },
949 |row| row.get(0),
950 )?;
951
952 if already_have_local {
953 return Ok(());
954 }
955
956 debug!("No overlay; cloning one for {:?}.", guid);
957 let changed = self.clone_mirror_to_overlay(guid)?;
958 if changed == 0 {
959 report_error!(
960 "logins-local-overlay-error",
961 "Failed to create local overlay for GUID {guid:?}."
962 );
963 return Err(Error::NoSuchRecord(guid.to_owned()));
964 }
965 Ok(())
966 }
967
968 fn clone_mirror_to_overlay(&self, guid: &str) -> Result<usize> {
969 Ok(self.execute_cached(&CLONE_SINGLE_MIRROR_SQL, &[(":guid", &guid as &dyn ToSql)])?)
970 }
971
972 pub fn wipe_local(&self) -> Result<usize> {
974 info!("Executing wipe_local on password engine!");
975 let tx = self.unchecked_transaction()?;
976 let mut row_count = 0;
977 row_count += self.execute("DELETE FROM loginsL", [])?;
978 row_count += self.execute("DELETE FROM loginsM", [])?;
979 row_count += self.execute("DELETE FROM loginsSyncMeta", [])?;
980 tx.commit()?;
981 Ok(row_count)
982 }
983
984 pub fn shutdown(self) -> Result<()> {
985 self.db.close().map_err(|(_, e)| Error::SqlError(e))
986 }
987}
988
989lazy_static! {
990 static ref GET_ALL_SQL: String = format!(
991 "SELECT {common_cols} FROM loginsL WHERE is_deleted = 0
992 UNION ALL
993 SELECT {common_cols} FROM loginsM WHERE is_overridden = 0",
994 common_cols = schema::COMMON_COLS,
995 );
996 static ref COUNT_ALL_SQL: String = format!(
997 "SELECT COUNT(*) FROM (
998 SELECT guid FROM loginsL WHERE is_deleted = 0
999 UNION ALL
1000 SELECT guid FROM loginsM WHERE is_overridden = 0
1001 )"
1002 );
1003 static ref COUNT_BY_ORIGIN_SQL: String = format!(
1004 "SELECT COUNT(*) FROM (
1005 SELECT guid FROM loginsL WHERE is_deleted = 0 AND origin = :origin
1006 UNION ALL
1007 SELECT guid FROM loginsM WHERE is_overridden = 0 AND origin = :origin
1008 )"
1009 );
1010 static ref COUNT_BY_FORM_ACTION_ORIGIN_SQL: String = format!(
1011 "SELECT COUNT(*) FROM (
1012 SELECT guid FROM loginsL WHERE is_deleted = 0 AND formActionOrigin = :form_action_origin
1013 UNION ALL
1014 SELECT guid FROM loginsM WHERE is_overridden = 0 AND formActionOrigin = :form_action_origin
1015 )"
1016 );
1017 static ref GET_BY_GUID_SQL: String = format!(
1018 "SELECT {common_cols}
1019 FROM loginsL
1020 WHERE is_deleted = 0
1021 AND guid = :guid
1022
1023 UNION ALL
1024
1025 SELECT {common_cols}
1026 FROM loginsM
1027 WHERE is_overridden IS NOT 1
1028 AND guid = :guid
1029 ORDER BY origin ASC
1030
1031 LIMIT 1",
1032 common_cols = schema::COMMON_COLS,
1033 );
1034 pub static ref CLONE_ENTIRE_MIRROR_SQL: String = format!(
1035 "INSERT OR IGNORE INTO loginsL ({common_cols}, local_modified, is_deleted, sync_status)
1036 SELECT {common_cols}, NULL AS local_modified, 0 AS is_deleted, 0 AS sync_status
1037 FROM loginsM",
1038 common_cols = schema::COMMON_COLS,
1039 );
1040 static ref CLONE_SINGLE_MIRROR_SQL: String =
1041 format!("{} WHERE guid = :guid", &*CLONE_ENTIRE_MIRROR_SQL,);
1042}
1043
1044#[cfg(not(feature = "keydb"))]
1045#[cfg(test)]
1046pub mod test_utils {
1047 use super::*;
1048 use crate::encryption::test_utils::decrypt_struct;
1049 use crate::login::test_utils::enc_login;
1050 use crate::SecureLoginFields;
1051 use sync15::ServerTimestamp;
1052
1053 pub fn insert_login(
1057 db: &LoginDb,
1058 guid: &str,
1059 local_login: Option<&str>,
1060 mirror_login: Option<&str>,
1061 ) {
1062 if let Some(password) = mirror_login {
1063 add_mirror(
1064 db,
1065 &enc_login(guid, password),
1066 &ServerTimestamp(util::system_time_ms_i64(std::time::SystemTime::now())),
1067 local_login.is_some(),
1068 )
1069 .unwrap();
1070 }
1071 if let Some(password) = local_login {
1072 db.insert_new_login(&enc_login(guid, password)).unwrap();
1073 }
1074 }
1075
1076 pub fn insert_encrypted_login(
1077 db: &LoginDb,
1078 local: &EncryptedLogin,
1079 mirror: &EncryptedLogin,
1080 server_modified: &ServerTimestamp,
1081 ) {
1082 db.insert_new_login(local).unwrap();
1083 add_mirror(db, mirror, server_modified, true).unwrap();
1084 }
1085
1086 pub fn add_mirror(
1087 db: &LoginDb,
1088 login: &EncryptedLogin,
1089 server_modified: &ServerTimestamp,
1090 is_overridden: bool,
1091 ) -> Result<()> {
1092 let sql = "
1093 INSERT OR IGNORE INTO loginsM (
1094 is_overridden,
1095 server_modified,
1096
1097 httpRealm,
1098 formActionOrigin,
1099 usernameField,
1100 passwordField,
1101 secFields,
1102 origin,
1103
1104 timesUsed,
1105 timeLastUsed,
1106 timePasswordChanged,
1107 timeCreated,
1108
1109 timeOfLastBreach,
1110 timeLastBreachAlertDismissed,
1111
1112 guid
1113 ) VALUES (
1114 :is_overridden,
1115 :server_modified,
1116
1117 :http_realm,
1118 :form_action_origin,
1119 :username_field,
1120 :password_field,
1121 :sec_fields,
1122 :origin,
1123
1124 :times_used,
1125 :time_last_used,
1126 :time_password_changed,
1127 :time_created,
1128
1129 :time_of_last_breach,
1130 :time_last_breach_alert_dismissed,
1131
1132 :guid
1133 )";
1134 let mut stmt = db.prepare_cached(sql)?;
1135
1136 stmt.execute(named_params! {
1137 ":is_overridden": is_overridden,
1138 ":server_modified": server_modified.as_millis(),
1139 ":http_realm": login.fields.http_realm,
1140 ":form_action_origin": login.fields.form_action_origin,
1141 ":username_field": login.fields.username_field,
1142 ":password_field": login.fields.password_field,
1143 ":origin": login.fields.origin,
1144 ":sec_fields": login.sec_fields,
1145 ":times_used": login.meta.times_used,
1146 ":time_last_used": login.meta.time_last_used,
1147 ":time_password_changed": login.meta.time_password_changed,
1148 ":time_created": login.meta.time_created,
1149 ":time_of_last_breach": login.fields.time_of_last_breach,
1150 ":time_last_breach_alert_dismissed": login.fields.time_last_breach_alert_dismissed,
1151 ":guid": login.guid_str(),
1152 })?;
1153 Ok(())
1154 }
1155
1156 pub fn get_local_guids(db: &LoginDb) -> Vec<String> {
1157 get_guids(db, "SELECT guid FROM loginsL")
1158 }
1159
1160 pub fn get_mirror_guids(db: &LoginDb) -> Vec<String> {
1161 get_guids(db, "SELECT guid FROM loginsM")
1162 }
1163
1164 fn get_guids(db: &LoginDb, sql: &str) -> Vec<String> {
1165 let mut stmt = db.prepare_cached(sql).unwrap();
1166 let mut res: Vec<String> = stmt
1167 .query_map([], |r| r.get(0))
1168 .unwrap()
1169 .map(|r| r.unwrap())
1170 .collect();
1171 res.sort();
1172 res
1173 }
1174
1175 pub fn get_server_modified(db: &LoginDb, guid: &str) -> i64 {
1176 db.conn_ext_query_one(&format!(
1177 "SELECT server_modified FROM loginsM WHERE guid='{}'",
1178 guid
1179 ))
1180 .unwrap()
1181 }
1182
1183 pub fn check_local_login(db: &LoginDb, guid: &str, password: &str, local_modified_gte: i64) {
1184 let row: (String, i64, bool) = db
1185 .query_row(
1186 "SELECT secFields, local_modified, is_deleted FROM loginsL WHERE guid=?",
1187 [guid],
1188 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1189 )
1190 .unwrap();
1191 let enc: SecureLoginFields = decrypt_struct(row.0);
1192 assert_eq!(enc.password, password);
1193 assert!(row.1 >= local_modified_gte);
1194 assert!(!row.2);
1195 }
1196
1197 pub fn check_mirror_login(
1198 db: &LoginDb,
1199 guid: &str,
1200 password: &str,
1201 server_modified: i64,
1202 is_overridden: bool,
1203 ) {
1204 let row: (String, i64, bool) = db
1205 .query_row(
1206 "SELECT secFields, server_modified, is_overridden FROM loginsM WHERE guid=?",
1207 [guid],
1208 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1209 )
1210 .unwrap();
1211 let enc: SecureLoginFields = decrypt_struct(row.0);
1212 assert_eq!(enc.password, password);
1213 assert_eq!(row.1, server_modified);
1214 assert_eq!(row.2, is_overridden);
1215 }
1216}
1217
1218#[cfg(not(feature = "keydb"))]
1219#[cfg(test)]
1220mod tests {
1221 use super::*;
1222 use crate::db::test_utils::{get_local_guids, get_mirror_guids};
1223 use crate::encryption::test_utils::TEST_ENCDEC;
1224 use crate::sync::merge::LocalLogin;
1225 use nss::ensure_initialized;
1226 use std::{thread, time};
1227
1228 #[test]
1229 fn test_username_dupe_semantics() {
1230 ensure_initialized();
1231 let mut login = LoginEntry {
1232 origin: "https://www.example.com".into(),
1233 http_realm: Some("https://www.example.com".into()),
1234 username: "test".into(),
1235 password: "sekret".into(),
1236 ..LoginEntry::default()
1237 };
1238
1239 let db = LoginDb::open_in_memory();
1240 db.add(login.clone(), &*TEST_ENCDEC)
1241 .expect("should be able to add first login");
1242
1243 let exp_err = "Invalid login: Login already exists";
1245 assert_eq!(
1246 db.add(login.clone(), &*TEST_ENCDEC)
1247 .unwrap_err()
1248 .to_string(),
1249 exp_err
1250 );
1251
1252 login.username = "".to_string();
1254 db.add(login.clone(), &*TEST_ENCDEC)
1255 .expect("empty login isn't a dupe");
1256
1257 assert_eq!(
1258 db.add(login, &*TEST_ENCDEC).unwrap_err().to_string(),
1259 exp_err
1260 );
1261
1262 assert_eq!(db.get_all().unwrap().len(), 2);
1264 }
1265
1266 #[test]
1267 fn test_add_many() {
1268 ensure_initialized();
1269
1270 let login_a = LoginEntry {
1271 origin: "https://a.example.com".into(),
1272 http_realm: Some("https://www.example.com".into()),
1273 username: "test".into(),
1274 password: "sekret".into(),
1275 ..LoginEntry::default()
1276 };
1277
1278 let login_b = LoginEntry {
1279 origin: "https://b.example.com".into(),
1280 http_realm: Some("https://www.example.com".into()),
1281 username: "test".into(),
1282 password: "sekret".into(),
1283 ..LoginEntry::default()
1284 };
1285
1286 let db = LoginDb::open_in_memory();
1287 let added = db
1288 .add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1289 .expect("should be able to add logins");
1290
1291 let [added_a, added_b] = added.as_slice() else {
1292 panic!("there should really be 2")
1293 };
1294
1295 let fetched_a = db
1296 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1297 .expect("should work")
1298 .expect("should get a record");
1299
1300 assert_eq!(fetched_a.fields.origin, login_a.origin);
1301
1302 let fetched_b = db
1303 .get_by_id(&added_b.as_ref().unwrap().meta.id)
1304 .expect("should work")
1305 .expect("should get a record");
1306
1307 assert_eq!(fetched_b.fields.origin, login_b.origin);
1308
1309 assert_eq!(db.count_all().unwrap(), 2);
1310 }
1311
1312 #[test]
1313 fn test_count_by_origin() {
1314 ensure_initialized();
1315
1316 let origin_a = "https://a.example.com";
1317 let login_a = LoginEntry {
1318 origin: origin_a.into(),
1319 http_realm: Some("https://www.example.com".into()),
1320 username: "test".into(),
1321 password: "sekret".into(),
1322 ..LoginEntry::default()
1323 };
1324
1325 let login_b = LoginEntry {
1326 origin: "https://b.example.com".into(),
1327 http_realm: Some("https://www.example.com".into()),
1328 username: "test".into(),
1329 password: "sekret".into(),
1330 ..LoginEntry::default()
1331 };
1332
1333 let origin_umlaut = "https://bücher.example.com";
1334 let login_umlaut = LoginEntry {
1335 origin: origin_umlaut.into(),
1336 http_realm: Some("https://www.example.com".into()),
1337 username: "test".into(),
1338 password: "sekret".into(),
1339 ..LoginEntry::default()
1340 };
1341
1342 let db = LoginDb::open_in_memory();
1343 db.add_many(
1344 vec![login_a.clone(), login_b.clone(), login_umlaut.clone()],
1345 &*TEST_ENCDEC,
1346 )
1347 .expect("should be able to add logins");
1348
1349 assert_eq!(db.count_by_origin(origin_a).unwrap(), 1);
1350 assert_eq!(db.count_by_origin(origin_umlaut).unwrap(), 1);
1351 }
1352
1353 #[test]
1354 fn test_count_by_form_action_origin() {
1355 ensure_initialized();
1356
1357 let origin_a = "https://a.example.com";
1358 let login_a = LoginEntry {
1359 origin: origin_a.into(),
1360 form_action_origin: Some(origin_a.into()),
1361 http_realm: Some("https://www.example.com".into()),
1362 username: "test".into(),
1363 password: "sekret".into(),
1364 ..LoginEntry::default()
1365 };
1366
1367 let login_b = LoginEntry {
1368 origin: "https://b.example.com".into(),
1369 form_action_origin: Some("https://b.example.com".into()),
1370 http_realm: Some("https://www.example.com".into()),
1371 username: "test".into(),
1372 password: "sekret".into(),
1373 ..LoginEntry::default()
1374 };
1375
1376 let origin_umlaut = "https://bücher.example.com";
1377 let login_umlaut = LoginEntry {
1378 origin: origin_umlaut.into(),
1379 form_action_origin: Some(origin_umlaut.into()),
1380 http_realm: Some("https://www.example.com".into()),
1381 username: "test".into(),
1382 password: "sekret".into(),
1383 ..LoginEntry::default()
1384 };
1385
1386 let db = LoginDb::open_in_memory();
1387 db.add_many(
1388 vec![login_a.clone(), login_b.clone(), login_umlaut.clone()],
1389 &*TEST_ENCDEC,
1390 )
1391 .expect("should be able to add logins");
1392
1393 assert_eq!(db.count_by_form_action_origin(origin_a).unwrap(), 1);
1394 assert_eq!(db.count_by_form_action_origin(origin_umlaut).unwrap(), 1);
1395 }
1396
1397 #[test]
1398 fn test_add_many_with_failed_constraint() {
1399 ensure_initialized();
1400
1401 let login_a = LoginEntry {
1402 origin: "https://example.com".into(),
1403 http_realm: Some("https://www.example.com".into()),
1404 username: "test".into(),
1405 password: "sekret".into(),
1406 ..LoginEntry::default()
1407 };
1408
1409 let login_b = LoginEntry {
1410 origin: "https://example.com".into(),
1412 http_realm: Some("https://www.example.com".into()),
1413 username: "test".into(),
1414 password: "sekret".into(),
1415 ..LoginEntry::default()
1416 };
1417
1418 let db = LoginDb::open_in_memory();
1419 let added = db
1420 .add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1421 .expect("should be able to add logins");
1422
1423 let [added_a, added_b] = added.as_slice() else {
1424 panic!("there should really be 2")
1425 };
1426
1427 let fetched_a = db
1429 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1430 .expect("should work")
1431 .expect("should get a record");
1432
1433 assert_eq!(fetched_a.fields.origin, login_a.origin);
1434
1435 assert!(!added_b.is_ok());
1437 }
1438
1439 #[test]
1440 fn test_add_with_meta() {
1441 ensure_initialized();
1442
1443 let guid = Guid::random();
1444 let now_ms = util::system_time_ms_i64(SystemTime::now());
1445 let login = LoginEntry {
1446 origin: "https://www.example.com".into(),
1447 http_realm: Some("https://www.example.com".into()),
1448 username: "test".into(),
1449 password: "sekret".into(),
1450 ..LoginEntry::default()
1451 };
1452 let meta = LoginMeta {
1453 id: guid.to_string(),
1454 time_created: now_ms,
1455 time_password_changed: now_ms + 100,
1456 time_last_used: now_ms + 10,
1457 times_used: 42,
1458 };
1459
1460 let db = LoginDb::open_in_memory();
1461 let entry_with_meta = LoginEntryWithMeta {
1462 entry: login.clone(),
1463 meta: meta.clone(),
1464 };
1465
1466 db.add_with_meta(entry_with_meta, &*TEST_ENCDEC)
1467 .expect("should be able to add login with record");
1468
1469 let fetched = db
1470 .get_by_id(&guid)
1471 .expect("should work")
1472 .expect("should get a record");
1473
1474 assert_eq!(fetched.meta, meta);
1475 }
1476
1477 #[test]
1478 fn test_add_with_meta_deleted() {
1479 ensure_initialized();
1480
1481 let guid = Guid::random();
1482 let now_ms = util::system_time_ms_i64(SystemTime::now());
1483 let login = LoginEntry {
1484 origin: "https://www.example.com".into(),
1485 http_realm: Some("https://www.example.com".into()),
1486 username: "test".into(),
1487 password: "sekret".into(),
1488 ..LoginEntry::default()
1489 };
1490 let meta = LoginMeta {
1491 id: guid.to_string(),
1492 time_created: now_ms,
1493 time_password_changed: now_ms + 100,
1494 time_last_used: now_ms + 10,
1495 times_used: 42,
1496 };
1497
1498 let db = LoginDb::open_in_memory();
1499 let entry_with_meta = LoginEntryWithMeta {
1500 entry: login.clone(),
1501 meta: meta.clone(),
1502 };
1503
1504 db.add_with_meta(entry_with_meta, &*TEST_ENCDEC)
1505 .expect("should be able to add login with record");
1506
1507 db.delete(&guid).expect("should be able to delete login");
1508
1509 let entry_with_meta2 = LoginEntryWithMeta {
1510 entry: login.clone(),
1511 meta: meta.clone(),
1512 };
1513
1514 db.add_with_meta(entry_with_meta2, &*TEST_ENCDEC)
1515 .expect("should be able to re-add login with record");
1516
1517 let fetched = db
1518 .get_by_id(&guid)
1519 .expect("should work")
1520 .expect("should get a record");
1521
1522 assert_eq!(fetched.meta, meta);
1523 }
1524
1525 #[test]
1526 fn test_unicode_submit() {
1527 ensure_initialized();
1528 let db = LoginDb::open_in_memory();
1529 let added = db
1530 .add(
1531 LoginEntry {
1532 form_action_origin: Some("http://😍.com".into()),
1533 origin: "http://😍.com".into(),
1534 http_realm: None,
1535 username_field: "😍".into(),
1536 password_field: "😍".into(),
1537 username: "😍".into(),
1538 password: "😍".into(),
1539 },
1540 &*TEST_ENCDEC,
1541 )
1542 .unwrap();
1543 let fetched = db
1544 .get_by_id(&added.meta.id)
1545 .expect("should work")
1546 .expect("should get a record");
1547 assert_eq!(added, fetched);
1548 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1549 assert_eq!(
1550 fetched.fields.form_action_origin,
1551 Some("http://xn--r28h.com".to_string())
1552 );
1553 assert_eq!(fetched.fields.username_field, "😍");
1554 assert_eq!(fetched.fields.password_field, "😍");
1555 let sec_fields = fetched.decrypt_fields(&*TEST_ENCDEC).unwrap();
1556 assert_eq!(sec_fields.username, "😍");
1557 assert_eq!(sec_fields.password, "😍");
1558 }
1559
1560 #[test]
1561 fn test_unicode_realm() {
1562 ensure_initialized();
1563 let db = LoginDb::open_in_memory();
1564 let added = db
1565 .add(
1566 LoginEntry {
1567 form_action_origin: None,
1568 origin: "http://😍.com".into(),
1569 http_realm: Some("😍😍".into()),
1570 username: "😍".into(),
1571 password: "😍".into(),
1572 ..Default::default()
1573 },
1574 &*TEST_ENCDEC,
1575 )
1576 .unwrap();
1577 let fetched = db
1578 .get_by_id(&added.meta.id)
1579 .expect("should work")
1580 .expect("should get a record");
1581 assert_eq!(added, fetched);
1582 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1583 assert_eq!(fetched.fields.http_realm.unwrap(), "😍😍");
1584 }
1585
1586 fn check_matches(db: &LoginDb, query: &str, expected: &[&str]) {
1587 let mut results = db
1588 .get_by_base_domain(query)
1589 .unwrap()
1590 .into_iter()
1591 .map(|l| l.fields.origin)
1592 .collect::<Vec<String>>();
1593 results.sort_unstable();
1594 let mut sorted = expected.to_owned();
1595 sorted.sort_unstable();
1596 assert_eq!(sorted, results);
1597 }
1598
1599 fn check_good_bad(
1600 good: Vec<&str>,
1601 bad: Vec<&str>,
1602 good_queries: Vec<&str>,
1603 zero_queries: Vec<&str>,
1604 ) {
1605 let db = LoginDb::open_in_memory();
1606 for h in good.iter().chain(bad.iter()) {
1607 db.add(
1608 LoginEntry {
1609 origin: (*h).into(),
1610 http_realm: Some((*h).into()),
1611 password: "test".into(),
1612 ..Default::default()
1613 },
1614 &*TEST_ENCDEC,
1615 )
1616 .unwrap();
1617 }
1618 for query in good_queries {
1619 check_matches(&db, query, &good);
1620 }
1621 for query in zero_queries {
1622 check_matches(&db, query, &[]);
1623 }
1624 }
1625
1626 #[test]
1627 fn test_get_by_base_domain_invalid() {
1628 check_good_bad(
1629 vec!["https://example.com"],
1630 vec![],
1631 vec![],
1632 vec!["invalid query"],
1633 );
1634 }
1635
1636 #[test]
1637 fn test_get_by_base_domain() {
1638 check_good_bad(
1639 vec![
1640 "https://example.com",
1641 "https://www.example.com",
1642 "http://www.example.com",
1643 "http://www.example.com:8080",
1644 "http://sub.example.com:8080",
1645 "https://sub.example.com:8080",
1646 "https://sub.sub.example.com",
1647 "ftp://sub.example.com",
1648 ],
1649 vec![
1650 "https://badexample.com",
1651 "https://example.co",
1652 "https://example.com.au",
1653 ],
1654 vec!["example.com"],
1655 vec!["foo.com"],
1656 );
1657 }
1658
1659 #[test]
1660 fn test_get_by_base_domain_punicode() {
1661 check_good_bad(
1664 vec![
1665 "http://xn--r28h.com", ],
1667 vec!["http://💖.com"],
1668 vec!["😍.com", "xn--r28h.com"],
1669 vec![],
1670 );
1671 }
1672
1673 #[test]
1674 fn test_get_by_base_domain_ipv4() {
1675 check_good_bad(
1676 vec!["http://127.0.0.1", "https://127.0.0.1:8000"],
1677 vec!["https://127.0.0.0", "https://example.com"],
1678 vec!["127.0.0.1"],
1679 vec!["127.0.0.2"],
1680 );
1681 }
1682
1683 #[test]
1684 fn test_get_by_base_domain_ipv6() {
1685 check_good_bad(
1686 vec!["http://[::1]", "https://[::1]:8000"],
1687 vec!["https://[0:0:0:0:0:0:1:1]", "https://example.com"],
1688 vec!["[::1]", "[0:0:0:0:0:0:0:1]"],
1689 vec!["[0:0:0:0:0:0:1:2]"],
1690 );
1691 }
1692
1693 #[test]
1694 fn test_add() {
1695 ensure_initialized();
1696 let db = LoginDb::open_in_memory();
1697 let to_add = LoginEntry {
1698 origin: "https://www.example.com".into(),
1699 http_realm: Some("https://www.example.com".into()),
1700 username: "test_user".into(),
1701 password: "test_password".into(),
1702 ..Default::default()
1703 };
1704 let login = db.add(to_add, &*TEST_ENCDEC).unwrap();
1705 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1706
1707 assert_eq!(login.fields.origin, login2.fields.origin);
1708 assert_eq!(login.fields.http_realm, login2.fields.http_realm);
1709 assert_eq!(login.sec_fields, login2.sec_fields);
1710 }
1711
1712 #[test]
1713 fn test_update() {
1714 ensure_initialized();
1715 let db = LoginDb::open_in_memory();
1716 let login = db
1717 .add(
1718 LoginEntry {
1719 origin: "https://www.example.com".into(),
1720 http_realm: Some("https://www.example.com".into()),
1721 username: "user1".into(),
1722 password: "password1".into(),
1723 ..Default::default()
1724 },
1725 &*TEST_ENCDEC,
1726 )
1727 .unwrap();
1728 db.update(
1729 &login.meta.id,
1730 LoginEntry {
1731 origin: "https://www.example2.com".into(),
1732 http_realm: Some("https://www.example2.com".into()),
1733 username: "user2".into(),
1734 password: "password2".into(),
1735 ..Default::default() },
1737 &*TEST_ENCDEC,
1738 )
1739 .unwrap();
1740
1741 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1742
1743 assert_eq!(login2.fields.origin, "https://www.example2.com");
1744 assert_eq!(
1745 login2.fields.http_realm,
1746 Some("https://www.example2.com".into())
1747 );
1748 let sec_fields = login2.decrypt_fields(&*TEST_ENCDEC).unwrap();
1749 assert_eq!(sec_fields.username, "user2");
1750 assert_eq!(sec_fields.password, "password2");
1751 }
1752
1753 #[test]
1754 fn test_touch() {
1755 ensure_initialized();
1756 let db = LoginDb::open_in_memory();
1757 let login = db
1758 .add(
1759 LoginEntry {
1760 origin: "https://www.example.com".into(),
1761 http_realm: Some("https://www.example.com".into()),
1762 username: "user1".into(),
1763 password: "password1".into(),
1764 ..Default::default()
1765 },
1766 &*TEST_ENCDEC,
1767 )
1768 .unwrap();
1769 thread::sleep(time::Duration::from_millis(50));
1771 db.touch(&login.meta.id).unwrap();
1772 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1773 assert!(login2.meta.time_last_used > login.meta.time_last_used);
1774 assert_eq!(login2.meta.times_used, login.meta.times_used + 1);
1775 }
1776
1777 #[test]
1778 fn test_breach_alerts() {
1779 ensure_initialized();
1780 let db = LoginDb::open_in_memory();
1781 let login = db
1782 .add(
1783 LoginEntry {
1784 origin: "https://www.example.com".into(),
1785 http_realm: Some("https://www.example.com".into()),
1786 username: "user1".into(),
1787 password: "password1".into(),
1788 ..Default::default()
1789 },
1790 &*TEST_ENCDEC,
1791 )
1792 .unwrap();
1793 assert!(login.fields.time_of_last_breach.is_none());
1795 assert!(!db.is_potentially_breached(&login.meta.id).unwrap());
1796 assert!(login.fields.time_last_breach_alert_dismissed.is_none());
1797
1798 thread::sleep(time::Duration::from_millis(50));
1800 let breach_time = util::system_time_ms_i64(SystemTime::now());
1801 db.record_breach(&login.meta.id, breach_time).unwrap();
1802 assert!(db.is_potentially_breached(&login.meta.id).unwrap());
1803 let login1 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1804 assert!(login1.fields.time_of_last_breach.is_some());
1805
1806 db.record_breach_alert_dismissal(&login.meta.id).unwrap();
1808 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1809 assert!(login2.fields.time_last_breach_alert_dismissed.is_some());
1810
1811 db.reset_all_breaches().unwrap();
1813 assert!(!db.is_potentially_breached(&login.meta.id).unwrap());
1814 let login3 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1815 assert!(login3.fields.time_of_last_breach.is_none());
1816
1817 thread::sleep(time::Duration::from_millis(50));
1819 let breach_time = util::system_time_ms_i64(SystemTime::now());
1820 db.record_breach(&login.meta.id, breach_time).unwrap();
1821 assert!(db.is_potentially_breached(&login.meta.id).unwrap());
1822
1823 db.update(
1825 &login.meta.id.clone(),
1826 LoginEntry {
1827 password: "changed-password".into(),
1828 ..login.clone().decrypt(&*TEST_ENCDEC).unwrap().entry()
1829 },
1830 &*TEST_ENCDEC,
1831 )
1832 .unwrap();
1833 assert!(!db.is_potentially_breached(&login.meta.id).unwrap());
1835 }
1836
1837 #[test]
1838 fn test_breach_alert_fields_not_overwritten_by_update() {
1839 ensure_initialized();
1840 let db = LoginDb::open_in_memory();
1841 let login = db
1842 .add(
1843 LoginEntry {
1844 origin: "https://www.example.com".into(),
1845 http_realm: Some("https://www.example.com".into()),
1846 username: "user1".into(),
1847 password: "password1".into(),
1848 ..Default::default()
1849 },
1850 &*TEST_ENCDEC,
1851 )
1852 .unwrap();
1853 assert!(!db.is_potentially_breached(&login.meta.id).unwrap());
1854
1855 thread::sleep(time::Duration::from_millis(50));
1857 let breach_time = util::system_time_ms_i64(SystemTime::now());
1858 db.record_breach(&login.meta.id, breach_time).unwrap();
1859 assert!(db.is_potentially_breached(&login.meta.id).unwrap());
1860
1861 db.update(
1863 &login.meta.id.clone(),
1864 LoginEntry {
1865 username_field: "changed-username-field".into(),
1866 ..login.clone().decrypt(&*TEST_ENCDEC).unwrap().entry()
1867 },
1868 &*TEST_ENCDEC,
1869 )
1870 .unwrap();
1871
1872 assert!(db.is_potentially_breached(&login.meta.id).unwrap());
1874 }
1875
1876 #[test]
1877 fn test_breach_alert_dismissal_with_specific_timestamp() {
1878 ensure_initialized();
1879 let db = LoginDb::open_in_memory();
1880 let login = db
1881 .add(
1882 LoginEntry {
1883 origin: "https://www.example.com".into(),
1884 http_realm: Some("https://www.example.com".into()),
1885 username: "user1".into(),
1886 password: "password1".into(),
1887 ..Default::default()
1888 },
1889 &*TEST_ENCDEC,
1890 )
1891 .unwrap();
1892
1893 let breach_time = login.meta.time_password_changed + 1000;
1896 db.record_breach(&login.meta.id, breach_time).unwrap();
1897 assert!(db.is_potentially_breached(&login.meta.id).unwrap());
1898
1899 let dismiss_time = breach_time + 500;
1901 db.record_breach_alert_dismissal_time(&login.meta.id, dismiss_time)
1902 .unwrap();
1903
1904 let retrieved = db
1906 .get_by_id(&login.meta.id)
1907 .unwrap()
1908 .unwrap()
1909 .decrypt(&*TEST_ENCDEC)
1910 .unwrap();
1911 assert_eq!(
1912 retrieved.time_last_breach_alert_dismissed,
1913 Some(dismiss_time)
1914 );
1915
1916 assert!(db.is_breach_alert_dismissed(&login.meta.id).unwrap());
1918
1919 let earlier_dismiss_time = breach_time - 100;
1921 db.record_breach_alert_dismissal_time(&login.meta.id, earlier_dismiss_time)
1922 .unwrap();
1923 assert!(!db.is_breach_alert_dismissed(&login.meta.id).unwrap());
1924 }
1925
1926 #[test]
1927 fn test_delete() {
1928 ensure_initialized();
1929 let db = LoginDb::open_in_memory();
1930 let login = db
1931 .add(
1932 LoginEntry {
1933 origin: "https://www.example.com".into(),
1934 http_realm: Some("https://www.example.com".into()),
1935 username: "test_user".into(),
1936 password: "test_password".into(),
1937 ..Default::default()
1938 },
1939 &*TEST_ENCDEC,
1940 )
1941 .unwrap();
1942
1943 assert!(db.delete(login.guid_str()).unwrap());
1944
1945 let local_login = db
1946 .query_row(
1947 "SELECT * FROM loginsL WHERE guid = :guid",
1948 named_params! { ":guid": login.guid_str() },
1949 |row| Ok(LocalLogin::test_raw_from_row(row).unwrap()),
1950 )
1951 .unwrap();
1952 assert_eq!(local_login.fields.http_realm, None);
1953 assert_eq!(local_login.fields.form_action_origin, None);
1954
1955 assert!(!db.exists(login.guid_str()).unwrap());
1956 }
1957
1958 #[test]
1959 fn test_delete_many() {
1960 ensure_initialized();
1961 let db = LoginDb::open_in_memory();
1962
1963 let login_a = db
1964 .add(
1965 LoginEntry {
1966 origin: "https://a.example.com".into(),
1967 http_realm: Some("https://www.example.com".into()),
1968 username: "test_user".into(),
1969 password: "test_password".into(),
1970 ..Default::default()
1971 },
1972 &*TEST_ENCDEC,
1973 )
1974 .unwrap();
1975
1976 let login_b = db
1977 .add(
1978 LoginEntry {
1979 origin: "https://b.example.com".into(),
1980 http_realm: Some("https://www.example.com".into()),
1981 username: "test_user".into(),
1982 password: "test_password".into(),
1983 ..Default::default()
1984 },
1985 &*TEST_ENCDEC,
1986 )
1987 .unwrap();
1988
1989 let result = db
1990 .delete_many(vec![login_a.guid_str(), login_b.guid_str()])
1991 .unwrap();
1992 assert!(result[0]);
1993 assert!(result[1]);
1994 assert!(!db.exists(login_a.guid_str()).unwrap());
1995 assert!(!db.exists(login_b.guid_str()).unwrap());
1996 }
1997
1998 #[test]
1999 fn test_subsequent_delete_many() {
2000 ensure_initialized();
2001 let db = LoginDb::open_in_memory();
2002
2003 let login = db
2004 .add(
2005 LoginEntry {
2006 origin: "https://a.example.com".into(),
2007 http_realm: Some("https://www.example.com".into()),
2008 username: "test_user".into(),
2009 password: "test_password".into(),
2010 ..Default::default()
2011 },
2012 &*TEST_ENCDEC,
2013 )
2014 .unwrap();
2015
2016 let result = db.delete_many(vec![login.guid_str()]).unwrap();
2017 assert!(result[0]);
2018 assert!(!db.exists(login.guid_str()).unwrap());
2019
2020 let result = db.delete_many(vec![login.guid_str()]).unwrap();
2021 assert!(!result[0]);
2022 }
2023
2024 #[test]
2025 fn test_delete_many_with_non_existent_id() {
2026 ensure_initialized();
2027 let db = LoginDb::open_in_memory();
2028
2029 let result = db.delete_many(vec![&Guid::random()]).unwrap();
2030 assert!(!result[0]);
2031 }
2032
2033 #[test]
2034 fn test_delete_local_for_remote_replacement() {
2035 ensure_initialized();
2036 let db = LoginDb::open_in_memory();
2037 let login = db
2038 .add(
2039 LoginEntry {
2040 origin: "https://www.example.com".into(),
2041 http_realm: Some("https://www.example.com".into()),
2042 username: "test_user".into(),
2043 password: "test_password".into(),
2044 ..Default::default()
2045 },
2046 &*TEST_ENCDEC,
2047 )
2048 .unwrap();
2049
2050 let result = db
2051 .delete_local_records_for_remote_replacement(vec![login.guid_str()])
2052 .unwrap();
2053
2054 let local_guids = get_local_guids(&db);
2055 assert_eq!(local_guids.len(), 0);
2056
2057 let mirror_guids = get_mirror_guids(&db);
2058 assert_eq!(mirror_guids.len(), 0);
2059
2060 assert_eq!(result.local_deleted, 1);
2061 }
2062
2063 mod test_find_login_to_update {
2064 use super::*;
2065
2066 fn make_entry(username: &str, password: &str) -> LoginEntry {
2067 LoginEntry {
2068 origin: "https://www.example.com".into(),
2069 http_realm: Some("the website".into()),
2070 username: username.into(),
2071 password: password.into(),
2072 ..Default::default()
2073 }
2074 }
2075
2076 fn make_saved_login(db: &LoginDb, username: &str, password: &str) -> Login {
2077 db.add(make_entry(username, password), &*TEST_ENCDEC)
2078 .unwrap()
2079 .decrypt(&*TEST_ENCDEC)
2080 .unwrap()
2081 }
2082
2083 #[test]
2084 fn test_match() {
2085 ensure_initialized();
2086 let db = LoginDb::open_in_memory();
2087 let login = make_saved_login(&db, "user", "pass");
2088 assert_eq!(
2089 Some(login),
2090 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2091 .unwrap(),
2092 );
2093 }
2094
2095 #[test]
2096 fn test_non_matches() {
2097 ensure_initialized();
2098 let db = LoginDb::open_in_memory();
2099 make_saved_login(&db, "other-user", "pass");
2101 db.add(
2103 LoginEntry {
2104 origin: "https://www.example.com".into(),
2105 http_realm: Some("the other website".into()),
2106 username: "user".into(),
2107 password: "pass".into(),
2108 ..Default::default()
2109 },
2110 &*TEST_ENCDEC,
2111 )
2112 .unwrap();
2113 db.add(
2115 LoginEntry {
2116 origin: "https://www.example.com".into(),
2117 form_action_origin: Some("https://www.example.com/".into()),
2118 username: "user".into(),
2119 password: "pass".into(),
2120 ..Default::default()
2121 },
2122 &*TEST_ENCDEC,
2123 )
2124 .unwrap();
2125 assert_eq!(
2126 None,
2127 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2128 .unwrap(),
2129 );
2130 }
2131
2132 #[test]
2133 fn test_match_blank_password() {
2134 ensure_initialized();
2135 let db = LoginDb::open_in_memory();
2136 let login = make_saved_login(&db, "", "pass");
2137 assert_eq!(
2138 Some(login),
2139 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2140 .unwrap(),
2141 );
2142 }
2143
2144 #[test]
2145 fn test_username_match_takes_precedence_over_blank_username() {
2146 ensure_initialized();
2147 let db = LoginDb::open_in_memory();
2148 make_saved_login(&db, "", "pass");
2149 let username_match = make_saved_login(&db, "user", "pass");
2150 assert_eq!(
2151 Some(username_match),
2152 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
2153 .unwrap(),
2154 );
2155 }
2156
2157 #[test]
2158 fn test_invalid_login() {
2159 ensure_initialized();
2160 let db = LoginDb::open_in_memory();
2161 assert!(db
2162 .find_login_to_update(
2163 LoginEntry {
2164 http_realm: None,
2165 form_action_origin: None,
2166 ..LoginEntry::default()
2167 },
2168 &*TEST_ENCDEC
2169 )
2170 .is_err());
2171 }
2172
2173 #[test]
2174 fn test_update_with_duplicate_login() {
2175 ensure_initialized();
2176 let db = LoginDb::open_in_memory();
2179 let login = make_saved_login(&db, "user", "pass");
2180 let mut dupe = login.clone().encrypt(&*TEST_ENCDEC).unwrap();
2181 dupe.meta.id = "different-guid".to_string();
2182 db.insert_new_login(&dupe).unwrap();
2183
2184 let mut entry = login.entry();
2185 entry.password = "pass2".to_string();
2186 db.update(&login.id, entry, &*TEST_ENCDEC).unwrap();
2187
2188 let mut entry = login.entry();
2189 entry.password = "pass3".to_string();
2190 db.add_or_update(entry, &*TEST_ENCDEC).unwrap();
2191 }
2192 }
2193}