Revision control
Copy as Markdown
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
use crate::db::LoginDb;
use crate::encryption::EncryptorDecryptor;
use crate::error::*;
use crate::login::{BulkResultEntry, EncryptedLogin, Login, LoginEntry, LoginEntryWithMeta};
use crate::schema;
use crate::LoginsSyncEngine;
use parking_lot::Mutex;
use sql_support::run_maintenance;
use std::path::Path;
use std::sync::{Arc, Weak};
use sync15::{
engine::{EngineSyncAssociation, SyncEngine, SyncEngineId},
ServerTimestamp,
};
#[derive(uniffi::Enum)]
pub enum LoginOrErrorMessage {
Login,
String,
}
// Our "sync manager" will use whatever is stashed here.
lazy_static::lazy_static! {
// Mutex: just taken long enough to update the inner stuff - needed
// to wrap the RefCell as they aren't `Sync`
static ref STORE_FOR_MANAGER: Mutex<Weak<LoginStore>> = Mutex::new(Weak::new());
}
/// Called by the sync manager to get a sync engine via the store previously
/// registered with the sync manager.
pub fn get_registered_sync_engine(engine_id: &SyncEngineId) -> Option<Box<dyn SyncEngine>> {
let weak = STORE_FOR_MANAGER.lock();
match weak.upgrade() {
None => None,
Some(store) => match create_sync_engine(store, engine_id) {
Ok(engine) => Some(engine),
Err(e) => {
report_error!("logins-sync-engine-create-error", "{e}");
None
}
},
}
}
fn create_sync_engine(
store: Arc<LoginStore>,
engine_id: &SyncEngineId,
) -> Result<Box<dyn SyncEngine>> {
match engine_id {
SyncEngineId::Passwords => Ok(Box::new(LoginsSyncEngine::new(Arc::clone(&store))?)),
// panicking here seems reasonable - it's a static error if this
// it hit, not something that runtime conditions can influence.
_ => unreachable!("can't provide unknown engine: {}", engine_id),
}
}
fn map_bulk_result_entry(
enc_login: Result<EncryptedLogin>,
encdec: &dyn EncryptorDecryptor,
) -> BulkResultEntry {
match enc_login {
Ok(enc_login) => match enc_login.decrypt(encdec) {
Ok(login) => BulkResultEntry::Success { login },
Err(error) => {
warn!("Login could not be decrypted. This indicates a fundamental problem with the encryption key.");
BulkResultEntry::Error {
message: error.to_string(),
}
}
},
Err(error) => BulkResultEntry::Error {
message: error.to_string(),
},
}
}
pub struct LoginStore {
pub db: Mutex<Option<LoginDb>>,
pub encdec: Arc<dyn EncryptorDecryptor>,
}
impl LoginStore {
#[handle_error(Error)]
pub fn new(path: impl AsRef<Path>, encdec: Arc<dyn EncryptorDecryptor>) -> ApiResult<Self> {
let db = Mutex::new(Some(LoginDb::open(path)?));
Ok(Self { db, encdec })
}
pub fn new_from_db(db: LoginDb, encdec: Arc<dyn EncryptorDecryptor>) -> Self {
Self {
db: Mutex::new(Some(db)),
encdec,
}
}
#[handle_error(Error)]
pub fn new_in_memory(encdec: Arc<dyn EncryptorDecryptor>) -> ApiResult<Self> {
let db = Mutex::new(Some(LoginDb::open_in_memory()?));
Ok(Self { db, encdec })
}
pub fn lock_db(&self) -> Result<parking_lot::MappedMutexGuard<'_, LoginDb>> {
parking_lot::MutexGuard::try_map(self.db.lock(), |conn| conn.as_mut())
.map_err(|_| Error::DatabaseClosed)
}
#[handle_error(Error)]
pub fn is_empty(&self) -> ApiResult<bool> {
self.lock_db()?.get_all().map(|logins| logins.is_empty())
}
#[handle_error(Error)]
pub fn list(&self) -> ApiResult<Vec<Login>> {
self.lock_db()?.get_all().and_then(|logins| {
logins
.into_iter()
.map(|login| login.decrypt(self.encdec.as_ref()))
.collect()
})
}
#[handle_error(Error)]
pub fn count(&self) -> ApiResult<i64> {
self.lock_db()?.count_all()
}
#[handle_error(Error)]
pub fn count_by_origin(&self, origin: &str) -> ApiResult<i64> {
self.lock_db()?.count_by_origin(origin)
}
#[handle_error(Error)]
pub fn count_by_form_action_origin(&self, form_action_origin: &str) -> ApiResult<i64> {
self.lock_db()?
.count_by_form_action_origin(form_action_origin)
}
#[handle_error(Error)]
pub fn get(&self, id: &str) -> ApiResult<Option<Login>> {
match self.lock_db()?.get_by_id(id) {
Ok(result) => match result {
Some(enc_login) => enc_login.decrypt(self.encdec.as_ref()).map(Some),
None => Ok(None),
},
Err(err) => Err(err),
}
}
#[handle_error(Error)]
pub fn get_by_base_domain(&self, base_domain: &str) -> ApiResult<Vec<Login>> {
self.lock_db()?
.get_by_base_domain(base_domain)
.and_then(|logins| {
logins
.into_iter()
.map(|login| login.decrypt(self.encdec.as_ref()))
.collect()
})
}
#[handle_error(Error)]
pub fn has_logins_by_base_domain(&self, base_domain: &str) -> ApiResult<bool> {
self.lock_db()?
.get_by_base_domain(base_domain)
.map(|logins| !logins.is_empty())
}
#[handle_error(Error)]
pub fn find_login_to_update(&self, entry: LoginEntry) -> ApiResult<Option<Login>> {
self.lock_db()?
.find_login_to_update(entry, self.encdec.as_ref())
}
#[handle_error(Error)]
pub fn touch(&self, id: &str) -> ApiResult<()> {
self.lock_db()?.touch(id)
}
#[handle_error(Error)]
pub fn delete(&self, id: &str) -> ApiResult<bool> {
self.lock_db()?.delete(id)
}
#[handle_error(Error)]
pub fn delete_many(&self, ids: Vec<String>) -> ApiResult<Vec<bool>> {
// Note we need to receive a vector of String here because `Vec<&str>` is not supported
// with UDL.
let ids: Vec<&str> = ids.iter().map(|id| &**id).collect();
self.lock_db()?.delete_many(ids)
}
#[handle_error(Error)]
pub fn delete_undecryptable_records_for_remote_replacement(self: Arc<Self>) -> ApiResult<()> {
// This function was created for the iOS logins verification logic that will
// remove records that prevent logins syncing. Once the verification logic is
// removed from iOS, this function can be removed from the store.
self.lock_db()?
.delete_undecryptable_records_for_remote_replacement(self.encdec.as_ref())?;
let engine = LoginsSyncEngine::new(Arc::clone(&self))?;
let db = self.lock_db()?;
engine.set_last_sync(&db, ServerTimestamp(0))?;
Ok(())
}
#[handle_error(Error)]
pub fn wipe_local(&self) -> ApiResult<()> {
self.lock_db()?.wipe_local()?;
Ok(())
}
#[handle_error(Error)]
pub fn reset(self: Arc<Self>) -> ApiResult<()> {
// Reset should not exist here - all resets should be done via the
// sync manager. It seems that actual consumers don't use this, but
// some tests do, so it remains for now.
let engine = LoginsSyncEngine::new(Arc::clone(&self))?;
engine.do_reset(&EngineSyncAssociation::Disconnected)?;
Ok(())
}
#[handle_error(Error)]
pub fn update(&self, id: &str, entry: LoginEntry) -> ApiResult<Login> {
self.lock_db()?
.update(id, entry, self.encdec.as_ref())
.and_then(|enc_login| enc_login.decrypt(self.encdec.as_ref()))
}
#[handle_error(Error)]
pub fn add(&self, entry: LoginEntry) -> ApiResult<Login> {
self.lock_db()?
.add(entry, self.encdec.as_ref())
.and_then(|enc_login| enc_login.decrypt(self.encdec.as_ref()))
}
#[handle_error(Error)]
pub fn add_many(&self, entries: Vec<LoginEntry>) -> ApiResult<Vec<BulkResultEntry>> {
self.lock_db()?
.add_many(entries, self.encdec.as_ref())
.map(|enc_logins| {
enc_logins
.into_iter()
.map(|enc_login| map_bulk_result_entry(enc_login, self.encdec.as_ref()))
.collect()
})
}
/// This method is intended to preserve metadata (LoginMeta) during a migration.
/// In normal operation, this method should not be used; instead,
/// use `add(entry)`, which manages the corresponding fields itself.
#[handle_error(Error)]
pub fn add_with_meta(&self, entry_with_meta: LoginEntryWithMeta) -> ApiResult<Login> {
self.lock_db()?
.add_with_meta(entry_with_meta, self.encdec.as_ref())
.and_then(|enc_login| enc_login.decrypt(self.encdec.as_ref()))
}
#[handle_error(Error)]
pub fn add_many_with_meta(
&self,
entries_with_meta: Vec<LoginEntryWithMeta>,
) -> ApiResult<Vec<BulkResultEntry>> {
self.lock_db()?
.add_many_with_meta(entries_with_meta, self.encdec.as_ref())
.map(|enc_logins| {
enc_logins
.into_iter()
.map(|enc_login| map_bulk_result_entry(enc_login, self.encdec.as_ref()))
.collect()
})
}
#[handle_error(Error)]
pub fn add_or_update(&self, entry: LoginEntry) -> ApiResult<Login> {
self.lock_db()?
.add_or_update(entry, self.encdec.as_ref())
.and_then(|enc_login| enc_login.decrypt(self.encdec.as_ref()))
}
#[handle_error(Error)]
pub fn set_checkpoint(&self, checkpoint: &str) -> ApiResult<()> {
self.lock_db()?
.put_meta(schema::CHECKPOINT_KEY, &checkpoint)
}
#[handle_error(Error)]
pub fn get_checkpoint(&self) -> ApiResult<Option<String>> {
self.lock_db()?.get_meta(schema::CHECKPOINT_KEY)
}
#[handle_error(Error)]
pub fn run_maintenance(&self) -> ApiResult<()> {
let conn = self.lock_db()?;
run_maintenance(&conn)?;
Ok(())
}
#[handle_error(Error)]
pub fn shutdown(&self) -> ApiResult<()> {
if let Some(conn) = self.db.lock().take() {
conn.shutdown()
} else {
Ok(())
}
}
// This allows the embedding app to say "make this instance available to
// the sync manager". The implementation is more like "offer to sync mgr"
// (thereby avoiding us needing to link with the sync manager) but
// `register_with_sync_manager()` is logically what's happening so that's
// the name it gets.
pub fn register_with_sync_manager(self: Arc<Self>) {
let mut state = STORE_FOR_MANAGER.lock();
*state = Arc::downgrade(&self);
}
// this isn't exposed by uniffi - currently the
// only consumer of this is our "example" (and hence why they
// are `pub` and not `pub(crate)`).
// We could probably make the example work with the sync manager - but then
// our example would link with places and logins etc, and it's not a big
// deal really.
#[handle_error(Error)]
pub fn create_logins_sync_engine(self: Arc<Self>) -> ApiResult<Box<dyn SyncEngine>> {
Ok(Box::new(LoginsSyncEngine::new(self)?) as Box<dyn SyncEngine>)
}
}
#[cfg(not(feature = "keydb"))]
#[cfg(test)]
mod test {
use super::*;
use crate::encryption::test_utils::TEST_ENCDEC;
use crate::util;
use more_asserts::*;
use nss::ensure_initialized;
use std::cmp::Reverse;
use std::time::SystemTime;
fn assert_logins_equiv(a: &LoginEntry, b: &Login) {
assert_eq!(a.origin, b.origin);
assert_eq!(a.form_action_origin, b.form_action_origin);
assert_eq!(a.http_realm, b.http_realm);
assert_eq!(a.username_field, b.username_field);
assert_eq!(a.password_field, b.password_field);
assert_eq!(b.username, a.username);
assert_eq!(b.password, a.password);
}
#[test]
fn test_general() {
ensure_initialized();
let store = LoginStore::new_in_memory(TEST_ENCDEC.clone()).unwrap();
let list = store.list().expect("Grabbing Empty list to work");
assert_eq!(list.len(), 0);
let start_us = util::system_time_ms_i64(SystemTime::now());
let a = LoginEntry {
username_field: "user_input".into(),
password_field: "pass_input".into(),
username: "coolperson21".into(),
password: "p4ssw0rd".into(),
..Default::default()
};
let b = LoginEntry {
http_realm: Some("Some String Here".into()),
username: "asdf".into(),
password: "fdsa".into(),
..Default::default()
};
let a_id = store.add(a.clone()).expect("added a").id;
let b_id = store.add(b.clone()).expect("added b").id;
let a_from_db = store
.get(&a_id)
.expect("Not to error getting a")
.expect("a to exist");
assert_logins_equiv(&a, &a_from_db);
assert_ge!(a_from_db.time_created, start_us);
assert_ge!(a_from_db.time_password_changed, start_us);
assert_ge!(a_from_db.time_last_used, start_us);
assert_eq!(a_from_db.times_used, 1);
let b_from_db = store
.get(&b_id)
.expect("Not to error getting b")
.expect("b to exist");
assert_logins_equiv(&LoginEntry { ..b.clone() }, &b_from_db);
assert_ge!(b_from_db.time_created, start_us);
assert_ge!(b_from_db.time_password_changed, start_us);
assert_ge!(b_from_db.time_last_used, start_us);
assert_eq!(b_from_db.times_used, 1);
let mut list = store.list().expect("Grabbing list to work");
assert_eq!(list.len(), 2);
let mut expect = vec![a_from_db, b_from_db.clone()];
list.sort_by_key(|b| Reverse(b.guid()));
expect.sort_by_key(|b| Reverse(b.guid()));
assert_eq!(list, expect);
store.delete(&a_id).expect("Successful delete");
assert!(store
.get(&a_id)
.expect("get after delete should still work")
.is_none());
let list = store.list().expect("Grabbing list to work");
assert_eq!(list.len(), 1);
assert_eq!(list[0], b_from_db);
let has_logins = store
.has_logins_by_base_domain("example2.com")
.expect("Expect a result for this origin");
assert!(has_logins);
let list = store
.get_by_base_domain("example2.com")
.expect("Expect a list for this origin");
assert_eq!(list.len(), 1);
assert_eq!(list[0], b_from_db);
let has_logins = store
.has_logins_by_base_domain("www.example.com")
.expect("Expect a result for this origin");
assert!(!has_logins);
let list = store
.get_by_base_domain("www.example.com")
.expect("Expect an empty list");
assert_eq!(list.len(), 0);
let now_us = util::system_time_ms_i64(SystemTime::now());
let b2 = LoginEntry {
username: b.username.to_owned(),
password: "newpass".into(),
..b
};
store
.update(&b_id, b2.clone())
.expect("update b should work");
let b_after_update = store
.get(&b_id)
.expect("Not to error getting b")
.expect("b to exist");
assert_logins_equiv(&b2, &b_after_update);
assert_ge!(b_after_update.time_created, start_us);
assert_le!(b_after_update.time_created, now_us);
assert_ge!(b_after_update.time_password_changed, now_us);
assert_ge!(b_after_update.time_last_used, now_us);
// Should be two even though we updated twice
assert_eq!(b_after_update.times_used, 2);
}
#[test]
fn test_checkpoint() {
ensure_initialized();
let store = LoginStore::new_in_memory(TEST_ENCDEC.clone()).unwrap();
let checkpoint = "a-checkpoint";
store.set_checkpoint(checkpoint).ok();
assert_eq!(store.get_checkpoint().unwrap().unwrap(), checkpoint);
}
#[test]
fn test_sync_manager_registration() {
ensure_initialized();
let store = Arc::new(LoginStore::new_in_memory(TEST_ENCDEC.clone()).unwrap());
assert_eq!(Arc::strong_count(&store), 1);
assert_eq!(Arc::weak_count(&store), 0);
Arc::clone(&store).register_with_sync_manager();
assert_eq!(Arc::strong_count(&store), 1);
assert_eq!(Arc::weak_count(&store), 1);
let registered = STORE_FOR_MANAGER.lock().upgrade().expect("should upgrade");
assert!(Arc::ptr_eq(&store, ®istered));
drop(registered);
// should be no new references
assert_eq!(Arc::strong_count(&store), 1);
assert_eq!(Arc::weak_count(&store), 1);
// dropping the registered object should drop the registration.
drop(store);
assert!(STORE_FOR_MANAGER.lock().upgrade().is_none());
}
#[test]
fn test_wipe_local_on_a_fresh_database_is_a_noop() {
ensure_initialized();
// If the database has data, then wipe_local() returns > 0 rows deleted
let db = LoginDb::open_in_memory().unwrap();
db.add_or_update(
LoginEntry {
username_field: "user_input".into(),
password_field: "pass_input".into(),
username: "coolperson21".into(),
password: "p4ssw0rd".into(),
..Default::default()
},
&TEST_ENCDEC.clone(),
)
.unwrap();
assert!(db.wipe_local().unwrap() > 0);
// If the database is empty, then wipe_local() returns 0 rows deleted
let db = LoginDb::open_in_memory().unwrap();
assert_eq!(db.wipe_local().unwrap(), 0);
}
#[test]
fn test_shutdown() {
ensure_initialized();
let store = LoginStore::new_in_memory(TEST_ENCDEC.clone()).unwrap();
store.shutdown().unwrap();
assert!(matches!(
store.list(),
Err(LoginsApiError::UnexpectedLoginsApiError { reason: _ })
));
}
}
#[test]
fn test_send() {
fn ensure_send<T: Send>() {}
ensure_send::<LoginStore>();
}