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)
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)
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)
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)
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_all(&self) -> ApiResult<Vec<String>> {
231        self.lock_db()?.delete_all()
232    }
233
234    #[handle_error(Error)]
235    pub fn delete_all_except_fxa(&self) -> ApiResult<Vec<String>> {
236        self.lock_db()?.delete_all_except_fxa()
237    }
238
239    #[handle_error(Error)]
240    pub fn delete_undecryptable_records_for_remote_replacement(
241        self: Arc<Self>,
242    ) -> ApiResult<LoginsDeletionMetrics> {
243        // This function was created for the iOS logins verification logic that will
244        // remove records that prevent logins syncing. Once the verification logic is
245        // removed from iOS, this function can be removed from the store.
246
247        // Creating an engine requires locking the DB, so make sure to do this first
248        let engine = LoginsSyncEngine::new(Arc::clone(&self))?;
249
250        let db = self.lock_db()?;
251        let deletion_stats = db.delete_undecryptable_records_for_remote_replacement()?;
252        engine.set_last_sync(&db, ServerTimestamp(0))?;
253        Ok(deletion_stats)
254    }
255
256    #[handle_error(Error)]
257    pub fn wipe_local(&self) -> ApiResult<()> {
258        self.lock_db()?.wipe_local()?;
259        Ok(())
260    }
261
262    #[handle_error(Error)]
263    pub fn wipe_local_except_fxa(&self) -> ApiResult<()> {
264        self.lock_db()?.wipe_local_except_fxa()?;
265        Ok(())
266    }
267
268    #[handle_error(Error)]
269    pub fn reset(self: Arc<Self>) -> ApiResult<()> {
270        // Reset should not exist here - all resets should be done via the
271        // sync manager. It seems that actual consumers don't use this, but
272        // some tests do, so it remains for now.
273        let engine = LoginsSyncEngine::new(Arc::clone(&self))?;
274        engine.do_reset(&EngineSyncAssociation::Disconnected)?;
275        Ok(())
276    }
277
278    #[handle_error(Error)]
279    pub fn update(&self, id: &str, entry: LoginEntry) -> ApiResult<Login> {
280        let db = self.lock_db()?;
281        db.update(id, entry)
282            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
283    }
284
285    #[handle_error(Error)]
286    pub fn add(&self, entry: LoginEntry) -> ApiResult<Login> {
287        let db = self.lock_db()?;
288        db.add(entry)
289            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
290    }
291
292    #[handle_error(Error)]
293    pub fn add_many(&self, entries: Vec<LoginEntry>) -> ApiResult<Vec<BulkResultEntry>> {
294        let db = self.lock_db()?;
295        let encdec = db.encdec.as_ref();
296        db.add_many(entries).map(|enc_logins| {
297            enc_logins
298                .into_iter()
299                .map(|enc_login| map_bulk_result_entry(enc_login, encdec))
300                .collect()
301        })
302    }
303
304    /// This method is intended to preserve metadata (LoginMeta) during a migration.
305    /// In normal operation, this method should not be used; instead,
306    /// use `add(entry)`, which manages the corresponding fields itself.
307    #[handle_error(Error)]
308    pub fn add_with_meta(&self, entry_with_meta: LoginEntryWithMeta) -> ApiResult<Login> {
309        let db = self.lock_db()?;
310        db.add_with_meta(entry_with_meta)
311            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
312    }
313
314    #[handle_error(Error)]
315    pub fn add_many_with_meta(
316        &self,
317        entries_with_meta: Vec<LoginEntryWithMeta>,
318    ) -> ApiResult<Vec<BulkResultEntry>> {
319        let db = self.lock_db()?;
320        let encdec = db.encdec.as_ref();
321        db.add_many_with_meta(entries_with_meta).map(|enc_logins| {
322            enc_logins
323                .into_iter()
324                .map(|enc_login| map_bulk_result_entry(enc_login, encdec))
325                .collect()
326        })
327    }
328
329    #[handle_error(Error)]
330    pub fn add_or_update(&self, entry: LoginEntry) -> ApiResult<Login> {
331        let db = self.lock_db()?;
332        db.add_or_update(entry)
333            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
334    }
335
336    #[handle_error(Error)]
337    pub fn run_maintenance(&self, options: Option<RunMaintenanceOptions>) -> ApiResult<()> {
338        let conn = self.lock_db()?;
339        let options = options.unwrap_or_default();
340        run_maintenance(&conn)?;
341        if options.delete_undecryptable_records_for_remote_replacement {
342            conn.delete_undecryptable_records_for_remote_replacement()?;
343        }
344        Ok(())
345    }
346
347    pub fn shutdown(&self) {
348        if let Some(db) = self.db.lock().take() {
349            let _ = db.shutdown();
350        }
351    }
352
353    // This allows the embedding app to say "make this instance available to
354    // the sync manager". The implementation is more like "offer to sync mgr"
355    // (thereby avoiding us needing to link with the sync manager) but
356    // `register_with_sync_manager()` is logically what's happening so that's
357    // the name it gets.
358    pub fn register_with_sync_manager(self: Arc<Self>) {
359        let mut state = STORE_FOR_MANAGER.lock();
360        *state = Arc::downgrade(&self);
361    }
362
363    // this isn't exposed by uniffi - currently the
364    // only consumer of this is our "example" (and hence why they
365    // are `pub` and not `pub(crate)`).
366    // We could probably make the example work with the sync manager - but then
367    // our example would link with places and logins etc, and it's not a big
368    // deal really.
369    #[handle_error(Error)]
370    pub fn create_logins_sync_engine(self: Arc<Self>) -> ApiResult<Box<dyn SyncEngine>> {
371        Ok(Box::new(LoginsSyncEngine::new(self)?) as Box<dyn SyncEngine>)
372    }
373}
374
375pub struct RunMaintenanceOptions {
376    pub delete_undecryptable_records_for_remote_replacement: bool,
377}
378
379impl Default for RunMaintenanceOptions {
380    fn default() -> Self {
381        Self {
382            delete_undecryptable_records_for_remote_replacement: true,
383        }
384    }
385}
386
387#[cfg(not(feature = "keydb"))]
388#[cfg(test)]
389mod tests {
390    use super::*;
391    use crate::util;
392    use nss_as::ensure_initialized;
393    use std::cmp::Reverse;
394    use std::time::SystemTime;
395
396    fn assert_logins_equiv(a: &LoginEntry, b: &Login) {
397        assert_eq!(a.origin, b.origin);
398        assert_eq!(a.form_action_origin, b.form_action_origin);
399        assert_eq!(a.http_realm, b.http_realm);
400        assert_eq!(a.username_field, b.username_field);
401        assert_eq!(a.password_field, b.password_field);
402        assert_eq!(b.username, a.username);
403        assert_eq!(b.password, a.password);
404    }
405
406    #[test]
407    fn test_general() {
408        ensure_initialized();
409
410        let store = LoginStore::new_in_memory();
411        let list = store.list().expect("Grabbing Empty list to work");
412        assert_eq!(list.len(), 0);
413        let start_us = util::system_time_ms_i64(SystemTime::now());
414
415        let a = LoginEntry {
416            origin: "https://www.example.com".into(),
417            form_action_origin: Some("https://www.example.com".into()),
418            username_field: "user_input".into(),
419            password_field: "pass_input".into(),
420            username: "coolperson21".into(),
421            password: "p4ssw0rd".into(),
422            ..Default::default()
423        };
424
425        let b = LoginEntry {
426            origin: "https://www.example2.com".into(),
427            http_realm: Some("Some String Here".into()),
428            username: "asdf".into(),
429            password: "fdsa".into(),
430            ..Default::default()
431        };
432        let a_id = store.add(a.clone()).expect("added a").id;
433        let b_id = store.add(b.clone()).expect("added b").id;
434
435        let a_from_db = store
436            .get(&a_id)
437            .expect("Not to error getting a")
438            .expect("a to exist");
439
440        assert_logins_equiv(&a, &a_from_db);
441        assert!(a_from_db.time_created >= start_us);
442        assert!(a_from_db.time_password_changed >= start_us);
443        assert!(a_from_db.time_last_used >= start_us);
444        assert_eq!(a_from_db.times_used, 1);
445
446        let b_from_db = store
447            .get(&b_id)
448            .expect("Not to error getting b")
449            .expect("b to exist");
450
451        assert_logins_equiv(&LoginEntry { ..b.clone() }, &b_from_db);
452        assert!(b_from_db.time_created >= start_us);
453        assert!(b_from_db.time_password_changed >= start_us);
454        assert!(b_from_db.time_last_used >= start_us);
455        assert_eq!(b_from_db.times_used, 1);
456
457        let mut list = store.list().expect("Grabbing list to work");
458        assert_eq!(list.len(), 2);
459
460        let mut expect = vec![a_from_db, b_from_db.clone()];
461
462        list.sort_by_key(|b| Reverse(b.guid()));
463        expect.sort_by_key(|b| Reverse(b.guid()));
464        assert_eq!(list, expect);
465
466        store.delete(&a_id).expect("Successful delete");
467        assert!(store
468            .get(&a_id)
469            .expect("get after delete should still work")
470            .is_none());
471
472        let list = store.list().expect("Grabbing list to work");
473        assert_eq!(list.len(), 1);
474        assert_eq!(list[0], b_from_db);
475
476        let has_logins = store
477            .has_logins_by_base_domain("example2.com")
478            .expect("Expect a result for this origin");
479        assert!(has_logins);
480
481        let list = store
482            .get_by_base_domain("example2.com")
483            .expect("Expect a list for this origin");
484        assert_eq!(list.len(), 1);
485        assert_eq!(list[0], b_from_db);
486
487        let has_logins = store
488            .has_logins_by_base_domain("www.example.com")
489            .expect("Expect a result for this origin");
490        assert!(!has_logins);
491
492        let list = store
493            .get_by_base_domain("www.example.com")
494            .expect("Expect an empty list");
495        assert_eq!(list.len(), 0);
496
497        let now_us = util::system_time_ms_i64(SystemTime::now());
498        let b2 = LoginEntry {
499            username: b.username.to_owned(),
500            password: "newpass".into(),
501            ..b
502        };
503
504        store
505            .update(&b_id, b2.clone())
506            .expect("update b should work");
507
508        let b_after_update = store
509            .get(&b_id)
510            .expect("Not to error getting b")
511            .expect("b to exist");
512
513        assert_logins_equiv(&b2, &b_after_update);
514        assert!(b_after_update.time_created >= start_us);
515        assert!(b_after_update.time_created <= now_us);
516        assert!(b_after_update.time_password_changed >= now_us);
517        // An edit is not a use: usage stats are unchanged by update().
518        assert_eq!(b_after_update.time_last_used, b_from_db.time_last_used);
519        assert_eq!(b_after_update.times_used, 1);
520    }
521
522    #[test]
523    fn test_sync_manager_registration() {
524        ensure_initialized();
525        let store = Arc::new(LoginStore::new_in_memory());
526        assert_eq!(Arc::strong_count(&store), 1);
527        assert_eq!(Arc::weak_count(&store), 0);
528        Arc::clone(&store).register_with_sync_manager();
529        assert_eq!(Arc::strong_count(&store), 1);
530        assert_eq!(Arc::weak_count(&store), 1);
531        let registered = STORE_FOR_MANAGER.lock().upgrade().expect("should upgrade");
532        assert!(Arc::ptr_eq(&store, &registered));
533        drop(registered);
534        // should be no new references
535        assert_eq!(Arc::strong_count(&store), 1);
536        assert_eq!(Arc::weak_count(&store), 1);
537        // dropping the registered object should drop the registration.
538        drop(store);
539        assert!(STORE_FOR_MANAGER.lock().upgrade().is_none());
540    }
541
542    #[test]
543    fn test_wipe_local_on_a_fresh_database_is_a_noop() {
544        ensure_initialized();
545        // If the database has data, then wipe_local() returns > 0 rows deleted
546        let db = LoginDb::open_in_memory();
547        db.add_or_update(LoginEntry {
548            origin: "https://www.example.com".into(),
549            form_action_origin: Some("https://www.example.com".into()),
550            username_field: "user_input".into(),
551            password_field: "pass_input".into(),
552            username: "coolperson21".into(),
553            password: "p4ssw0rd".into(),
554            ..Default::default()
555        })
556        .unwrap();
557        assert!(db.wipe_local().unwrap() > 0);
558
559        // If the database is empty, then wipe_local() returns 0 rows deleted
560        let db = LoginDb::open_in_memory();
561        assert_eq!(db.wipe_local().unwrap(), 0);
562    }
563
564    #[test]
565    fn test_shutdown() {
566        ensure_initialized();
567        let store = LoginStore::new_in_memory();
568        store.shutdown();
569        assert!(matches!(
570            store.list(),
571            Err(LoginsApiError::UnexpectedLoginsApiError { reason: _ })
572        ));
573        assert!(store.db.lock().is_none());
574    }
575
576    #[test]
577    fn test_delete_undecryptable_records_for_remote_replacement() {
578        ensure_initialized();
579        let store = Arc::new(LoginStore::new_in_memory());
580        // Not much of a test, but let's make sure this doesn't deadlock at least.
581        store
582            .delete_undecryptable_records_for_remote_replacement()
583            .unwrap();
584    }
585}
586
587#[test]
588fn test_send() {
589    fn ensure_send<T: Send>() {}
590    ensure_send::<LoginStore>();
591}
592
593#[cfg(feature = "keydb")]
594#[cfg(test)]
595mod tests_keydb {
596    use super::*;
597    use crate::{ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator};
598    use async_trait::async_trait;
599    use nss_as::ensure_initialized_with_profile_dir;
600    use std::path::PathBuf;
601
602    struct MockPrimaryPasswordAuthenticator {
603        password: String,
604    }
605
606    #[async_trait]
607    impl PrimaryPasswordAuthenticator for MockPrimaryPasswordAuthenticator {
608        async fn get_primary_password(&self) -> ApiResult<String> {
609            Ok(self.password.clone())
610        }
611        async fn on_authentication_success(&self) -> ApiResult<()> {
612            Ok(())
613        }
614        async fn on_authentication_failure(&self) -> ApiResult<()> {
615            Ok(())
616        }
617    }
618
619    fn profile_path() -> PathBuf {
620        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
621            .join("../support/rc_crypto/nss/fixtures/profile")
622    }
623
624    #[test]
625    fn decrypting_logins_with_primary_password() {
626        ensure_initialized_with_profile_dir(profile_path());
627
628        // `password` is the primary password of the profile fixture
629        let primary_password_authenticator = MockPrimaryPasswordAuthenticator {
630            password: "password".to_string(),
631        };
632        let key_manager = NSSKeyManager::new(Arc::new(primary_password_authenticator));
633        let encdec = ManagedEncryptorDecryptor::new(Arc::new(key_manager));
634        let store = LoginStore::new(profile_path().join("logins.db"), Arc::new(encdec))
635            .expect("store from fixtures");
636        let list = store.list().expect("Grabbing list to work");
637
638        assert_eq!(list.len(), 1);
639
640        assert_eq!(list[0].origin, "https://www.example.com");
641        assert_eq!(list[0].username, "test");
642        assert_eq!(list[0].password, "test");
643    }
644}