1use crate::db::{LoginDb, LoginsDeletionMetrics};
5use crate::encryption::EncryptorDecryptor;
6use crate::error::*;
7use crate::login::{BulkResultEntry, EncryptedLogin, Login, LoginEntry, LoginEntryWithMeta};
8use crate::schema;
9use crate::LoginsSyncEngine;
10use parking_lot::Mutex;
11use sql_support::run_maintenance;
12use std::path::Path;
13use std::sync::{Arc, Weak};
14use sync15::{
15 engine::{EngineSyncAssociation, SyncEngine, SyncEngineId},
16 ServerTimestamp,
17};
18
19#[derive(uniffi::Enum)]
20pub enum LoginOrErrorMessage {
21 Login,
22 String,
23}
24
25lazy_static::lazy_static! {
27 static ref STORE_FOR_MANAGER: Mutex<Weak<LoginStore>> = Mutex::new(Weak::new());
30}
31
32pub fn get_registered_sync_engine(engine_id: &SyncEngineId) -> Option<Box<dyn SyncEngine>> {
35 let weak = STORE_FOR_MANAGER.lock();
36 match weak.upgrade() {
37 None => None,
38 Some(store) => match create_sync_engine(store, engine_id) {
39 Ok(engine) => Some(engine),
40 Err(e) => {
41 report_error!("logins-sync-engine-create-error", "{e}");
42 None
43 }
44 },
45 }
46}
47
48fn create_sync_engine(
49 store: Arc<LoginStore>,
50 engine_id: &SyncEngineId,
51) -> Result<Box<dyn SyncEngine>> {
52 match engine_id {
53 SyncEngineId::Passwords => Ok(Box::new(LoginsSyncEngine::new(Arc::clone(&store))?)),
54 _ => unreachable!("can't provide unknown engine: {}", engine_id),
57 }
58}
59
60fn map_bulk_result_entry(
61 enc_login: Result<EncryptedLogin>,
62 encdec: &dyn EncryptorDecryptor,
63) -> BulkResultEntry {
64 match enc_login {
65 Ok(enc_login) => match enc_login.decrypt(encdec) {
66 Ok(login) => BulkResultEntry::Success { login },
67 Err(error) => {
68 warn!("Login could not be decrypted. This indicates a fundamental problem with the encryption key.");
69 BulkResultEntry::Error {
70 message: error.to_string(),
71 }
72 }
73 },
74 Err(error) => BulkResultEntry::Error {
75 message: error.to_string(),
76 },
77 }
78}
79
80pub struct LoginStore {
81 pub db: Mutex<Option<LoginDb>>,
82}
83
84impl LoginStore {
85 #[handle_error(Error)]
86 pub fn new(path: impl AsRef<Path>, encdec: Arc<dyn EncryptorDecryptor>) -> ApiResult<Self> {
87 let db = Mutex::new(Some(LoginDb::open(path, encdec)?));
88 Ok(Self { db })
89 }
90
91 pub fn new_from_db(db: LoginDb) -> Self {
92 let db = Mutex::new(Some(db));
93 Self { db }
94 }
95
96 #[cfg(test)]
98 pub fn new_in_memory() -> Self {
99 let db = Mutex::new(Some(LoginDb::open_in_memory()));
100 Self { db }
101 }
102
103 pub fn lock_db(&self) -> Result<parking_lot::MappedMutexGuard<'_, LoginDb>> {
104 parking_lot::MutexGuard::try_map(self.db.lock(), |db| db.as_mut())
105 .map_err(|_| Error::DatabaseClosed)
106 }
107
108 #[handle_error(Error)]
109 pub fn is_empty(&self) -> ApiResult<bool> {
110 Ok(self.lock_db()?.count_all()? == 0)
111 }
112
113 #[handle_error(Error)]
114 pub fn list(&self) -> ApiResult<Vec<Login>> {
115 let db = self.lock_db()?;
116 db.get_all().and_then(|logins| {
117 logins
118 .into_iter()
119 .map(|login| login.decrypt(db.encdec.as_ref()))
120 .collect()
121 })
122 }
123
124 #[handle_error(Error)]
125 pub fn count(&self) -> ApiResult<i64> {
126 self.lock_db()?.count_all()
127 }
128
129 #[handle_error(Error)]
130 pub fn count_by_origin(&self, origin: &str) -> ApiResult<i64> {
131 self.lock_db()?.count_by_origin(origin)
132 }
133
134 #[handle_error(Error)]
135 pub fn count_by_form_action_origin(&self, form_action_origin: &str) -> ApiResult<i64> {
136 self.lock_db()?
137 .count_by_form_action_origin(form_action_origin)
138 }
139
140 #[handle_error(Error)]
141 pub fn get(&self, id: &str) -> ApiResult<Option<Login>> {
142 let db = self.lock_db()?;
143 match db.get_by_id(id) {
144 Ok(result) => match result {
145 Some(enc_login) => enc_login.decrypt(db.encdec.as_ref()).map(Some),
146 None => Ok(None),
147 },
148 Err(err) => Err(err),
149 }
150 }
151
152 #[handle_error(Error)]
153 pub fn get_by_base_domain(&self, base_domain: &str) -> ApiResult<Vec<Login>> {
154 let db = self.lock_db()?;
155 db.get_by_base_domain(base_domain).and_then(|logins| {
156 logins
157 .into_iter()
158 .map(|login| login.decrypt(db.encdec.as_ref()))
159 .collect()
160 })
161 }
162
163 #[handle_error(Error)]
164 pub fn has_logins_by_base_domain(&self, base_domain: &str) -> ApiResult<bool> {
165 self.lock_db()?
166 .get_by_base_domain(base_domain)
167 .map(|logins| !logins.is_empty())
168 }
169
170 #[handle_error(Error)]
171 pub fn find_login_to_update(&self, entry: LoginEntry) -> ApiResult<Option<Login>> {
172 let db = self.lock_db()?;
173 db.find_login_to_update(entry, db.encdec.as_ref())
174 }
175
176 #[handle_error(Error)]
177 pub fn touch(&self, id: &str) -> ApiResult<()> {
178 self.lock_db()?.touch(id)
179 }
180
181 #[handle_error(Error)]
182 pub fn is_potentially_breached(&self, id: &str) -> ApiResult<bool> {
183 self.lock_db()?.is_potentially_breached(id)
184 }
185
186 #[handle_error(Error)]
187 pub fn are_potentially_vulnerable_passwords(&self, ids: Vec<String>) -> ApiResult<Vec<String>> {
188 let db = self.lock_db()?;
190 let ids: Vec<&str> = ids.iter().map(|id| &**id).collect();
191 db.are_potentially_vulnerable_passwords(&ids, db.encdec.as_ref())
192 }
193
194 #[handle_error(Error)]
195 pub fn is_potentially_vulnerable_password(&self, id: &str) -> ApiResult<bool> {
196 let db = self.lock_db()?;
197 db.is_potentially_vulnerable_password(id, db.encdec.as_ref())
198 }
199
200 #[handle_error(Error)]
201 pub fn record_potentially_vulnerable_passwords(&self, passwords: Vec<String>) -> ApiResult<()> {
202 let db = self.lock_db()?;
203 db.record_potentially_vulnerable_passwords(passwords, db.encdec.as_ref())
204 }
205
206 #[handle_error(Error)]
207 pub fn record_breach(&self, id: &str, timestamp: i64) -> ApiResult<()> {
208 let db = self.lock_db()?;
209 db.record_breach(id, timestamp, db.encdec.as_ref())
210 }
211
212 #[handle_error(Error)]
213 pub fn reset_all_breaches(&self) -> ApiResult<()> {
214 self.lock_db()?.reset_all_breaches()
215 }
216
217 #[handle_error(Error)]
218 pub fn is_breach_alert_dismissed(&self, id: &str) -> ApiResult<bool> {
219 self.lock_db()?.is_breach_alert_dismissed(id)
220 }
221
222 #[handle_error(Error)]
223 pub fn record_breach_alert_dismissal(&self, id: &str) -> ApiResult<()> {
224 self.lock_db()?.record_breach_alert_dismissal(id)
225 }
226
227 #[handle_error(Error)]
228 pub fn record_breach_alert_dismissal_time(&self, id: &str, timestamp: i64) -> ApiResult<()> {
229 self.lock_db()?
230 .record_breach_alert_dismissal_time(id, timestamp)
231 }
232
233 #[handle_error(Error)]
234 pub fn delete(&self, id: &str) -> ApiResult<bool> {
235 self.lock_db()?.delete(id)
236 }
237
238 #[handle_error(Error)]
239 pub fn delete_many(&self, ids: Vec<String>) -> ApiResult<Vec<bool>> {
240 let ids: Vec<&str> = ids.iter().map(|id| &**id).collect();
243 self.lock_db()?.delete_many(ids)
244 }
245
246 #[handle_error(Error)]
247 pub fn delete_undecryptable_records_for_remote_replacement(
248 self: Arc<Self>,
249 ) -> ApiResult<LoginsDeletionMetrics> {
250 let engine = LoginsSyncEngine::new(Arc::clone(&self))?;
256
257 let db = self.lock_db()?;
258 let deletion_stats =
259 db.delete_undecryptable_records_for_remote_replacement(db.encdec.as_ref())?;
260 engine.set_last_sync(&db, ServerTimestamp(0))?;
261 Ok(deletion_stats)
262 }
263
264 #[handle_error(Error)]
265 pub fn wipe_local(&self) -> ApiResult<()> {
266 self.lock_db()?.wipe_local()?;
267 Ok(())
268 }
269
270 #[handle_error(Error)]
271 pub fn reset(self: Arc<Self>) -> ApiResult<()> {
272 let engine = LoginsSyncEngine::new(Arc::clone(&self))?;
276 engine.do_reset(&EngineSyncAssociation::Disconnected)?;
277 Ok(())
278 }
279
280 #[handle_error(Error)]
281 pub fn update(&self, id: &str, entry: LoginEntry) -> ApiResult<Login> {
282 let db = self.lock_db()?;
283 db.update(id, entry, db.encdec.as_ref())
284 .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
285 }
286
287 #[handle_error(Error)]
288 pub fn add(&self, entry: LoginEntry) -> ApiResult<Login> {
289 let db = self.lock_db()?;
290 db.add(entry, db.encdec.as_ref())
291 .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
292 }
293
294 #[handle_error(Error)]
295 pub fn add_many(&self, entries: Vec<LoginEntry>) -> ApiResult<Vec<BulkResultEntry>> {
296 let db = self.lock_db()?;
297 db.add_many(entries, db.encdec.as_ref()).map(|enc_logins| {
298 enc_logins
299 .into_iter()
300 .map(|enc_login| map_bulk_result_entry(enc_login, db.encdec.as_ref()))
301 .collect()
302 })
303 }
304
305 #[handle_error(Error)]
309 pub fn add_with_meta(&self, entry_with_meta: LoginEntryWithMeta) -> ApiResult<Login> {
310 let db = self.lock_db()?;
311 db.add_with_meta(entry_with_meta, db.encdec.as_ref())
312 .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
313 }
314
315 #[handle_error(Error)]
316 pub fn add_many_with_meta(
317 &self,
318 entries_with_meta: Vec<LoginEntryWithMeta>,
319 ) -> ApiResult<Vec<BulkResultEntry>> {
320 let db = self.lock_db()?;
321 db.add_many_with_meta(entries_with_meta, db.encdec.as_ref())
322 .map(|enc_logins| {
323 enc_logins
324 .into_iter()
325 .map(|enc_login| map_bulk_result_entry(enc_login, db.encdec.as_ref()))
326 .collect()
327 })
328 }
329
330 #[handle_error(Error)]
331 pub fn add_or_update(&self, entry: LoginEntry) -> ApiResult<Login> {
332 let db = self.lock_db()?;
333 db.add_or_update(entry, db.encdec.as_ref())
334 .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
335 }
336
337 #[handle_error(Error)]
338 pub fn set_checkpoint(&self, checkpoint: &str) -> ApiResult<()> {
339 self.lock_db()?
340 .put_meta(schema::CHECKPOINT_KEY, &checkpoint)
341 }
342
343 #[handle_error(Error)]
344 pub fn get_checkpoint(&self) -> ApiResult<Option<String>> {
345 self.lock_db()?.get_meta(schema::CHECKPOINT_KEY)
346 }
347
348 #[handle_error(Error)]
349 pub fn run_maintenance(&self) -> ApiResult<()> {
350 let conn = self.lock_db()?;
351 run_maintenance(&conn)?;
352 Ok(())
353 }
354
355 pub fn shutdown(&self) {
356 if let Some(db) = self.db.lock().take() {
357 let _ = db.shutdown();
358 }
359 }
360
361 pub fn register_with_sync_manager(self: Arc<Self>) {
367 let mut state = STORE_FOR_MANAGER.lock();
368 *state = Arc::downgrade(&self);
369 }
370
371 #[handle_error(Error)]
378 pub fn create_logins_sync_engine(self: Arc<Self>) -> ApiResult<Box<dyn SyncEngine>> {
379 Ok(Box::new(LoginsSyncEngine::new(self)?) as Box<dyn SyncEngine>)
380 }
381}
382
383#[cfg(not(feature = "keydb"))]
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use crate::encryption::test_utils::TEST_ENCDEC;
388 use crate::util;
389 use nss::ensure_initialized;
390 use std::cmp::Reverse;
391 use std::time::SystemTime;
392
393 fn assert_logins_equiv(a: &LoginEntry, b: &Login) {
394 assert_eq!(a.origin, b.origin);
395 assert_eq!(a.form_action_origin, b.form_action_origin);
396 assert_eq!(a.http_realm, b.http_realm);
397 assert_eq!(a.username_field, b.username_field);
398 assert_eq!(a.password_field, b.password_field);
399 assert_eq!(b.username, a.username);
400 assert_eq!(b.password, a.password);
401 }
402
403 #[test]
404 fn test_general() {
405 ensure_initialized();
406
407 let store = LoginStore::new_in_memory();
408 let list = store.list().expect("Grabbing Empty list to work");
409 assert_eq!(list.len(), 0);
410 let start_us = util::system_time_ms_i64(SystemTime::now());
411
412 let a = LoginEntry {
413 origin: "https://www.example.com".into(),
414 form_action_origin: Some("https://www.example.com".into()),
415 username_field: "user_input".into(),
416 password_field: "pass_input".into(),
417 username: "coolperson21".into(),
418 password: "p4ssw0rd".into(),
419 ..Default::default()
420 };
421
422 let b = LoginEntry {
423 origin: "https://www.example2.com".into(),
424 http_realm: Some("Some String Here".into()),
425 username: "asdf".into(),
426 password: "fdsa".into(),
427 ..Default::default()
428 };
429 let a_id = store.add(a.clone()).expect("added a").id;
430 let b_id = store.add(b.clone()).expect("added b").id;
431
432 let a_from_db = store
433 .get(&a_id)
434 .expect("Not to error getting a")
435 .expect("a to exist");
436
437 assert_logins_equiv(&a, &a_from_db);
438 assert!(a_from_db.time_created >= start_us);
439 assert!(a_from_db.time_password_changed >= start_us);
440 assert!(a_from_db.time_last_used >= start_us);
441 assert_eq!(a_from_db.times_used, 1);
442
443 let b_from_db = store
444 .get(&b_id)
445 .expect("Not to error getting b")
446 .expect("b to exist");
447
448 assert_logins_equiv(&LoginEntry { ..b.clone() }, &b_from_db);
449 assert!(b_from_db.time_created >= start_us);
450 assert!(b_from_db.time_password_changed >= start_us);
451 assert!(b_from_db.time_last_used >= start_us);
452 assert_eq!(b_from_db.times_used, 1);
453
454 let mut list = store.list().expect("Grabbing list to work");
455 assert_eq!(list.len(), 2);
456
457 let mut expect = vec![a_from_db, b_from_db.clone()];
458
459 list.sort_by_key(|b| Reverse(b.guid()));
460 expect.sort_by_key(|b| Reverse(b.guid()));
461 assert_eq!(list, expect);
462
463 store.delete(&a_id).expect("Successful delete");
464 assert!(store
465 .get(&a_id)
466 .expect("get after delete should still work")
467 .is_none());
468
469 let list = store.list().expect("Grabbing list to work");
470 assert_eq!(list.len(), 1);
471 assert_eq!(list[0], b_from_db);
472
473 let has_logins = store
474 .has_logins_by_base_domain("example2.com")
475 .expect("Expect a result for this origin");
476 assert!(has_logins);
477
478 let list = store
479 .get_by_base_domain("example2.com")
480 .expect("Expect a list for this origin");
481 assert_eq!(list.len(), 1);
482 assert_eq!(list[0], b_from_db);
483
484 let has_logins = store
485 .has_logins_by_base_domain("www.example.com")
486 .expect("Expect a result for this origin");
487 assert!(!has_logins);
488
489 let list = store
490 .get_by_base_domain("www.example.com")
491 .expect("Expect an empty list");
492 assert_eq!(list.len(), 0);
493
494 let now_us = util::system_time_ms_i64(SystemTime::now());
495 let b2 = LoginEntry {
496 username: b.username.to_owned(),
497 password: "newpass".into(),
498 ..b
499 };
500
501 store
502 .update(&b_id, b2.clone())
503 .expect("update b should work");
504
505 let b_after_update = store
506 .get(&b_id)
507 .expect("Not to error getting b")
508 .expect("b to exist");
509
510 assert_logins_equiv(&b2, &b_after_update);
511 assert!(b_after_update.time_created >= start_us);
512 assert!(b_after_update.time_created <= now_us);
513 assert!(b_after_update.time_password_changed >= now_us);
514 assert!(b_after_update.time_last_used >= now_us);
515 assert_eq!(b_after_update.times_used, 2);
517 }
518
519 #[test]
520 fn test_checkpoint() {
521 ensure_initialized();
522 let store = LoginStore::new_in_memory();
523 let checkpoint = "a-checkpoint";
524 store.set_checkpoint(checkpoint).ok();
525 assert_eq!(store.get_checkpoint().unwrap().unwrap(), checkpoint);
526 }
527
528 #[test]
529 fn test_sync_manager_registration() {
530 ensure_initialized();
531 let store = Arc::new(LoginStore::new_in_memory());
532 assert_eq!(Arc::strong_count(&store), 1);
533 assert_eq!(Arc::weak_count(&store), 0);
534 Arc::clone(&store).register_with_sync_manager();
535 assert_eq!(Arc::strong_count(&store), 1);
536 assert_eq!(Arc::weak_count(&store), 1);
537 let registered = STORE_FOR_MANAGER.lock().upgrade().expect("should upgrade");
538 assert!(Arc::ptr_eq(&store, ®istered));
539 drop(registered);
540 assert_eq!(Arc::strong_count(&store), 1);
542 assert_eq!(Arc::weak_count(&store), 1);
543 drop(store);
545 assert!(STORE_FOR_MANAGER.lock().upgrade().is_none());
546 }
547
548 #[test]
549 fn test_wipe_local_on_a_fresh_database_is_a_noop() {
550 ensure_initialized();
551 let db = LoginDb::open_in_memory();
553 db.add_or_update(
554 LoginEntry {
555 origin: "https://www.example.com".into(),
556 form_action_origin: Some("https://www.example.com".into()),
557 username_field: "user_input".into(),
558 password_field: "pass_input".into(),
559 username: "coolperson21".into(),
560 password: "p4ssw0rd".into(),
561 ..Default::default()
562 },
563 &TEST_ENCDEC.clone(),
564 )
565 .unwrap();
566 assert!(db.wipe_local().unwrap() > 0);
567
568 let db = LoginDb::open_in_memory();
570 assert_eq!(db.wipe_local().unwrap(), 0);
571 }
572
573 #[test]
574 fn test_shutdown() {
575 ensure_initialized();
576 let store = LoginStore::new_in_memory();
577 store.shutdown();
578 assert!(matches!(
579 store.list(),
580 Err(LoginsApiError::UnexpectedLoginsApiError { reason: _ })
581 ));
582 assert!(store.db.lock().is_none());
583 }
584
585 #[test]
586 fn test_delete_undecryptable_records_for_remote_replacement() {
587 ensure_initialized();
588 let store = Arc::new(LoginStore::new_in_memory());
589 store
591 .delete_undecryptable_records_for_remote_replacement()
592 .unwrap();
593 }
594}
595
596#[test]
597fn test_send() {
598 fn ensure_send<T: Send>() {}
599 ensure_send::<LoginStore>();
600}
601
602#[cfg(feature = "keydb")]
603#[cfg(test)]
604mod tests_keydb {
605 use super::*;
606 use crate::{ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator};
607 use async_trait::async_trait;
608 use nss::ensure_initialized_with_profile_dir;
609 use std::path::PathBuf;
610
611 struct MockPrimaryPasswordAuthenticator {
612 password: String,
613 }
614
615 #[async_trait]
616 impl PrimaryPasswordAuthenticator for MockPrimaryPasswordAuthenticator {
617 async fn get_primary_password(&self) -> ApiResult<String> {
618 Ok(self.password.clone())
619 }
620 async fn on_authentication_success(&self) -> ApiResult<()> {
621 Ok(())
622 }
623 async fn on_authentication_failure(&self) -> ApiResult<()> {
624 Ok(())
625 }
626 }
627
628 fn profile_path() -> PathBuf {
629 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
630 .join("../support/rc_crypto/nss/fixtures/profile")
631 }
632
633 #[test]
634 fn decrypting_logins_with_primary_password() {
635 ensure_initialized_with_profile_dir(profile_path());
636
637 let primary_password_authenticator = MockPrimaryPasswordAuthenticator {
639 password: "password".to_string(),
640 };
641 let key_manager = NSSKeyManager::new(Arc::new(primary_password_authenticator));
642 let encdec = ManagedEncryptorDecryptor::new(Arc::new(key_manager));
643 let store = LoginStore::new(profile_path().join("logins.db"), Arc::new(encdec))
644 .expect("store from fixtures");
645 let list = store.list().expect("Grabbing list to work");
646
647 assert_eq!(list.len(), 1);
648
649 assert_eq!(list[0].origin, "https://www.example.com");
650 assert_eq!(list[0].username, "test");
651 assert_eq!(list[0].password, "test");
652 }
653}