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