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