diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d9241ce4..4188205b4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # v148.0 (In progress) +### Logins +- Add breach alert support, including a database migration to version 3, + new `Login` fields (`time_of_last_breach`, `time_last_breach_alert_dismissed`), + and new `LoginStore` APIs (`record_breach`, `reset_all_breaches`, `is_potentially_breached`, `record_breach_alert_dismissal`, `is_breach_alert_dismissed`). ([#7127](https://github.com/mozilla/application-services/pull/7127)) + [Full Changelog](In progress) ### Ads Client diff --git a/components/logins/src/db.rs b/components/logins/src/db.rs index 8336bcdc45..e577c4980b 100644 --- a/components/logins/src/db.rs +++ b/components/logins/src/db.rs @@ -306,6 +306,74 @@ impl LoginDb { Ok(()) } + pub fn record_breach(&self, id: &str, timestamp: i64) -> Result<()> { + let tx = self.unchecked_transaction()?; + self.ensure_local_overlay_exists(id)?; + self.mark_mirror_overridden(id)?; + self.execute_cached( + "UPDATE loginsL + SET timeOfLastBreach = :now_millis + WHERE guid = :guid + AND is_deleted = 0", + named_params! { + ":now_millis": timestamp, + ":guid": id, + }, + )?; + tx.commit()?; + Ok(()) + } + + pub fn is_potentially_breached(&self, id: &str) -> Result { + let is_potentially_breached: bool = self.db.query_row( + "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid AND timeOfLastBreach IS NOT NULL AND timeOfLastBreach > timePasswordChanged)", + named_params! { ":guid": id }, + |row| row.get(0), + )?; + Ok(is_potentially_breached) + } + + pub fn reset_all_breaches(&self) -> Result<()> { + let tx = self.unchecked_transaction()?; + self.execute_cached( + "UPDATE loginsL + SET timeOfLastBreach = NULL + WHERE timeOfLastBreach IS NOT NULL + AND is_deleted = 0", + [], + )?; + tx.commit()?; + Ok(()) + } + + pub fn is_breach_alert_dismissed(&self, id: &str) -> Result { + let is_breach_alert_dismissed: bool = self.db.query_row( + "SELECT EXISTS(SELECT 1 FROM loginsL WHERE guid = :guid AND timeOfLastBreach < timeLastBreachAlertDismissed)", + named_params! { ":guid": id }, + |row| row.get(0), + )?; + Ok(is_breach_alert_dismissed) + } + + pub fn record_breach_alert_dismissal(&self, id: &str) -> Result<()> { + let tx = self.unchecked_transaction()?; + self.ensure_local_overlay_exists(id)?; + self.mark_mirror_overridden(id)?; + let now_ms = util::system_time_ms_i64(SystemTime::now()); + self.execute_cached( + "UPDATE loginsL + SET timeLastBreachAlertDismissed = :now_millis + WHERE guid = :guid + AND is_deleted = 0", + named_params! { + ":now_millis": now_ms, + ":guid": id, + }, + )?; + tx.commit()?; + Ok(()) + } + // The single place we insert new rows or update existing local rows. // just the SQL - no validation or anything. fn insert_new_login(&self, login: &EncryptedLogin) -> Result<()> { @@ -322,6 +390,8 @@ impl LoginDb { timeCreated, timeLastUsed, timePasswordChanged, + timeOfLastBreach, + timeLastBreachAlertDismissed, local_modified, is_deleted, sync_status @@ -337,6 +407,8 @@ impl LoginDb { :time_created, :time_last_used, :time_password_changed, + :time_of_last_breach, + :time_last_breach_alert_dismissed, :local_modified, 0, -- is_deleted {new} -- sync_status @@ -356,6 +428,8 @@ impl LoginDb { ":times_used": login.meta.times_used, ":time_last_used": login.meta.time_last_used, ":time_password_changed": login.meta.time_password_changed, + ":time_of_last_breach": login.fields.time_of_last_breach, + ":time_last_breach_alert_dismissed": login.fields.time_last_breach_alert_dismissed, ":local_modified": login.meta.time_created, ":sec_fields": login.sec_fields, ":guid": login.guid(), @@ -368,18 +442,20 @@ impl LoginDb { // assumes the "local overlay" exists, so the guid must too. let sql = format!( "UPDATE loginsL - SET local_modified = :now_millis, - timeLastUsed = :time_last_used, - timePasswordChanged = :time_password_changed, - httpRealm = :http_realm, - formActionOrigin = :form_action_origin, - usernameField = :username_field, - passwordField = :password_field, - timesUsed = :times_used, - secFields = :sec_fields, - origin = :origin, + SET local_modified = :now_millis, + timeLastUsed = :time_last_used, + timePasswordChanged = :time_password_changed, + timeOfLastBreach = :time_of_last_breach, + timeLastBreachAlertDismissed = :time_last_breach_alert_dismissed, + httpRealm = :http_realm, + formActionOrigin = :form_action_origin, + usernameField = :username_field, + passwordField = :password_field, + timesUsed = :times_used, + secFields = :sec_fields, + origin = :origin, -- leave New records as they are, otherwise update them to `changed` - sync_status = max(sync_status, {changed}) + sync_status = max(sync_status, {changed}) WHERE guid = :guid", changed = SyncStatus::Changed as u8 ); @@ -397,6 +473,8 @@ impl LoginDb { ":time_password_changed": login.meta.time_password_changed, ":sec_fields": login.sec_fields, ":guid": &login.meta.id, + ":time_of_last_breach": login.fields.time_of_last_breach, + ":time_last_breach_alert_dismissed": login.fields.time_last_breach_alert_dismissed, // time_last_used has been set to now. ":now_millis": login.meta.time_last_used, }, @@ -459,6 +537,8 @@ impl LoginDb { http_realm: new_entry.http_realm, username_field: new_entry.username_field, password_field: new_entry.password_field, + time_of_last_breach: None, + time_last_breach_alert_dismissed: None, }, sec_fields, }; @@ -573,6 +653,8 @@ impl LoginDb { http_realm: entry.http_realm, username_field: entry.username_field, password_field: entry.password_field, + time_of_last_breach: None, + time_last_breach_alert_dismissed: None, }, sec_fields, }; @@ -1018,6 +1100,9 @@ pub mod test_utils { timePasswordChanged, timeCreated, + timeOfLastBreach, + timeLastBreachAlertDismissed, + guid ) VALUES ( :is_overridden, @@ -1035,6 +1120,9 @@ pub mod test_utils { :time_password_changed, :time_created, + :time_of_last_breach, + :time_last_breach_alert_dismissed, + :guid )"; let mut stmt = db.prepare_cached(sql)?; @@ -1052,6 +1140,8 @@ pub mod test_utils { ":time_last_used": login.meta.time_last_used, ":time_password_changed": login.meta.time_password_changed, ":time_created": login.meta.time_created, + ":time_of_last_breach": login.fields.time_of_last_breach, + ":time_last_breach_alert_dismissed": login.fields.time_last_breach_alert_dismissed, ":guid": login.guid_str(), })?; Ok(()) @@ -1678,6 +1768,64 @@ mod tests { assert_eq!(login2.meta.times_used, login.meta.times_used + 1); } + #[test] + fn test_breach_alerts() { + ensure_initialized(); + let db = LoginDb::open_in_memory(); + let login = db + .add( + LoginEntry { + origin: "https://www.example.com".into(), + http_realm: Some("https://www.example.com".into()), + username: "user1".into(), + password: "password1".into(), + ..Default::default() + }, + &*TEST_ENCDEC, + ) + .unwrap(); + // initial state + assert!(login.fields.time_of_last_breach.is_none()); + assert!(!db.is_potentially_breached(&login.meta.id).unwrap()); + assert!(login.fields.time_last_breach_alert_dismissed.is_none()); + + // set + let now_ms = util::system_time_ms_i64(SystemTime::now()); + db.record_breach(&login.meta.id, now_ms).unwrap(); + assert!(db.is_potentially_breached(&login.meta.id).unwrap()); + let login1 = db.get_by_id(&login.meta.id).unwrap().unwrap(); + assert!(login1.fields.time_of_last_breach.is_some()); + + // dismiss + db.record_breach_alert_dismissal(&login.meta.id).unwrap(); + let login2 = db.get_by_id(&login.meta.id).unwrap().unwrap(); + assert!(login2.fields.time_last_breach_alert_dismissed.is_some()); + + // reset + db.reset_all_breaches().unwrap(); + assert!(!db.is_potentially_breached(&login.meta.id).unwrap()); + let login3 = db.get_by_id(&login.meta.id).unwrap().unwrap(); + assert!(login3.fields.time_of_last_breach.is_none()); + + // set again + let now_ms = util::system_time_ms_i64(SystemTime::now()); + db.record_breach(&login.meta.id, now_ms).unwrap(); + assert!(db.is_potentially_breached(&login.meta.id).unwrap()); + + // now change password + db.update( + &login.meta.id.clone(), + LoginEntry { + password: "changed-password".into(), + ..login.clone().decrypt(&*TEST_ENCDEC).unwrap().entry() + }, + &*TEST_ENCDEC, + ) + .unwrap(); + // not breached anymore + assert!(!db.is_potentially_breached(&login.meta.id).unwrap()); + } + #[test] fn test_delete() { ensure_initialized(); diff --git a/components/logins/src/error.rs b/components/logins/src/error.rs index 0837a4c76d..a08c20225a 100644 --- a/components/logins/src/error.rs +++ b/components/logins/src/error.rs @@ -113,6 +113,9 @@ pub enum Error { #[error("Migration Error: {0}")] MigrationError(String), + + #[error("IncompatibleVersion: {0}")] + IncompatibleVersion(i64), } /// Error::InvalidLogin subtypes diff --git a/components/logins/src/login.rs b/components/logins/src/login.rs index e3e150a722..e486c0f0bf 100644 --- a/components/logins/src/login.rs +++ b/components/logins/src/login.rs @@ -292,6 +292,8 @@ pub struct LoginFields { pub http_realm: Option, pub username_field: String, pub password_field: String, + pub time_of_last_breach: Option, + pub time_last_breach_alert_dismissed: Option, } /// LoginEntry fields that are stored encrypted @@ -355,6 +357,9 @@ pub struct LoginEntryWithMeta { } /// A bulk insert result entry, returned by `add_many` and `add_many_with_records` +/// Please note that although the success case is much larger than the error case, this is +/// negligible in real life, as we expect a very small success/error ratio. +#[allow(clippy::large_enum_variant)] pub enum BulkResultEntry { Success { login: Login }, Error { message: String }, @@ -465,6 +470,10 @@ pub struct Login { // secure fields pub username: String, pub password: String, + + // breach alerts + pub time_of_last_breach: Option, + pub time_last_breach_alert_dismissed: Option, } impl Login { @@ -484,6 +493,9 @@ impl Login { username: sec_fields.username, password: sec_fields.password, + + time_of_last_breach: fields.time_last_breach_alert_dismissed, + time_last_breach_alert_dismissed: fields.time_last_breach_alert_dismissed, } } @@ -525,6 +537,8 @@ impl Login { http_realm: self.http_realm, username_field: self.username_field, password_field: self.password_field, + time_of_last_breach: self.time_last_breach_alert_dismissed, + time_last_breach_alert_dismissed: self.time_last_breach_alert_dismissed, }, sec_fields, }) @@ -581,6 +595,10 @@ impl EncryptedLogin { username_field: string_or_default(row, "usernameField")?, password_field: string_or_default(row, "passwordField")?, + + time_of_last_breach: row.get::<_, Option>("timeOfLastBreach")?, + time_last_breach_alert_dismissed: row + .get::<_, Option>("timeLastBreachAlertDismissed")?, }, sec_fields: row.get("secFields")?, }; diff --git a/components/logins/src/logins.udl b/components/logins/src/logins.udl index 080489fb76..7d3fd75ce3 100644 --- a/components/logins/src/logins.udl +++ b/components/logins/src/logins.udl @@ -92,6 +92,10 @@ dictionary Login { // secure login fields string password; string username; + + // breach alert fields + i64? time_of_last_breach; + i64? time_last_breach_alert_dismissed; }; /// Metrics tracking deletion of logins that cannot be decrypted, see `delete_undecryptable_records_for_remote_replacement` @@ -228,6 +232,27 @@ interface LoginStore { [Throws=LoginsApiError] void touch([ByRef] string id); + /// Determines whether a login’s password is potentially breached, based on the breach date and the time of the last password change. + [Throws=LoginsApiError] + boolean is_potentially_breached([ByRef] string id); + + /// Stores a known breach date for a login. + /// In Firefox Desktop this is updated once per session from Remote Settings. + [Throws=LoginsApiError] + void record_breach([ByRef] string id, i64 timestamp); + + /// Removes all recorded breaches for all logins (i.e. sets time_of_last_breach to null). + [Throws=LoginsApiError] + void reset_all_breaches(); + + /// Determines whether a breach alert has been dismissed, based on the breach date and the alert dismissal timestamp. + [Throws=LoginsApiError] + boolean is_breach_alert_dismissed([ByRef] string id); + + /// Stores the time at which the user dismissed the breach alert for a login. + [Throws=LoginsApiError] + void record_breach_alert_dismissal([ByRef] string id); + [Throws=LoginsApiError] boolean is_empty(); diff --git a/components/logins/src/schema.rs b/components/logins/src/schema.rs index 79376b01e0..8d7c1ddad7 100644 --- a/components/logins/src/schema.rs +++ b/components/logins/src/schema.rs @@ -95,7 +95,8 @@ use sql_support::ConnExt; /// Version 1: SQLCipher -> plaintext migration. /// Version 2: addition of `loginsM.enc_unknown_fields`. -pub(super) const VERSION: i64 = 2; +/// Version 3: addition of `timeOfLastBreach` and `timeLastBreachAlertDismissed`. +pub(super) const VERSION: i64 = 3; /// Every column shared by both tables except for `id` /// @@ -125,29 +126,34 @@ pub const COMMON_COLS: &str = " timeCreated, timeLastUsed, timePasswordChanged, - timesUsed + timesUsed, + timeOfLastBreach, + timeLastBreachAlertDismissed "; const COMMON_SQL: &str = " - id INTEGER PRIMARY KEY AUTOINCREMENT, - origin TEXT NOT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + origin TEXT NOT NULL, -- Exactly one of httpRealm or formActionOrigin should be set - httpRealm TEXT, - formActionOrigin TEXT, - usernameField TEXT, - passwordField TEXT, - timesUsed INTEGER NOT NULL DEFAULT 0, - timeCreated INTEGER NOT NULL, - timeLastUsed INTEGER, - timePasswordChanged INTEGER NOT NULL, - secFields TEXT, - guid TEXT NOT NULL UNIQUE + httpRealm TEXT, + formActionOrigin TEXT, + usernameField TEXT, + passwordField TEXT, + timesUsed INTEGER NOT NULL DEFAULT 0, + timeCreated INTEGER NOT NULL, + timeLastUsed INTEGER, + timePasswordChanged INTEGER NOT NULL, + timeOfLastBreach INTEGER, + timeLastBreachAlertDismissed INTEGER, + secFields TEXT, + guid TEXT NOT NULL UNIQUE "; lazy_static! { static ref CREATE_LOCAL_TABLE_SQL: String = format!( "CREATE TABLE IF NOT EXISTS loginsL ( {common_sql}, + -- Milliseconds, or NULL if never modified locally. local_modified INTEGER, @@ -220,26 +226,40 @@ pub(crate) fn init(db: &Connection) -> Result<()> { #[allow(clippy::unnecessary_wraps)] fn upgrade(db: &Connection, from: i64) -> Result<()> { debug!("Upgrading schema from {} to {}", from, VERSION); + if from == VERSION { return Ok(()); } - assert_ne!( - from, 0, - "Upgrading from user_version = 0 should already be handled (in `init`)" - ); - // Schema upgrades. - if from == 1 { - // Just one new nullable column makes this fairly easy - db.execute_batch("ALTER TABLE loginsM ADD enc_unknown_fields TEXT;")?; + for version in from..VERSION { + upgrade_from(db, version)?; } - // XXX - next migration, be sure to: - // from = 2; - // if from == 2 ... + db.execute_batch(&SET_VERSION_SQL)?; Ok(()) } +fn upgrade_from(db: &Connection, from: i64) -> Result<()> { + // Schema upgrades. + match from { + 0 => Err(Error::IncompatibleVersion(from)), + + // Just one new nullable column makes this fairly easy + 1 => Ok(db.execute_batch("ALTER TABLE loginsM ADD enc_unknown_fields TEXT;")?), + + // again, easy migratable nullable columns + 2 => Ok(db.execute_batch( + "ALTER TABLE loginsL ADD timeOfLastBreach INTEGER; + ALTER TABLE loginsM ADD timeOfLastBreach INTEGER; + ALTER TABLE loginsL ADD timeLastBreachAlertDismissed INTEGER; + ALTER TABLE loginsM ADD timeLastBreachAlertDismissed INTEGER;", + )?), + + // next migration, add here + _ => Err(Error::IncompatibleVersion(from)), + } +} + pub(crate) fn create(db: &Connection) -> Result<()> { debug!("Creating schema"); db.execute_all(&[ @@ -272,10 +292,35 @@ mod tests { } #[test] - fn test_upgrade_v1() { + fn test_upgrade() { ensure_initialized(); // manually setup a V1 schema. let connection = Connection::open_in_memory().unwrap(); + connection + .execute_batch( + " + CREATE TABLE IF NOT EXISTS loginsL ( + -- this was common_sql as at v1 + id INTEGER PRIMARY KEY AUTOINCREMENT, + origin TEXT NOT NULL, + httpRealm TEXT, + formActionOrigin TEXT, + usernameField TEXT, + passwordField TEXT, + timesUsed INTEGER NOT NULL DEFAULT 0, + timeCreated INTEGER NOT NULL, + timeLastUsed INTEGER, + timePasswordChanged INTEGER NOT NULL, + secFields TEXT, + + local_modified INTEGER, + + is_deleted TINYINT NOT NULL DEFAULT 0, + sync_status TINYINT NOT NULL DEFAULT 0 + ); + ", + ) + .unwrap(); connection .execute_batch( " @@ -314,8 +359,18 @@ mod tests { let version = db.conn_ext_query_one::("PRAGMA user_version").unwrap(); assert_eq!(version, VERSION); - // and ensure sql selecting the new column works. + // ensure we have migrated to v2 db.execute_batch("SELECT enc_unknown_fields FROM loginsM") .unwrap(); + + // and ensure we have also migrated to v3 + db.execute_batch("SELECT timeOfLastBreach FROM loginsL") + .unwrap(); + db.execute_batch("SELECT timeOfLastBreach FROM loginsM") + .unwrap(); + db.execute_batch("SELECT timeLastBreachAlertDismissed FROM loginsL") + .unwrap(); + db.execute_batch("SELECT timeLastBreachAlertDismissed FROM loginsM") + .unwrap(); } } diff --git a/components/logins/src/store.rs b/components/logins/src/store.rs index 405c9b0efa..666d5ea528 100644 --- a/components/logins/src/store.rs +++ b/components/logins/src/store.rs @@ -178,6 +178,31 @@ impl LoginStore { self.lock_db()?.touch(id) } + #[handle_error(Error)] + pub fn is_potentially_breached(&self, id: &str) -> ApiResult { + self.lock_db()?.is_potentially_breached(id) + } + + #[handle_error(Error)] + pub fn record_breach(&self, id: &str, timestamp: i64) -> ApiResult<()> { + self.lock_db()?.record_breach(id, timestamp) + } + + #[handle_error(Error)] + pub fn reset_all_breaches(&self) -> ApiResult<()> { + self.lock_db()?.reset_all_breaches() + } + + #[handle_error(Error)] + pub fn is_breach_alert_dismissed(&self, id: &str) -> ApiResult { + self.lock_db()?.is_breach_alert_dismissed(id) + } + + #[handle_error(Error)] + pub fn record_breach_alert_dismissal(&self, id: &str) -> ApiResult<()> { + self.lock_db()?.record_breach_alert_dismissal(id) + } + #[handle_error(Error)] pub fn delete(&self, id: &str) -> ApiResult { self.lock_db()?.delete(id) diff --git a/components/logins/src/sync/engine.rs b/components/logins/src/sync/engine.rs index 1cb484e5c4..0cde79e7b2 100644 --- a/components/logins/src/sync/engine.rs +++ b/components/logins/src/sync/engine.rs @@ -99,7 +99,7 @@ impl LoginsSyncEngine { let local_modified = UNIX_EPOCH + Duration::from_millis(dupe.meta.time_password_changed as u64); let local = LocalLogin::Alive { - login: dupe, + login: Box::new(dupe), local_modified, }; plan.plan_two_way_merge(local, (upstream, upstream_time)); diff --git a/components/logins/src/sync/merge.rs b/components/logins/src/sync/merge.rs index 2786d64e7f..a64edf67a9 100644 --- a/components/logins/src/sync/merge.rs +++ b/components/logins/src/sync/merge.rs @@ -40,7 +40,7 @@ pub(crate) enum LocalLogin { local_modified: SystemTime, }, Alive { - login: EncryptedLogin, + login: Box, local_modified: SystemTime, }, } @@ -72,7 +72,7 @@ impl LocalLogin { error_support::report_error!("logins-crypto", "empty ciphertext in the db",); } LocalLogin::Alive { - login, + login: Box::new(login), local_modified, } }) diff --git a/components/logins/src/sync/payload.rs b/components/logins/src/sync/payload.rs index a10a634b94..4901c16cb5 100644 --- a/components/logins/src/sync/payload.rs +++ b/components/logins/src/sync/payload.rs @@ -70,6 +70,8 @@ impl IncomingLogin { http_realm: p.http_realm, username_field: p.username_field, password_field: p.password_field, + time_of_last_breach: p.time_of_last_breach, + time_last_breach_alert_dismissed: p.time_last_breach_alert_dismissed, }; let original_sec_fields = SecureLoginFields { username: p.username, @@ -86,6 +88,8 @@ impl IncomingLogin { http_realm: login_entry.http_realm, username_field: login_entry.username_field, password_field: login_entry.password_field, + time_of_last_breach: None, + time_last_breach_alert_dismissed: None, }; let id = String::from(p.guid); let sec_fields = SecureLoginFields { @@ -169,6 +173,14 @@ pub struct LoginPayload { // Additional "unknown" round-tripped fields. #[serde(flatten)] unknown_fields: UnknownFields, + + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_timestamp")] + pub time_of_last_breach: Option, + + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_timestamp")] + pub time_last_breach_alert_dismissed: Option, } // These probably should be on the payload itself, but one refactor at a time! @@ -197,6 +209,8 @@ impl EncryptedLogin { time_password_changed: self.meta.time_password_changed, time_last_used: self.meta.time_last_used, times_used: self.meta.times_used, + time_of_last_breach: self.fields.time_of_last_breach, + time_last_breach_alert_dismissed: self.fields.time_last_breach_alert_dismissed, unknown_fields, }, )?) @@ -217,6 +231,18 @@ where Ok(i64::deserialize(deserializer).unwrap_or_default().max(0)) } +// Quiet clippy, since this function is passed to deserialiaze_with... +#[allow(clippy::unnecessary_wraps)] +fn deserialize_optional_timestamp<'de, D>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: serde::de::Deserializer<'de>, +{ + use serde::de::Deserialize; + Ok(i64::deserialize(deserializer).ok()) +} + #[cfg(not(feature = "keydb"))] #[cfg(test)] mod tests { diff --git a/components/logins/src/sync/update_plan.rs b/components/logins/src/sync/update_plan.rs index 122a2f7d40..50a77583cc 100644 --- a/components/logins/src/sync/update_plan.rs +++ b/components/logins/src/sync/update_plan.rs @@ -449,7 +449,7 @@ mod tests { // And since the local age is 100, then the server should win. let server_record_timestamp = now.checked_sub(Duration::from_secs(1)).unwrap(); let local_login = LocalLogin::Alive { - login: login.clone(), + login: Box::new(login.clone()), local_modified, }; @@ -516,7 +516,7 @@ mod tests { // And since the local age is 1, the local record should win! let server_record_timestamp = now.checked_sub(Duration::from_secs(500)).unwrap(); let local_login = LocalLogin::Alive { - login: login.clone(), + login: Box::new(login.clone()), local_modified, }; let mirror_login = MirrorLogin { diff --git a/components/support/rc_crypto/nss/fixtures/profile/logins.db b/components/support/rc_crypto/nss/fixtures/profile/logins.db index 93d7ce7517..ebe1a36f4b 100644 Binary files a/components/support/rc_crypto/nss/fixtures/profile/logins.db and b/components/support/rc_crypto/nss/fixtures/profile/logins.db differ