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