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 fn insert_new_login(&self, login: &EncryptedLogin) -> Result<()> {
312 let sql = format!(
313 "INSERT OR REPLACE INTO loginsL (
314 origin,
315 httpRealm,
316 formActionOrigin,
317 usernameField,
318 passwordField,
319 timesUsed,
320 secFields,
321 guid,
322 timeCreated,
323 timeLastUsed,
324 timePasswordChanged,
325 local_modified,
326 is_deleted,
327 sync_status
328 ) VALUES (
329 :origin,
330 :http_realm,
331 :form_action_origin,
332 :username_field,
333 :password_field,
334 :times_used,
335 :sec_fields,
336 :guid,
337 :time_created,
338 :time_last_used,
339 :time_password_changed,
340 :local_modified,
341 0, -- is_deleted
342 {new} -- sync_status
343 )",
344 new = SyncStatus::New as u8
345 );
346
347 self.execute(
348 &sql,
349 named_params! {
350 ":origin": login.fields.origin,
351 ":http_realm": login.fields.http_realm,
352 ":form_action_origin": login.fields.form_action_origin,
353 ":username_field": login.fields.username_field,
354 ":password_field": login.fields.password_field,
355 ":time_created": login.meta.time_created,
356 ":times_used": login.meta.times_used,
357 ":time_last_used": login.meta.time_last_used,
358 ":time_password_changed": login.meta.time_password_changed,
359 ":local_modified": login.meta.time_created,
360 ":sec_fields": login.sec_fields,
361 ":guid": login.guid(),
362 },
363 )?;
364 Ok(())
365 }
366
367 fn update_existing_login(&self, login: &EncryptedLogin) -> Result<()> {
368 let sql = format!(
370 "UPDATE loginsL
371 SET local_modified = :now_millis,
372 timeLastUsed = :time_last_used,
373 timePasswordChanged = :time_password_changed,
374 httpRealm = :http_realm,
375 formActionOrigin = :form_action_origin,
376 usernameField = :username_field,
377 passwordField = :password_field,
378 timesUsed = :times_used,
379 secFields = :sec_fields,
380 origin = :origin,
381 -- leave New records as they are, otherwise update them to `changed`
382 sync_status = max(sync_status, {changed})
383 WHERE guid = :guid",
384 changed = SyncStatus::Changed as u8
385 );
386
387 self.db.execute(
388 &sql,
389 named_params! {
390 ":origin": login.fields.origin,
391 ":http_realm": login.fields.http_realm,
392 ":form_action_origin": login.fields.form_action_origin,
393 ":username_field": login.fields.username_field,
394 ":password_field": login.fields.password_field,
395 ":time_last_used": login.meta.time_last_used,
396 ":times_used": login.meta.times_used,
397 ":time_password_changed": login.meta.time_password_changed,
398 ":sec_fields": login.sec_fields,
399 ":guid": &login.meta.id,
400 ":now_millis": login.meta.time_last_used,
402 },
403 )?;
404 Ok(())
405 }
406
407 pub fn add_many(
409 &self,
410 entries: Vec<LoginEntry>,
411 encdec: &dyn EncryptorDecryptor,
412 ) -> Result<Vec<Result<EncryptedLogin>>> {
413 let now_ms = util::system_time_ms_i64(SystemTime::now());
414
415 let entries_with_meta = entries
416 .into_iter()
417 .map(|entry| {
418 let guid = Guid::random();
419 LoginEntryWithMeta {
420 entry,
421 meta: LoginMeta {
422 id: guid.to_string(),
423 time_created: now_ms,
424 time_password_changed: now_ms,
425 time_last_used: now_ms,
426 times_used: 1,
427 },
428 }
429 })
430 .collect();
431
432 self.add_many_with_meta(entries_with_meta, encdec)
433 }
434
435 pub fn add_many_with_meta(
439 &self,
440 entries_with_meta: Vec<LoginEntryWithMeta>,
441 encdec: &dyn EncryptorDecryptor,
442 ) -> Result<Vec<Result<EncryptedLogin>>> {
443 let tx = self.unchecked_transaction()?;
444 let mut results = vec![];
445 for entry_with_meta in entries_with_meta {
446 let guid = Guid::from_string(entry_with_meta.meta.id.clone());
447 match self.fixup_and_check_for_dupes(&guid, entry_with_meta.entry, encdec) {
448 Ok(new_entry) => {
449 let sec_fields = SecureLoginFields {
450 username: new_entry.username,
451 password: new_entry.password,
452 }
453 .encrypt(encdec, &entry_with_meta.meta.id)?;
454 let encrypted_login = EncryptedLogin {
455 meta: entry_with_meta.meta,
456 fields: LoginFields {
457 origin: new_entry.origin,
458 form_action_origin: new_entry.form_action_origin,
459 http_realm: new_entry.http_realm,
460 username_field: new_entry.username_field,
461 password_field: new_entry.password_field,
462 },
463 sec_fields,
464 };
465 let result = self
466 .insert_new_login(&encrypted_login)
467 .map(|_| encrypted_login);
468 results.push(result);
469 }
470
471 Err(error) => results.push(Err(error)),
472 }
473 }
474 tx.commit()?;
475 Ok(results)
476 }
477
478 pub fn add(
479 &self,
480 entry: LoginEntry,
481 encdec: &dyn EncryptorDecryptor,
482 ) -> Result<EncryptedLogin> {
483 let guid = Guid::random();
484 let now_ms = util::system_time_ms_i64(SystemTime::now());
485
486 let entry_with_meta = LoginEntryWithMeta {
487 entry,
488 meta: LoginMeta {
489 id: guid.to_string(),
490 time_created: now_ms,
491 time_password_changed: now_ms,
492 time_last_used: now_ms,
493 times_used: 1,
494 },
495 };
496
497 self.add_with_meta(entry_with_meta, encdec)
498 }
499
500 pub fn add_with_meta(
504 &self,
505 entry_with_meta: LoginEntryWithMeta,
506 encdec: &dyn EncryptorDecryptor,
507 ) -> Result<EncryptedLogin> {
508 let mut results = self.add_many_with_meta(vec![entry_with_meta], encdec)?;
509 results.pop().expect("there should be a single result")
510 }
511
512 pub fn update(
513 &self,
514 sguid: &str,
515 entry: LoginEntry,
516 encdec: &dyn EncryptorDecryptor,
517 ) -> Result<EncryptedLogin> {
518 let guid = Guid::new(sguid);
519 let now_ms = util::system_time_ms_i64(SystemTime::now());
520 let tx = self.unchecked_transaction()?;
521
522 let entry = entry.fixup()?;
523
524 if self.check_for_dupes(&guid, &entry, encdec).is_err() {
530 let has_mirror_row: bool = self
532 .db
533 .conn_ext_query_one("SELECT EXISTS (SELECT 1 FROM loginsM)")?;
534 let has_http_realm = entry.http_realm.is_some();
535 let has_form_action_origin = entry.form_action_origin.is_some();
536 report_error!(
537 "logins-duplicate-in-update",
538 "(mirror: {has_mirror_row}, realm: {has_http_realm}, form_origin: {has_form_action_origin})");
539 }
540
541 self.ensure_local_overlay_exists(&guid)?;
543 self.mark_mirror_overridden(&guid)?;
544
545 let existing = match self.get_by_id(sguid)? {
547 Some(e) => e.decrypt(encdec)?,
548 None => return Err(Error::NoSuchRecord(sguid.to_owned())),
549 };
550 let time_password_changed = if existing.password == entry.password {
551 existing.time_password_changed
552 } else {
553 now_ms
554 };
555
556 let sec_fields = SecureLoginFields {
558 username: entry.username,
559 password: entry.password,
560 }
561 .encrypt(encdec, &existing.id)?;
562 let result = EncryptedLogin {
563 meta: LoginMeta {
564 id: existing.id,
565 time_created: existing.time_created,
566 time_password_changed,
567 time_last_used: now_ms,
568 times_used: existing.times_used + 1,
569 },
570 fields: LoginFields {
571 origin: entry.origin,
572 form_action_origin: entry.form_action_origin,
573 http_realm: entry.http_realm,
574 username_field: entry.username_field,
575 password_field: entry.password_field,
576 },
577 sec_fields,
578 };
579
580 self.update_existing_login(&result)?;
581 tx.commit()?;
582 Ok(result)
583 }
584
585 pub fn add_or_update(
586 &self,
587 entry: LoginEntry,
588 encdec: &dyn EncryptorDecryptor,
589 ) -> Result<EncryptedLogin> {
590 let entry = entry.fixup()?;
592 match self.find_login_to_update(entry.clone(), encdec)? {
593 Some(login) => self.update(&login.id, entry, encdec),
594 None => self.add(entry, encdec),
595 }
596 }
597
598 pub fn fixup_and_check_for_dupes(
599 &self,
600 guid: &Guid,
601 entry: LoginEntry,
602 encdec: &dyn EncryptorDecryptor,
603 ) -> Result<LoginEntry> {
604 let entry = entry.fixup()?;
605 self.check_for_dupes(guid, &entry, encdec)?;
606 Ok(entry)
607 }
608
609 pub fn check_for_dupes(
610 &self,
611 guid: &Guid,
612 entry: &LoginEntry,
613 encdec: &dyn EncryptorDecryptor,
614 ) -> Result<()> {
615 if self.dupe_exists(guid, entry, encdec)? {
616 return Err(InvalidLogin::DuplicateLogin.into());
617 }
618 Ok(())
619 }
620
621 pub fn dupe_exists(
622 &self,
623 guid: &Guid,
624 entry: &LoginEntry,
625 encdec: &dyn EncryptorDecryptor,
626 ) -> Result<bool> {
627 Ok(self.find_dupe(guid, entry, encdec)?.is_some())
628 }
629
630 pub fn find_dupe(
631 &self,
632 guid: &Guid,
633 entry: &LoginEntry,
634 encdec: &dyn EncryptorDecryptor,
635 ) -> Result<Option<Guid>> {
636 for possible in self.get_by_entry_target(entry)? {
637 if possible.guid() != *guid {
638 let pos_sec_fields = possible.decrypt_fields(encdec)?;
639 if pos_sec_fields.username == entry.username {
640 return Ok(Some(possible.guid()));
641 }
642 }
643 }
644 Ok(None)
645 }
646
647 fn get_by_entry_target(&self, entry: &LoginEntry) -> Result<Vec<EncryptedLogin>> {
657 lazy_static::lazy_static! {
659 static ref GET_BY_FORM_ACTION_ORIGIN: String = format!(
660 "SELECT {common_cols} FROM loginsL
661 WHERE is_deleted = 0
662 AND origin = :origin
663 AND formActionOrigin = :form_action_origin
664
665 UNION ALL
666
667 SELECT {common_cols} FROM loginsM
668 WHERE is_overridden = 0
669 AND origin = :origin
670 AND formActionOrigin = :form_action_origin
671 ",
672 common_cols = schema::COMMON_COLS
673 );
674 static ref GET_BY_HTTP_REALM: String = format!(
675 "SELECT {common_cols} FROM loginsL
676 WHERE is_deleted = 0
677 AND origin = :origin
678 AND httpRealm = :http_realm
679
680 UNION ALL
681
682 SELECT {common_cols} FROM loginsM
683 WHERE is_overridden = 0
684 AND origin = :origin
685 AND httpRealm = :http_realm
686 ",
687 common_cols = schema::COMMON_COLS
688 );
689 }
690 match (entry.form_action_origin.as_ref(), entry.http_realm.as_ref()) {
691 (Some(form_action_origin), None) => {
692 let params = named_params! {
693 ":origin": &entry.origin,
694 ":form_action_origin": form_action_origin,
695 };
696 self.db
697 .prepare_cached(&GET_BY_FORM_ACTION_ORIGIN)?
698 .query_and_then(params, EncryptedLogin::from_row)?
699 .collect()
700 }
701 (None, Some(http_realm)) => {
702 let params = named_params! {
703 ":origin": &entry.origin,
704 ":http_realm": http_realm,
705 };
706 self.db
707 .prepare_cached(&GET_BY_HTTP_REALM)?
708 .query_and_then(params, EncryptedLogin::from_row)?
709 .collect()
710 }
711 (Some(_), Some(_)) => Err(InvalidLogin::BothTargets.into()),
712 (None, None) => Err(InvalidLogin::NoTarget.into()),
713 }
714 }
715
716 pub fn exists(&self, id: &str) -> Result<bool> {
717 Ok(self.db.query_row(
718 "SELECT EXISTS(
719 SELECT 1 FROM loginsL
720 WHERE guid = :guid AND is_deleted = 0
721 UNION ALL
722 SELECT 1 FROM loginsM
723 WHERE guid = :guid AND is_overridden IS NOT 1
724 )",
725 named_params! { ":guid": id },
726 |row| row.get(0),
727 )?)
728 }
729
730 pub fn delete(&self, id: &str) -> Result<bool> {
733 let mut results = self.delete_many(vec![id])?;
734 Ok(results.pop().expect("there should be a single result"))
735 }
736
737 pub fn delete_many(&self, ids: Vec<&str>) -> Result<Vec<bool>> {
740 let tx = self.unchecked_transaction_imm()?;
741 let sql = format!(
742 "
743 UPDATE loginsL
744 SET local_modified = :now_ms,
745 sync_status = {status_changed},
746 is_deleted = 1,
747 secFields = '',
748 origin = '',
749 httpRealm = NULL,
750 formActionOrigin = NULL
751 WHERE guid = :guid AND is_deleted IS FALSE
752 ",
753 status_changed = SyncStatus::Changed as u8
754 );
755 let mut stmt = self.db.prepare_cached(&sql)?;
756
757 let mut result = vec![];
758
759 for id in ids {
760 let now_ms = util::system_time_ms_i64(SystemTime::now());
761
762 let update_result = stmt.execute(named_params! { ":now_ms": now_ms, ":guid": id })?;
764
765 let exists = update_result == 1;
766
767 self.execute(
769 "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
770 named_params! { ":guid": id },
771 )?;
772
773 self.execute(&format!("
776 INSERT OR IGNORE INTO loginsL
777 (guid, local_modified, is_deleted, sync_status, origin, timeCreated, timePasswordChanged, secFields)
778 SELECT guid, :now_ms, 1, {changed}, '', timeCreated, :now_ms, ''
779 FROM loginsM
780 WHERE guid = :guid",
781 changed = SyncStatus::Changed as u8),
782 named_params! { ":now_ms": now_ms, ":guid": id })?;
783
784 result.push(exists);
785 }
786
787 tx.commit()?;
788
789 Ok(result)
790 }
791
792 pub fn delete_undecryptable_records_for_remote_replacement(
793 &self,
794 encdec: &dyn EncryptorDecryptor,
795 ) -> Result<LoginsDeletionMetrics> {
796 let corrupted_logins = self
798 .get_all()?
799 .into_iter()
800 .filter(|login| login.clone().decrypt(encdec).is_err())
801 .collect::<Vec<_>>();
802 let ids = corrupted_logins
803 .iter()
804 .map(|login| login.guid_str())
805 .collect::<Vec<_>>();
806
807 self.delete_local_records_for_remote_replacement(ids)
808 }
809
810 pub fn delete_local_records_for_remote_replacement(
811 &self,
812 ids: Vec<&str>,
813 ) -> Result<LoginsDeletionMetrics> {
814 let tx = self.unchecked_transaction_imm()?;
815 let mut local_deleted = 0;
816 let mut mirror_deleted = 0;
817
818 sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
819 let deleted = self.execute(
820 &format!(
821 "DELETE FROM loginsL WHERE guid IN ({})",
822 sql_support::repeat_sql_values(chunk.len())
823 ),
824 rusqlite::params_from_iter(chunk),
825 )?;
826 local_deleted += deleted;
827 Ok(())
828 })?;
829
830 sql_support::each_chunk(&ids, |chunk, _| -> Result<()> {
831 let deleted = self.execute(
832 &format!(
833 "DELETE FROM loginsM WHERE guid IN ({})",
834 sql_support::repeat_sql_values(chunk.len())
835 ),
836 rusqlite::params_from_iter(chunk),
837 )?;
838 mirror_deleted += deleted;
839 Ok(())
840 })?;
841
842 tx.commit()?;
843 Ok(LoginsDeletionMetrics {
844 local_deleted: local_deleted as u64,
845 mirror_deleted: mirror_deleted as u64,
846 })
847 }
848
849 fn mark_mirror_overridden(&self, guid: &str) -> Result<()> {
850 self.execute_cached(
851 "UPDATE loginsM SET is_overridden = 1 WHERE guid = :guid",
852 named_params! { ":guid": guid },
853 )?;
854 Ok(())
855 }
856
857 fn ensure_local_overlay_exists(&self, guid: &str) -> Result<()> {
858 let already_have_local: bool = self.db.query_row(
859 "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid)",
860 named_params! { ":guid": guid },
861 |row| row.get(0),
862 )?;
863
864 if already_have_local {
865 return Ok(());
866 }
867
868 debug!("No overlay; cloning one for {:?}.", guid);
869 let changed = self.clone_mirror_to_overlay(guid)?;
870 if changed == 0 {
871 report_error!(
872 "logins-local-overlay-error",
873 "Failed to create local overlay for GUID {guid:?}."
874 );
875 return Err(Error::NoSuchRecord(guid.to_owned()));
876 }
877 Ok(())
878 }
879
880 fn clone_mirror_to_overlay(&self, guid: &str) -> Result<usize> {
881 Ok(self.execute_cached(&CLONE_SINGLE_MIRROR_SQL, &[(":guid", &guid as &dyn ToSql)])?)
882 }
883
884 pub fn wipe_local(&self) -> Result<usize> {
886 info!("Executing wipe_local on password engine!");
887 let tx = self.unchecked_transaction()?;
888 let mut row_count = 0;
889 row_count += self.execute("DELETE FROM loginsL", [])?;
890 row_count += self.execute("DELETE FROM loginsM", [])?;
891 row_count += self.execute("DELETE FROM loginsSyncMeta", [])?;
892 tx.commit()?;
893 Ok(row_count)
894 }
895
896 pub fn shutdown(self) -> Result<()> {
897 self.db.close().map_err(|(_, e)| Error::SqlError(e))
898 }
899}
900
901lazy_static! {
902 static ref GET_ALL_SQL: String = format!(
903 "SELECT {common_cols} FROM loginsL WHERE is_deleted = 0
904 UNION ALL
905 SELECT {common_cols} FROM loginsM WHERE is_overridden = 0",
906 common_cols = schema::COMMON_COLS,
907 );
908 static ref COUNT_ALL_SQL: String = format!(
909 "SELECT COUNT(*) FROM (
910 SELECT guid FROM loginsL WHERE is_deleted = 0
911 UNION ALL
912 SELECT guid FROM loginsM WHERE is_overridden = 0
913 )"
914 );
915 static ref COUNT_BY_ORIGIN_SQL: String = format!(
916 "SELECT COUNT(*) FROM (
917 SELECT guid FROM loginsL WHERE is_deleted = 0 AND origin = :origin
918 UNION ALL
919 SELECT guid FROM loginsM WHERE is_overridden = 0 AND origin = :origin
920 )"
921 );
922 static ref COUNT_BY_FORM_ACTION_ORIGIN_SQL: String = format!(
923 "SELECT COUNT(*) FROM (
924 SELECT guid FROM loginsL WHERE is_deleted = 0 AND formActionOrigin = :form_action_origin
925 UNION ALL
926 SELECT guid FROM loginsM WHERE is_overridden = 0 AND formActionOrigin = :form_action_origin
927 )"
928 );
929 static ref GET_BY_GUID_SQL: String = format!(
930 "SELECT {common_cols}
931 FROM loginsL
932 WHERE is_deleted = 0
933 AND guid = :guid
934
935 UNION ALL
936
937 SELECT {common_cols}
938 FROM loginsM
939 WHERE is_overridden IS NOT 1
940 AND guid = :guid
941 ORDER BY origin ASC
942
943 LIMIT 1",
944 common_cols = schema::COMMON_COLS,
945 );
946 pub static ref CLONE_ENTIRE_MIRROR_SQL: String = format!(
947 "INSERT OR IGNORE INTO loginsL ({common_cols}, local_modified, is_deleted, sync_status)
948 SELECT {common_cols}, NULL AS local_modified, 0 AS is_deleted, 0 AS sync_status
949 FROM loginsM",
950 common_cols = schema::COMMON_COLS,
951 );
952 static ref CLONE_SINGLE_MIRROR_SQL: String =
953 format!("{} WHERE guid = :guid", &*CLONE_ENTIRE_MIRROR_SQL,);
954}
955
956#[cfg(not(feature = "keydb"))]
957#[cfg(test)]
958pub mod test_utils {
959 use super::*;
960 use crate::encryption::test_utils::decrypt_struct;
961 use crate::login::test_utils::enc_login;
962 use crate::SecureLoginFields;
963 use sync15::ServerTimestamp;
964
965 pub fn insert_login(
969 db: &LoginDb,
970 guid: &str,
971 local_login: Option<&str>,
972 mirror_login: Option<&str>,
973 ) {
974 if let Some(password) = mirror_login {
975 add_mirror(
976 db,
977 &enc_login(guid, password),
978 &ServerTimestamp(util::system_time_ms_i64(std::time::SystemTime::now())),
979 local_login.is_some(),
980 )
981 .unwrap();
982 }
983 if let Some(password) = local_login {
984 db.insert_new_login(&enc_login(guid, password)).unwrap();
985 }
986 }
987
988 pub fn insert_encrypted_login(
989 db: &LoginDb,
990 local: &EncryptedLogin,
991 mirror: &EncryptedLogin,
992 server_modified: &ServerTimestamp,
993 ) {
994 db.insert_new_login(local).unwrap();
995 add_mirror(db, mirror, server_modified, true).unwrap();
996 }
997
998 pub fn add_mirror(
999 db: &LoginDb,
1000 login: &EncryptedLogin,
1001 server_modified: &ServerTimestamp,
1002 is_overridden: bool,
1003 ) -> Result<()> {
1004 let sql = "
1005 INSERT OR IGNORE INTO loginsM (
1006 is_overridden,
1007 server_modified,
1008
1009 httpRealm,
1010 formActionOrigin,
1011 usernameField,
1012 passwordField,
1013 secFields,
1014 origin,
1015
1016 timesUsed,
1017 timeLastUsed,
1018 timePasswordChanged,
1019 timeCreated,
1020
1021 guid
1022 ) VALUES (
1023 :is_overridden,
1024 :server_modified,
1025
1026 :http_realm,
1027 :form_action_origin,
1028 :username_field,
1029 :password_field,
1030 :sec_fields,
1031 :origin,
1032
1033 :times_used,
1034 :time_last_used,
1035 :time_password_changed,
1036 :time_created,
1037
1038 :guid
1039 )";
1040 let mut stmt = db.prepare_cached(sql)?;
1041
1042 stmt.execute(named_params! {
1043 ":is_overridden": is_overridden,
1044 ":server_modified": server_modified.as_millis(),
1045 ":http_realm": login.fields.http_realm,
1046 ":form_action_origin": login.fields.form_action_origin,
1047 ":username_field": login.fields.username_field,
1048 ":password_field": login.fields.password_field,
1049 ":origin": login.fields.origin,
1050 ":sec_fields": login.sec_fields,
1051 ":times_used": login.meta.times_used,
1052 ":time_last_used": login.meta.time_last_used,
1053 ":time_password_changed": login.meta.time_password_changed,
1054 ":time_created": login.meta.time_created,
1055 ":guid": login.guid_str(),
1056 })?;
1057 Ok(())
1058 }
1059
1060 pub fn get_local_guids(db: &LoginDb) -> Vec<String> {
1061 get_guids(db, "SELECT guid FROM loginsL")
1062 }
1063
1064 pub fn get_mirror_guids(db: &LoginDb) -> Vec<String> {
1065 get_guids(db, "SELECT guid FROM loginsM")
1066 }
1067
1068 fn get_guids(db: &LoginDb, sql: &str) -> Vec<String> {
1069 let mut stmt = db.prepare_cached(sql).unwrap();
1070 let mut res: Vec<String> = stmt
1071 .query_map([], |r| r.get(0))
1072 .unwrap()
1073 .map(|r| r.unwrap())
1074 .collect();
1075 res.sort();
1076 res
1077 }
1078
1079 pub fn get_server_modified(db: &LoginDb, guid: &str) -> i64 {
1080 db.conn_ext_query_one(&format!(
1081 "SELECT server_modified FROM loginsM WHERE guid='{}'",
1082 guid
1083 ))
1084 .unwrap()
1085 }
1086
1087 pub fn check_local_login(db: &LoginDb, guid: &str, password: &str, local_modified_gte: i64) {
1088 let row: (String, i64, bool) = db
1089 .query_row(
1090 "SELECT secFields, local_modified, is_deleted FROM loginsL WHERE guid=?",
1091 [guid],
1092 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1093 )
1094 .unwrap();
1095 let enc: SecureLoginFields = decrypt_struct(row.0);
1096 assert_eq!(enc.password, password);
1097 assert!(row.1 >= local_modified_gte);
1098 assert!(!row.2);
1099 }
1100
1101 pub fn check_mirror_login(
1102 db: &LoginDb,
1103 guid: &str,
1104 password: &str,
1105 server_modified: i64,
1106 is_overridden: bool,
1107 ) {
1108 let row: (String, i64, bool) = db
1109 .query_row(
1110 "SELECT secFields, server_modified, is_overridden FROM loginsM WHERE guid=?",
1111 [guid],
1112 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1113 )
1114 .unwrap();
1115 let enc: SecureLoginFields = decrypt_struct(row.0);
1116 assert_eq!(enc.password, password);
1117 assert_eq!(row.1, server_modified);
1118 assert_eq!(row.2, is_overridden);
1119 }
1120}
1121
1122#[cfg(not(feature = "keydb"))]
1123#[cfg(test)]
1124mod tests {
1125 use super::*;
1126 use crate::db::test_utils::{get_local_guids, get_mirror_guids};
1127 use crate::encryption::test_utils::TEST_ENCDEC;
1128 use crate::sync::merge::LocalLogin;
1129 use nss::ensure_initialized;
1130 use std::{thread, time};
1131
1132 #[test]
1133 fn test_username_dupe_semantics() {
1134 ensure_initialized();
1135 let mut login = LoginEntry {
1136 origin: "https://www.example.com".into(),
1137 http_realm: Some("https://www.example.com".into()),
1138 username: "test".into(),
1139 password: "sekret".into(),
1140 ..LoginEntry::default()
1141 };
1142
1143 let db = LoginDb::open_in_memory();
1144 db.add(login.clone(), &*TEST_ENCDEC)
1145 .expect("should be able to add first login");
1146
1147 let exp_err = "Invalid login: Login already exists";
1149 assert_eq!(
1150 db.add(login.clone(), &*TEST_ENCDEC)
1151 .unwrap_err()
1152 .to_string(),
1153 exp_err
1154 );
1155
1156 login.username = "".to_string();
1158 db.add(login.clone(), &*TEST_ENCDEC)
1159 .expect("empty login isn't a dupe");
1160
1161 assert_eq!(
1162 db.add(login, &*TEST_ENCDEC).unwrap_err().to_string(),
1163 exp_err
1164 );
1165
1166 assert_eq!(db.get_all().unwrap().len(), 2);
1168 }
1169
1170 #[test]
1171 fn test_add_many() {
1172 ensure_initialized();
1173
1174 let login_a = LoginEntry {
1175 origin: "https://a.example.com".into(),
1176 http_realm: Some("https://www.example.com".into()),
1177 username: "test".into(),
1178 password: "sekret".into(),
1179 ..LoginEntry::default()
1180 };
1181
1182 let login_b = LoginEntry {
1183 origin: "https://b.example.com".into(),
1184 http_realm: Some("https://www.example.com".into()),
1185 username: "test".into(),
1186 password: "sekret".into(),
1187 ..LoginEntry::default()
1188 };
1189
1190 let db = LoginDb::open_in_memory();
1191 let added = db
1192 .add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1193 .expect("should be able to add logins");
1194
1195 let [added_a, added_b] = added.as_slice() else {
1196 panic!("there should really be 2")
1197 };
1198
1199 let fetched_a = db
1200 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1201 .expect("should work")
1202 .expect("should get a record");
1203
1204 assert_eq!(fetched_a.fields.origin, login_a.origin);
1205
1206 let fetched_b = db
1207 .get_by_id(&added_b.as_ref().unwrap().meta.id)
1208 .expect("should work")
1209 .expect("should get a record");
1210
1211 assert_eq!(fetched_b.fields.origin, login_b.origin);
1212
1213 assert_eq!(db.count_all().unwrap(), 2);
1214 }
1215
1216 #[test]
1217 fn test_count_by_origin() {
1218 ensure_initialized();
1219
1220 let origin_a = "https://a.example.com";
1221 let login_a = LoginEntry {
1222 origin: origin_a.into(),
1223 http_realm: Some("https://www.example.com".into()),
1224 username: "test".into(),
1225 password: "sekret".into(),
1226 ..LoginEntry::default()
1227 };
1228
1229 let login_b = LoginEntry {
1230 origin: "https://b.example.com".into(),
1231 http_realm: Some("https://www.example.com".into()),
1232 username: "test".into(),
1233 password: "sekret".into(),
1234 ..LoginEntry::default()
1235 };
1236
1237 let origin_umlaut = "https://bücher.example.com";
1238 let login_umlaut = LoginEntry {
1239 origin: origin_umlaut.into(),
1240 http_realm: Some("https://www.example.com".into()),
1241 username: "test".into(),
1242 password: "sekret".into(),
1243 ..LoginEntry::default()
1244 };
1245
1246 let db = LoginDb::open_in_memory();
1247 db.add_many(
1248 vec![login_a.clone(), login_b.clone(), login_umlaut.clone()],
1249 &*TEST_ENCDEC,
1250 )
1251 .expect("should be able to add logins");
1252
1253 assert_eq!(db.count_by_origin(origin_a).unwrap(), 1);
1254 assert_eq!(db.count_by_origin(origin_umlaut).unwrap(), 1);
1255 }
1256
1257 #[test]
1258 fn test_count_by_form_action_origin() {
1259 ensure_initialized();
1260
1261 let origin_a = "https://a.example.com";
1262 let login_a = LoginEntry {
1263 origin: origin_a.into(),
1264 form_action_origin: Some(origin_a.into()),
1265 http_realm: Some("https://www.example.com".into()),
1266 username: "test".into(),
1267 password: "sekret".into(),
1268 ..LoginEntry::default()
1269 };
1270
1271 let login_b = LoginEntry {
1272 origin: "https://b.example.com".into(),
1273 form_action_origin: Some("https://b.example.com".into()),
1274 http_realm: Some("https://www.example.com".into()),
1275 username: "test".into(),
1276 password: "sekret".into(),
1277 ..LoginEntry::default()
1278 };
1279
1280 let origin_umlaut = "https://bücher.example.com";
1281 let login_umlaut = LoginEntry {
1282 origin: origin_umlaut.into(),
1283 form_action_origin: Some(origin_umlaut.into()),
1284 http_realm: Some("https://www.example.com".into()),
1285 username: "test".into(),
1286 password: "sekret".into(),
1287 ..LoginEntry::default()
1288 };
1289
1290 let db = LoginDb::open_in_memory();
1291 db.add_many(
1292 vec![login_a.clone(), login_b.clone(), login_umlaut.clone()],
1293 &*TEST_ENCDEC,
1294 )
1295 .expect("should be able to add logins");
1296
1297 assert_eq!(db.count_by_form_action_origin(origin_a).unwrap(), 1);
1298 assert_eq!(db.count_by_form_action_origin(origin_umlaut).unwrap(), 1);
1299 }
1300
1301 #[test]
1302 fn test_add_many_with_failed_constraint() {
1303 ensure_initialized();
1304
1305 let login_a = LoginEntry {
1306 origin: "https://example.com".into(),
1307 http_realm: Some("https://www.example.com".into()),
1308 username: "test".into(),
1309 password: "sekret".into(),
1310 ..LoginEntry::default()
1311 };
1312
1313 let login_b = LoginEntry {
1314 origin: "https://example.com".into(),
1316 http_realm: Some("https://www.example.com".into()),
1317 username: "test".into(),
1318 password: "sekret".into(),
1319 ..LoginEntry::default()
1320 };
1321
1322 let db = LoginDb::open_in_memory();
1323 let added = db
1324 .add_many(vec![login_a.clone(), login_b.clone()], &*TEST_ENCDEC)
1325 .expect("should be able to add logins");
1326
1327 let [added_a, added_b] = added.as_slice() else {
1328 panic!("there should really be 2")
1329 };
1330
1331 let fetched_a = db
1333 .get_by_id(&added_a.as_ref().unwrap().meta.id)
1334 .expect("should work")
1335 .expect("should get a record");
1336
1337 assert_eq!(fetched_a.fields.origin, login_a.origin);
1338
1339 assert!(!added_b.is_ok());
1341 }
1342
1343 #[test]
1344 fn test_add_with_meta() {
1345 ensure_initialized();
1346
1347 let guid = Guid::random();
1348 let now_ms = util::system_time_ms_i64(SystemTime::now());
1349 let login = LoginEntry {
1350 origin: "https://www.example.com".into(),
1351 http_realm: Some("https://www.example.com".into()),
1352 username: "test".into(),
1353 password: "sekret".into(),
1354 ..LoginEntry::default()
1355 };
1356 let meta = LoginMeta {
1357 id: guid.to_string(),
1358 time_created: now_ms,
1359 time_password_changed: now_ms + 100,
1360 time_last_used: now_ms + 10,
1361 times_used: 42,
1362 };
1363
1364 let db = LoginDb::open_in_memory();
1365 let entry_with_meta = LoginEntryWithMeta {
1366 entry: login.clone(),
1367 meta: meta.clone(),
1368 };
1369
1370 db.add_with_meta(entry_with_meta, &*TEST_ENCDEC)
1371 .expect("should be able to add login with record");
1372
1373 let fetched = db
1374 .get_by_id(&guid)
1375 .expect("should work")
1376 .expect("should get a record");
1377
1378 assert_eq!(fetched.meta, meta);
1379 }
1380
1381 #[test]
1382 fn test_add_with_meta_deleted() {
1383 ensure_initialized();
1384
1385 let guid = Guid::random();
1386 let now_ms = util::system_time_ms_i64(SystemTime::now());
1387 let login = LoginEntry {
1388 origin: "https://www.example.com".into(),
1389 http_realm: Some("https://www.example.com".into()),
1390 username: "test".into(),
1391 password: "sekret".into(),
1392 ..LoginEntry::default()
1393 };
1394 let meta = LoginMeta {
1395 id: guid.to_string(),
1396 time_created: now_ms,
1397 time_password_changed: now_ms + 100,
1398 time_last_used: now_ms + 10,
1399 times_used: 42,
1400 };
1401
1402 let db = LoginDb::open_in_memory();
1403 let entry_with_meta = LoginEntryWithMeta {
1404 entry: login.clone(),
1405 meta: meta.clone(),
1406 };
1407
1408 db.add_with_meta(entry_with_meta, &*TEST_ENCDEC)
1409 .expect("should be able to add login with record");
1410
1411 db.delete(&guid).expect("should be able to delete login");
1412
1413 let entry_with_meta2 = LoginEntryWithMeta {
1414 entry: login.clone(),
1415 meta: meta.clone(),
1416 };
1417
1418 db.add_with_meta(entry_with_meta2, &*TEST_ENCDEC)
1419 .expect("should be able to re-add login with record");
1420
1421 let fetched = db
1422 .get_by_id(&guid)
1423 .expect("should work")
1424 .expect("should get a record");
1425
1426 assert_eq!(fetched.meta, meta);
1427 }
1428
1429 #[test]
1430 fn test_unicode_submit() {
1431 ensure_initialized();
1432 let db = LoginDb::open_in_memory();
1433 let added = db
1434 .add(
1435 LoginEntry {
1436 form_action_origin: Some("http://😍.com".into()),
1437 origin: "http://😍.com".into(),
1438 http_realm: None,
1439 username_field: "😍".into(),
1440 password_field: "😍".into(),
1441 username: "😍".into(),
1442 password: "😍".into(),
1443 },
1444 &*TEST_ENCDEC,
1445 )
1446 .unwrap();
1447 let fetched = db
1448 .get_by_id(&added.meta.id)
1449 .expect("should work")
1450 .expect("should get a record");
1451 assert_eq!(added, fetched);
1452 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1453 assert_eq!(
1454 fetched.fields.form_action_origin,
1455 Some("http://xn--r28h.com".to_string())
1456 );
1457 assert_eq!(fetched.fields.username_field, "😍");
1458 assert_eq!(fetched.fields.password_field, "😍");
1459 let sec_fields = fetched.decrypt_fields(&*TEST_ENCDEC).unwrap();
1460 assert_eq!(sec_fields.username, "😍");
1461 assert_eq!(sec_fields.password, "😍");
1462 }
1463
1464 #[test]
1465 fn test_unicode_realm() {
1466 ensure_initialized();
1467 let db = LoginDb::open_in_memory();
1468 let added = db
1469 .add(
1470 LoginEntry {
1471 form_action_origin: None,
1472 origin: "http://😍.com".into(),
1473 http_realm: Some("😍😍".into()),
1474 username: "😍".into(),
1475 password: "😍".into(),
1476 ..Default::default()
1477 },
1478 &*TEST_ENCDEC,
1479 )
1480 .unwrap();
1481 let fetched = db
1482 .get_by_id(&added.meta.id)
1483 .expect("should work")
1484 .expect("should get a record");
1485 assert_eq!(added, fetched);
1486 assert_eq!(fetched.fields.origin, "http://xn--r28h.com");
1487 assert_eq!(fetched.fields.http_realm.unwrap(), "😍😍");
1488 }
1489
1490 fn check_matches(db: &LoginDb, query: &str, expected: &[&str]) {
1491 let mut results = db
1492 .get_by_base_domain(query)
1493 .unwrap()
1494 .into_iter()
1495 .map(|l| l.fields.origin)
1496 .collect::<Vec<String>>();
1497 results.sort_unstable();
1498 let mut sorted = expected.to_owned();
1499 sorted.sort_unstable();
1500 assert_eq!(sorted, results);
1501 }
1502
1503 fn check_good_bad(
1504 good: Vec<&str>,
1505 bad: Vec<&str>,
1506 good_queries: Vec<&str>,
1507 zero_queries: Vec<&str>,
1508 ) {
1509 let db = LoginDb::open_in_memory();
1510 for h in good.iter().chain(bad.iter()) {
1511 db.add(
1512 LoginEntry {
1513 origin: (*h).into(),
1514 http_realm: Some((*h).into()),
1515 password: "test".into(),
1516 ..Default::default()
1517 },
1518 &*TEST_ENCDEC,
1519 )
1520 .unwrap();
1521 }
1522 for query in good_queries {
1523 check_matches(&db, query, &good);
1524 }
1525 for query in zero_queries {
1526 check_matches(&db, query, &[]);
1527 }
1528 }
1529
1530 #[test]
1531 fn test_get_by_base_domain_invalid() {
1532 check_good_bad(
1533 vec!["https://example.com"],
1534 vec![],
1535 vec![],
1536 vec!["invalid query"],
1537 );
1538 }
1539
1540 #[test]
1541 fn test_get_by_base_domain() {
1542 check_good_bad(
1543 vec![
1544 "https://example.com",
1545 "https://www.example.com",
1546 "http://www.example.com",
1547 "http://www.example.com:8080",
1548 "http://sub.example.com:8080",
1549 "https://sub.example.com:8080",
1550 "https://sub.sub.example.com",
1551 "ftp://sub.example.com",
1552 ],
1553 vec![
1554 "https://badexample.com",
1555 "https://example.co",
1556 "https://example.com.au",
1557 ],
1558 vec!["example.com"],
1559 vec!["foo.com"],
1560 );
1561 }
1562
1563 #[test]
1564 fn test_get_by_base_domain_punicode() {
1565 check_good_bad(
1568 vec![
1569 "http://xn--r28h.com", ],
1571 vec!["http://💖.com"],
1572 vec!["😍.com", "xn--r28h.com"],
1573 vec![],
1574 );
1575 }
1576
1577 #[test]
1578 fn test_get_by_base_domain_ipv4() {
1579 check_good_bad(
1580 vec!["http://127.0.0.1", "https://127.0.0.1:8000"],
1581 vec!["https://127.0.0.0", "https://example.com"],
1582 vec!["127.0.0.1"],
1583 vec!["127.0.0.2"],
1584 );
1585 }
1586
1587 #[test]
1588 fn test_get_by_base_domain_ipv6() {
1589 check_good_bad(
1590 vec!["http://[::1]", "https://[::1]:8000"],
1591 vec!["https://[0:0:0:0:0:0:1:1]", "https://example.com"],
1592 vec!["[::1]", "[0:0:0:0:0:0:0:1]"],
1593 vec!["[0:0:0:0:0:0:1:2]"],
1594 );
1595 }
1596
1597 #[test]
1598 fn test_add() {
1599 ensure_initialized();
1600 let db = LoginDb::open_in_memory();
1601 let to_add = LoginEntry {
1602 origin: "https://www.example.com".into(),
1603 http_realm: Some("https://www.example.com".into()),
1604 username: "test_user".into(),
1605 password: "test_password".into(),
1606 ..Default::default()
1607 };
1608 let login = db.add(to_add, &*TEST_ENCDEC).unwrap();
1609 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1610
1611 assert_eq!(login.fields.origin, login2.fields.origin);
1612 assert_eq!(login.fields.http_realm, login2.fields.http_realm);
1613 assert_eq!(login.sec_fields, login2.sec_fields);
1614 }
1615
1616 #[test]
1617 fn test_update() {
1618 ensure_initialized();
1619 let db = LoginDb::open_in_memory();
1620 let login = db
1621 .add(
1622 LoginEntry {
1623 origin: "https://www.example.com".into(),
1624 http_realm: Some("https://www.example.com".into()),
1625 username: "user1".into(),
1626 password: "password1".into(),
1627 ..Default::default()
1628 },
1629 &*TEST_ENCDEC,
1630 )
1631 .unwrap();
1632 db.update(
1633 &login.meta.id,
1634 LoginEntry {
1635 origin: "https://www.example2.com".into(),
1636 http_realm: Some("https://www.example2.com".into()),
1637 username: "user2".into(),
1638 password: "password2".into(),
1639 ..Default::default() },
1641 &*TEST_ENCDEC,
1642 )
1643 .unwrap();
1644
1645 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1646
1647 assert_eq!(login2.fields.origin, "https://www.example2.com");
1648 assert_eq!(
1649 login2.fields.http_realm,
1650 Some("https://www.example2.com".into())
1651 );
1652 let sec_fields = login2.decrypt_fields(&*TEST_ENCDEC).unwrap();
1653 assert_eq!(sec_fields.username, "user2");
1654 assert_eq!(sec_fields.password, "password2");
1655 }
1656
1657 #[test]
1658 fn test_touch() {
1659 ensure_initialized();
1660 let db = LoginDb::open_in_memory();
1661 let login = db
1662 .add(
1663 LoginEntry {
1664 origin: "https://www.example.com".into(),
1665 http_realm: Some("https://www.example.com".into()),
1666 username: "user1".into(),
1667 password: "password1".into(),
1668 ..Default::default()
1669 },
1670 &*TEST_ENCDEC,
1671 )
1672 .unwrap();
1673 thread::sleep(time::Duration::from_millis(50));
1675 db.touch(&login.meta.id).unwrap();
1676 let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap();
1677 assert!(login2.meta.time_last_used > login.meta.time_last_used);
1678 assert_eq!(login2.meta.times_used, login.meta.times_used + 1);
1679 }
1680
1681 #[test]
1682 fn test_delete() {
1683 ensure_initialized();
1684 let db = LoginDb::open_in_memory();
1685 let login = db
1686 .add(
1687 LoginEntry {
1688 origin: "https://www.example.com".into(),
1689 http_realm: Some("https://www.example.com".into()),
1690 username: "test_user".into(),
1691 password: "test_password".into(),
1692 ..Default::default()
1693 },
1694 &*TEST_ENCDEC,
1695 )
1696 .unwrap();
1697
1698 assert!(db.delete(login.guid_str()).unwrap());
1699
1700 let local_login = db
1701 .query_row(
1702 "SELECT * FROM loginsL WHERE guid = :guid",
1703 named_params! { ":guid": login.guid_str() },
1704 |row| Ok(LocalLogin::test_raw_from_row(row).unwrap()),
1705 )
1706 .unwrap();
1707 assert_eq!(local_login.fields.http_realm, None);
1708 assert_eq!(local_login.fields.form_action_origin, None);
1709
1710 assert!(!db.exists(login.guid_str()).unwrap());
1711 }
1712
1713 #[test]
1714 fn test_delete_many() {
1715 ensure_initialized();
1716 let db = LoginDb::open_in_memory();
1717
1718 let login_a = db
1719 .add(
1720 LoginEntry {
1721 origin: "https://a.example.com".into(),
1722 http_realm: Some("https://www.example.com".into()),
1723 username: "test_user".into(),
1724 password: "test_password".into(),
1725 ..Default::default()
1726 },
1727 &*TEST_ENCDEC,
1728 )
1729 .unwrap();
1730
1731 let login_b = db
1732 .add(
1733 LoginEntry {
1734 origin: "https://b.example.com".into(),
1735 http_realm: Some("https://www.example.com".into()),
1736 username: "test_user".into(),
1737 password: "test_password".into(),
1738 ..Default::default()
1739 },
1740 &*TEST_ENCDEC,
1741 )
1742 .unwrap();
1743
1744 let result = db
1745 .delete_many(vec![login_a.guid_str(), login_b.guid_str()])
1746 .unwrap();
1747 assert!(result[0]);
1748 assert!(result[1]);
1749 assert!(!db.exists(login_a.guid_str()).unwrap());
1750 assert!(!db.exists(login_b.guid_str()).unwrap());
1751 }
1752
1753 #[test]
1754 fn test_subsequent_delete_many() {
1755 ensure_initialized();
1756 let db = LoginDb::open_in_memory();
1757
1758 let login = db
1759 .add(
1760 LoginEntry {
1761 origin: "https://a.example.com".into(),
1762 http_realm: Some("https://www.example.com".into()),
1763 username: "test_user".into(),
1764 password: "test_password".into(),
1765 ..Default::default()
1766 },
1767 &*TEST_ENCDEC,
1768 )
1769 .unwrap();
1770
1771 let result = db.delete_many(vec![login.guid_str()]).unwrap();
1772 assert!(result[0]);
1773 assert!(!db.exists(login.guid_str()).unwrap());
1774
1775 let result = db.delete_many(vec![login.guid_str()]).unwrap();
1776 assert!(!result[0]);
1777 }
1778
1779 #[test]
1780 fn test_delete_many_with_non_existent_id() {
1781 ensure_initialized();
1782 let db = LoginDb::open_in_memory();
1783
1784 let result = db.delete_many(vec![&Guid::random()]).unwrap();
1785 assert!(!result[0]);
1786 }
1787
1788 #[test]
1789 fn test_delete_local_for_remote_replacement() {
1790 ensure_initialized();
1791 let db = LoginDb::open_in_memory();
1792 let login = db
1793 .add(
1794 LoginEntry {
1795 origin: "https://www.example.com".into(),
1796 http_realm: Some("https://www.example.com".into()),
1797 username: "test_user".into(),
1798 password: "test_password".into(),
1799 ..Default::default()
1800 },
1801 &*TEST_ENCDEC,
1802 )
1803 .unwrap();
1804
1805 let result = db
1806 .delete_local_records_for_remote_replacement(vec![login.guid_str()])
1807 .unwrap();
1808
1809 let local_guids = get_local_guids(&db);
1810 assert_eq!(local_guids.len(), 0);
1811
1812 let mirror_guids = get_mirror_guids(&db);
1813 assert_eq!(mirror_guids.len(), 0);
1814
1815 assert_eq!(result.local_deleted, 1);
1816 }
1817
1818 mod test_find_login_to_update {
1819 use super::*;
1820
1821 fn make_entry(username: &str, password: &str) -> LoginEntry {
1822 LoginEntry {
1823 origin: "https://www.example.com".into(),
1824 http_realm: Some("the website".into()),
1825 username: username.into(),
1826 password: password.into(),
1827 ..Default::default()
1828 }
1829 }
1830
1831 fn make_saved_login(db: &LoginDb, username: &str, password: &str) -> Login {
1832 db.add(make_entry(username, password), &*TEST_ENCDEC)
1833 .unwrap()
1834 .decrypt(&*TEST_ENCDEC)
1835 .unwrap()
1836 }
1837
1838 #[test]
1839 fn test_match() {
1840 ensure_initialized();
1841 let db = LoginDb::open_in_memory();
1842 let login = make_saved_login(&db, "user", "pass");
1843 assert_eq!(
1844 Some(login),
1845 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
1846 .unwrap(),
1847 );
1848 }
1849
1850 #[test]
1851 fn test_non_matches() {
1852 ensure_initialized();
1853 let db = LoginDb::open_in_memory();
1854 make_saved_login(&db, "other-user", "pass");
1856 db.add(
1858 LoginEntry {
1859 origin: "https://www.example.com".into(),
1860 http_realm: Some("the other website".into()),
1861 username: "user".into(),
1862 password: "pass".into(),
1863 ..Default::default()
1864 },
1865 &*TEST_ENCDEC,
1866 )
1867 .unwrap();
1868 db.add(
1870 LoginEntry {
1871 origin: "https://www.example.com".into(),
1872 form_action_origin: Some("https://www.example.com/".into()),
1873 username: "user".into(),
1874 password: "pass".into(),
1875 ..Default::default()
1876 },
1877 &*TEST_ENCDEC,
1878 )
1879 .unwrap();
1880 assert_eq!(
1881 None,
1882 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
1883 .unwrap(),
1884 );
1885 }
1886
1887 #[test]
1888 fn test_match_blank_password() {
1889 ensure_initialized();
1890 let db = LoginDb::open_in_memory();
1891 let login = make_saved_login(&db, "", "pass");
1892 assert_eq!(
1893 Some(login),
1894 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
1895 .unwrap(),
1896 );
1897 }
1898
1899 #[test]
1900 fn test_username_match_takes_precedence_over_blank_username() {
1901 ensure_initialized();
1902 let db = LoginDb::open_in_memory();
1903 make_saved_login(&db, "", "pass");
1904 let username_match = make_saved_login(&db, "user", "pass");
1905 assert_eq!(
1906 Some(username_match),
1907 db.find_login_to_update(make_entry("user", "pass"), &*TEST_ENCDEC)
1908 .unwrap(),
1909 );
1910 }
1911
1912 #[test]
1913 fn test_invalid_login() {
1914 ensure_initialized();
1915 let db = LoginDb::open_in_memory();
1916 assert!(db
1917 .find_login_to_update(
1918 LoginEntry {
1919 http_realm: None,
1920 form_action_origin: None,
1921 ..LoginEntry::default()
1922 },
1923 &*TEST_ENCDEC
1924 )
1925 .is_err());
1926 }
1927
1928 #[test]
1929 fn test_update_with_duplicate_login() {
1930 ensure_initialized();
1931 let db = LoginDb::open_in_memory();
1934 let login = make_saved_login(&db, "user", "pass");
1935 let mut dupe = login.clone().encrypt(&*TEST_ENCDEC).unwrap();
1936 dupe.meta.id = "different-guid".to_string();
1937 db.insert_new_login(&dupe).unwrap();
1938
1939 let mut entry = login.entry();
1940 entry.password = "pass2".to_string();
1941 db.update(&login.id, entry, &*TEST_ENCDEC).unwrap();
1942
1943 let mut entry = login.entry();
1944 entry.password = "pass3".to_string();
1945 db.add_or_update(entry, &*TEST_ENCDEC).unwrap();
1946 }
1947 }
1948}