logins/
store.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4use crate::db::{LoginDb, LoginsDeletionMetrics};
5use crate::encryption::EncryptorDecryptor;
6use crate::error::*;
7use crate::login::{BulkResultEntry, EncryptedLogin, Login, LoginEntry, LoginEntryWithMeta};
8use crate::LoginsSyncEngine;
9use parking_lot::Mutex;
10use sql_support::run_maintenance;
11use std::path::Path;
12use std::sync::{Arc, Weak};
13use sync15::{
14    engine::{EngineSyncAssociation, SyncEngine, SyncEngineId},
15    ServerTimestamp,
16};
17
18#[derive(uniffi::Enum)]
19pub enum LoginOrErrorMessage {
20    Login,
21    String,
22}
23
24// Our "sync manager" will use whatever is stashed here.
25lazy_static::lazy_static! {
26    // Mutex: just taken long enough to update the inner stuff - needed
27    //        to wrap the RefCell as they aren't `Sync`
28    static ref STORE_FOR_MANAGER: Mutex<Weak<LoginStore>> = Mutex::new(Weak::new());
29}
30
31/// Called by the sync manager to get a sync engine via the store previously
32/// registered with the sync manager.
33pub fn get_registered_sync_engine(engine_id: &SyncEngineId) -> Option<Box<dyn SyncEngine>> {
34    let weak = STORE_FOR_MANAGER.lock();
35    match weak.upgrade() {
36        None => None,
37        Some(store) => match create_sync_engine(store, engine_id) {
38            Ok(engine) => Some(engine),
39            Err(e) => {
40                report_error!("logins-sync-engine-create-error", "{e}");
41                None
42            }
43        },
44    }
45}
46
47fn create_sync_engine(
48    store: Arc<LoginStore>,
49    engine_id: &SyncEngineId,
50) -> Result<Box<dyn SyncEngine>> {
51    match engine_id {
52        SyncEngineId::Passwords => Ok(Box::new(LoginsSyncEngine::new(Arc::clone(&store))?)),
53        // panicking here seems reasonable - it's a static error if this
54        // it hit, not something that runtime conditions can influence.
55        _ => unreachable!("can't provide unknown engine: {}", engine_id),
56    }
57}
58
59fn map_bulk_result_entry(
60    enc_login: Result<EncryptedLogin>,
61    encdec: &dyn EncryptorDecryptor,
62) -> BulkResultEntry {
63    match enc_login {
64        Ok(enc_login) => match enc_login.decrypt(encdec) {
65            Ok(login) => BulkResultEntry::Success { login },
66            Err(error) => {
67                warn!("Login could not be decrypted. This indicates a fundamental problem with the encryption key.");
68                BulkResultEntry::Error {
69                    message: error.to_string(),
70                }
71            }
72        },
73        Err(error) => BulkResultEntry::Error {
74            message: error.to_string(),
75        },
76    }
77}
78
79pub struct LoginStore {
80    pub db: Mutex<Option<LoginDb>>,
81}
82
83impl LoginStore {
84    #[handle_error(Error)]
85    pub fn new(path: impl AsRef<Path>, encdec: Arc<dyn EncryptorDecryptor>) -> ApiResult<Self> {
86        let db = Mutex::new(Some(LoginDb::open(path, encdec)?));
87        Ok(Self { db })
88    }
89
90    pub fn new_from_db(db: LoginDb) -> Self {
91        let db = Mutex::new(Some(db));
92        Self { db }
93    }
94
95    // Only used for tests, but it's `pub` the `sync-test` crate uses it.
96    #[cfg(test)]
97    pub fn new_in_memory() -> Self {
98        let db = Mutex::new(Some(LoginDb::open_in_memory()));
99        Self { db }
100    }
101
102    pub fn lock_db(&self) -> Result<parking_lot::MappedMutexGuard<'_, LoginDb>> {
103        parking_lot::MutexGuard::try_map(self.db.lock(), |db| db.as_mut())
104            .map_err(|_| Error::DatabaseClosed)
105    }
106
107    #[handle_error(Error)]
108    pub fn is_empty(&self) -> ApiResult<bool> {
109        Ok(self.lock_db()?.count_all()? == 0)
110    }
111
112    #[handle_error(Error)]
113    pub fn list(&self) -> ApiResult<Vec<Login>> {
114        let db = self.lock_db()?;
115        db.get_all().and_then(|logins| {
116            logins
117                .into_iter()
118                .map(|login| login.decrypt(db.encdec.as_ref()))
119                .collect()
120        })
121    }
122
123    #[handle_error(Error)]
124    pub fn count(&self) -> ApiResult<i64> {
125        self.lock_db()?.count_all()
126    }
127
128    #[handle_error(Error)]
129    pub fn count_by_origin(&self, origin: &str) -> ApiResult<i64> {
130        self.lock_db()?.count_by_origin(origin)
131    }
132
133    #[handle_error(Error)]
134    pub fn count_by_form_action_origin(&self, form_action_origin: &str) -> ApiResult<i64> {
135        self.lock_db()?
136            .count_by_form_action_origin(form_action_origin)
137    }
138
139    #[handle_error(Error)]
140    pub fn get(&self, id: &str) -> ApiResult<Option<Login>> {
141        let db = self.lock_db()?;
142        match db.get_by_id(id) {
143            Ok(result) => match result {
144                Some(enc_login) => enc_login.decrypt(db.encdec.as_ref()).map(Some),
145                None => Ok(None),
146            },
147            Err(err) => Err(err),
148        }
149    }
150
151    #[handle_error(Error)]
152    pub fn get_by_base_domain(&self, base_domain: &str) -> ApiResult<Vec<Login>> {
153        let db = self.lock_db()?;
154        db.get_by_base_domain(base_domain).and_then(|logins| {
155            logins
156                .into_iter()
157                .map(|login| login.decrypt(db.encdec.as_ref()))
158                .collect()
159        })
160    }
161
162    #[handle_error(Error)]
163    pub fn has_logins_by_base_domain(&self, base_domain: &str) -> ApiResult<bool> {
164        self.lock_db()?
165            .get_by_base_domain(base_domain)
166            .map(|logins| !logins.is_empty())
167    }
168
169    #[handle_error(Error)]
170    pub fn find_login_to_update(&self, entry: LoginEntry) -> ApiResult<Option<Login>> {
171        let db = self.lock_db()?;
172        db.find_login_to_update(entry, db.encdec.as_ref())
173    }
174
175    #[handle_error(Error)]
176    pub fn touch(&self, id: &str) -> ApiResult<()> {
177        self.lock_db()?.touch(id)
178    }
179
180    #[handle_error(Error)]
181    pub fn are_potentially_vulnerable_passwords(&self, ids: Vec<String>) -> ApiResult<Vec<String>> {
182        // Note: Vec<&str> is not supported with UDL, so we receive Vec<String> and convert
183        let db = self.lock_db()?;
184        let ids: Vec<&str> = ids.iter().map(|id| &**id).collect();
185        db.are_potentially_vulnerable_passwords(&ids, db.encdec.as_ref())
186    }
187
188    #[handle_error(Error)]
189    pub fn is_potentially_vulnerable_password(&self, id: &str) -> ApiResult<bool> {
190        let db = self.lock_db()?;
191        db.is_potentially_vulnerable_password(id, db.encdec.as_ref())
192    }
193
194    #[handle_error(Error)]
195    pub fn record_potentially_vulnerable_passwords(&self, passwords: Vec<String>) -> ApiResult<()> {
196        let db = self.lock_db()?;
197        db.record_potentially_vulnerable_passwords(passwords, db.encdec.as_ref())
198    }
199
200    #[handle_error(Error)]
201    pub fn reset_all_breaches(&self) -> ApiResult<()> {
202        self.lock_db()?.reset_all_breaches()
203    }
204
205    #[handle_error(Error)]
206    pub fn record_breach_alert_dismissal(&self, id: &str) -> ApiResult<()> {
207        self.lock_db()?.record_breach_alert_dismissal(id)
208    }
209
210    #[handle_error(Error)]
211    pub fn record_breach_alert_dismissal_time(&self, id: &str, timestamp: i64) -> ApiResult<()> {
212        self.lock_db()?
213            .record_breach_alert_dismissal_time(id, timestamp)
214    }
215
216    #[handle_error(Error)]
217    pub fn delete(&self, id: &str) -> ApiResult<bool> {
218        self.lock_db()?.delete(id)
219    }
220
221    #[handle_error(Error)]
222    pub fn delete_many(&self, ids: Vec<String>) -> ApiResult<Vec<bool>> {
223        // Note we need to receive a vector of String here because `Vec<&str>` is not supported
224        // with UDL.
225        let ids: Vec<&str> = ids.iter().map(|id| &**id).collect();
226        self.lock_db()?.delete_many(ids)
227    }
228
229    #[handle_error(Error)]
230    pub fn delete_undecryptable_records_for_remote_replacement(
231        self: Arc<Self>,
232    ) -> ApiResult<LoginsDeletionMetrics> {
233        // This function was created for the iOS logins verification logic that will
234        // remove records that prevent logins syncing. Once the verification logic is
235        // removed from iOS, this function can be removed from the store.
236
237        // Creating an engine requires locking the DB, so make sure to do this first
238        let engine = LoginsSyncEngine::new(Arc::clone(&self))?;
239
240        let db = self.lock_db()?;
241        let deletion_stats =
242            db.delete_undecryptable_records_for_remote_replacement(db.encdec.as_ref())?;
243        engine.set_last_sync(&db, ServerTimestamp(0))?;
244        Ok(deletion_stats)
245    }
246
247    #[handle_error(Error)]
248    pub fn wipe_local(&self) -> ApiResult<()> {
249        self.lock_db()?.wipe_local()?;
250        Ok(())
251    }
252
253    #[handle_error(Error)]
254    pub fn reset(self: Arc<Self>) -> ApiResult<()> {
255        // Reset should not exist here - all resets should be done via the
256        // sync manager. It seems that actual consumers don't use this, but
257        // some tests do, so it remains for now.
258        let engine = LoginsSyncEngine::new(Arc::clone(&self))?;
259        engine.do_reset(&EngineSyncAssociation::Disconnected)?;
260        Ok(())
261    }
262
263    #[handle_error(Error)]
264    pub fn update(&self, id: &str, entry: LoginEntry) -> ApiResult<Login> {
265        let db = self.lock_db()?;
266        db.update(id, entry, db.encdec.as_ref())
267            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
268    }
269
270    #[handle_error(Error)]
271    pub fn add(&self, entry: LoginEntry) -> ApiResult<Login> {
272        let db = self.lock_db()?;
273        db.add(entry, db.encdec.as_ref())
274            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
275    }
276
277    #[handle_error(Error)]
278    pub fn add_many(&self, entries: Vec<LoginEntry>) -> ApiResult<Vec<BulkResultEntry>> {
279        let db = self.lock_db()?;
280        db.add_many(entries, db.encdec.as_ref()).map(|enc_logins| {
281            enc_logins
282                .into_iter()
283                .map(|enc_login| map_bulk_result_entry(enc_login, db.encdec.as_ref()))
284                .collect()
285        })
286    }
287
288    /// This method is intended to preserve metadata (LoginMeta) during a migration.
289    /// In normal operation, this method should not be used; instead,
290    /// use `add(entry)`, which manages the corresponding fields itself.
291    #[handle_error(Error)]
292    pub fn add_with_meta(&self, entry_with_meta: LoginEntryWithMeta) -> ApiResult<Login> {
293        let db = self.lock_db()?;
294        db.add_with_meta(entry_with_meta, db.encdec.as_ref())
295            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
296    }
297
298    #[handle_error(Error)]
299    pub fn add_many_with_meta(
300        &self,
301        entries_with_meta: Vec<LoginEntryWithMeta>,
302    ) -> ApiResult<Vec<BulkResultEntry>> {
303        let db = self.lock_db()?;
304        db.add_many_with_meta(entries_with_meta, db.encdec.as_ref())
305            .map(|enc_logins| {
306                enc_logins
307                    .into_iter()
308                    .map(|enc_login| map_bulk_result_entry(enc_login, db.encdec.as_ref()))
309                    .collect()
310            })
311    }
312
313    #[handle_error(Error)]
314    pub fn add_or_update(&self, entry: LoginEntry) -> ApiResult<Login> {
315        let db = self.lock_db()?;
316        db.add_or_update(entry, db.encdec.as_ref())
317            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
318    }
319
320    #[handle_error(Error)]
321    pub fn run_maintenance(&self, options: Option<RunMaintenanceOptions>) -> ApiResult<()> {
322        let conn = self.lock_db()?;
323        let options = options.unwrap_or_default();
324        run_maintenance(&conn)?;
325        if options.delete_undecryptable_records_for_remote_replacement {
326            conn.delete_undecryptable_records_for_remote_replacement(conn.encdec.as_ref())?;
327        }
328        Ok(())
329    }
330
331    pub fn shutdown(&self) {
332        if let Some(db) = self.db.lock().take() {
333            let _ = db.shutdown();
334        }
335    }
336
337    // This allows the embedding app to say "make this instance available to
338    // the sync manager". The implementation is more like "offer to sync mgr"
339    // (thereby avoiding us needing to link with the sync manager) but
340    // `register_with_sync_manager()` is logically what's happening so that's
341    // the name it gets.
342    pub fn register_with_sync_manager(self: Arc<Self>) {
343        let mut state = STORE_FOR_MANAGER.lock();
344        *state = Arc::downgrade(&self);
345    }
346
347    // this isn't exposed by uniffi - currently the
348    // only consumer of this is our "example" (and hence why they
349    // are `pub` and not `pub(crate)`).
350    // We could probably make the example work with the sync manager - but then
351    // our example would link with places and logins etc, and it's not a big
352    // deal really.
353    #[handle_error(Error)]
354    pub fn create_logins_sync_engine(self: Arc<Self>) -> ApiResult<Box<dyn SyncEngine>> {
355        Ok(Box::new(LoginsSyncEngine::new(self)?) as Box<dyn SyncEngine>)
356    }
357}
358
359pub struct RunMaintenanceOptions {
360    pub delete_undecryptable_records_for_remote_replacement: bool,
361}
362
363impl Default for RunMaintenanceOptions {
364    fn default() -> Self {
365        Self {
366            delete_undecryptable_records_for_remote_replacement: true,
367        }
368    }
369}
370
371#[cfg(not(feature = "keydb"))]
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use crate::encryption::test_utils::TEST_ENCDEC;
376    use crate::util;
377    use nss_as::ensure_initialized;
378    use std::cmp::Reverse;
379    use std::time::SystemTime;
380
381    fn assert_logins_equiv(a: &LoginEntry, b: &Login) {
382        assert_eq!(a.origin, b.origin);
383        assert_eq!(a.form_action_origin, b.form_action_origin);
384        assert_eq!(a.http_realm, b.http_realm);
385        assert_eq!(a.username_field, b.username_field);
386        assert_eq!(a.password_field, b.password_field);
387        assert_eq!(b.username, a.username);
388        assert_eq!(b.password, a.password);
389    }
390
391    #[test]
392    fn test_general() {
393        ensure_initialized();
394
395        let store = LoginStore::new_in_memory();
396        let list = store.list().expect("Grabbing Empty list to work");
397        assert_eq!(list.len(), 0);
398        let start_us = util::system_time_ms_i64(SystemTime::now());
399
400        let a = LoginEntry {
401            origin: "https://www.example.com".into(),
402            form_action_origin: Some("https://www.example.com".into()),
403            username_field: "user_input".into(),
404            password_field: "pass_input".into(),
405            username: "coolperson21".into(),
406            password: "p4ssw0rd".into(),
407            ..Default::default()
408        };
409
410        let b = LoginEntry {
411            origin: "https://www.example2.com".into(),
412            http_realm: Some("Some String Here".into()),
413            username: "asdf".into(),
414            password: "fdsa".into(),
415            ..Default::default()
416        };
417        let a_id = store.add(a.clone()).expect("added a").id;
418        let b_id = store.add(b.clone()).expect("added b").id;
419
420        let a_from_db = store
421            .get(&a_id)
422            .expect("Not to error getting a")
423            .expect("a to exist");
424
425        assert_logins_equiv(&a, &a_from_db);
426        assert!(a_from_db.time_created >= start_us);
427        assert!(a_from_db.time_password_changed >= start_us);
428        assert!(a_from_db.time_last_used >= start_us);
429        assert_eq!(a_from_db.times_used, 1);
430
431        let b_from_db = store
432            .get(&b_id)
433            .expect("Not to error getting b")
434            .expect("b to exist");
435
436        assert_logins_equiv(&LoginEntry { ..b.clone() }, &b_from_db);
437        assert!(b_from_db.time_created >= start_us);
438        assert!(b_from_db.time_password_changed >= start_us);
439        assert!(b_from_db.time_last_used >= start_us);
440        assert_eq!(b_from_db.times_used, 1);
441
442        let mut list = store.list().expect("Grabbing list to work");
443        assert_eq!(list.len(), 2);
444
445        let mut expect = vec![a_from_db, b_from_db.clone()];
446
447        list.sort_by_key(|b| Reverse(b.guid()));
448        expect.sort_by_key(|b| Reverse(b.guid()));
449        assert_eq!(list, expect);
450
451        store.delete(&a_id).expect("Successful delete");
452        assert!(store
453            .get(&a_id)
454            .expect("get after delete should still work")
455            .is_none());
456
457        let list = store.list().expect("Grabbing list to work");
458        assert_eq!(list.len(), 1);
459        assert_eq!(list[0], b_from_db);
460
461        let has_logins = store
462            .has_logins_by_base_domain("example2.com")
463            .expect("Expect a result for this origin");
464        assert!(has_logins);
465
466        let list = store
467            .get_by_base_domain("example2.com")
468            .expect("Expect a list for this origin");
469        assert_eq!(list.len(), 1);
470        assert_eq!(list[0], b_from_db);
471
472        let has_logins = store
473            .has_logins_by_base_domain("www.example.com")
474            .expect("Expect a result for this origin");
475        assert!(!has_logins);
476
477        let list = store
478            .get_by_base_domain("www.example.com")
479            .expect("Expect an empty list");
480        assert_eq!(list.len(), 0);
481
482        let now_us = util::system_time_ms_i64(SystemTime::now());
483        let b2 = LoginEntry {
484            username: b.username.to_owned(),
485            password: "newpass".into(),
486            ..b
487        };
488
489        store
490            .update(&b_id, b2.clone())
491            .expect("update b should work");
492
493        let b_after_update = store
494            .get(&b_id)
495            .expect("Not to error getting b")
496            .expect("b to exist");
497
498        assert_logins_equiv(&b2, &b_after_update);
499        assert!(b_after_update.time_created >= start_us);
500        assert!(b_after_update.time_created <= now_us);
501        assert!(b_after_update.time_password_changed >= now_us);
502        assert!(b_after_update.time_last_used >= now_us);
503        // Should be two even though we updated twice
504        assert_eq!(b_after_update.times_used, 2);
505    }
506
507    #[test]
508    fn test_sync_manager_registration() {
509        ensure_initialized();
510        let store = Arc::new(LoginStore::new_in_memory());
511        assert_eq!(Arc::strong_count(&store), 1);
512        assert_eq!(Arc::weak_count(&store), 0);
513        Arc::clone(&store).register_with_sync_manager();
514        assert_eq!(Arc::strong_count(&store), 1);
515        assert_eq!(Arc::weak_count(&store), 1);
516        let registered = STORE_FOR_MANAGER.lock().upgrade().expect("should upgrade");
517        assert!(Arc::ptr_eq(&store, &registered));
518        drop(registered);
519        // should be no new references
520        assert_eq!(Arc::strong_count(&store), 1);
521        assert_eq!(Arc::weak_count(&store), 1);
522        // dropping the registered object should drop the registration.
523        drop(store);
524        assert!(STORE_FOR_MANAGER.lock().upgrade().is_none());
525    }
526
527    #[test]
528    fn test_wipe_local_on_a_fresh_database_is_a_noop() {
529        ensure_initialized();
530        // If the database has data, then wipe_local() returns > 0 rows deleted
531        let db = LoginDb::open_in_memory();
532        db.add_or_update(
533            LoginEntry {
534                origin: "https://www.example.com".into(),
535                form_action_origin: Some("https://www.example.com".into()),
536                username_field: "user_input".into(),
537                password_field: "pass_input".into(),
538                username: "coolperson21".into(),
539                password: "p4ssw0rd".into(),
540                ..Default::default()
541            },
542            &TEST_ENCDEC.clone(),
543        )
544        .unwrap();
545        assert!(db.wipe_local().unwrap() > 0);
546
547        // If the database is empty, then wipe_local() returns 0 rows deleted
548        let db = LoginDb::open_in_memory();
549        assert_eq!(db.wipe_local().unwrap(), 0);
550    }
551
552    #[test]
553    fn test_shutdown() {
554        ensure_initialized();
555        let store = LoginStore::new_in_memory();
556        store.shutdown();
557        assert!(matches!(
558            store.list(),
559            Err(LoginsApiError::UnexpectedLoginsApiError { reason: _ })
560        ));
561        assert!(store.db.lock().is_none());
562    }
563
564    #[test]
565    fn test_delete_undecryptable_records_for_remote_replacement() {
566        ensure_initialized();
567        let store = Arc::new(LoginStore::new_in_memory());
568        // Not much of a test, but let's make sure this doesn't deadlock at least.
569        store
570            .delete_undecryptable_records_for_remote_replacement()
571            .unwrap();
572    }
573}
574
575#[test]
576fn test_send() {
577    fn ensure_send<T: Send>() {}
578    ensure_send::<LoginStore>();
579}
580
581#[cfg(feature = "keydb")]
582#[cfg(test)]
583mod tests_keydb {
584    use super::*;
585    use crate::{ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator};
586    use async_trait::async_trait;
587    use nss_as::ensure_initialized_with_profile_dir;
588    use std::path::PathBuf;
589
590    struct MockPrimaryPasswordAuthenticator {
591        password: String,
592    }
593
594    #[async_trait]
595    impl PrimaryPasswordAuthenticator for MockPrimaryPasswordAuthenticator {
596        async fn get_primary_password(&self) -> ApiResult<String> {
597            Ok(self.password.clone())
598        }
599        async fn on_authentication_success(&self) -> ApiResult<()> {
600            Ok(())
601        }
602        async fn on_authentication_failure(&self) -> ApiResult<()> {
603            Ok(())
604        }
605    }
606
607    fn profile_path() -> PathBuf {
608        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
609            .join("../support/rc_crypto/nss/fixtures/profile")
610    }
611
612    #[test]
613    fn decrypting_logins_with_primary_password() {
614        ensure_initialized_with_profile_dir(profile_path());
615
616        // `password` is the primary password of the profile fixture
617        let primary_password_authenticator = MockPrimaryPasswordAuthenticator {
618            password: "password".to_string(),
619        };
620        let key_manager = NSSKeyManager::new(Arc::new(primary_password_authenticator));
621        let encdec = ManagedEncryptorDecryptor::new(Arc::new(key_manager));
622        let store = LoginStore::new(profile_path().join("logins.db"), Arc::new(encdec))
623            .expect("store from fixtures");
624        let list = store.list().expect("Grabbing list to work");
625
626        assert_eq!(list.len(), 1);
627
628        assert_eq!(list[0].origin, "https://www.example.com");
629        assert_eq!(list[0].username, "test");
630        assert_eq!(list[0].password, "test");
631    }
632}