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::{
8    BulkResultEntry, EncryptedLogin, Login, LoginCandidate, LoginEntry, LoginEntryWithMeta,
9};
10use crate::LoginsSyncEngine;
11use parking_lot::Mutex;
12use sql_support::run_maintenance;
13use std::path::Path;
14use std::sync::{Arc, Weak};
15use sync15::{
16    engine::{EngineSyncAssociation, SyncEngine, SyncEngineId},
17    ServerTimestamp,
18};
19
20#[derive(uniffi::Enum)]
21pub enum LoginOrErrorMessage {
22    Login,
23    String,
24}
25
26// Our "sync manager" will use whatever is stashed here.
27lazy_static::lazy_static! {
28    // Mutex: just taken long enough to update the inner stuff - needed
29    //        to wrap the RefCell as they aren't `Sync`
30    static ref STORE_FOR_MANAGER: Mutex<Weak<LoginStore>> = Mutex::new(Weak::new());
31}
32
33/// Called by the sync manager to get a sync engine via the store previously
34/// registered with the sync manager.
35pub fn get_registered_sync_engine(engine_id: &SyncEngineId) -> Option<Box<dyn SyncEngine>> {
36    let weak = STORE_FOR_MANAGER.lock();
37    match weak.upgrade() {
38        None => None,
39        Some(store) => match create_sync_engine(store, engine_id) {
40            Ok(engine) => Some(engine),
41            Err(e) => {
42                report_error!("logins-sync-engine-create-error", "{e}");
43                None
44            }
45        },
46    }
47}
48
49fn create_sync_engine(
50    store: Arc<LoginStore>,
51    engine_id: &SyncEngineId,
52) -> Result<Box<dyn SyncEngine>> {
53    match engine_id {
54        SyncEngineId::Passwords => Ok(Box::new(LoginsSyncEngine::new(Arc::clone(&store))?)),
55        // panicking here seems reasonable - it's a static error if this
56        // it hit, not something that runtime conditions can influence.
57        _ => unreachable!("can't provide unknown engine: {}", engine_id),
58    }
59}
60
61fn map_bulk_result_entry(
62    enc_login: Result<EncryptedLogin>,
63    encdec: &dyn EncryptorDecryptor,
64) -> BulkResultEntry {
65    match enc_login {
66        Ok(enc_login) => match enc_login.decrypt(encdec) {
67            Ok(login) => BulkResultEntry::Success { login },
68            Err(error) => {
69                warn!("Login could not be decrypted. This indicates a fundamental problem with the encryption key.");
70                BulkResultEntry::Error {
71                    message: error.to_string(),
72                }
73            }
74        },
75        Err(error) => BulkResultEntry::Error {
76            message: error.to_string(),
77        },
78    }
79}
80
81pub struct LoginStore {
82    pub db: Mutex<Option<LoginDb>>,
83}
84
85impl LoginStore {
86    #[handle_error(Error)]
87    pub fn new(path: impl AsRef<Path>, encdec: Arc<dyn EncryptorDecryptor>) -> ApiResult<Self> {
88        let db = Mutex::new(Some(LoginDb::open(path, encdec)?));
89        Ok(Self { db })
90    }
91
92    pub fn new_from_db(db: LoginDb) -> Self {
93        let db = Mutex::new(Some(db));
94        Self { db }
95    }
96
97    // Only used for tests, but it's `pub` the `sync-test` crate uses it.
98    #[cfg(test)]
99    pub fn new_in_memory() -> Self {
100        let db = Mutex::new(Some(LoginDb::open_in_memory()));
101        Self { db }
102    }
103
104    pub fn lock_db(&self) -> Result<parking_lot::MappedMutexGuard<'_, LoginDb>> {
105        parking_lot::MutexGuard::try_map(self.db.lock(), |db| db.as_mut())
106            .map_err(|_| Error::DatabaseClosed)
107    }
108
109    #[handle_error(Error)]
110    pub fn is_empty(&self) -> ApiResult<bool> {
111        Ok(self.lock_db()?.count_all()? == 0)
112    }
113
114    #[handle_error(Error)]
115    pub fn list(&self) -> ApiResult<Vec<Login>> {
116        let db = self.lock_db()?;
117        db.get_all().and_then(|logins| {
118            logins
119                .into_iter()
120                .map(|login| login.decrypt(db.encdec.as_ref()))
121                .collect()
122        })
123    }
124
125    /// List all logins without decrypting them.
126    ///
127    /// Unlike `list()` this never touches the encryption key, so consumers which only need the
128    /// cleartext fields to decide which logins they care about can filter without forcing the
129    /// user to authenticate.  Feed the ids of the matches to `get_many()`.
130    #[handle_error(Error)]
131    pub fn list_candidates(&self) -> ApiResult<Vec<LoginCandidate>> {
132        Ok(self
133            .lock_db()?
134            .get_all()?
135            .into_iter()
136            .map(LoginCandidate::from)
137            .collect())
138    }
139
140    #[handle_error(Error)]
141    pub fn count(&self) -> ApiResult<i64> {
142        self.lock_db()?.count_all()
143    }
144
145    #[handle_error(Error)]
146    pub fn count_by_origin(&self, origin: &str) -> ApiResult<i64> {
147        self.lock_db()?.count_by_origin(origin)
148    }
149
150    #[handle_error(Error)]
151    pub fn count_by_form_action_origin(&self, form_action_origin: &str) -> ApiResult<i64> {
152        self.lock_db()?
153            .count_by_form_action_origin(form_action_origin)
154    }
155
156    #[handle_error(Error)]
157    pub fn get(&self, id: &str) -> ApiResult<Option<Login>> {
158        let db = self.lock_db()?;
159        match db.get_by_id(id) {
160            Ok(result) => match result {
161                Some(enc_login) => enc_login.decrypt(db.encdec.as_ref()).map(Some),
162                None => Ok(None),
163            },
164            Err(err) => Err(err),
165        }
166    }
167
168    /// Get the logins with the given ids, decrypting them.
169    ///
170    /// This is the other half of `list_candidates()`: having filtered on the cleartext fields,
171    /// only pay for decrypting the logins which actually matched.  Ids we don't have a login for
172    /// are skipped; a login we can't decrypt fails the call, as it does for `list()`.
173    #[handle_error(Error)]
174    pub fn get_many(&self, ids: Vec<String>) -> ApiResult<Vec<Login>> {
175        let db = self.lock_db()?;
176        db.get_many(&ids)?
177            .into_iter()
178            .map(|login| login.decrypt(db.encdec.as_ref()))
179            .collect()
180    }
181
182    #[handle_error(Error)]
183    pub fn get_by_base_domain(&self, base_domain: &str) -> ApiResult<Vec<Login>> {
184        let db = self.lock_db()?;
185        db.get_by_base_domain(base_domain).and_then(|logins| {
186            logins
187                .into_iter()
188                .map(|login| login.decrypt(db.encdec.as_ref()))
189                .collect()
190        })
191    }
192
193    #[handle_error(Error)]
194    pub fn has_logins_by_base_domain(&self, base_domain: &str) -> ApiResult<bool> {
195        self.lock_db()?
196            .get_by_base_domain(base_domain)
197            .map(|logins| !logins.is_empty())
198    }
199
200    #[handle_error(Error)]
201    pub fn find_login_to_update(&self, entry: LoginEntry) -> ApiResult<Option<Login>> {
202        let db = self.lock_db()?;
203        db.find_login_to_update(entry)
204    }
205
206    #[handle_error(Error)]
207    pub fn touch(&self, id: &str) -> ApiResult<()> {
208        self.lock_db()?.touch(id)
209    }
210
211    #[handle_error(Error)]
212    pub fn are_potentially_vulnerable_passwords(&self, ids: Vec<String>) -> ApiResult<Vec<String>> {
213        // Note: Vec<&str> is not supported with UDL, so we receive Vec<String> and convert
214        let db = self.lock_db()?;
215        let ids: Vec<&str> = ids.iter().map(|id| &**id).collect();
216        db.are_potentially_vulnerable_passwords(&ids)
217    }
218
219    #[handle_error(Error)]
220    pub fn is_potentially_vulnerable_password(&self, id: &str) -> ApiResult<bool> {
221        let db = self.lock_db()?;
222        db.is_potentially_vulnerable_password(id)
223    }
224
225    #[handle_error(Error)]
226    pub fn record_potentially_vulnerable_passwords(&self, passwords: Vec<String>) -> ApiResult<()> {
227        let db = self.lock_db()?;
228        db.record_potentially_vulnerable_passwords(passwords)
229    }
230
231    #[handle_error(Error)]
232    pub fn reset_all_breaches(&self) -> ApiResult<()> {
233        self.lock_db()?.reset_all_breaches()
234    }
235
236    #[handle_error(Error)]
237    pub fn record_breach_alert_dismissal(&self, id: &str) -> ApiResult<()> {
238        self.lock_db()?.record_breach_alert_dismissal(id)
239    }
240
241    #[handle_error(Error)]
242    pub fn record_breach_alert_dismissal_time(&self, id: &str, timestamp: i64) -> ApiResult<()> {
243        self.lock_db()?
244            .record_breach_alert_dismissal_time(id, timestamp)
245    }
246
247    #[handle_error(Error)]
248    pub fn delete(&self, id: &str) -> ApiResult<bool> {
249        self.lock_db()?.delete(id)
250    }
251
252    #[handle_error(Error)]
253    pub fn delete_many(&self, ids: Vec<String>) -> ApiResult<Vec<bool>> {
254        // Note we need to receive a vector of String here because `Vec<&str>` is not supported
255        // with UDL.
256        let ids: Vec<&str> = ids.iter().map(|id| &**id).collect();
257        self.lock_db()?.delete_many(ids)
258    }
259
260    #[handle_error(Error)]
261    pub fn delete_all(&self) -> ApiResult<Vec<String>> {
262        self.lock_db()?.delete_all()
263    }
264
265    #[handle_error(Error)]
266    pub fn delete_all_except_fxa(&self) -> ApiResult<Vec<String>> {
267        self.lock_db()?.delete_all_except_fxa()
268    }
269
270    #[handle_error(Error)]
271    pub fn delete_undecryptable_records_for_remote_replacement(
272        self: Arc<Self>,
273    ) -> ApiResult<LoginsDeletionMetrics> {
274        // This function was created for the iOS logins verification logic that will
275        // remove records that prevent logins syncing. Once the verification logic is
276        // removed from iOS, this function can be removed from the store.
277
278        // Creating an engine requires locking the DB, so make sure to do this first
279        let engine = LoginsSyncEngine::new(Arc::clone(&self))?;
280
281        let db = self.lock_db()?;
282        let deletion_stats = db.delete_undecryptable_records_for_remote_replacement()?;
283        engine.set_last_sync(&db, ServerTimestamp(0))?;
284        Ok(deletion_stats)
285    }
286
287    #[handle_error(Error)]
288    pub fn wipe_local(&self) -> ApiResult<()> {
289        self.lock_db()?.wipe_local()?;
290        Ok(())
291    }
292
293    #[handle_error(Error)]
294    pub fn wipe_local_except_fxa(&self) -> ApiResult<()> {
295        self.lock_db()?.wipe_local_except_fxa()?;
296        Ok(())
297    }
298
299    #[handle_error(Error)]
300    pub fn reset(self: Arc<Self>) -> ApiResult<()> {
301        // Reset should not exist here - all resets should be done via the
302        // sync manager. It seems that actual consumers don't use this, but
303        // some tests do, so it remains for now.
304        let engine = LoginsSyncEngine::new(Arc::clone(&self))?;
305        engine.do_reset(&EngineSyncAssociation::Disconnected)?;
306        Ok(())
307    }
308
309    #[handle_error(Error)]
310    pub fn update(&self, id: &str, entry: LoginEntry) -> ApiResult<Login> {
311        let db = self.lock_db()?;
312        db.update(id, entry)
313            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
314    }
315
316    #[handle_error(Error)]
317    pub fn add(&self, entry: LoginEntry) -> ApiResult<Login> {
318        let db = self.lock_db()?;
319        db.add(entry)
320            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
321    }
322
323    #[handle_error(Error)]
324    pub fn add_many(&self, entries: Vec<LoginEntry>) -> ApiResult<Vec<BulkResultEntry>> {
325        let db = self.lock_db()?;
326        let encdec = db.encdec.as_ref();
327        db.add_many(entries).map(|enc_logins| {
328            enc_logins
329                .into_iter()
330                .map(|enc_login| map_bulk_result_entry(enc_login, encdec))
331                .collect()
332        })
333    }
334
335    /// This method is intended to preserve metadata (LoginMeta) during a migration.
336    /// In normal operation, this method should not be used; instead,
337    /// use `add(entry)`, which manages the corresponding fields itself.
338    #[handle_error(Error)]
339    pub fn add_with_meta(&self, entry_with_meta: LoginEntryWithMeta) -> ApiResult<Login> {
340        let db = self.lock_db()?;
341        db.add_with_meta(entry_with_meta)
342            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
343    }
344
345    #[handle_error(Error)]
346    pub fn add_many_with_meta(
347        &self,
348        entries_with_meta: Vec<LoginEntryWithMeta>,
349    ) -> ApiResult<Vec<BulkResultEntry>> {
350        let db = self.lock_db()?;
351        let encdec = db.encdec.as_ref();
352        db.add_many_with_meta(entries_with_meta).map(|enc_logins| {
353            enc_logins
354                .into_iter()
355                .map(|enc_login| map_bulk_result_entry(enc_login, encdec))
356                .collect()
357        })
358    }
359
360    #[handle_error(Error)]
361    pub fn add_or_update(&self, entry: LoginEntry) -> ApiResult<Login> {
362        let db = self.lock_db()?;
363        db.add_or_update(entry)
364            .and_then(|enc_login| enc_login.decrypt(db.encdec.as_ref()))
365    }
366
367    #[handle_error(Error)]
368    pub fn run_maintenance(&self, options: Option<RunMaintenanceOptions>) -> ApiResult<()> {
369        let conn = self.lock_db()?;
370        let options = options.unwrap_or_default();
371        run_maintenance(&conn)?;
372        if options.delete_undecryptable_records_for_remote_replacement {
373            conn.delete_undecryptable_records_for_remote_replacement()?;
374        }
375        Ok(())
376    }
377
378    pub fn shutdown(&self) {
379        if let Some(db) = self.db.lock().take() {
380            let _ = db.shutdown();
381        }
382    }
383
384    // This allows the embedding app to say "make this instance available to
385    // the sync manager". The implementation is more like "offer to sync mgr"
386    // (thereby avoiding us needing to link with the sync manager) but
387    // `register_with_sync_manager()` is logically what's happening so that's
388    // the name it gets.
389    pub fn register_with_sync_manager(self: Arc<Self>) {
390        let mut state = STORE_FOR_MANAGER.lock();
391        *state = Arc::downgrade(&self);
392    }
393
394    // this isn't exposed by uniffi - currently the
395    // only consumer of this is our "example" (and hence why they
396    // are `pub` and not `pub(crate)`).
397    // We could probably make the example work with the sync manager - but then
398    // our example would link with places and logins etc, and it's not a big
399    // deal really.
400    #[handle_error(Error)]
401    pub fn create_logins_sync_engine(self: Arc<Self>) -> ApiResult<Box<dyn SyncEngine>> {
402        Ok(Box::new(LoginsSyncEngine::new(self)?) as Box<dyn SyncEngine>)
403    }
404}
405
406pub struct RunMaintenanceOptions {
407    pub delete_undecryptable_records_for_remote_replacement: bool,
408}
409
410impl Default for RunMaintenanceOptions {
411    fn default() -> Self {
412        Self {
413            delete_undecryptable_records_for_remote_replacement: true,
414        }
415    }
416}
417
418#[cfg(not(feature = "keydb"))]
419#[cfg(test)]
420mod tests {
421    use super::*;
422    use crate::encryption::{create_key, KeyManager, ManagedEncryptorDecryptor};
423    use crate::util;
424    use nss_as::ensure_initialized;
425    use std::cmp::Reverse;
426    use std::sync::atomic::{AtomicUsize, Ordering};
427    use std::time::SystemTime;
428
429    fn assert_logins_equiv(a: &LoginEntry, b: &Login) {
430        assert_eq!(a.origin, b.origin);
431        assert_eq!(a.form_action_origin, b.form_action_origin);
432        assert_eq!(a.http_realm, b.http_realm);
433        assert_eq!(a.username_field, b.username_field);
434        assert_eq!(a.password_field, b.password_field);
435        assert_eq!(b.username, a.username);
436        assert_eq!(b.password, a.password);
437    }
438
439    #[test]
440    fn test_general() {
441        ensure_initialized();
442
443        let store = LoginStore::new_in_memory();
444        let list = store.list().expect("Grabbing Empty list to work");
445        assert_eq!(list.len(), 0);
446        let start_us = util::system_time_ms_i64(SystemTime::now());
447
448        let a = LoginEntry {
449            origin: "https://www.example.com".into(),
450            form_action_origin: Some("https://www.example.com".into()),
451            username_field: "user_input".into(),
452            password_field: "pass_input".into(),
453            username: "user".into(),
454            password: "password".into(),
455            ..Default::default()
456        };
457
458        let b = LoginEntry {
459            origin: "https://www.example2.com".into(),
460            http_realm: Some("Some String Here".into()),
461            username: "asdf".into(),
462            password: "fdsa".into(),
463            ..Default::default()
464        };
465        let a_id = store.add(a.clone()).expect("added a").id;
466        let b_id = store.add(b.clone()).expect("added b").id;
467
468        let a_from_db = store
469            .get(&a_id)
470            .expect("Not to error getting a")
471            .expect("a to exist");
472
473        assert_logins_equiv(&a, &a_from_db);
474        assert!(a_from_db.time_created >= start_us);
475        assert!(a_from_db.time_password_changed >= start_us);
476        assert!(a_from_db.time_last_used >= start_us);
477        assert_eq!(a_from_db.times_used, 1);
478
479        let b_from_db = store
480            .get(&b_id)
481            .expect("Not to error getting b")
482            .expect("b to exist");
483
484        assert_logins_equiv(&LoginEntry { ..b.clone() }, &b_from_db);
485        assert!(b_from_db.time_created >= start_us);
486        assert!(b_from_db.time_password_changed >= start_us);
487        assert!(b_from_db.time_last_used >= start_us);
488        assert_eq!(b_from_db.times_used, 1);
489
490        let mut list = store.list().expect("Grabbing list to work");
491        assert_eq!(list.len(), 2);
492
493        let mut expect = vec![a_from_db, b_from_db.clone()];
494
495        list.sort_by_key(|b| Reverse(b.guid()));
496        expect.sort_by_key(|b| Reverse(b.guid()));
497        assert_eq!(list, expect);
498
499        store.delete(&a_id).expect("Successful delete");
500        assert!(store
501            .get(&a_id)
502            .expect("get after delete should still work")
503            .is_none());
504
505        let list = store.list().expect("Grabbing list to work");
506        assert_eq!(list.len(), 1);
507        assert_eq!(list[0], b_from_db);
508
509        let has_logins = store
510            .has_logins_by_base_domain("example2.com")
511            .expect("Expect a result for this origin");
512        assert!(has_logins);
513
514        let list = store
515            .get_by_base_domain("example2.com")
516            .expect("Expect a list for this origin");
517        assert_eq!(list.len(), 1);
518        assert_eq!(list[0], b_from_db);
519
520        let has_logins = store
521            .has_logins_by_base_domain("www.example.com")
522            .expect("Expect a result for this origin");
523        assert!(!has_logins);
524
525        let list = store
526            .get_by_base_domain("www.example.com")
527            .expect("Expect an empty list");
528        assert_eq!(list.len(), 0);
529
530        let now_us = util::system_time_ms_i64(SystemTime::now());
531        let b2 = LoginEntry {
532            username: b.username.to_owned(),
533            password: "newpass".into(),
534            ..b
535        };
536
537        store
538            .update(&b_id, b2.clone())
539            .expect("update b should work");
540
541        let b_after_update = store
542            .get(&b_id)
543            .expect("Not to error getting b")
544            .expect("b to exist");
545
546        assert_logins_equiv(&b2, &b_after_update);
547        assert!(b_after_update.time_created >= start_us);
548        assert!(b_after_update.time_created <= now_us);
549        assert!(b_after_update.time_password_changed >= now_us);
550        // An edit is not a use: usage stats are unchanged by update().
551        assert_eq!(b_after_update.time_last_used, b_from_db.time_last_used);
552        assert_eq!(b_after_update.times_used, 1);
553    }
554
555    #[test]
556    fn test_sync_manager_registration() {
557        ensure_initialized();
558        let store = Arc::new(LoginStore::new_in_memory());
559        assert_eq!(Arc::strong_count(&store), 1);
560        assert_eq!(Arc::weak_count(&store), 0);
561        Arc::clone(&store).register_with_sync_manager();
562        assert_eq!(Arc::strong_count(&store), 1);
563        assert_eq!(Arc::weak_count(&store), 1);
564        let registered = STORE_FOR_MANAGER.lock().upgrade().expect("should upgrade");
565        assert!(Arc::ptr_eq(&store, &registered));
566        drop(registered);
567        // should be no new references
568        assert_eq!(Arc::strong_count(&store), 1);
569        assert_eq!(Arc::weak_count(&store), 1);
570        // dropping the registered object should drop the registration.
571        drop(store);
572        assert!(STORE_FOR_MANAGER.lock().upgrade().is_none());
573    }
574
575    #[test]
576    fn test_wipe_local_on_a_fresh_database_is_a_noop() {
577        ensure_initialized();
578        // If the database has data, then wipe_local() returns > 0 rows deleted
579        let db = LoginDb::open_in_memory();
580        db.add_or_update(LoginEntry {
581            origin: "https://www.example.com".into(),
582            form_action_origin: Some("https://www.example.com".into()),
583            username_field: "user_input".into(),
584            password_field: "pass_input".into(),
585            username: "coolperson21".into(),
586            password: "p4ssw0rd".into(),
587            ..Default::default()
588        })
589        .unwrap();
590        assert!(db.wipe_local().unwrap() > 0);
591
592        // If the database is empty, then wipe_local() returns 0 rows deleted
593        let db = LoginDb::open_in_memory();
594        assert_eq!(db.wipe_local().unwrap(), 0);
595    }
596
597    #[test]
598    fn test_shutdown() {
599        ensure_initialized();
600        let store = LoginStore::new_in_memory();
601        store.shutdown();
602        assert!(matches!(
603            store.list(),
604            Err(LoginsApiError::UnexpectedLoginsApiError { reason: _ })
605        ));
606        assert!(store.db.lock().is_none());
607    }
608
609    /// A `KeyManager` which counts how often it was asked for the key, so we can prove
610    /// `list_candidates()` never asks.
611    struct CountingKeyManager {
612        key: String,
613        calls: AtomicUsize,
614    }
615
616    impl KeyManager for CountingKeyManager {
617        fn get_key(&self) -> ApiResult<Vec<u8>> {
618            self.calls.fetch_add(1, Ordering::SeqCst);
619            Ok(self.key.as_bytes().into())
620        }
621    }
622
623    fn store_with_encdec(encdec: Arc<dyn EncryptorDecryptor>) -> LoginStore {
624        let conn = rusqlite::Connection::open_in_memory().unwrap();
625        LoginStore::new_from_db(LoginDb::with_connection(conn, encdec).unwrap())
626    }
627
628    fn test_entry(origin: &str, username: &str) -> LoginEntry {
629        LoginEntry {
630            origin: origin.into(),
631            http_realm: Some("Some Realm".into()),
632            username: username.into(),
633            password: "p4ssw0rd".into(),
634            ..Default::default()
635        }
636    }
637
638    #[test]
639    fn test_list_candidates_does_not_need_the_key() {
640        ensure_initialized();
641
642        let key_manager = Arc::new(CountingKeyManager {
643            key: create_key().unwrap(),
644            calls: AtomicUsize::new(0),
645        });
646        let store = store_with_encdec(Arc::new(ManagedEncryptorDecryptor::new(
647            key_manager.clone(),
648        )));
649
650        let a = store
651            .add(test_entry("https://www.a.com", "a-user"))
652            .unwrap();
653        let b = store
654            .add(test_entry("https://www.b.com", "b-user"))
655            .unwrap();
656
657        // Adding needed the key; listing the candidates must not.
658        assert!(key_manager.calls.load(Ordering::SeqCst) > 0);
659        key_manager.calls.store(0, Ordering::SeqCst);
660
661        let mut candidates = store.list_candidates().unwrap();
662        assert_eq!(key_manager.calls.load(Ordering::SeqCst), 0);
663
664        candidates.sort_by(|l, r| l.origin.cmp(&r.origin));
665        assert_eq!(candidates.len(), 2);
666        for (candidate, login) in candidates.iter().zip([&a, &b]) {
667            assert_eq!(candidate.id, login.id);
668            assert_eq!(candidate.origin, login.origin);
669            assert_eq!(candidate.http_realm, login.http_realm);
670            assert_eq!(candidate.form_action_origin, login.form_action_origin);
671            assert_eq!(candidate.username_field, login.username_field);
672            assert_eq!(candidate.password_field, login.password_field);
673            assert_eq!(candidate.times_used, login.times_used);
674            assert_eq!(candidate.time_created, login.time_created);
675            assert_eq!(candidate.time_last_used, login.time_last_used);
676            assert_eq!(candidate.time_password_changed, login.time_password_changed);
677            assert_eq!(
678                candidate.time_last_breach_alert_dismissed,
679                login.time_last_breach_alert_dismissed
680            );
681        }
682    }
683
684    #[test]
685    fn test_get_many() {
686        ensure_initialized();
687
688        let store = LoginStore::new_in_memory();
689        let a = store
690            .add(test_entry("https://www.a.com", "a-user"))
691            .unwrap();
692        let b = store
693            .add(test_entry("https://www.b.com", "b-user"))
694            .unwrap();
695        store
696            .add(test_entry("https://www.c.com", "c-user"))
697            .unwrap();
698
699        assert_eq!(store.get_many(vec![]).unwrap(), vec![]);
700
701        // Ids we don't know about are skipped rather than being an error.  Note the results come
702        // back in the db's order, not the order of the ids we asked for.
703        let mut got = store
704            .get_many(vec![b.id.clone(), "no-such-guid".to_string(), a.id.clone()])
705            .unwrap();
706        got.sort_by(|l, r| l.origin.cmp(&r.origin));
707        assert_eq!(got, vec![a, b]);
708    }
709
710    #[test]
711    fn test_get_many_with_an_undecryptable_login() {
712        ensure_initialized();
713
714        let store = LoginStore::new_in_memory();
715        let a = store
716            .add(test_entry("https://www.a.com", "a-user"))
717            .unwrap();
718        let b = store
719            .add(test_entry("https://www.b.com", "b-user"))
720            .unwrap();
721
722        store
723            .lock_db()
724            .unwrap()
725            .db
726            .execute(
727                "UPDATE loginsL SET secFields = 'not-a-ciphertext' WHERE guid = ?",
728                [&b.id],
729            )
730            .unwrap();
731
732        // As with `list()`, one login we can't read fails the whole call.
733        assert!(matches!(
734            store.get_many(vec![a.id.clone(), b.id]),
735            Err(LoginsApiError::UnexpectedLoginsApiError { .. })
736        ));
737    }
738
739    #[test]
740    fn test_delete_undecryptable_records_for_remote_replacement() {
741        ensure_initialized();
742        let store = Arc::new(LoginStore::new_in_memory());
743        // Not much of a test, but let's make sure this doesn't deadlock at least.
744        store
745            .delete_undecryptable_records_for_remote_replacement()
746            .unwrap();
747    }
748}
749
750#[test]
751fn test_send() {
752    fn ensure_send<T: Send>() {}
753    ensure_send::<LoginStore>();
754}
755
756#[cfg(feature = "keydb")]
757#[cfg(test)]
758mod tests_keydb {
759    use super::*;
760    use crate::{ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator};
761    use async_trait::async_trait;
762    use nss_as::ensure_initialized_with_profile_dir;
763    use std::path::PathBuf;
764
765    struct MockPrimaryPasswordAuthenticator {
766        password: String,
767    }
768
769    #[async_trait]
770    impl PrimaryPasswordAuthenticator for MockPrimaryPasswordAuthenticator {
771        async fn get_primary_password(&self) -> ApiResult<String> {
772            Ok(self.password.clone())
773        }
774        async fn on_authentication_success(&self) -> ApiResult<()> {
775            Ok(())
776        }
777        async fn on_authentication_failure(&self) -> ApiResult<()> {
778            Ok(())
779        }
780    }
781
782    fn profile_path() -> PathBuf {
783        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
784            .join("../support/rc_crypto/nss/fixtures/profile")
785    }
786
787    #[test]
788    fn decrypting_logins_with_primary_password() {
789        ensure_initialized_with_profile_dir(profile_path());
790
791        // `password` is the primary password of the profile fixture
792        let primary_password_authenticator = MockPrimaryPasswordAuthenticator {
793            password: "password".to_string(),
794        };
795        let key_manager = NSSKeyManager::new(Arc::new(primary_password_authenticator));
796        let encdec = ManagedEncryptorDecryptor::new(Arc::new(key_manager));
797        let store = LoginStore::new(profile_path().join("logins.db"), Arc::new(encdec))
798            .expect("store from fixtures");
799        let list = store.list().expect("Grabbing list to work");
800
801        assert_eq!(list.len(), 1);
802
803        assert_eq!(list[0].origin, "https://www.example.com");
804        assert_eq!(list[0].username, "test");
805        assert_eq!(list[0].password, "test");
806    }
807}