From e74448727b6e03b2154fac61c9adf23dacf2d6bf Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 30 Jul 2026 15:40:08 -0700 Subject: [PATCH 01/32] Create encryption crate with login crypto code --- Cargo.lock | 18 + Cargo.toml | 1 + components/support/encryption/Cargo.toml | 31 ++ .../support/encryption/src/encryption.rs | 516 ++++++++++++++++++ components/support/encryption/src/error.rs | 111 ++++ components/support/encryption/src/lib.rs | 22 + 6 files changed, 699 insertions(+) create mode 100644 components/support/encryption/Cargo.toml create mode 100644 components/support/encryption/src/encryption.rs create mode 100644 components/support/encryption/src/error.rs create mode 100644 components/support/encryption/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 625905a7e70..4c51b0dcd3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1105,6 +1105,24 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encryption" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "error-support", + "futures", + "jwcrypto", + "lazy_static", + "nss-as", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.3", + "uniffi", +] + [[package]] name = "env_logger" version = "0.10.2" diff --git a/Cargo.toml b/Cargo.toml index c8379d4d827..9afc549b397 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "components/search", "components/suggest", "components/suggest/suggest-bench", + "components/support/encryption", "components/support/error", "components/support/error/tests", "components/support/find-places-db", diff --git a/components/support/encryption/Cargo.toml b/components/support/encryption/Cargo.toml new file mode 100644 index 00000000000..20dd7715455 --- /dev/null +++ b/components/support/encryption/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "encryption" +version = "0.1.0" +edition = "2024" +authors = ["Naomi Kirby "] + +[features] +default = [] +# Enables `NSSKeyManager`, which stores the logins encryption key in NSS's +# `key4.db` (wrapped with a key derived from the primary password, if set). +# Used on Desktop to integrate with the existing NSS key store and primary +# password flow. +keydb = ["nss-as/keydb", "dep:async-trait", "dep:futures"] + +[dependencies] +uniffi = { version = "0.31" } +anyhow = "1.0" +async-trait = { version = "0.1", optional = true } +error-support = { path = "../error" } +futures = { version = "0.3", optional = true, features = ["executor"] } +jwcrypto = { path = "../jwcrypto" } +lazy_static = "1.4" +nss-as = { path = "../rc_crypto/nss", default-features = false } +serde = "1" +serde_derive = "1" +serde_json = "1" + +thiserror = "2" + +[build-dependencies] +uniffi = { version = "0.31", features = ["build"] } \ No newline at end of file diff --git a/components/support/encryption/src/encryption.rs b/components/support/encryption/src/encryption.rs new file mode 100644 index 00000000000..ca2a40b4176 --- /dev/null +++ b/components/support/encryption/src/encryption.rs @@ -0,0 +1,516 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +// This is the *local* encryption support - it has nothing to do with the +// encryption used by sync. + +// For context, what "local encryption" means in this context is: +// * We use regular sqlite, but ensure that sensitive data is encrypted in the DB in the +// `secure_fields` column. The encryption key is managed by the app. +// * The `decrypt_struct` and `encrypt_struct` functions are used to convert between an encrypted +// `secure_fields` string and a decrypted `SecureFields` struct +// * Most API functions return `EncryptedLogin` which has its data encrypted. +// +// This makes life tricky for Sync - sync has its own encryption and its own +// management of sync keys. The entire records are encrypted on the server - +// so the record on the server has the plain-text data (which is then +// encrypted as part of the entire record), so: +// * When transforming a record from the DB into a Sync record, we need to +// *decrypt* the data. +// * When transforming a record from Sync into a DB record, we need to *encrypt* +// the data. +// +// So Sync needs to know the key etc, and that needs to get passed down +// multiple layers, from the app saying "sync now" all the way down to the +// low level sync code. +// To make life a little easier, we do that via a struct. +// +// Consumers of the Login component have 3 options for setting up encryption: +// 1. Implement EncryptorDecryptor directly +// eg `LoginStore::new(MyEncryptorDecryptor)` +// 2. Implement KeyManager and use ManagedEncryptorDecryptor +// eg `LoginStore::new(ManagedEncryptorDecryptor::new(MyKeyManager))` +// 3. Generate a single key and create a StaticKeyManager and use it together with +// ManagedEncryptorDecryptor +// eg `LoginStore::new(ManagedEncryptorDecryptor::new(StaticKeyManager { key: myKey }))` +// +// You can implement EncryptorDecryptor directly to keep full control over the encryption +// algorithm. For example, on the desktop, this could make use of NSS's SecretDecoderRing to +// achieve transparent key management. +// +// If the application wants to keep the current encryption, like Android and iOS, for example, but +// control the key management itself, the KeyManager can be implemented and the encryption can be +// done on the Rust side with the ManagedEncryptorDecryptor. +// +// In tests or some command line tools, it can be practical to use a static key that does not +// change at runtime and is already present when the LoginsStore is initialized. In this case, it +// makes sense to use the provided StaticKeyManager. + +use crate::error::*; +use std::sync::Arc; + +#[cfg(feature = "keydb")] +use futures::executor::block_on; + +#[cfg(feature = "keydb")] +use async_trait::async_trait; + +#[cfg(feature = "keydb")] +use nss_as::assert_initialized as assert_nss_initialized; +#[cfg(feature = "keydb")] +use nss_as::pk11::sym_key::{ + authenticate_with_primary_password, authentication_with_primary_password_is_needed, + get_or_create_aes256_key, +}; + +/// This is the generic EncryptorDecryptor trait, as handed over to the Store during initialization. +/// Consumers can implement either this generic trait and bring in their own crypto, or leverage the +/// ManagedEncryptorDecryptor below, which provides encryption algorithms out of the box. +/// +/// Note that EncryptorDecryptor must not call any LoginStore methods. The login store can call out +/// to the EncryptorDecryptor when it's internal mutex is held so calling back in to the LoginStore +/// may deadlock. +pub trait EncryptorDecryptor: Send + Sync { + fn encrypt(&self, cleartext: Vec) -> ApiResult>; + fn decrypt(&self, ciphertext: Vec) -> ApiResult>; +} + +impl EncryptorDecryptor for Arc { + fn encrypt(&self, clearbytes: Vec) -> ApiResult> { + (**self).encrypt(clearbytes) + } + + fn decrypt(&self, cipherbytes: Vec) -> ApiResult> { + (**self).decrypt(cipherbytes) + } +} + +/// The ManagedEncryptorDecryptor makes use of the NSS provided cryptographic algorithms. The +/// ManagedEncryptorDecryptor uses a KeyManager for encryption key retrieval. +pub struct ManagedEncryptorDecryptor { + key_manager: Arc, +} + +impl ManagedEncryptorDecryptor { + pub fn new(key_manager: Arc) -> Self { + Self { key_manager } + } +} + +impl EncryptorDecryptor for ManagedEncryptorDecryptor { + fn encrypt(&self, clearbytes: Vec) -> ApiResult> { + let keybytes = self + .key_manager + .get_key() + .map_err(|_| LoginsApiError::MissingKey)?; + let key = std::str::from_utf8(&keybytes).map_err(|_| LoginsApiError::InvalidKey)?; + + let encdec = jwcrypto::EncryptorDecryptor::new(key) + .map_err(|_: jwcrypto::JwCryptoError| LoginsApiError::InvalidKey)?; + + let cleartext = + std::str::from_utf8(&clearbytes).map_err(|e| LoginsApiError::EncryptionFailed { + reason: e.to_string(), + })?; + encdec + .encrypt(cleartext) + .map_err( + |e: jwcrypto::JwCryptoError| LoginsApiError::EncryptionFailed { + reason: e.to_string(), + }, + ) + .map(|text| text.into()) + } + + fn decrypt(&self, cipherbytes: Vec) -> ApiResult> { + let keybytes = self + .key_manager + .get_key() + .map_err(|_| LoginsApiError::MissingKey)?; + let key = std::str::from_utf8(&keybytes).map_err(|_| LoginsApiError::InvalidKey)?; + + let encdec = jwcrypto::EncryptorDecryptor::new(key) + .map_err(|_: jwcrypto::JwCryptoError| LoginsApiError::InvalidKey)?; + + let ciphertext = + std::str::from_utf8(&cipherbytes).map_err(|e| LoginsApiError::DecryptionFailed { + reason: e.to_string(), + })?; + encdec + .decrypt(ciphertext) + .map_err( + |e: jwcrypto::JwCryptoError| LoginsApiError::DecryptionFailed { + reason: e.to_string(), + }, + ) + .map(|text| text.into()) + } +} + +/// Consumers can implement the KeyManager in combination with the ManagedEncryptorDecryptor to hand +/// over the encryption key whenever encryption or decryption happens. +pub trait KeyManager: Send + Sync { + fn get_key(&self) -> ApiResult>; +} + +/// Last but not least we provide a StaticKeyManager, which can be +/// used in cases where there is a single key during runtime, for example in tests. +pub struct StaticKeyManager { + key: String, +} + +impl StaticKeyManager { + pub fn new(key: String) -> Self { + Self { key } + } +} + +impl KeyManager for StaticKeyManager { + #[handle_error(Error)] + fn get_key(&self) -> ApiResult> { + Ok(self.key.as_bytes().into()) + } +} + +/// `PrimaryPasswordAuthenticator` is used in conjunction with `NSSKeyManager` to provide the +/// primary password and the success or failure actions of the authentication process. +#[cfg(feature = "keydb")] +#[async_trait] +pub trait PrimaryPasswordAuthenticator: Send + Sync { + /// Get a primary password for authentication, otherwise return the + /// AuthenticationCancelled error to cancel the authentication process. + async fn get_primary_password(&self) -> ApiResult; + async fn on_authentication_success(&self) -> ApiResult<()>; + async fn on_authentication_failure(&self) -> ApiResult<()>; +} + +/// Use the `NSSKeyManager` to use NSS for key management. +/// +/// NSS stores keys in `key4.db` within the profile and wraps the key with a key derived from the +/// primary password, if set. It defers to the provided `PrimaryPasswordAuthenticator` +/// implementation to handle user authentication. Note that if no primary password is set, the +/// wrapping key is deterministically derived from an empty string. +/// +/// Make sure to initialize NSS using `ensure_initialized_with_profile_dir` before creating a +/// NSSKeyManager. +/// +/// # Examples +/// ```no_run +/// use async_trait::async_trait; +/// use encryption::KeyManager; +/// use encryption::{PrimaryPasswordAuthenticator, LoginsApiError, NSSKeyManager}; +/// use std::sync::Arc; +/// +/// struct MyPrimaryPasswordAuthenticator {} +/// +/// #[async_trait] +/// impl PrimaryPasswordAuthenticator for MyPrimaryPasswordAuthenticator { +/// async fn get_primary_password(&self) -> Result { +/// // Most likely, you would want to prompt for a password. +/// // let password = prompt_string("primary password").unwrap_or_default(); +/// Ok("secret".to_string()) +/// } +/// +/// async fn on_authentication_success(&self) -> Result<(), LoginsApiError> { +/// println!("success"); +/// Ok(()) +/// } +/// +/// async fn on_authentication_failure(&self) -> Result<(), LoginsApiError> { +/// println!("this did not work, please try again:"); +/// Ok(()) +/// } +/// } +/// let key_manager = NSSKeyManager::new("example", Arc::new(MyPrimaryPasswordAuthenticator {})); +/// assert_eq!(key_manager.get_key().unwrap().len(), 63); +/// ``` +#[cfg(feature = "keydb")] +pub struct NSSKeyManager { + key_name: String, + primary_password_authenticator: Arc, +} + +#[cfg(feature = "keydb")] +impl NSSKeyManager { + /// Initialize new `NSSKeyManager` with a given `PrimaryPasswordAuthenticator`. + /// There must be a previous initializiation of NSS before initializing + /// `NSSKeyManager`, otherwise this panics. + pub fn new(key_name: &str, primary_password_authenticator: Arc) -> Self { + assert_nss_initialized(); + Self { + key_name: key_name.to_string(), + primary_password_authenticator, + } + } + + pub fn into_dyn_key_manager(self: Arc) -> Arc { + self + } +} + +// wrapp `authentication_with_primary_password_is_needed` into an ApiResult +#[cfg(feature = "keydb")] +fn api_authentication_with_primary_password_is_needed() -> ApiResult { + authentication_with_primary_password_is_needed().map_err(|e: nss_as::Error| { + LoginsApiError::NSSAuthenticationError { + reason: e.to_string(), + } + }) +} + +// wrapp `authenticate_with_primary_password` into an ApiResult +#[cfg(feature = "keydb")] +fn api_authenticate_with_primary_password(primary_password: &str) -> ApiResult { + authenticate_with_primary_password(primary_password).map_err(|e: nss_as::Error| { + LoginsApiError::NSSAuthenticationError { + reason: e.to_string(), + } + }) +} + +#[cfg(feature = "keydb")] +impl KeyManager for NSSKeyManager { + fn get_key(&self) -> ApiResult> { + if api_authentication_with_primary_password_is_needed()? { + let primary_password = + block_on(self.primary_password_authenticator.get_primary_password())?; + let mut result = api_authenticate_with_primary_password(&primary_password)?; + + if result { + block_on( + self.primary_password_authenticator + .on_authentication_success(), + )?; + } else { + while !result { + block_on( + self.primary_password_authenticator + .on_authentication_failure(), + )?; + + let primary_password = + block_on(self.primary_password_authenticator.get_primary_password())?; + result = api_authenticate_with_primary_password(&primary_password)?; + } + block_on( + self.primary_password_authenticator + .on_authentication_success(), + )?; + } + } + + let key = get_or_create_aes256_key(self.key_name.as_str()).map_err(|_| LoginsApiError::MissingKey)?; + let mut bytes: Vec = Vec::new(); + serde_json::to_writer( + &mut bytes, + &jwcrypto::Jwk::new_direct_from_bytes(None, &key), + ) + .unwrap(); + Ok(bytes) + } +} + +#[handle_error(Error)] +pub fn create_canary(text: &str, key: &str) -> ApiResult { + Ok(jwcrypto::EncryptorDecryptor::new(key)?.create_canary(text)?) +} + +pub fn check_canary(canary: &str, text: &str, key: &str) -> ApiResult { + let encdec = jwcrypto::EncryptorDecryptor::new(key) + .map_err(|_: jwcrypto::JwCryptoError| LoginsApiError::InvalidKey)?; + Ok(encdec.check_canary(canary, text).unwrap_or(false)) +} + +#[handle_error(Error)] +pub fn create_key() -> ApiResult { + Ok(jwcrypto::EncryptorDecryptor::create_key()?) +} + +#[cfg(test)] +pub mod test_utils { + use super::*; + use serde::{de::DeserializeOwned, Serialize}; + + lazy_static::lazy_static! { + pub static ref TEST_ENCRYPTION_KEY: String = serde_json::to_string(&jwcrypto::Jwk::new_direct_key(Some("test-key".to_string())).unwrap()).unwrap(); + pub static ref TEST_ENCDEC: Arc = Arc::new(ManagedEncryptorDecryptor::new(Arc::new(StaticKeyManager { key: TEST_ENCRYPTION_KEY.clone() }))); + } + + pub fn encrypt_struct(fields: &T) -> String { + let string = serde_json::to_string(fields).unwrap(); + let cipherbytes = TEST_ENCDEC.encrypt(string.as_bytes().into()).unwrap(); + std::str::from_utf8(&cipherbytes).unwrap().to_owned() + } + pub fn decrypt_struct(ciphertext: String) -> T { + let jsonbytes = TEST_ENCDEC.decrypt(ciphertext.as_bytes().into()).unwrap(); + serde_json::from_str(std::str::from_utf8(&jsonbytes).unwrap()).unwrap() + } +} + +#[cfg(not(feature = "keydb"))] +#[cfg(test)] +mod tests { + use super::*; + use nss_as::ensure_initialized; + + #[test] + fn test_static_key_manager() { + ensure_initialized(); + let key = create_key().unwrap(); + let key_manager = StaticKeyManager { key: key.clone() }; + assert_eq!(key.as_bytes(), key_manager.get_key().unwrap()); + } + + #[test] + fn test_managed_encdec_with_invalid_key() { + ensure_initialized(); + let key_manager = Arc::new(StaticKeyManager { + key: "bad_key".to_owned(), + }); + let encdec = ManagedEncryptorDecryptor { key_manager }; + assert!(matches!( + encdec.encrypt("secret".as_bytes().into()).err().unwrap(), + LoginsApiError::InvalidKey + )); + } + + #[test] + fn test_managed_encdec_with_missing_key() { + ensure_initialized(); + struct MyKeyManager {} + impl KeyManager for MyKeyManager { + fn get_key(&self) -> ApiResult> { + Err(LoginsApiError::MissingKey) + } + } + let key_manager = Arc::new(MyKeyManager {}); + let encdec = ManagedEncryptorDecryptor { key_manager }; + assert!(matches!( + encdec.encrypt("secret".as_bytes().into()).err().unwrap(), + LoginsApiError::MissingKey + )); + } + + #[test] + fn test_managed_encdec() { + ensure_initialized(); + let key = create_key().unwrap(); + let key_manager = Arc::new(StaticKeyManager { key }); + let encdec = ManagedEncryptorDecryptor { key_manager }; + let cleartext = "secret"; + let ciphertext = encdec.encrypt(cleartext.as_bytes().into()).unwrap(); + assert_eq!( + encdec.decrypt(ciphertext.clone()).unwrap(), + cleartext.as_bytes() + ); + let other_encdec = ManagedEncryptorDecryptor { + key_manager: Arc::new(StaticKeyManager { + key: create_key().unwrap(), + }), + }; + + assert_eq!( + other_encdec.decrypt(ciphertext).err().unwrap().to_string(), + "decryption failed: Crypto error: NSS error: NSS error: -8190 " + ); + } + + #[test] + fn test_key_error() { + let storage_err = jwcrypto::EncryptorDecryptor::new("bad-key").err().unwrap(); + println!("{storage_err:?}"); + assert!(matches!(storage_err, jwcrypto::JwCryptoError::InvalidKey)); + } + + #[test] + fn test_canary_functionality() { + ensure_initialized(); + const CANARY_TEXT: &str = "Arbitrary sequence of text"; + let key = create_key().unwrap(); + let canary = create_canary(CANARY_TEXT, &key).unwrap(); + assert!(check_canary(&canary, CANARY_TEXT, &key).unwrap()); + + let different_key = create_key().unwrap(); + assert!(!check_canary(&canary, CANARY_TEXT, &different_key).unwrap()); + + let bad_key = "bad_key".to_owned(); + assert!(matches!( + check_canary(&canary, CANARY_TEXT, &bad_key).err().unwrap(), + LoginsApiError::InvalidKey + )); + } +} + +#[cfg(feature = "keydb")] +#[cfg(test)] +mod tests_keydb { + use super::*; + use nss_as::ensure_initialized_with_profile_dir; + use std::path::PathBuf; + + struct MockPrimaryPasswordAuthenticator { + password: String, + } + + #[async_trait] + impl PrimaryPasswordAuthenticator for MockPrimaryPasswordAuthenticator { + async fn get_primary_password(&self) -> ApiResult { + Ok(self.password.clone()) + } + async fn on_authentication_success(&self) -> ApiResult<()> { + Ok(()) + } + async fn on_authentication_failure(&self) -> ApiResult<()> { + Ok(()) + } + } + + fn profile_path() -> PathBuf { + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../rc_crypto/nss/fixtures/profile") + } + + #[test] + fn test_ensure_initialized_with_profile_dir() { + ensure_initialized_with_profile_dir(profile_path()); + } + + #[test] + fn test_create_key() { + ensure_initialized_with_profile_dir(profile_path()); + let key = create_key().unwrap(); + assert_eq!(key.len(), 63) + } + + #[test] + fn test_nss_key_manager() { + ensure_initialized_with_profile_dir(profile_path()); + // `password` is the primary password of the profile fixture + let mock_primary_password_authenticator = MockPrimaryPasswordAuthenticator { + password: "password".to_string(), + }; + let nss_key_manager = NSSKeyManager { + key_name: String::from("as-logins-key"), + primary_password_authenticator: Arc::new(mock_primary_password_authenticator), + }; + // key from fixtures/profile/key4.db + assert_eq!( + nss_key_manager.get_key().unwrap(), + [ + 123, 34, 107, 116, 121, 34, 58, 34, 111, 99, 116, 34, 44, 34, 107, 34, 58, 34, 66, + 74, 104, 84, 108, 103, 51, 118, 56, 49, 65, 66, 51, 118, 87, 50, 71, 122, 54, 104, + 69, 54, 84, 116, 75, 83, 112, 85, 102, 84, 86, 75, 73, 83, 99, 74, 45, 77, 78, 83, + 67, 117, 99, 34, 125 + ] + .to_vec() + ) + } + + #[test] + fn test_primary_password_authentication() { + ensure_initialized_with_profile_dir(profile_path()); + assert!(authenticate_with_primary_password("password").unwrap()); + } +} diff --git a/components/support/encryption/src/error.rs b/components/support/encryption/src/error.rs new file mode 100644 index 00000000000..4edd2a431c7 --- /dev/null +++ b/components/support/encryption/src/error.rs @@ -0,0 +1,111 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub type Result = std::result::Result; +// Functions which are part of the public API should use this Result. +pub type ApiResult = std::result::Result; + +pub use error_support::{breadcrumb, handle_error, report_error}; +pub use error_support::{debug, error, info, trace, warn}; + +use error_support::{ErrorHandling, GetErrorHandling}; +use jwcrypto::JwCryptoError; + +// Errors we return via the public interface. +#[derive(Debug, thiserror::Error)] +pub enum LoginsApiError { + #[error("NSS not initialized")] + NSSUninitialized, + + #[error("NSS error during authentication: {reason}")] + NSSAuthenticationError { reason: String }, + + #[error("error during authentication: {reason}")] + AuthenticationError { reason: String }, + + #[error("authentication cancelled")] + AuthenticationCanceled, + + #[error("Encryption key is missing.")] + MissingKey, + + #[error("Encryption key is not valid.")] + InvalidKey, + + #[error("encryption failed: {reason}")] + EncryptionFailed { reason: String }, + + #[error("decryption failed: {reason}")] + DecryptionFailed { reason: String }, + + #[error("{reason}")] + Interrupted { reason: String }, + + #[error("Unexpected Error: {reason}")] + UnexpectedLoginsApiError { reason: String }, +} + +/// Logins error type +/// These are "internal" errors used by the implementation. This error type +/// is never returned to the consumer. +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Database is closed")] + DatabaseClosed, + + // Fennec import only works on empty logins tables. + #[error("The logins tables are not empty")] + NonEmptyTable, + + #[error("encryption failed: {0:?}")] + EncryptionFailed(String), + + #[error("decryption failed: {0:?}")] + DecryptionFailed(String), + + #[error("CryptoError({0})")] + CryptoError(#[from] JwCryptoError), + + #[error("IOError: {0}")] + IOError(#[from] std::io::Error), +} + +// Define how our internal errors are handled and converted to external errors +// See `support/error/README.md` for how this works, especially the warning about PII. +impl GetErrorHandling for Error { + type ExternalError = LoginsApiError; + + fn get_error_handling(&self) -> ErrorHandling { + match self { + // Unexpected errors that we report to Sentry. We should watch the reports for these + // and do one or more of these things if we see them: + // - Fix the underlying issue + // - Add breadcrumbs or other context to help uncover the issue + // - Decide that these are expected errors and move them to the above case + _ => ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError { + reason: self.to_string(), + }) + .report_error("logins-unexpected"), + } + } +} + +// The bridged sync engine (`sync::bridge`) deals in `anyhow::Result`, as that's +// what the `sync15` BridgedEngine traits use. This lets UniFFI map those errors +// onto our public error type when the bridge methods are exposed via the UDL. +impl From for LoginsApiError { + fn from(value: anyhow::Error) -> Self { + LoginsApiError::UnexpectedLoginsApiError { + reason: value.to_string(), + } + } +} + +impl From for LoginsApiError { + fn from(error: uniffi::UnexpectedUniFFICallbackError) -> Self { + LoginsApiError::UnexpectedLoginsApiError { + reason: error.to_string(), + } + } +} diff --git a/components/support/encryption/src/lib.rs b/components/support/encryption/src/lib.rs new file mode 100644 index 00000000000..6be83c705f0 --- /dev/null +++ b/components/support/encryption/src/lib.rs @@ -0,0 +1,22 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![allow(unknown_lints)] +#![warn(rust_2018_idioms)] + +#[macro_use] +mod error; + +mod encryption; + +pub use crate::encryption::{ + EncryptorDecryptor, KeyManager, ManagedEncryptorDecryptor, StaticKeyManager, +}; + +#[cfg(feature = "keydb")] +pub use crate::encryption::{NSSKeyManager, PrimaryPasswordAuthenticator}; + +use crate::encryption::{check_canary, create_canary, create_key}; +pub use crate::error::*; +use std::sync::Arc; From a5088946791eb716ae31c8aab20a131cebed63be Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Fri, 31 Jul 2026 09:28:40 -0700 Subject: [PATCH 02/32] Rename LoginApiError to EncryptionApiError --- .../support/encryption/src/encryption.rs | 44 +++++++++---------- components/support/encryption/src/error.rs | 18 ++++---- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/components/support/encryption/src/encryption.rs b/components/support/encryption/src/encryption.rs index ca2a40b4176..0f4d75c72e2 100644 --- a/components/support/encryption/src/encryption.rs +++ b/components/support/encryption/src/encryption.rs @@ -104,20 +104,20 @@ impl EncryptorDecryptor for ManagedEncryptorDecryptor { let keybytes = self .key_manager .get_key() - .map_err(|_| LoginsApiError::MissingKey)?; - let key = std::str::from_utf8(&keybytes).map_err(|_| LoginsApiError::InvalidKey)?; + .map_err(|_| EncryptionApiError::MissingKey)?; + let key = std::str::from_utf8(&keybytes).map_err(|_| EncryptionApiError::InvalidKey)?; let encdec = jwcrypto::EncryptorDecryptor::new(key) - .map_err(|_: jwcrypto::JwCryptoError| LoginsApiError::InvalidKey)?; + .map_err(|_: jwcrypto::JwCryptoError| EncryptionApiError::InvalidKey)?; let cleartext = - std::str::from_utf8(&clearbytes).map_err(|e| LoginsApiError::EncryptionFailed { + std::str::from_utf8(&clearbytes).map_err(|e| EncryptionApiError::EncryptionFailed { reason: e.to_string(), })?; encdec .encrypt(cleartext) .map_err( - |e: jwcrypto::JwCryptoError| LoginsApiError::EncryptionFailed { + |e: jwcrypto::JwCryptoError| EncryptionApiError::EncryptionFailed { reason: e.to_string(), }, ) @@ -128,20 +128,20 @@ impl EncryptorDecryptor for ManagedEncryptorDecryptor { let keybytes = self .key_manager .get_key() - .map_err(|_| LoginsApiError::MissingKey)?; - let key = std::str::from_utf8(&keybytes).map_err(|_| LoginsApiError::InvalidKey)?; + .map_err(|_| EncryptionApiError::MissingKey)?; + let key = std::str::from_utf8(&keybytes).map_err(|_| EncryptionApiError::InvalidKey)?; let encdec = jwcrypto::EncryptorDecryptor::new(key) - .map_err(|_: jwcrypto::JwCryptoError| LoginsApiError::InvalidKey)?; + .map_err(|_: jwcrypto::JwCryptoError| EncryptionApiError::InvalidKey)?; let ciphertext = - std::str::from_utf8(&cipherbytes).map_err(|e| LoginsApiError::DecryptionFailed { + std::str::from_utf8(&cipherbytes).map_err(|e| EncryptionApiError::DecryptionFailed { reason: e.to_string(), })?; encdec .decrypt(ciphertext) .map_err( - |e: jwcrypto::JwCryptoError| LoginsApiError::DecryptionFailed { + |e: jwcrypto::JwCryptoError| EncryptionApiError::DecryptionFailed { reason: e.to_string(), }, ) @@ -200,25 +200,25 @@ pub trait PrimaryPasswordAuthenticator: Send + Sync { /// ```no_run /// use async_trait::async_trait; /// use encryption::KeyManager; -/// use encryption::{PrimaryPasswordAuthenticator, LoginsApiError, NSSKeyManager}; +/// use encryption::{PrimaryPasswordAuthenticator, EncryptionApiError, NSSKeyManager}; /// use std::sync::Arc; /// /// struct MyPrimaryPasswordAuthenticator {} /// /// #[async_trait] /// impl PrimaryPasswordAuthenticator for MyPrimaryPasswordAuthenticator { -/// async fn get_primary_password(&self) -> Result { +/// async fn get_primary_password(&self) -> Result { /// // Most likely, you would want to prompt for a password. /// // let password = prompt_string("primary password").unwrap_or_default(); /// Ok("secret".to_string()) /// } /// -/// async fn on_authentication_success(&self) -> Result<(), LoginsApiError> { +/// async fn on_authentication_success(&self) -> Result<(), EncryptionApiError> { /// println!("success"); /// Ok(()) /// } /// -/// async fn on_authentication_failure(&self) -> Result<(), LoginsApiError> { +/// async fn on_authentication_failure(&self) -> Result<(), EncryptionApiError> { /// println!("this did not work, please try again:"); /// Ok(()) /// } @@ -254,7 +254,7 @@ impl NSSKeyManager { #[cfg(feature = "keydb")] fn api_authentication_with_primary_password_is_needed() -> ApiResult { authentication_with_primary_password_is_needed().map_err(|e: nss_as::Error| { - LoginsApiError::NSSAuthenticationError { + EncryptionApiError::NSSAuthenticationError { reason: e.to_string(), } }) @@ -264,7 +264,7 @@ fn api_authentication_with_primary_password_is_needed() -> ApiResult { #[cfg(feature = "keydb")] fn api_authenticate_with_primary_password(primary_password: &str) -> ApiResult { authenticate_with_primary_password(primary_password).map_err(|e: nss_as::Error| { - LoginsApiError::NSSAuthenticationError { + EncryptionApiError::NSSAuthenticationError { reason: e.to_string(), } }) @@ -301,7 +301,7 @@ impl KeyManager for NSSKeyManager { } } - let key = get_or_create_aes256_key(self.key_name.as_str()).map_err(|_| LoginsApiError::MissingKey)?; + let key = get_or_create_aes256_key(self.key_name.as_str()).map_err(|_| EncryptionApiError::MissingKey)?; let mut bytes: Vec = Vec::new(); serde_json::to_writer( &mut bytes, @@ -319,7 +319,7 @@ pub fn create_canary(text: &str, key: &str) -> ApiResult { pub fn check_canary(canary: &str, text: &str, key: &str) -> ApiResult { let encdec = jwcrypto::EncryptorDecryptor::new(key) - .map_err(|_: jwcrypto::JwCryptoError| LoginsApiError::InvalidKey)?; + .map_err(|_: jwcrypto::JwCryptoError| EncryptionApiError::InvalidKey)?; Ok(encdec.check_canary(canary, text).unwrap_or(false)) } @@ -372,7 +372,7 @@ mod tests { let encdec = ManagedEncryptorDecryptor { key_manager }; assert!(matches!( encdec.encrypt("secret".as_bytes().into()).err().unwrap(), - LoginsApiError::InvalidKey + EncryptionApiError::InvalidKey )); } @@ -382,14 +382,14 @@ mod tests { struct MyKeyManager {} impl KeyManager for MyKeyManager { fn get_key(&self) -> ApiResult> { - Err(LoginsApiError::MissingKey) + Err(EncryptionApiError::MissingKey) } } let key_manager = Arc::new(MyKeyManager {}); let encdec = ManagedEncryptorDecryptor { key_manager }; assert!(matches!( encdec.encrypt("secret".as_bytes().into()).err().unwrap(), - LoginsApiError::MissingKey + EncryptionApiError::MissingKey )); } @@ -438,7 +438,7 @@ mod tests { let bad_key = "bad_key".to_owned(); assert!(matches!( check_canary(&canary, CANARY_TEXT, &bad_key).err().unwrap(), - LoginsApiError::InvalidKey + EncryptionApiError::InvalidKey )); } } diff --git a/components/support/encryption/src/error.rs b/components/support/encryption/src/error.rs index 4edd2a431c7..941cc6efdcd 100644 --- a/components/support/encryption/src/error.rs +++ b/components/support/encryption/src/error.rs @@ -4,7 +4,7 @@ pub type Result = std::result::Result; // Functions which are part of the public API should use this Result. -pub type ApiResult = std::result::Result; +pub type ApiResult = std::result::Result; pub use error_support::{breadcrumb, handle_error, report_error}; pub use error_support::{debug, error, info, trace, warn}; @@ -14,7 +14,7 @@ use jwcrypto::JwCryptoError; // Errors we return via the public interface. #[derive(Debug, thiserror::Error)] -pub enum LoginsApiError { +pub enum EncryptionApiError { #[error("NSS not initialized")] NSSUninitialized, @@ -43,7 +43,7 @@ pub enum LoginsApiError { Interrupted { reason: String }, #[error("Unexpected Error: {reason}")] - UnexpectedLoginsApiError { reason: String }, + UnexpectedEncryptionApiError { reason: String }, } /// Logins error type @@ -74,7 +74,7 @@ pub enum Error { // Define how our internal errors are handled and converted to external errors // See `support/error/README.md` for how this works, especially the warning about PII. impl GetErrorHandling for Error { - type ExternalError = LoginsApiError; + type ExternalError = EncryptionApiError; fn get_error_handling(&self) -> ErrorHandling { match self { @@ -83,7 +83,7 @@ impl GetErrorHandling for Error { // - Fix the underlying issue // - Add breadcrumbs or other context to help uncover the issue // - Decide that these are expected errors and move them to the above case - _ => ErrorHandling::convert(LoginsApiError::UnexpectedLoginsApiError { + _ => ErrorHandling::convert(EncryptionApiError::UnexpectedEncryptionApiError { reason: self.to_string(), }) .report_error("logins-unexpected"), @@ -94,17 +94,17 @@ impl GetErrorHandling for Error { // The bridged sync engine (`sync::bridge`) deals in `anyhow::Result`, as that's // what the `sync15` BridgedEngine traits use. This lets UniFFI map those errors // onto our public error type when the bridge methods are exposed via the UDL. -impl From for LoginsApiError { +impl From for EncryptionApiError { fn from(value: anyhow::Error) -> Self { - LoginsApiError::UnexpectedLoginsApiError { + EncryptionApiError::UnexpectedEncryptionApiError { reason: value.to_string(), } } } -impl From for LoginsApiError { +impl From for EncryptionApiError { fn from(error: uniffi::UnexpectedUniFFICallbackError) -> Self { - LoginsApiError::UnexpectedLoginsApiError { + EncryptionApiError::UnexpectedEncryptionApiError { reason: error.to_string(), } } From a88c4d73c55f759ebd4674938209df919678214e Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Tue, 4 Aug 2026 10:57:02 -0700 Subject: [PATCH 03/32] Make the login key functions wrappers of the encryption crate --- Cargo.lock | 1 + components/logins/Cargo.toml | 3 ++- components/logins/src/encryption.rs | 8 +++----- components/logins/src/error.rs | 25 +++++++++++++++++++++++- components/support/encryption/src/lib.rs | 2 +- 5 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c51b0dcd3e..40d068f511c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2551,6 +2551,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "encryption", "error-support", "futures", "interrupt-support", diff --git a/components/logins/Cargo.toml b/components/logins/Cargo.toml index 2f126f9eba2..df60196f7f3 100644 --- a/components/logins/Cargo.toml +++ b/components/logins/Cargo.toml @@ -12,7 +12,7 @@ default = [] # `key4.db` (wrapped with a key derived from the primary password, if set). # Used on Desktop to integrate with the existing NSS key store and primary # password flow. -keydb = ["nss-as/keydb", "dep:async-trait", "dep:futures"] +keydb = ["nss-as/keydb", "encryption/keydb", "dep:async-trait", "dep:futures"] # Allows logins with empty passwords to be imported. Used on Desktop during # migration to accept existing logins that have empty passwords. allow_empty_passwords = [] @@ -53,6 +53,7 @@ anyhow = "1.0" uniffi = { version = "0.31" } async-trait = { version = "0.1", optional = true } futures = { version = "0.3", optional = true, features = ["executor"] } +encryption = { path = "../support/encryption", default-features = false } [build-dependencies] uniffi = { version = "0.31", features = ["build"] } diff --git a/components/logins/src/encryption.rs b/components/logins/src/encryption.rs index 9b180e5eb46..3ca528951e5 100644 --- a/components/logins/src/encryption.rs +++ b/components/logins/src/encryption.rs @@ -343,18 +343,16 @@ impl KeyManager for NSSKeyManager { #[handle_error(Error)] pub fn create_canary(text: &str, key: &str) -> ApiResult { - Ok(jwcrypto::EncryptorDecryptor::new(key)?.create_canary(text)?) + Ok(encryption::create_canary(text, key)?) } pub fn check_canary(canary: &str, text: &str, key: &str) -> ApiResult { - let encdec = jwcrypto::EncryptorDecryptor::new(key) - .map_err(|_: jwcrypto::JwCryptoError| LoginsApiError::InvalidKey)?; - Ok(encdec.check_canary(canary, text).unwrap_or(false)) + Ok(encryption::check_canary(canary, text, key)?) } #[handle_error(Error)] pub fn create_key() -> ApiResult { - Ok(jwcrypto::EncryptorDecryptor::create_key()?) + Ok(encryption::create_key()?) } #[cfg(test)] diff --git a/components/logins/src/error.rs b/components/logins/src/error.rs index 7d8c27d2f68..01456fa7db7 100644 --- a/components/logins/src/error.rs +++ b/components/logins/src/error.rs @@ -12,6 +12,7 @@ pub use error_support::{debug, error, info, trace, warn}; use error_support::{ErrorHandling, GetErrorHandling}; use jwcrypto::JwCryptoError; +use encryption::EncryptionApiError; // Errors we return via the public interface. #[derive(Debug, thiserror::Error)] @@ -96,7 +97,10 @@ pub enum Error { InvalidPath(OsString), #[error("CryptoError({0})")] - CryptoError(#[from] JwCryptoError), + CryptoError(#[from] EncryptionApiError), + + #[error("CryptoError({0})")] + JwCryptoError(#[from] JwCryptoError), #[error("{0}")] Interrupted(#[from] interrupt_support::Interrupted), @@ -214,3 +218,22 @@ impl From for LoginsApiError { } } } + +impl From for LoginsApiError { + fn from(error: EncryptionApiError) -> Self { + match error { + EncryptionApiError::NSSUninitialized => Self::NSSUninitialized, + EncryptionApiError::NSSAuthenticationError{reason: x} => Self::NSSAuthenticationError { + reason: x + }, + EncryptionApiError::AuthenticationError{reason: x} => Self::AuthenticationError { reason: x }, + EncryptionApiError::AuthenticationCanceled => Self::AuthenticationCanceled, + EncryptionApiError::MissingKey => Self::MissingKey, + EncryptionApiError::InvalidKey => Self::InvalidKey, + EncryptionApiError::EncryptionFailed{reason: x} => Self::EncryptionFailed { reason: x }, + EncryptionApiError::DecryptionFailed{reason: x} => Self::DecryptionFailed { reason: x }, + EncryptionApiError::Interrupted{reason: x} => Self::Interrupted { reason: x }, + EncryptionApiError::UnexpectedEncryptionApiError{reason: x} => Self::UnexpectedLoginsApiError { reason: x }, + } + } +} diff --git a/components/support/encryption/src/lib.rs b/components/support/encryption/src/lib.rs index 6be83c705f0..8ef600cd941 100644 --- a/components/support/encryption/src/lib.rs +++ b/components/support/encryption/src/lib.rs @@ -17,6 +17,6 @@ pub use crate::encryption::{ #[cfg(feature = "keydb")] pub use crate::encryption::{NSSKeyManager, PrimaryPasswordAuthenticator}; -use crate::encryption::{check_canary, create_canary, create_key}; +pub use crate::encryption::{check_canary, create_canary, create_key}; pub use crate::error::*; use std::sync::Arc; From d6de42e40ec287f61203b6b397f6672c107db5d2 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 12 Aug 2026 15:40:06 -0700 Subject: [PATCH 04/32] Add uniffi interfaces to the encryption crate --- components/support/encryption/Cargo.toml | 2 +- components/support/encryption/build.rs | 8 ++ .../support/encryption/src/encryption.rs | 7 ++ .../support/encryption/src/encryption.udl | 81 +++++++++++++++++++ components/support/encryption/src/lib.rs | 1 + 5 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 components/support/encryption/build.rs create mode 100644 components/support/encryption/src/encryption.udl diff --git a/components/support/encryption/Cargo.toml b/components/support/encryption/Cargo.toml index 20dd7715455..d008b53cb2c 100644 --- a/components/support/encryption/Cargo.toml +++ b/components/support/encryption/Cargo.toml @@ -28,4 +28,4 @@ serde_json = "1" thiserror = "2" [build-dependencies] -uniffi = { version = "0.31", features = ["build"] } \ No newline at end of file +uniffi = { version = "0.31", features = ["build"] } diff --git a/components/support/encryption/build.rs b/components/support/encryption/build.rs new file mode 100644 index 00000000000..523c9cbf2aa --- /dev/null +++ b/components/support/encryption/build.rs @@ -0,0 +1,8 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +fn main() { + uniffi::generate_scaffolding("./src/encryption.udl").unwrap(); +} diff --git a/components/support/encryption/src/encryption.rs b/components/support/encryption/src/encryption.rs index 0f4d75c72e2..0199df1aa62 100644 --- a/components/support/encryption/src/encryption.rs +++ b/components/support/encryption/src/encryption.rs @@ -72,6 +72,7 @@ use nss_as::pk11::sym_key::{ /// Note that EncryptorDecryptor must not call any LoginStore methods. The login store can call out /// to the EncryptorDecryptor when it's internal mutex is held so calling back in to the LoginStore /// may deadlock. +#[uniffi::trait_interface] pub trait EncryptorDecryptor: Send + Sync { fn encrypt(&self, cleartext: Vec) -> ApiResult>; fn decrypt(&self, ciphertext: Vec) -> ApiResult>; @@ -94,6 +95,7 @@ pub struct ManagedEncryptorDecryptor { } impl ManagedEncryptorDecryptor { + #[uniffi::constructor()] pub fn new(key_manager: Arc) -> Self { Self { key_manager } } @@ -151,6 +153,7 @@ impl EncryptorDecryptor for ManagedEncryptorDecryptor { /// Consumers can implement the KeyManager in combination with the ManagedEncryptorDecryptor to hand /// over the encryption key whenever encryption or decryption happens. +#[uniffi::trait_interface] pub trait KeyManager: Send + Sync { fn get_key(&self) -> ApiResult>; } @@ -177,6 +180,7 @@ impl KeyManager for StaticKeyManager { /// `PrimaryPasswordAuthenticator` is used in conjunction with `NSSKeyManager` to provide the /// primary password and the success or failure actions of the authentication process. #[cfg(feature = "keydb")] +#[uniffi::export(with_foreign)] #[async_trait] pub trait PrimaryPasswordAuthenticator: Send + Sync { /// Get a primary password for authentication, otherwise return the @@ -227,16 +231,19 @@ pub trait PrimaryPasswordAuthenticator: Send + Sync { /// assert_eq!(key_manager.get_key().unwrap().len(), 63); /// ``` #[cfg(feature = "keydb")] +#[derive(uniffi::Object)] pub struct NSSKeyManager { key_name: String, primary_password_authenticator: Arc, } #[cfg(feature = "keydb")] +#[uniffi::export] impl NSSKeyManager { /// Initialize new `NSSKeyManager` with a given `PrimaryPasswordAuthenticator`. /// There must be a previous initializiation of NSS before initializing /// `NSSKeyManager`, otherwise this panics. + #[uniffi::constructor()] pub fn new(key_name: &str, primary_password_authenticator: Arc) -> Self { assert_nss_initialized(); Self { diff --git a/components/support/encryption/src/encryption.udl b/components/support/encryption/src/encryption.udl new file mode 100644 index 00000000000..6db7c4fd9ba --- /dev/null +++ b/components/support/encryption/src/encryption.udl @@ -0,0 +1,81 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +namespace encryption { + /// We expose the crypto primitives on the namespace + + /// Create a new, random, encryption key. + [Throws=EncryptionApiError] + string create_key(); + + /// Create a "canary" string, which can be used to test if the encryption + //key is still valid for the logins data + [Throws=EncryptionApiError] + string create_canary([ByRef]string text, [ByRef]string encryption_key); + + /// Check that key is still valid using the output of `create_canary`. + //`text` much match the text you initially passed to `create_canary()` + [Throws=EncryptionApiError] + boolean check_canary([ByRef]string canary, [ByRef]string text, [ByRef]string encryption_key); +}; + +/// These are the errors returned by our public API. +[Error] +interface EncryptionApiError { + /// NSS not initialized. + NSSUninitialized(); + + /// NSS error during authentication + NSSAuthenticationError(string reason); + + /// error during authentication (in PrimaryPasswordAuthenticator) + AuthenticationError(string reason); + + /// authentication has been cancelled. + AuthenticationCanceled(); + + /// Encryption key is missing. + MissingKey(); + + /// Encryption key is not valid. + InvalidKey(); + + /// encryption failed + EncryptionFailed(string reason); + + /// decryption failed + DecryptionFailed(string reason); + + /// An operation was interrupted at the request of the consuming app. + Interrupted(string reason); + + /// something internal went wrong which doesn't have a public error value + /// because the consuming app can not reasonably take any action to resolve it. + /// The underlying error will have been logged and reported. + /// (ideally would just be `Unexpected`, but that would be a breaking change) + UnexpectedEncryptionApiError(string reason); +}; + +[Trait, WithForeign] +interface EncryptorDecryptor { + [Throws=EncryptionApiError] + bytes encrypt(bytes cleartext); + + [Throws=EncryptionApiError] + bytes decrypt(bytes ciphertext); +}; + +[Trait, WithForeign] +interface KeyManager { + [Throws=EncryptionApiError] + bytes get_key(); +}; + +interface StaticKeyManager { + constructor(string key); +}; + +interface ManagedEncryptorDecryptor { + constructor(KeyManager key_manager); +}; diff --git a/components/support/encryption/src/lib.rs b/components/support/encryption/src/lib.rs index 8ef600cd941..1b350f79ffa 100644 --- a/components/support/encryption/src/lib.rs +++ b/components/support/encryption/src/lib.rs @@ -13,6 +13,7 @@ mod encryption; pub use crate::encryption::{ EncryptorDecryptor, KeyManager, ManagedEncryptorDecryptor, StaticKeyManager, }; +uniffi::include_scaffolding!("encryption"); #[cfg(feature = "keydb")] pub use crate::encryption::{NSSKeyManager, PrimaryPasswordAuthenticator}; From af37a09a860ee147435edc7a6fda274b99c289a0 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 12 Aug 2026 15:45:12 -0700 Subject: [PATCH 05/32] Drop logins/src/encryption.rs in favor of new crate --- components/logins/src/db.rs | 2 +- components/logins/src/encryption.rs | 561 ------------------ components/logins/src/lib.rs | 33 +- components/logins/src/login.rs | 2 +- components/logins/src/logins.udl | 28 +- components/logins/src/store.rs | 8 +- .../support/encryption/src/encryption.rs | 61 +- examples/sync-pass/src/sync-pass.rs | 2 +- 8 files changed, 97 insertions(+), 600 deletions(-) delete mode 100644 components/logins/src/encryption.rs diff --git a/components/logins/src/db.rs b/components/logins/src/db.rs index 89ccec39c05..7486f34324f 100644 --- a/components/logins/src/db.rs +++ b/components/logins/src/db.rs @@ -85,7 +85,7 @@ impl LoginDb { #[cfg(test)] pub fn open_in_memory() -> Self { let encdec: Arc = - crate::encryption::test_utils::TEST_ENCDEC.clone(); + crate::test_utils::TEST_ENCDEC.clone(); Self::with_connection(Connection::open_in_memory().unwrap(), encdec).unwrap() } diff --git a/components/logins/src/encryption.rs b/components/logins/src/encryption.rs deleted file mode 100644 index 3ca528951e5..00000000000 --- a/components/logins/src/encryption.rs +++ /dev/null @@ -1,561 +0,0 @@ -/* 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 - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -// This is the *local* encryption support - it has nothing to do with the -// encryption used by sync. - -// For context, what "local encryption" means in this context is: -// * We use regular sqlite, but ensure that sensitive data is encrypted in the DB in the -// `secure_fields` column. The encryption key is managed by the app. -// * The `decrypt_struct` and `encrypt_struct` functions are used to convert between an encrypted -// `secure_fields` string and a decrypted `SecureFields` struct -// * Most API functions return `EncryptedLogin` which has its data encrypted. -// -// This makes life tricky for Sync - sync has its own encryption and its own -// management of sync keys. The entire records are encrypted on the server - -// so the record on the server has the plain-text data (which is then -// encrypted as part of the entire record), so: -// * When transforming a record from the DB into a Sync record, we need to -// *decrypt* the data. -// * When transforming a record from Sync into a DB record, we need to *encrypt* -// the data. -// -// So Sync needs to know the key etc, and that needs to get passed down -// multiple layers, from the app saying "sync now" all the way down to the -// low level sync code. -// To make life a little easier, we do that via a struct. -// -// Consumers of the Login component have 3 options for setting up encryption: -// 1. Implement EncryptorDecryptor directly -// eg `LoginStore::new(MyEncryptorDecryptor)` -// 2. Implement KeyManager and use ManagedEncryptorDecryptor -// eg `LoginStore::new(ManagedEncryptorDecryptor::new(MyKeyManager))` -// 3. Generate a single key and create a StaticKeyManager and use it together with -// ManagedEncryptorDecryptor -// eg `LoginStore::new(ManagedEncryptorDecryptor::new(StaticKeyManager { key: myKey }))` -// -// You can implement EncryptorDecryptor directly to keep full control over the encryption -// algorithm. For example, on the desktop, this could make use of NSS's SecretDecoderRing to -// achieve transparent key management. -// -// If the application wants to keep the current encryption, like Android and iOS, for example, but -// control the key management itself, the KeyManager can be implemented and the encryption can be -// done on the Rust side with the ManagedEncryptorDecryptor. -// -// In tests or some command line tools, it can be practical to use a static key that does not -// change at runtime and is already present when the LoginsStore is initialized. In this case, it -// makes sense to use the provided StaticKeyManager. - -// work around not yet having https://github.com/mozilla/uniffi-rs/pull/2963. -#![allow(const_evaluatable_unchecked)] - -use crate::error::*; -use std::sync::Arc; - -#[cfg(feature = "keydb")] -use futures::executor::block_on; - -#[cfg(feature = "keydb")] -use async_trait::async_trait; - -#[cfg(feature = "keydb")] -use parking_lot::RwLock; - -#[cfg(feature = "keydb")] -use nss_as::assert_initialized as assert_nss_initialized; -#[cfg(feature = "keydb")] -use nss_as::pk11::sym_key::{ - authenticate_with_primary_password, authentication_with_primary_password_is_needed, - get_or_create_aes256_key, -}; - -/// This is the generic EncryptorDecryptor trait, as handed over to the Store during initialization. -/// Consumers can implement either this generic trait and bring in their own crypto, or leverage the -/// ManagedEncryptorDecryptor below, which provides encryption algorithms out of the box. -/// -/// Note that EncryptorDecryptor must not call any LoginStore methods. The login store can call out -/// to the EncryptorDecryptor when it's internal mutex is held so calling back in to the LoginStore -/// may deadlock. -#[uniffi::trait_interface] -pub trait EncryptorDecryptor: Send + Sync { - fn encrypt(&self, cleartext: Vec) -> ApiResult>; - fn decrypt(&self, ciphertext: Vec) -> ApiResult>; -} - -impl EncryptorDecryptor for Arc { - fn encrypt(&self, clearbytes: Vec) -> ApiResult> { - (**self).encrypt(clearbytes) - } - - fn decrypt(&self, cipherbytes: Vec) -> ApiResult> { - (**self).decrypt(cipherbytes) - } -} - -/// The ManagedEncryptorDecryptor makes use of the NSS provided cryptographic algorithms. The -/// ManagedEncryptorDecryptor uses a KeyManager for encryption key retrieval. -pub struct ManagedEncryptorDecryptor { - key_manager: Arc, -} - -impl ManagedEncryptorDecryptor { - #[uniffi::constructor()] - pub fn new(key_manager: Arc) -> Self { - Self { key_manager } - } -} - -impl EncryptorDecryptor for ManagedEncryptorDecryptor { - fn encrypt(&self, clearbytes: Vec) -> ApiResult> { - let keybytes = self - .key_manager - .get_key() - .map_err(|_| LoginsApiError::MissingKey)?; - let key = std::str::from_utf8(&keybytes).map_err(|_| LoginsApiError::InvalidKey)?; - - let encdec = jwcrypto::EncryptorDecryptor::new(key) - .map_err(|_: jwcrypto::JwCryptoError| LoginsApiError::InvalidKey)?; - - let cleartext = - std::str::from_utf8(&clearbytes).map_err(|e| LoginsApiError::EncryptionFailed { - reason: e.to_string(), - })?; - encdec - .encrypt(cleartext) - .map_err( - |e: jwcrypto::JwCryptoError| LoginsApiError::EncryptionFailed { - reason: e.to_string(), - }, - ) - .map(|text| text.into()) - } - - fn decrypt(&self, cipherbytes: Vec) -> ApiResult> { - let keybytes = self - .key_manager - .get_key() - .map_err(|_| LoginsApiError::MissingKey)?; - let key = std::str::from_utf8(&keybytes).map_err(|_| LoginsApiError::InvalidKey)?; - - let encdec = jwcrypto::EncryptorDecryptor::new(key) - .map_err(|_: jwcrypto::JwCryptoError| LoginsApiError::InvalidKey)?; - - let ciphertext = - std::str::from_utf8(&cipherbytes).map_err(|e| LoginsApiError::DecryptionFailed { - reason: e.to_string(), - })?; - encdec - .decrypt(ciphertext) - .map_err( - |e: jwcrypto::JwCryptoError| LoginsApiError::DecryptionFailed { - reason: e.to_string(), - }, - ) - .map(|text| text.into()) - } -} - -/// Consumers can implement the KeyManager in combination with the ManagedEncryptorDecryptor to hand -/// over the encryption key whenever encryption or decryption happens. -#[uniffi::trait_interface] -pub trait KeyManager: Send + Sync { - fn get_key(&self) -> ApiResult>; -} - -/// Last but not least we provide a StaticKeyManager, which can be -/// used in cases where there is a single key during runtime, for example in tests. -pub struct StaticKeyManager { - key: String, -} - -impl StaticKeyManager { - pub fn new(key: String) -> Self { - Self { key } - } -} - -impl KeyManager for StaticKeyManager { - #[handle_error(Error)] - fn get_key(&self) -> ApiResult> { - Ok(self.key.as_bytes().into()) - } -} - -/// `PrimaryPasswordAuthenticator` is used in conjunction with `NSSKeyManager` to provide the -/// primary password and the success or failure actions of the authentication process. -#[cfg(feature = "keydb")] -#[uniffi::export(with_foreign)] -#[async_trait] -pub trait PrimaryPasswordAuthenticator: Send + Sync { - /// Get a primary password for authentication, otherwise return the - /// AuthenticationCancelled error to cancel the authentication process. - async fn get_primary_password(&self) -> ApiResult; - async fn on_authentication_success(&self) -> ApiResult<()>; - async fn on_authentication_failure(&self) -> ApiResult<()>; -} - -/// Use the `NSSKeyManager` to use NSS for key management. -/// -/// NSS stores keys in `key4.db` within the profile and wraps the key with a key derived from the -/// primary password, if set. It defers to the provided `PrimaryPasswordAuthenticator` -/// implementation to handle user authentication. Note that if no primary password is set, the -/// wrapping key is deterministically derived from an empty string. -/// -/// Make sure to initialize NSS using `ensure_initialized_with_profile_dir` before creating a -/// NSSKeyManager. -/// -/// The key is cached after the first retrieval, since fetching it from NSS costs at least one -/// token round-trip. The cache is dropped whenever the token turns out to be locked again. -/// -/// # Examples -/// ```no_run -/// use async_trait::async_trait; -/// use logins::encryption::KeyManager; -/// use logins::{PrimaryPasswordAuthenticator, LoginsApiError, NSSKeyManager}; -/// use std::sync::Arc; -/// -/// struct MyPrimaryPasswordAuthenticator {} -/// -/// #[async_trait] -/// impl PrimaryPasswordAuthenticator for MyPrimaryPasswordAuthenticator { -/// async fn get_primary_password(&self) -> Result { -/// // Most likely, you would want to prompt for a password. -/// // let password = prompt_string("primary password").unwrap_or_default(); -/// Ok("secret".to_string()) -/// } -/// -/// async fn on_authentication_success(&self) -> Result<(), LoginsApiError> { -/// println!("success"); -/// Ok(()) -/// } -/// -/// async fn on_authentication_failure(&self) -> Result<(), LoginsApiError> { -/// println!("this did not work, please try again:"); -/// Ok(()) -/// } -/// } -/// let key_manager = NSSKeyManager::new(Arc::new(MyPrimaryPasswordAuthenticator {})); -/// assert_eq!(key_manager.get_key().unwrap().len(), 63); -/// ``` -#[cfg(feature = "keydb")] -#[derive(uniffi::Object)] -pub struct NSSKeyManager { - primary_password_authenticator: Arc, - cached_key: RwLock>>, -} - -#[cfg(feature = "keydb")] -#[uniffi::export] -impl NSSKeyManager { - /// Initialize new `NSSKeyManager` with a given `PrimaryPasswordAuthenticator`. - /// There must be a previous initializiation of NSS before initializing - /// `NSSKeyManager`, otherwise this panics. - #[uniffi::constructor()] - pub fn new(primary_password_authenticator: Arc) -> Self { - assert_nss_initialized(); - Self { - primary_password_authenticator, - cached_key: RwLock::new(None), - } - } - - pub fn into_dyn_key_manager(self: Arc) -> Arc { - self - } -} - -/// Identifier for the logins key, under which the key is stored in NSS. -#[cfg(feature = "keydb")] -static KEY_NAME: &str = "as-logins-key"; - -// wrapp `authentication_with_primary_password_is_needed` into an ApiResult -#[cfg(feature = "keydb")] -fn api_authentication_with_primary_password_is_needed() -> ApiResult { - authentication_with_primary_password_is_needed().map_err(|e: nss_as::Error| { - LoginsApiError::NSSAuthenticationError { - reason: e.to_string(), - } - }) -} - -// wrapp `authenticate_with_primary_password` into an ApiResult -#[cfg(feature = "keydb")] -fn api_authenticate_with_primary_password(primary_password: &str) -> ApiResult { - authenticate_with_primary_password(primary_password).map_err(|e: nss_as::Error| { - LoginsApiError::NSSAuthenticationError { - reason: e.to_string(), - } - }) -} - -#[cfg(feature = "keydb")] -impl KeyManager for NSSKeyManager { - fn get_key(&self) -> ApiResult> { - if api_authentication_with_primary_password_is_needed()? { - // The token locked again since we cached the key, so the cached copy must go. - *self.cached_key.write() = None; - - let primary_password = - block_on(self.primary_password_authenticator.get_primary_password())?; - let mut result = api_authenticate_with_primary_password(&primary_password)?; - - if result { - block_on( - self.primary_password_authenticator - .on_authentication_success(), - )?; - } else { - while !result { - block_on( - self.primary_password_authenticator - .on_authentication_failure(), - )?; - - let primary_password = - block_on(self.primary_password_authenticator.get_primary_password())?; - result = api_authenticate_with_primary_password(&primary_password)?; - } - block_on( - self.primary_password_authenticator - .on_authentication_success(), - )?; - } - } - - let cached = self.cached_key.read().clone(); - if let Some(bytes) = cached { - return Ok(bytes); - } - - let key = get_or_create_aes256_key(KEY_NAME).map_err(|_| LoginsApiError::MissingKey)?; - let mut bytes: Vec = Vec::new(); - serde_json::to_writer( - &mut bytes, - &jwcrypto::Jwk::new_direct_from_bytes(None, &key), - ) - .unwrap(); - *self.cached_key.write() = Some(bytes.clone()); - Ok(bytes) - } -} - -#[handle_error(Error)] -pub fn create_canary(text: &str, key: &str) -> ApiResult { - Ok(encryption::create_canary(text, key)?) -} - -pub fn check_canary(canary: &str, text: &str, key: &str) -> ApiResult { - Ok(encryption::check_canary(canary, text, key)?) -} - -#[handle_error(Error)] -pub fn create_key() -> ApiResult { - Ok(encryption::create_key()?) -} - -#[cfg(test)] -pub mod test_utils { - use super::*; - use serde::{de::DeserializeOwned, Serialize}; - - lazy_static::lazy_static! { - pub static ref TEST_ENCRYPTION_KEY: String = serde_json::to_string(&jwcrypto::Jwk::new_direct_key(Some("test-key".to_string())).unwrap()).unwrap(); - pub static ref TEST_ENCDEC: Arc = Arc::new(ManagedEncryptorDecryptor::new(Arc::new(StaticKeyManager { key: TEST_ENCRYPTION_KEY.clone() }))); - } - - pub fn encrypt_struct(fields: &T) -> String { - let string = serde_json::to_string(fields).unwrap(); - let cipherbytes = TEST_ENCDEC.encrypt(string.as_bytes().into()).unwrap(); - std::str::from_utf8(&cipherbytes).unwrap().to_owned() - } - pub fn decrypt_struct(ciphertext: String) -> T { - let jsonbytes = TEST_ENCDEC.decrypt(ciphertext.as_bytes().into()).unwrap(); - serde_json::from_str(std::str::from_utf8(&jsonbytes).unwrap()).unwrap() - } -} - -#[cfg(not(feature = "keydb"))] -#[cfg(test)] -mod tests { - use super::*; - use nss_as::ensure_initialized; - - #[test] - fn test_static_key_manager() { - ensure_initialized(); - let key = create_key().unwrap(); - let key_manager = StaticKeyManager { key: key.clone() }; - assert_eq!(key.as_bytes(), key_manager.get_key().unwrap()); - } - - #[test] - fn test_managed_encdec_with_invalid_key() { - ensure_initialized(); - let key_manager = Arc::new(StaticKeyManager { - key: "bad_key".to_owned(), - }); - let encdec = ManagedEncryptorDecryptor { key_manager }; - assert!(matches!( - encdec.encrypt("secret".as_bytes().into()).err().unwrap(), - LoginsApiError::InvalidKey - )); - } - - #[test] - fn test_managed_encdec_with_missing_key() { - ensure_initialized(); - struct MyKeyManager {} - impl KeyManager for MyKeyManager { - fn get_key(&self) -> ApiResult> { - Err(LoginsApiError::MissingKey) - } - } - let key_manager = Arc::new(MyKeyManager {}); - let encdec = ManagedEncryptorDecryptor { key_manager }; - assert!(matches!( - encdec.encrypt("secret".as_bytes().into()).err().unwrap(), - LoginsApiError::MissingKey - )); - } - - #[test] - fn test_managed_encdec() { - ensure_initialized(); - let key = create_key().unwrap(); - let key_manager = Arc::new(StaticKeyManager { key }); - let encdec = ManagedEncryptorDecryptor { key_manager }; - let cleartext = "secret"; - let ciphertext = encdec.encrypt(cleartext.as_bytes().into()).unwrap(); - assert_eq!( - encdec.decrypt(ciphertext.clone()).unwrap(), - cleartext.as_bytes() - ); - let other_encdec = ManagedEncryptorDecryptor { - key_manager: Arc::new(StaticKeyManager { - key: create_key().unwrap(), - }), - }; - - assert_eq!( - other_encdec.decrypt(ciphertext).err().unwrap().to_string(), - "decryption failed: Crypto error: NSS error: NSS error: -8190 " - ); - } - - #[test] - fn test_key_error() { - let storage_err = jwcrypto::EncryptorDecryptor::new("bad-key").err().unwrap(); - println!("{storage_err:?}"); - assert!(matches!(storage_err, jwcrypto::JwCryptoError::InvalidKey)); - } - - #[test] - fn test_canary_functionality() { - ensure_initialized(); - const CANARY_TEXT: &str = "Arbitrary sequence of text"; - let key = create_key().unwrap(); - let canary = create_canary(CANARY_TEXT, &key).unwrap(); - assert!(check_canary(&canary, CANARY_TEXT, &key).unwrap()); - - let different_key = create_key().unwrap(); - assert!(!check_canary(&canary, CANARY_TEXT, &different_key).unwrap()); - - let bad_key = "bad_key".to_owned(); - assert!(matches!( - check_canary(&canary, CANARY_TEXT, &bad_key).err().unwrap(), - LoginsApiError::InvalidKey - )); - } -} - -#[cfg(feature = "keydb")] -#[cfg(test)] -mod tests_keydb { - use super::*; - use nss_as::ensure_initialized_with_profile_dir; - use std::path::PathBuf; - - struct MockPrimaryPasswordAuthenticator { - password: String, - } - - #[async_trait] - impl PrimaryPasswordAuthenticator for MockPrimaryPasswordAuthenticator { - async fn get_primary_password(&self) -> ApiResult { - Ok(self.password.clone()) - } - async fn on_authentication_success(&self) -> ApiResult<()> { - Ok(()) - } - async fn on_authentication_failure(&self) -> ApiResult<()> { - Ok(()) - } - } - - fn profile_path() -> PathBuf { - std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../support/rc_crypto/nss/fixtures/profile") - } - - #[test] - fn test_ensure_initialized_with_profile_dir() { - ensure_initialized_with_profile_dir(profile_path()); - } - - #[test] - fn test_create_key() { - ensure_initialized_with_profile_dir(profile_path()); - let key = create_key().unwrap(); - assert_eq!(key.len(), 63) - } - - #[test] - fn test_nss_key_manager() { - ensure_initialized_with_profile_dir(profile_path()); - // `password` is the primary password of the profile fixture - let mock_primary_password_authenticator = MockPrimaryPasswordAuthenticator { - password: "password".to_string(), - }; - let nss_key_manager = NSSKeyManager::new(Arc::new(mock_primary_password_authenticator)); - // key from fixtures/profile/key4.db - let expected = [ - 123, 34, 107, 116, 121, 34, 58, 34, 111, 99, 116, 34, 44, 34, 107, 34, 58, 34, 66, 74, - 104, 84, 108, 103, 51, 118, 56, 49, 65, 66, 51, 118, 87, 50, 71, 122, 54, 104, 69, 54, - 84, 116, 75, 83, 112, 85, 102, 84, 86, 75, 73, 83, 99, 74, 45, 77, 78, 83, 67, 117, 99, - 34, 125, - ] - .to_vec(); - assert_eq!(nss_key_manager.get_key().unwrap(), expected); - } - - #[test] - fn test_nss_key_manager_caching() { - ensure_initialized_with_profile_dir(profile_path()); - // `password` is the primary password of the profile fixture - let nss_key_manager = NSSKeyManager::new(Arc::new(MockPrimaryPasswordAuthenticator { - password: "password".to_string(), - })); - - let key = nss_key_manager.get_key().unwrap(); - assert_eq!(*nss_key_manager.cached_key.read(), Some(key.clone())); - - // A sentinel in the cache tells a cached key apart from a freshly fetched one. - let sentinel = b"sentinel".to_vec(); - *nss_key_manager.cached_key.write() = Some(sentinel.clone()); - assert_eq!(nss_key_manager.get_key().unwrap(), sentinel); - - // Authenticating with a wrong password logs out of the token, so it is locked again and - // the cache must be dropped. - assert!(!authenticate_with_primary_password("wrong password").unwrap()); - assert_eq!(nss_key_manager.get_key().unwrap(), key); - assert_eq!(*nss_key_manager.cached_key.read(), Some(key)); - } - - #[test] - fn test_primary_password_authentication() { - ensure_initialized_with_profile_dir(profile_path()); - assert!(authenticate_with_primary_password("password").unwrap()); - } -} diff --git a/components/logins/src/lib.rs b/components/logins/src/lib.rs index cd430b4e131..3e75724c4c9 100644 --- a/components/logins/src/lib.rs +++ b/components/logins/src/lib.rs @@ -10,19 +10,19 @@ mod error; mod login; mod db; -pub mod encryption; +pub use encryption; mod schema; mod store; mod sync; mod util; -use crate::encryption::{ +use encryption::{ EncryptorDecryptor, KeyManager, ManagedEncryptorDecryptor, StaticKeyManager, }; uniffi::include_scaffolding!("logins"); #[cfg(feature = "keydb")] -pub use crate::encryption::{NSSKeyManager, PrimaryPasswordAuthenticator}; +pub use encryption::{NSSKeyManager, PrimaryPasswordAuthenticator}; pub use crate::db::{LoginDb, LoginsDeletionMetrics}; use crate::encryption::{check_canary, create_canary, create_key}; @@ -32,6 +32,10 @@ pub use crate::store::*; pub use crate::sync::{LoginsBridgedEngine, LoginsSyncEngine}; use std::sync::Arc; +/// Identifier for the logins key, under which the key is stored in NSS. +#[cfg(feature = "keydb")] +static KEY_NAME: &str = "as-logins-key"; + // Utility function to create a StaticKeyManager to be used for the time being until support lands // for [trait implementation of an UniFFI // interface](https://mozilla.github.io/uniffi-rs/next/proc_macro/index.html#structs-implementing-traits) @@ -68,8 +72,29 @@ pub fn create_login_store_with_nss_keymanager( primary_password_authenticator: Arc, ) -> ApiResult> { let encdec: ManagedEncryptorDecryptor = ManagedEncryptorDecryptor::new(Arc::new( - NSSKeyManager::new(primary_password_authenticator), + NSSKeyManager::new(KEY_NAME, primary_password_authenticator), )); let store = LoginStore::new(path, Arc::new(encdec))?; Ok(Arc::new(store)) } + +#[cfg(test)] +pub mod test_utils { + use super::*; + use serde::{de::DeserializeOwned, Serialize}; + + lazy_static::lazy_static! { + pub static ref TEST_ENCRYPTION_KEY: String = serde_json::to_string(&jwcrypto::Jwk::new_direct_key(Some("test-key".to_string())).unwrap()).unwrap(); + pub static ref TEST_ENCDEC: Arc = Arc::new(ManagedEncryptorDecryptor::new(Arc::new(StaticKeyManager::new(TEST_ENCRYPTION_KEY.clone())))); + } + + pub fn encrypt_struct(fields: &T) -> String { + let string = serde_json::to_string(fields).unwrap(); + let cipherbytes = TEST_ENCDEC.encrypt(string.as_bytes().into()).unwrap(); + std::str::from_utf8(&cipherbytes).unwrap().to_owned() + } + pub fn decrypt_struct(ciphertext: String) -> T { + let jsonbytes = TEST_ENCDEC.decrypt(ciphertext.as_bytes().into()).unwrap(); + serde_json::from_str(std::str::from_utf8(&jsonbytes).unwrap()).unwrap() + } +} diff --git a/components/logins/src/login.rs b/components/logins/src/login.rs index f7e20eff805..55e9152d772 100644 --- a/components/logins/src/login.rs +++ b/components/logins/src/login.rs @@ -964,7 +964,7 @@ impl ValidateAndFixup for LoginEntry { #[cfg(test)] pub mod test_utils { use super::*; - use crate::encryption::test_utils::encrypt_struct; + use crate::test_utils::encrypt_struct; // Factory function to make a new login // diff --git a/components/logins/src/logins.udl b/components/logins/src/logins.udl index 8f0492d945b..88276c7abcf 100644 --- a/components/logins/src/logins.udl +++ b/components/logins/src/logins.udl @@ -170,28 +170,20 @@ interface LoginsApiError { UnexpectedLoginsApiError(string reason); }; -[Trait, WithForeign] -interface EncryptorDecryptor { - [Throws=LoginsApiError] - bytes encrypt(bytes cleartext); +[External = "encryption"] +typedef interface EncryptionApiError; - [Throws=LoginsApiError] - bytes decrypt(bytes ciphertext); -}; +[External = "encryption"] +typedef trait EncryptorDecryptor; -[Trait, WithForeign] -interface KeyManager { - [Throws=LoginsApiError] - bytes get_key(); -}; +[External = "encryption"] +typedef trait KeyManager; -interface StaticKeyManager { - constructor(string key); -}; +[External = "encryption"] +typedef interface StaticKeyManager; -interface ManagedEncryptorDecryptor { - constructor(KeyManager key_manager); -}; +[External = "encryption"] +typedef interface ManagedEncryptorDecryptor; interface LoginStore { [Throws=LoginsApiError] diff --git a/components/logins/src/store.rs b/components/logins/src/store.rs index 0854c12e582..6cab0b32e96 100644 --- a/components/logins/src/store.rs +++ b/components/logins/src/store.rs @@ -768,13 +768,13 @@ mod tests_keydb { #[async_trait] impl PrimaryPasswordAuthenticator for MockPrimaryPasswordAuthenticator { - async fn get_primary_password(&self) -> ApiResult { + async fn get_primary_password(&self) -> encryption::ApiResult { Ok(self.password.clone()) } - async fn on_authentication_success(&self) -> ApiResult<()> { + async fn on_authentication_success(&self) -> encryption::ApiResult<()> { Ok(()) } - async fn on_authentication_failure(&self) -> ApiResult<()> { + async fn on_authentication_failure(&self) -> encryption::ApiResult<()> { Ok(()) } } @@ -792,7 +792,7 @@ mod tests_keydb { let primary_password_authenticator = MockPrimaryPasswordAuthenticator { password: "password".to_string(), }; - let key_manager = NSSKeyManager::new(Arc::new(primary_password_authenticator)); + let key_manager = NSSKeyManager::new(crate::KEY_NAME, Arc::new(primary_password_authenticator)); let encdec = ManagedEncryptorDecryptor::new(Arc::new(key_manager)); let store = LoginStore::new(profile_path().join("logins.db"), Arc::new(encdec)) .expect("store from fixtures"); diff --git a/components/support/encryption/src/encryption.rs b/components/support/encryption/src/encryption.rs index 0199df1aa62..976de0fbd73 100644 --- a/components/support/encryption/src/encryption.rs +++ b/components/support/encryption/src/encryption.rs @@ -48,6 +48,9 @@ // change at runtime and is already present when the LoginsStore is initialized. In this case, it // makes sense to use the provided StaticKeyManager. +// work around not yet having https://github.com/mozilla/uniffi-rs/pull/2963. +#![allow(const_evaluatable_unchecked)] + use crate::error::*; use std::sync::Arc; @@ -57,6 +60,9 @@ use futures::executor::block_on; #[cfg(feature = "keydb")] use async_trait::async_trait; +#[cfg(feature = "keydb")] +use parking_lot::RwLock; + #[cfg(feature = "keydb")] use nss_as::assert_initialized as assert_nss_initialized; #[cfg(feature = "keydb")] @@ -200,6 +206,9 @@ pub trait PrimaryPasswordAuthenticator: Send + Sync { /// Make sure to initialize NSS using `ensure_initialized_with_profile_dir` before creating a /// NSSKeyManager. /// +/// The key is cached after the first retrieval, since fetching it from NSS costs at least one +/// token round-trip. The cache is dropped whenever the token turns out to be locked again. +/// /// # Examples /// ```no_run /// use async_trait::async_trait; @@ -235,6 +244,7 @@ pub trait PrimaryPasswordAuthenticator: Send + Sync { pub struct NSSKeyManager { key_name: String, primary_password_authenticator: Arc, + cached_key: RwLock>>, } #[cfg(feature = "keydb")] @@ -249,6 +259,7 @@ impl NSSKeyManager { Self { key_name: key_name.to_string(), primary_password_authenticator, + cached_key: RwLock::new(None), } } @@ -281,6 +292,9 @@ fn api_authenticate_with_primary_password(primary_password: &str) -> ApiResult ApiResult> { if api_authentication_with_primary_password_is_needed()? { + // The token locked again since we cached the key, so the cached copy must go. + *self.cached_key.write() = None; + let primary_password = block_on(self.primary_password_authenticator.get_primary_password())?; let mut result = api_authenticate_with_primary_password(&primary_password)?; @@ -308,6 +322,11 @@ impl KeyManager for NSSKeyManager { } } + let cached = self.cached_key.read().clone(); + if let Some(bytes) = cached { + return Ok(bytes); + } + let key = get_or_create_aes256_key(self.key_name.as_str()).map_err(|_| EncryptionApiError::MissingKey)?; let mut bytes: Vec = Vec::new(); serde_json::to_writer( @@ -315,6 +334,7 @@ impl KeyManager for NSSKeyManager { &jwcrypto::Jwk::new_direct_from_bytes(None, &key), ) .unwrap(); + *self.cached_key.write() = Some(bytes.clone()); Ok(bytes) } } @@ -503,16 +523,37 @@ mod tests_keydb { primary_password_authenticator: Arc::new(mock_primary_password_authenticator), }; // key from fixtures/profile/key4.db - assert_eq!( - nss_key_manager.get_key().unwrap(), - [ - 123, 34, 107, 116, 121, 34, 58, 34, 111, 99, 116, 34, 44, 34, 107, 34, 58, 34, 66, - 74, 104, 84, 108, 103, 51, 118, 56, 49, 65, 66, 51, 118, 87, 50, 71, 122, 54, 104, - 69, 54, 84, 116, 75, 83, 112, 85, 102, 84, 86, 75, 73, 83, 99, 74, 45, 77, 78, 83, - 67, 117, 99, 34, 125 - ] - .to_vec() - ) + let expected = [ + 123, 34, 107, 116, 121, 34, 58, 34, 111, 99, 116, 34, 44, 34, 107, 34, 58, 34, 66, 74, + 104, 84, 108, 103, 51, 118, 56, 49, 65, 66, 51, 118, 87, 50, 71, 122, 54, 104, 69, 54, + 84, 116, 75, 83, 112, 85, 102, 84, 86, 75, 73, 83, 99, 74, 45, 77, 78, 83, 67, 117, 99, + 34, 125, + ] + .to_vec(); + assert_eq!(nss_key_manager.get_key().unwrap(), expected); + } + + #[test] + fn test_nss_key_manager_caching() { + ensure_initialized_with_profile_dir(profile_path()); + // `password` is the primary password of the profile fixture + let nss_key_manager = NSSKeyManager::new(Arc::new(MockPrimaryPasswordAuthenticator { + password: "password".to_string(), + })); + + let key = nss_key_manager.get_key().unwrap(); + assert_eq!(*nss_key_manager.cached_key.read(), Some(key.clone())); + + // A sentinel in the cache tells a cached key apart from a freshly fetched one. + let sentinel = b"sentinel".to_vec(); + *nss_key_manager.cached_key.write() = Some(sentinel.clone()); + assert_eq!(nss_key_manager.get_key().unwrap(), sentinel); + + // Authenticating with a wrong password logs out of the token, so it is locked again and + // the cache must be dropped. + assert!(!authenticate_with_primary_password("wrong password").unwrap()); + assert_eq!(nss_key_manager.get_key().unwrap(), key); + assert_eq!(*nss_key_manager.cached_key.read(), Some(key)); } #[test] diff --git a/examples/sync-pass/src/sync-pass.rs b/examples/sync-pass/src/sync-pass.rs index d27b4ad4385..3c27cb8c1ac 100644 --- a/examples/sync-pass/src/sync-pass.rs +++ b/examples/sync-pass/src/sync-pass.rs @@ -7,7 +7,7 @@ use cli_support::fxa_creds::{get_default_fxa_config, CliFxa, SYNC_SCOPE}; use cli_support::prompt::{prompt_char, prompt_password, prompt_string, prompt_usize}; -use logins::encryption::{ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator}; +use encryption::{ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator}; use logins::{Login, LoginEntry, LoginStore, LoginsApiError, LoginsSyncEngine, ValidateAndFixup}; use async_trait::async_trait; From 53f7f64f1b111e7d62dd75ff5b133208d1173de5 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 12 Aug 2026 15:55:46 -0700 Subject: [PATCH 06/32] Fixup the sync-pass example --- Cargo.lock | 1 + examples/sync-pass/Cargo.toml | 1 + examples/sync-pass/src/sync-pass.rs | 11 ++++++----- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40d068f511c..b23795e851e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1311,6 +1311,7 @@ dependencies = [ "chrono", "clap", "cli-support", + "encryption", "fxa-client", "init_rust_components", "log", diff --git a/examples/sync-pass/Cargo.toml b/examples/sync-pass/Cargo.toml index 7c43428d973..692e6e1abc6 100644 --- a/examples/sync-pass/Cargo.toml +++ b/examples/sync-pass/Cargo.toml @@ -13,6 +13,7 @@ path = "src/sync-pass.rs" [dev-dependencies] base64 = "0.21" logins = { path = "../../components/logins", features = ["keydb"] } +encryption = { path = "../../components/support/encryption", features = ["keydb"] } sync15 = { path = "../../components/sync15" } sync-guid = { path = "../../components/support/guid" } log = "0.4" diff --git a/examples/sync-pass/src/sync-pass.rs b/examples/sync-pass/src/sync-pass.rs index 3c27cb8c1ac..38b1504a70d 100644 --- a/examples/sync-pass/src/sync-pass.rs +++ b/examples/sync-pass/src/sync-pass.rs @@ -7,7 +7,7 @@ use cli_support::fxa_creds::{get_default_fxa_config, CliFxa, SYNC_SCOPE}; use cli_support::prompt::{prompt_char, prompt_password, prompt_string, prompt_usize}; -use encryption::{ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator}; +use encryption::{EncryptionApiError, ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator}; use logins::{Login, LoginEntry, LoginStore, LoginsApiError, LoginsSyncEngine, ValidateAndFixup}; use async_trait::async_trait; @@ -296,24 +296,25 @@ fn prompt_record_id(s: &LoginStore, action: &str) -> Result> { struct MyPrimaryPasswordAuthenticator {} #[async_trait] impl PrimaryPasswordAuthenticator for MyPrimaryPasswordAuthenticator { - async fn get_primary_password(&self) -> Result { + async fn get_primary_password(&self) -> Result { let password = prompt_password("primary password").unwrap_or_default(); Ok(password) } - async fn on_authentication_success(&self) -> Result<(), LoginsApiError> { + async fn on_authentication_success(&self) -> Result<(), EncryptionApiError> { println!("success"); Ok(()) } - async fn on_authentication_failure(&self) -> Result<(), LoginsApiError> { + async fn on_authentication_failure(&self) -> Result<(), EncryptionApiError> { println!("this did not work, please try again:"); Ok(()) } } fn open_database(db_path: &str) -> Result { - let key_manager = NSSKeyManager::new(Arc::new(MyPrimaryPasswordAuthenticator {})); + let key_name: &str = "as-login-key"; + let key_manager = NSSKeyManager::new(key_name, Arc::new(MyPrimaryPasswordAuthenticator {})); let encdec = Arc::new(ManagedEncryptorDecryptor::new(Arc::new(key_manager))); let store = LoginStore::new(db_path, encdec)?; Ok(store) From c6a8a2c17062ca198297cc07520fe50e356a9483 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 12 Aug 2026 16:02:06 -0700 Subject: [PATCH 07/32] NSSKeyManager::new() should take a std::String instead of std::str --- components/logins/src/lib.rs | 2 +- components/logins/src/store.rs | 2 +- components/support/encryption/src/encryption.rs | 4 ++-- examples/sync-pass/src/sync-pass.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/logins/src/lib.rs b/components/logins/src/lib.rs index 3e75724c4c9..40b161abeb3 100644 --- a/components/logins/src/lib.rs +++ b/components/logins/src/lib.rs @@ -72,7 +72,7 @@ pub fn create_login_store_with_nss_keymanager( primary_password_authenticator: Arc, ) -> ApiResult> { let encdec: ManagedEncryptorDecryptor = ManagedEncryptorDecryptor::new(Arc::new( - NSSKeyManager::new(KEY_NAME, primary_password_authenticator), + NSSKeyManager::new(KEY_NAME.to_string(), primary_password_authenticator), )); let store = LoginStore::new(path, Arc::new(encdec))?; Ok(Arc::new(store)) diff --git a/components/logins/src/store.rs b/components/logins/src/store.rs index 6cab0b32e96..8c14badc817 100644 --- a/components/logins/src/store.rs +++ b/components/logins/src/store.rs @@ -792,7 +792,7 @@ mod tests_keydb { let primary_password_authenticator = MockPrimaryPasswordAuthenticator { password: "password".to_string(), }; - let key_manager = NSSKeyManager::new(crate::KEY_NAME, Arc::new(primary_password_authenticator)); + let key_manager = NSSKeyManager::new(crate::KEY_NAME.to_string(), Arc::new(primary_password_authenticator)); let encdec = ManagedEncryptorDecryptor::new(Arc::new(key_manager)); let store = LoginStore::new(profile_path().join("logins.db"), Arc::new(encdec)) .expect("store from fixtures"); diff --git a/components/support/encryption/src/encryption.rs b/components/support/encryption/src/encryption.rs index 976de0fbd73..86a775b123e 100644 --- a/components/support/encryption/src/encryption.rs +++ b/components/support/encryption/src/encryption.rs @@ -254,10 +254,10 @@ impl NSSKeyManager { /// There must be a previous initializiation of NSS before initializing /// `NSSKeyManager`, otherwise this panics. #[uniffi::constructor()] - pub fn new(key_name: &str, primary_password_authenticator: Arc) -> Self { + pub fn new(key_name: String, primary_password_authenticator: Arc) -> Self { assert_nss_initialized(); Self { - key_name: key_name.to_string(), + key_name: key_name, primary_password_authenticator, cached_key: RwLock::new(None), } diff --git a/examples/sync-pass/src/sync-pass.rs b/examples/sync-pass/src/sync-pass.rs index 38b1504a70d..6d68465489b 100644 --- a/examples/sync-pass/src/sync-pass.rs +++ b/examples/sync-pass/src/sync-pass.rs @@ -314,7 +314,7 @@ impl PrimaryPasswordAuthenticator for MyPrimaryPasswordAuthenticator { fn open_database(db_path: &str) -> Result { let key_name: &str = "as-login-key"; - let key_manager = NSSKeyManager::new(key_name, Arc::new(MyPrimaryPasswordAuthenticator {})); + let key_manager = NSSKeyManager::new(key_name.to_string(), Arc::new(MyPrimaryPasswordAuthenticator {})); let encdec = Arc::new(ManagedEncryptorDecryptor::new(Arc::new(key_manager))); let store = LoginStore::new(db_path, encdec)?; Ok(store) From 8c7eadacecc4c8c76e8e014d2ae38e42ff8e44ee Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 13 Aug 2026 08:23:51 -0700 Subject: [PATCH 08/32] Fixup test_utils import when keydb feature not enabled --- components/logins/src/db.rs | 4 ++-- components/logins/src/schema.rs | 2 +- components/logins/src/sync/engine.rs | 2 +- components/logins/src/sync/merge.rs | 2 +- components/logins/src/sync/payload.rs | 2 +- components/logins/src/sync/update_plan.rs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/logins/src/db.rs b/components/logins/src/db.rs index 7486f34324f..0b6693ec41f 100644 --- a/components/logins/src/db.rs +++ b/components/logins/src/db.rs @@ -1202,7 +1202,7 @@ lazy_static! { #[cfg(test)] pub mod test_utils { use super::*; - use crate::encryption::test_utils::decrypt_struct; + use crate::test_utils::decrypt_struct; use crate::login::test_utils::enc_login; use crate::SecureLoginFields; use sync15::ServerTimestamp; @@ -1374,7 +1374,7 @@ pub mod test_utils { mod tests { use super::*; use crate::db::test_utils::{get_local_guids, get_mirror_guids}; - use crate::encryption::test_utils::TEST_ENCDEC; + use crate::test_utils::TEST_ENCDEC; use crate::sync::merge::LocalLogin; use nss_as::ensure_initialized; use std::{thread, time}; diff --git a/components/logins/src/schema.rs b/components/logins/src/schema.rs index bac2c335c86..221a6283563 100644 --- a/components/logins/src/schema.rs +++ b/components/logins/src/schema.rs @@ -295,7 +295,7 @@ pub(crate) fn create(db: &Connection) -> Result<()> { #[cfg(test)] mod tests { use super::*; - use crate::encryption::test_utils::TEST_ENCDEC; + use crate::test_utils::TEST_ENCDEC; use crate::LoginDb; use nss_as::ensure_initialized; use rusqlite::Connection; diff --git a/components/logins/src/sync/engine.rs b/components/logins/src/sync/engine.rs index 6ca424433c2..dc74897c040 100644 --- a/components/logins/src/sync/engine.rs +++ b/components/logins/src/sync/engine.rs @@ -540,7 +540,7 @@ impl SyncEngine for LoginsSyncEngine { mod tests { use super::*; use crate::db::test_utils::insert_login; - use crate::encryption::test_utils::TEST_ENCDEC; + use crate::test_utils::TEST_ENCDEC; use crate::login::test_utils::enc_login; use crate::{LoginEntry, LoginFields, LoginMeta, SecureLoginFields}; use nss_as::ensure_initialized; diff --git a/components/logins/src/sync/merge.rs b/components/logins/src/sync/merge.rs index e0b4b7c7ae2..31929a8f604 100644 --- a/components/logins/src/sync/merge.rs +++ b/components/logins/src/sync/merge.rs @@ -380,7 +380,7 @@ impl EncryptedLogin { #[cfg(test)] mod tests { use super::*; - use crate::encryption::test_utils::TEST_ENCDEC; + use crate::test_utils::TEST_ENCDEC; use nss_as::ensure_initialized; #[test] diff --git a/components/logins/src/sync/payload.rs b/components/logins/src/sync/payload.rs index a273221f95a..4333f121925 100644 --- a/components/logins/src/sync/payload.rs +++ b/components/logins/src/sync/payload.rs @@ -239,7 +239,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::encryption::test_utils::{encrypt_struct, TEST_ENCDEC}; + use crate::test_utils::{encrypt_struct, TEST_ENCDEC}; use crate::sync::merge::SyncLoginData; use crate::{EncryptedLogin, LoginFields, LoginMeta, SecureLoginFields}; use sync15::bso::IncomingBso; diff --git a/components/logins/src/sync/update_plan.rs b/components/logins/src/sync/update_plan.rs index c890b3f34da..91f7f66945e 100644 --- a/components/logins/src/sync/update_plan.rs +++ b/components/logins/src/sync/update_plan.rs @@ -325,7 +325,7 @@ mod tests { get_server_modified, insert_encrypted_login, insert_login, }; use crate::db::LoginDb; - use crate::encryption::test_utils::TEST_ENCDEC; + use crate::test_utils::TEST_ENCDEC; use crate::login::test_utils::enc_login; fn inc_login(id: &str, password: &str) -> crate::sync::IncomingLogin { From 943abbfcb6d65fbfe95be07f14bc2851c302b068 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 13 Aug 2026 09:46:47 -0700 Subject: [PATCH 09/32] Fix doc tests --- components/support/encryption/src/encryption.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/support/encryption/src/encryption.rs b/components/support/encryption/src/encryption.rs index 86a775b123e..eae41c9c94a 100644 --- a/components/support/encryption/src/encryption.rs +++ b/components/support/encryption/src/encryption.rs @@ -236,7 +236,7 @@ pub trait PrimaryPasswordAuthenticator: Send + Sync { /// Ok(()) /// } /// } -/// let key_manager = NSSKeyManager::new("example", Arc::new(MyPrimaryPasswordAuthenticator {})); +/// let key_manager = NSSKeyManager::new(String::from("example"), Arc::new(MyPrimaryPasswordAuthenticator {})); /// assert_eq!(key_manager.get_key().unwrap().len(), 63); /// ``` #[cfg(feature = "keydb")] From c097382d3f175f97ec06509a2b827205d806b979 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 13 Aug 2026 10:19:05 -0700 Subject: [PATCH 10/32] Remove unused std::sync::Arc --- components/support/encryption/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/components/support/encryption/src/lib.rs b/components/support/encryption/src/lib.rs index 1b350f79ffa..9dd64b94f23 100644 --- a/components/support/encryption/src/lib.rs +++ b/components/support/encryption/src/lib.rs @@ -20,4 +20,3 @@ pub use crate::encryption::{NSSKeyManager, PrimaryPasswordAuthenticator}; pub use crate::encryption::{check_canary, create_canary, create_key}; pub use crate::error::*; -use std::sync::Arc; From 6f5ef46b8d183febe5c338a21f105de615f82691 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 13 Aug 2026 10:31:36 -0700 Subject: [PATCH 11/32] Attempt to fixup formatting checks --- components/logins/src/db.rs | 7 +++-- components/logins/src/error.rs | 26 ++++++++++++------- components/logins/src/lib.rs | 4 +-- components/logins/src/store.rs | 5 +++- components/logins/src/sync/engine.rs | 2 +- components/logins/src/sync/payload.rs | 2 +- components/logins/src/sync/update_plan.rs | 2 +- .../support/encryption/src/encryption.rs | 17 +++++++----- examples/sync-pass/src/sync-pass.rs | 9 +++++-- 9 files changed, 46 insertions(+), 28 deletions(-) diff --git a/components/logins/src/db.rs b/components/logins/src/db.rs index 0b6693ec41f..f7065c0d3ed 100644 --- a/components/logins/src/db.rs +++ b/components/logins/src/db.rs @@ -84,8 +84,7 @@ impl LoginDb { #[cfg(test)] pub fn open_in_memory() -> Self { - let encdec: Arc = - crate::test_utils::TEST_ENCDEC.clone(); + let encdec: Arc = crate::test_utils::TEST_ENCDEC.clone(); Self::with_connection(Connection::open_in_memory().unwrap(), encdec).unwrap() } @@ -1202,8 +1201,8 @@ lazy_static! { #[cfg(test)] pub mod test_utils { use super::*; - use crate::test_utils::decrypt_struct; use crate::login::test_utils::enc_login; + use crate::test_utils::decrypt_struct; use crate::SecureLoginFields; use sync15::ServerTimestamp; @@ -1374,8 +1373,8 @@ pub mod test_utils { mod tests { use super::*; use crate::db::test_utils::{get_local_guids, get_mirror_guids}; - use crate::test_utils::TEST_ENCDEC; use crate::sync::merge::LocalLogin; + use crate::test_utils::TEST_ENCDEC; use nss_as::ensure_initialized; use std::{thread, time}; diff --git a/components/logins/src/error.rs b/components/logins/src/error.rs index 01456fa7db7..ac7f6e9aeae 100644 --- a/components/logins/src/error.rs +++ b/components/logins/src/error.rs @@ -10,9 +10,9 @@ pub type ApiResult = std::result::Result; pub use error_support::{breadcrumb, handle_error, report_error}; pub use error_support::{debug, error, info, trace, warn}; +use encryption::EncryptionApiError; use error_support::{ErrorHandling, GetErrorHandling}; use jwcrypto::JwCryptoError; -use encryption::EncryptionApiError; // Errors we return via the public interface. #[derive(Debug, thiserror::Error)] @@ -223,17 +223,25 @@ impl From for LoginsApiError { fn from(error: EncryptionApiError) -> Self { match error { EncryptionApiError::NSSUninitialized => Self::NSSUninitialized, - EncryptionApiError::NSSAuthenticationError{reason: x} => Self::NSSAuthenticationError { - reason: x - }, - EncryptionApiError::AuthenticationError{reason: x} => Self::AuthenticationError { reason: x }, + EncryptionApiError::NSSAuthenticationError { reason: x } => { + Self::NSSAuthenticationError { reason: x } + } + EncryptionApiError::AuthenticationError { reason: x } => { + Self::AuthenticationError { reason: x } + } EncryptionApiError::AuthenticationCanceled => Self::AuthenticationCanceled, EncryptionApiError::MissingKey => Self::MissingKey, EncryptionApiError::InvalidKey => Self::InvalidKey, - EncryptionApiError::EncryptionFailed{reason: x} => Self::EncryptionFailed { reason: x }, - EncryptionApiError::DecryptionFailed{reason: x} => Self::DecryptionFailed { reason: x }, - EncryptionApiError::Interrupted{reason: x} => Self::Interrupted { reason: x }, - EncryptionApiError::UnexpectedEncryptionApiError{reason: x} => Self::UnexpectedLoginsApiError { reason: x }, + EncryptionApiError::EncryptionFailed { reason: x } => { + Self::EncryptionFailed { reason: x } + } + EncryptionApiError::DecryptionFailed { reason: x } => { + Self::DecryptionFailed { reason: x } + } + EncryptionApiError::Interrupted { reason: x } => Self::Interrupted { reason: x }, + EncryptionApiError::UnexpectedEncryptionApiError { reason: x } => { + Self::UnexpectedLoginsApiError { reason: x } + } } } } diff --git a/components/logins/src/lib.rs b/components/logins/src/lib.rs index 40b161abeb3..06db39e0300 100644 --- a/components/logins/src/lib.rs +++ b/components/logins/src/lib.rs @@ -16,9 +16,7 @@ mod store; mod sync; mod util; -use encryption::{ - EncryptorDecryptor, KeyManager, ManagedEncryptorDecryptor, StaticKeyManager, -}; +use encryption::{EncryptorDecryptor, KeyManager, ManagedEncryptorDecryptor, StaticKeyManager}; uniffi::include_scaffolding!("logins"); #[cfg(feature = "keydb")] diff --git a/components/logins/src/store.rs b/components/logins/src/store.rs index 8c14badc817..b0e12886512 100644 --- a/components/logins/src/store.rs +++ b/components/logins/src/store.rs @@ -792,7 +792,10 @@ mod tests_keydb { let primary_password_authenticator = MockPrimaryPasswordAuthenticator { password: "password".to_string(), }; - let key_manager = NSSKeyManager::new(crate::KEY_NAME.to_string(), Arc::new(primary_password_authenticator)); + let key_manager = NSSKeyManager::new( + crate::KEY_NAME.to_string(), + Arc::new(primary_password_authenticator), + ); let encdec = ManagedEncryptorDecryptor::new(Arc::new(key_manager)); let store = LoginStore::new(profile_path().join("logins.db"), Arc::new(encdec)) .expect("store from fixtures"); diff --git a/components/logins/src/sync/engine.rs b/components/logins/src/sync/engine.rs index dc74897c040..3eb62287ee9 100644 --- a/components/logins/src/sync/engine.rs +++ b/components/logins/src/sync/engine.rs @@ -540,8 +540,8 @@ impl SyncEngine for LoginsSyncEngine { mod tests { use super::*; use crate::db::test_utils::insert_login; - use crate::test_utils::TEST_ENCDEC; use crate::login::test_utils::enc_login; + use crate::test_utils::TEST_ENCDEC; use crate::{LoginEntry, LoginFields, LoginMeta, SecureLoginFields}; use nss_as::ensure_initialized; use std::collections::HashMap; diff --git a/components/logins/src/sync/payload.rs b/components/logins/src/sync/payload.rs index 4333f121925..d3c6eddee89 100644 --- a/components/logins/src/sync/payload.rs +++ b/components/logins/src/sync/payload.rs @@ -239,8 +239,8 @@ where #[cfg(test)] mod tests { use super::*; - use crate::test_utils::{encrypt_struct, TEST_ENCDEC}; use crate::sync::merge::SyncLoginData; + use crate::test_utils::{encrypt_struct, TEST_ENCDEC}; use crate::{EncryptedLogin, LoginFields, LoginMeta, SecureLoginFields}; use sync15::bso::IncomingBso; diff --git a/components/logins/src/sync/update_plan.rs b/components/logins/src/sync/update_plan.rs index 91f7f66945e..3769614a4e9 100644 --- a/components/logins/src/sync/update_plan.rs +++ b/components/logins/src/sync/update_plan.rs @@ -325,8 +325,8 @@ mod tests { get_server_modified, insert_encrypted_login, insert_login, }; use crate::db::LoginDb; - use crate::test_utils::TEST_ENCDEC; use crate::login::test_utils::enc_login; + use crate::test_utils::TEST_ENCDEC; fn inc_login(id: &str, password: &str) -> crate::sync::IncomingLogin { IncomingLogin { diff --git a/components/support/encryption/src/encryption.rs b/components/support/encryption/src/encryption.rs index eae41c9c94a..2b4f3dd809a 100644 --- a/components/support/encryption/src/encryption.rs +++ b/components/support/encryption/src/encryption.rs @@ -142,10 +142,11 @@ impl EncryptorDecryptor for ManagedEncryptorDecryptor { let encdec = jwcrypto::EncryptorDecryptor::new(key) .map_err(|_: jwcrypto::JwCryptoError| EncryptionApiError::InvalidKey)?; - let ciphertext = - std::str::from_utf8(&cipherbytes).map_err(|e| EncryptionApiError::DecryptionFailed { + let ciphertext = std::str::from_utf8(&cipherbytes).map_err(|e| { + EncryptionApiError::DecryptionFailed { reason: e.to_string(), - })?; + } + })?; encdec .decrypt(ciphertext) .map_err( @@ -254,7 +255,10 @@ impl NSSKeyManager { /// There must be a previous initializiation of NSS before initializing /// `NSSKeyManager`, otherwise this panics. #[uniffi::constructor()] - pub fn new(key_name: String, primary_password_authenticator: Arc) -> Self { + pub fn new( + key_name: String, + primary_password_authenticator: Arc, + ) -> Self { assert_nss_initialized(); Self { key_name: key_name, @@ -327,7 +331,8 @@ impl KeyManager for NSSKeyManager { return Ok(bytes); } - let key = get_or_create_aes256_key(self.key_name.as_str()).map_err(|_| EncryptionApiError::MissingKey)?; + let key = get_or_create_aes256_key(self.key_name.as_str()) + .map_err(|_| EncryptionApiError::MissingKey)?; let mut bytes: Vec = Vec::new(); serde_json::to_writer( &mut bytes, @@ -358,7 +363,7 @@ pub fn create_key() -> ApiResult { #[cfg(test)] pub mod test_utils { use super::*; - use serde::{de::DeserializeOwned, Serialize}; + use serde::{Serialize, de::DeserializeOwned}; lazy_static::lazy_static! { pub static ref TEST_ENCRYPTION_KEY: String = serde_json::to_string(&jwcrypto::Jwk::new_direct_key(Some("test-key".to_string())).unwrap()).unwrap(); diff --git a/examples/sync-pass/src/sync-pass.rs b/examples/sync-pass/src/sync-pass.rs index 6d68465489b..5d8a686f35b 100644 --- a/examples/sync-pass/src/sync-pass.rs +++ b/examples/sync-pass/src/sync-pass.rs @@ -7,7 +7,9 @@ use cli_support::fxa_creds::{get_default_fxa_config, CliFxa, SYNC_SCOPE}; use cli_support::prompt::{prompt_char, prompt_password, prompt_string, prompt_usize}; -use encryption::{EncryptionApiError, ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator}; +use encryption::{ + EncryptionApiError, ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator, +}; use logins::{Login, LoginEntry, LoginStore, LoginsApiError, LoginsSyncEngine, ValidateAndFixup}; use async_trait::async_trait; @@ -314,7 +316,10 @@ impl PrimaryPasswordAuthenticator for MyPrimaryPasswordAuthenticator { fn open_database(db_path: &str) -> Result { let key_name: &str = "as-login-key"; - let key_manager = NSSKeyManager::new(key_name.to_string(), Arc::new(MyPrimaryPasswordAuthenticator {})); + let key_manager = NSSKeyManager::new( + key_name.to_string(), + Arc::new(MyPrimaryPasswordAuthenticator {}), + ); let encdec = Arc::new(ManagedEncryptorDecryptor::new(Arc::new(key_manager))); let store = LoginStore::new(db_path, encdec)?; Ok(store) From d9404a4e6659a291c8b3855cf51624ebe81695e9 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 13 Aug 2026 12:40:56 -0700 Subject: [PATCH 12/32] Add android bindings too? --- .buildconfig-android.yml | 7 ++++++ Cargo.lock | 1 + components/logins/android/build.gradle | 1 + .../support/encryption/android/build.gradle | 10 +++++++++ .../encryption/android/proguard-rules.pro | 22 +++++++++++++++++++ .../android/src/main/AndroidManifest.xml | 2 ++ components/support/encryption/uniffi.toml | 2 ++ megazords/full/Cargo.toml | 1 + megazords/full/src/lib.rs | 1 + 9 files changed, 47 insertions(+) create mode 100644 components/support/encryption/android/build.gradle create mode 100644 components/support/encryption/android/proguard-rules.pro create mode 100644 components/support/encryption/android/src/main/AndroidManifest.xml create mode 100644 components/support/encryption/uniffi.toml diff --git a/.buildconfig-android.yml b/.buildconfig-android.yml index 98aed1a0628..41ecf7faf54 100644 --- a/.buildconfig-android.yml +++ b/.buildconfig-android.yml @@ -166,3 +166,10 @@ projects: type: aar description: Client for Firefox Relay. + encryption: + path: components/support/encryption/android + artifactId: encryption + publications: + - name: encryption + type: aar + description: Credential encryption support diff --git a/Cargo.lock b/Cargo.lock index b23795e851e..3f261177828 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2626,6 +2626,7 @@ dependencies = [ "ads-client", "autofill", "crashtest", + "encryption", "error-support", "fxa-client", "init_rust_components", diff --git a/components/logins/android/build.gradle b/components/logins/android/build.gradle index 4834fb15f14..ee6e1839282 100644 --- a/components/logins/android/build.gradle +++ b/components/logins/android/build.gradle @@ -44,6 +44,7 @@ dependencies { // Part of the public API. api project(':init_rust_components') api project(':sync15') + api project(':encryption') implementation project(':init_rust_components') diff --git a/components/support/encryption/android/build.gradle b/components/support/encryption/android/build.gradle new file mode 100644 index 00000000000..dbff5f8dbbe --- /dev/null +++ b/components/support/encryption/android/build.gradle @@ -0,0 +1,10 @@ +apply from: "$appServicesRootDir/build-scripts/component-common.gradle" +apply from: "$appServicesRootDir/publish.gradle" + +android { + namespace 'org.mozilla.appservices.encryption' +} + +ext.configureUniFFIBindgen("encryption") +ext.dependsOnTheMegazord() +ext.configurePublish() diff --git a/components/support/encryption/android/proguard-rules.pro b/components/support/encryption/android/proguard-rules.pro new file mode 100644 index 00000000000..cf504086aa2 --- /dev/null +++ b/components/support/encryption/android/proguard-rules.pro @@ -0,0 +1,22 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + diff --git a/components/support/encryption/android/src/main/AndroidManifest.xml b/components/support/encryption/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..e9861f59ac5 --- /dev/null +++ b/components/support/encryption/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/components/support/encryption/uniffi.toml b/components/support/encryption/uniffi.toml new file mode 100644 index 00000000000..a3496acdf2d --- /dev/null +++ b/components/support/encryption/uniffi.toml @@ -0,0 +1,2 @@ +[bindings.kotlin] +package_name = "mozilla.appservices.encryption" diff --git a/megazords/full/Cargo.toml b/megazords/full/Cargo.toml index da6d3a77133..f5b91c1752b 100644 --- a/megazords/full/Cargo.toml +++ b/megazords/full/Cargo.toml @@ -40,6 +40,7 @@ mozilla-central-workspace-hack = { version = "0.1", features = ["megazord"], opt # NSS link chain. The megazord cdylib produces a self-contained Rust artifact # and needs static mozpkix + pure_virtual on top of the NSS dylibs. rc_crypto = { path = "../../components/support/rc_crypto" } +encryption = { path = "../../components/support/encryption" } [features] mozbuild-rustlib = ["rc_crypto/mozbuild-rustlib"] diff --git a/megazords/full/src/lib.rs b/megazords/full/src/lib.rs index 62b19107279..5f553483342 100644 --- a/megazords/full/src/lib.rs +++ b/megazords/full/src/lib.rs @@ -12,6 +12,7 @@ use std::os::raw::c_char; pub use ads_client; pub use autofill; pub use crashtest; +pub use encryption; pub use error_support; pub use fxa_client; pub use init_rust_components; From d350b60ea16e7aac6123aff47eb95ca65d2d9d20 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 13 Aug 2026 13:00:03 -0700 Subject: [PATCH 13/32] Maybe fix android KeyManager import? --- .../java/mozilla/appservices/logins/DatabaseLoginsStorage.kt | 1 + .../java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt b/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt index cfb2412eac3..c1a4faade02 100644 --- a/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt +++ b/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt @@ -13,6 +13,7 @@ package mozilla.appservices.logins * on version updates. */ +import mozilla.appservices.encryption.KeyManager import mozilla.telemetry.glean.private.CounterMetricType import mozilla.telemetry.glean.private.LabeledMetricType import org.mozilla.appservices.logins.GleanMetrics.LoginsStore as LoginsStoreMetrics diff --git a/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt b/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt index d00935121c2..876a7c0094b 100644 --- a/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt +++ b/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt @@ -5,6 +5,7 @@ package mozilla.appservices.logins import androidx.test.core.app.ApplicationProvider +import mozilla.appservices.encryption.KeyManager import mozilla.appservices.RustComponentsInitializer import mozilla.appservices.syncmanager.SyncManager import mozilla.telemetry.glean.testing.GleanTestRule From f7e60b041a8654363333d638ef06466e2fd8c763 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 13 Aug 2026 17:13:25 -0700 Subject: [PATCH 14/32] Try to make the clippy linter happy --- .../support/encryption/src/encryption.rs | 23 +-------------- components/support/encryption/src/error.rs | 29 +++++++------------ 2 files changed, 11 insertions(+), 41 deletions(-) diff --git a/components/support/encryption/src/encryption.rs b/components/support/encryption/src/encryption.rs index 2b4f3dd809a..acf5bc7d22d 100644 --- a/components/support/encryption/src/encryption.rs +++ b/components/support/encryption/src/encryption.rs @@ -261,7 +261,7 @@ impl NSSKeyManager { ) -> Self { assert_nss_initialized(); Self { - key_name: key_name, + key_name, primary_password_authenticator, cached_key: RwLock::new(None), } @@ -360,27 +360,6 @@ pub fn create_key() -> ApiResult { Ok(jwcrypto::EncryptorDecryptor::create_key()?) } -#[cfg(test)] -pub mod test_utils { - use super::*; - use serde::{Serialize, de::DeserializeOwned}; - - lazy_static::lazy_static! { - pub static ref TEST_ENCRYPTION_KEY: String = serde_json::to_string(&jwcrypto::Jwk::new_direct_key(Some("test-key".to_string())).unwrap()).unwrap(); - pub static ref TEST_ENCDEC: Arc = Arc::new(ManagedEncryptorDecryptor::new(Arc::new(StaticKeyManager { key: TEST_ENCRYPTION_KEY.clone() }))); - } - - pub fn encrypt_struct(fields: &T) -> String { - let string = serde_json::to_string(fields).unwrap(); - let cipherbytes = TEST_ENCDEC.encrypt(string.as_bytes().into()).unwrap(); - std::str::from_utf8(&cipherbytes).unwrap().to_owned() - } - pub fn decrypt_struct(ciphertext: String) -> T { - let jsonbytes = TEST_ENCDEC.decrypt(ciphertext.as_bytes().into()).unwrap(); - serde_json::from_str(std::str::from_utf8(&jsonbytes).unwrap()).unwrap() - } -} - #[cfg(not(feature = "keydb"))] #[cfg(test)] mod tests { diff --git a/components/support/encryption/src/error.rs b/components/support/encryption/src/error.rs index 941cc6efdcd..3e3d8a41d61 100644 --- a/components/support/encryption/src/error.rs +++ b/components/support/encryption/src/error.rs @@ -46,18 +46,11 @@ pub enum EncryptionApiError { UnexpectedEncryptionApiError { reason: String }, } -/// Logins error type +/// Encryption error type /// These are "internal" errors used by the implementation. This error type /// is never returned to the consumer. #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Database is closed")] - DatabaseClosed, - - // Fennec import only works on empty logins tables. - #[error("The logins tables are not empty")] - NonEmptyTable, - #[error("encryption failed: {0:?}")] EncryptionFailed(String), @@ -77,17 +70,15 @@ impl GetErrorHandling for Error { type ExternalError = EncryptionApiError; fn get_error_handling(&self) -> ErrorHandling { - match self { - // Unexpected errors that we report to Sentry. We should watch the reports for these - // and do one or more of these things if we see them: - // - Fix the underlying issue - // - Add breadcrumbs or other context to help uncover the issue - // - Decide that these are expected errors and move them to the above case - _ => ErrorHandling::convert(EncryptionApiError::UnexpectedEncryptionApiError { - reason: self.to_string(), - }) - .report_error("logins-unexpected"), - } + // Unexpected errors that we report to Sentry. We should watch the reports for these + // and do one or more of these things if we see them: + // - Fix the underlying issue + // - Add breadcrumbs or other context to help uncover the issue + // - Decide that these are expected errors and move them to the above case + ErrorHandling::convert(EncryptionApiError::UnexpectedEncryptionApiError { + reason: self.to_string(), + }) + .report_error("encryption-unexpected") } } From 8c4bd880db73d013a25ceb8852bdb6ad7bcf5416 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 13 Aug 2026 17:56:44 -0700 Subject: [PATCH 15/32] And once more with feeling --- .../mozilla/appservices/logins/DatabaseLoginsStorageTest.kt | 2 +- examples/sync-pass/src/sync-pass.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt b/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt index 876a7c0094b..7a690fb2399 100644 --- a/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt +++ b/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt @@ -5,8 +5,8 @@ package mozilla.appservices.logins import androidx.test.core.app.ApplicationProvider -import mozilla.appservices.encryption.KeyManager import mozilla.appservices.RustComponentsInitializer +import mozilla.appservices.encryption.KeyManager import mozilla.appservices.syncmanager.SyncManager import mozilla.telemetry.glean.testing.GleanTestRule import org.junit.Assert.assertEquals diff --git a/examples/sync-pass/src/sync-pass.rs b/examples/sync-pass/src/sync-pass.rs index 5d8a686f35b..e832b817674 100644 --- a/examples/sync-pass/src/sync-pass.rs +++ b/examples/sync-pass/src/sync-pass.rs @@ -10,7 +10,7 @@ use cli_support::prompt::{prompt_char, prompt_password, prompt_string, prompt_us use encryption::{ EncryptionApiError, ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator, }; -use logins::{Login, LoginEntry, LoginStore, LoginsApiError, LoginsSyncEngine, ValidateAndFixup}; +use logins::{Login, LoginEntry, LoginStore, LoginsSyncEngine, ValidateAndFixup}; use async_trait::async_trait; use std::sync::Arc; From c3166d62dac7a830ef988eb081676f6799aa0eb8 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Fri, 14 Aug 2026 16:00:12 -0700 Subject: [PATCH 16/32] I think the test enc_login() function is only used when keydb is unset --- components/logins/src/login.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/components/logins/src/login.rs b/components/logins/src/login.rs index 55e9152d772..86b5992c8ac 100644 --- a/components/logins/src/login.rs +++ b/components/logins/src/login.rs @@ -961,6 +961,7 @@ impl ValidateAndFixup for LoginEntry { } } +#[cfg(not(feature = "keydb"))] #[cfg(test)] pub mod test_utils { use super::*; From 696c35791f42bbfc64f87265f0fbf13448ee9673 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Mon, 17 Aug 2026 08:45:40 -0700 Subject: [PATCH 17/32] Fix key_name used by sync-pass --- examples/sync-pass/src/sync-pass.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/sync-pass/src/sync-pass.rs b/examples/sync-pass/src/sync-pass.rs index e832b817674..fc18f3f5417 100644 --- a/examples/sync-pass/src/sync-pass.rs +++ b/examples/sync-pass/src/sync-pass.rs @@ -315,7 +315,7 @@ impl PrimaryPasswordAuthenticator for MyPrimaryPasswordAuthenticator { } fn open_database(db_path: &str) -> Result { - let key_name: &str = "as-login-key"; + let key_name: &str = "as-logins-key"; let key_manager = NSSKeyManager::new( key_name.to_string(), Arc::new(MyPrimaryPasswordAuthenticator {}), From 085f323aee5b453a2c354ce458e09ab2a19864b1 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Mon, 17 Aug 2026 08:47:52 -0700 Subject: [PATCH 18/32] Add megazord bindings for iOS and Android --- Cargo.lock | 1 + megazords/fenix-dylib/megazord_stub.c | 1 + megazords/ios-rust/Cargo.toml | 1 + megazords/ios-rust/src/lib.rs | 1 + 4 files changed, 4 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 3f261177828..0327a7b64b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2671,6 +2671,7 @@ dependencies = [ "autofill", "context_id", "crashtest", + "encryption", "error-support", "fxa-client", "init_rust_components", diff --git a/megazords/fenix-dylib/megazord_stub.c b/megazords/fenix-dylib/megazord_stub.c index 2d59c462746..246344d380b 100644 --- a/megazords/fenix-dylib/megazord_stub.c +++ b/megazords/fenix-dylib/megazord_stub.c @@ -13,6 +13,7 @@ extern int MOZ_EXPORT ffi_ads_client_uniffi_contract_version(); extern int MOZ_EXPORT ffi_autofill_uniffi_contract_version(); extern int MOZ_EXPORT ffi_crashtest_uniffi_contract_version(); +extern int MOZ_EXPORT ffi_encryption_uniffi_contract_version(); extern int MOZ_EXPORT ffi_fxa_client_uniffi_contract_version(); extern int MOZ_EXPORT ffi_init_rust_components_uniffi_contract_version(); extern int MOZ_EXPORT ffi_logins_uniffi_contract_version(); diff --git a/megazords/ios-rust/Cargo.toml b/megazords/ios-rust/Cargo.toml index a44e55f4fc4..dc340be9995 100644 --- a/megazords/ios-rust/Cargo.toml +++ b/megazords/ios-rust/Cargo.toml @@ -23,6 +23,7 @@ places = { path = "../../components/places" } remote_settings = { path = "../../components/remote_settings", features=["telemetry-submission"] } suggest = { path = "../../components/suggest" } sync15 = { path = "../../components/sync15" } +encryption = { path = "../../components/support/encryption" } error-support = { path = "../../components/support/error" } tracing-support = { path = "../../components/support/tracing" } sync_manager = { path = "../../components/sync_manager" } diff --git a/megazords/ios-rust/src/lib.rs b/megazords/ios-rust/src/lib.rs index 0580781e784..95597f727a7 100644 --- a/megazords/ios-rust/src/lib.rs +++ b/megazords/ios-rust/src/lib.rs @@ -10,6 +10,7 @@ pub use as_ohttp_client; pub use autofill; pub use context_id; pub use crashtest; +pub use encryption; pub use error_support; pub use fxa_client; pub use init_rust_components; From 7524b80a76121011e0b90c8eec445c5480c5ab6e Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Mon, 17 Aug 2026 08:56:32 -0700 Subject: [PATCH 19/32] Add Swift bindings to uniffi as well --- components/support/encryption/uniffi.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/support/encryption/uniffi.toml b/components/support/encryption/uniffi.toml index a3496acdf2d..1743e038695 100644 --- a/components/support/encryption/uniffi.toml +++ b/components/support/encryption/uniffi.toml @@ -1,2 +1,7 @@ [bindings.kotlin] package_name = "mozilla.appservices.encryption" +omit_checksums = true + +[bindings.swift] +ffi_module_name = "MozillaRustComponents" +ffi_module_filename = "encryptionFFI" From 209bf053ebec314da8904394545690dabc482254 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Mon, 17 Aug 2026 09:02:34 -0700 Subject: [PATCH 20/32] Align build.gradle to latest template --- components/support/encryption/android/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/support/encryption/android/build.gradle b/components/support/encryption/android/build.gradle index dbff5f8dbbe..b7ac211ac67 100644 --- a/components/support/encryption/android/build.gradle +++ b/components/support/encryption/android/build.gradle @@ -1,5 +1,5 @@ apply from: "$appServicesRootDir/build-scripts/component-common.gradle" -apply from: "$appServicesRootDir/publish.gradle" +apply from: "$publishDir/publish.gradle" android { namespace 'org.mozilla.appservices.encryption' @@ -7,4 +7,4 @@ android { ext.configureUniFFIBindgen("encryption") ext.dependsOnTheMegazord() -ext.configurePublish() +ext.configurePublish(appServicesGroupId, project.name, project.ext.description) From 3b67d6ef17d10b7a3d2cdd7b710428e9ee1ad04c Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Mon, 17 Aug 2026 09:02:59 -0700 Subject: [PATCH 21/32] Add license, drop unused deps and set to rust 2021 edition --- Cargo.lock | 3 --- components/support/encryption/Cargo.toml | 6 ++---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0327a7b64b6..1602a8f7566 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1114,10 +1114,7 @@ dependencies = [ "error-support", "futures", "jwcrypto", - "lazy_static", "nss-as", - "serde", - "serde_derive", "serde_json", "thiserror 2.0.3", "uniffi", diff --git a/components/support/encryption/Cargo.toml b/components/support/encryption/Cargo.toml index d008b53cb2c..1f4f056dbd2 100644 --- a/components/support/encryption/Cargo.toml +++ b/components/support/encryption/Cargo.toml @@ -1,8 +1,9 @@ [package] name = "encryption" version = "0.1.0" -edition = "2024" +edition = "2021" authors = ["Naomi Kirby "] +license = "MPL-2.0" [features] default = [] @@ -19,10 +20,7 @@ async-trait = { version = "0.1", optional = true } error-support = { path = "../error" } futures = { version = "0.3", optional = true, features = ["executor"] } jwcrypto = { path = "../jwcrypto" } -lazy_static = "1.4" nss-as = { path = "../rc_crypto/nss", default-features = false } -serde = "1" -serde_derive = "1" serde_json = "1" thiserror = "2" From b1c2941c3c08c579064ff77c633e84763bf329c2 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Tue, 18 Aug 2026 06:59:04 -0700 Subject: [PATCH 22/32] Remove some unused stuff from logins.udl --- components/logins/src/logins.udl | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/components/logins/src/logins.udl b/components/logins/src/logins.udl index 88276c7abcf..33a65d3c39f 100644 --- a/components/logins/src/logins.udl +++ b/components/logins/src/logins.udl @@ -171,19 +171,10 @@ interface LoginsApiError { }; [External = "encryption"] -typedef interface EncryptionApiError; +typedef trait_with_foreign EncryptorDecryptor; [External = "encryption"] -typedef trait EncryptorDecryptor; - -[External = "encryption"] -typedef trait KeyManager; - -[External = "encryption"] -typedef interface StaticKeyManager; - -[External = "encryption"] -typedef interface ManagedEncryptorDecryptor; +typedef trait_with_foreign KeyManager; interface LoginStore { [Throws=LoginsApiError] From 4b5402d8925598b0cab1f1f70decb73dbab73082 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 19 Aug 2026 01:36:48 -0700 Subject: [PATCH 23/32] Add parking_lot dependency after rebase --- Cargo.lock | 1 + components/support/encryption/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 1602a8f7566..731f1eae7fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1115,6 +1115,7 @@ dependencies = [ "futures", "jwcrypto", "nss-as", + "parking_lot", "serde_json", "thiserror 2.0.3", "uniffi", diff --git a/components/support/encryption/Cargo.toml b/components/support/encryption/Cargo.toml index 1f4f056dbd2..b7c4a61a862 100644 --- a/components/support/encryption/Cargo.toml +++ b/components/support/encryption/Cargo.toml @@ -22,6 +22,7 @@ futures = { version = "0.3", optional = true, features = ["executor"] } jwcrypto = { path = "../jwcrypto" } nss-as = { path = "../rc_crypto/nss", default-features = false } serde_json = "1" +parking_lot = ">=0.11,<=0.12" thiserror = "2" From 39a9868c60fd9910cfbe28927fe6b4a6635dd9fd Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 19 Aug 2026 02:03:08 -0700 Subject: [PATCH 24/32] Add create_key wrappers back after rebase --- components/logins/src/lib.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/components/logins/src/lib.rs b/components/logins/src/lib.rs index 06db39e0300..67f059c44cb 100644 --- a/components/logins/src/lib.rs +++ b/components/logins/src/lib.rs @@ -23,7 +23,6 @@ uniffi::include_scaffolding!("logins"); pub use encryption::{NSSKeyManager, PrimaryPasswordAuthenticator}; pub use crate::db::{LoginDb, LoginsDeletionMetrics}; -use crate::encryption::{check_canary, create_canary, create_key}; pub use crate::error::*; pub use crate::login::*; pub use crate::store::*; @@ -59,6 +58,21 @@ pub fn create_login_store_with_static_key_manager(path: String, key: String) -> Arc::new(store) } + +#[handle_error(Error)] +pub fn create_canary(text: &str, key: &str) -> ApiResult { + Ok(encryption::create_canary(text, key)?) +} + +pub fn check_canary(canary: &str, text: &str, key: &str) -> ApiResult { + Ok(encryption::check_canary(canary, text, key)?) +} + +#[handle_error(Error)] +pub fn create_key() -> ApiResult { + Ok(encryption::create_key()?) +} + // Create a LoginStore with NSSKeyManager by passing in a db path and a PrimaryPasswordAuthenticator. // // Note this is only temporarily needed until a bug with UniFFI and JavaScript is fixed, which From a322759099fa42635dbdbc6644d9536bed264a57 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 19 Aug 2026 02:18:30 -0700 Subject: [PATCH 25/32] Fix keydb tests after rebase --- components/logins/src/lib.rs | 1 - components/support/encryption/src/encryption.rs | 16 ++++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/components/logins/src/lib.rs b/components/logins/src/lib.rs index 67f059c44cb..d7c70d324dd 100644 --- a/components/logins/src/lib.rs +++ b/components/logins/src/lib.rs @@ -58,7 +58,6 @@ pub fn create_login_store_with_static_key_manager(path: String, key: String) -> Arc::new(store) } - #[handle_error(Error)] pub fn create_canary(text: &str, key: &str) -> ApiResult { Ok(encryption::create_canary(text, key)?) diff --git a/components/support/encryption/src/encryption.rs b/components/support/encryption/src/encryption.rs index acf5bc7d22d..c8982b88b96 100644 --- a/components/support/encryption/src/encryption.rs +++ b/components/support/encryption/src/encryption.rs @@ -502,10 +502,10 @@ mod tests_keydb { let mock_primary_password_authenticator = MockPrimaryPasswordAuthenticator { password: "password".to_string(), }; - let nss_key_manager = NSSKeyManager { - key_name: String::from("as-logins-key"), - primary_password_authenticator: Arc::new(mock_primary_password_authenticator), - }; + let nss_key_manager = NSSKeyManager::new( + String::from("as-logins-key"), + Arc::new(mock_primary_password_authenticator), + ); // key from fixtures/profile/key4.db let expected = [ 123, 34, 107, 116, 121, 34, 58, 34, 111, 99, 116, 34, 44, 34, 107, 34, 58, 34, 66, 74, @@ -521,9 +521,13 @@ mod tests_keydb { fn test_nss_key_manager_caching() { ensure_initialized_with_profile_dir(profile_path()); // `password` is the primary password of the profile fixture - let nss_key_manager = NSSKeyManager::new(Arc::new(MockPrimaryPasswordAuthenticator { + let mock_primary_password_authenticator = MockPrimaryPasswordAuthenticator { password: "password".to_string(), - })); + }; + let nss_key_manager = NSSKeyManager::new( + String::from("as-logins-key"), + Arc::new(mock_primary_password_authenticator), + ); let key = nss_key_manager.get_key().unwrap(); assert_eq!(*nss_key_manager.cached_key.read(), Some(key.clone())); From 73a002eee03ebfa511ca7a340a9761541e02c9c0 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 26 Aug 2026 16:03:57 -0700 Subject: [PATCH 26/32] Rename crate from encryption to db-crypto --- .buildconfig-android.yml | 10 +-- Cargo.lock | 40 ++++++------ Cargo.toml | 2 +- components/logins/Cargo.toml | 4 +- components/logins/android/build.gradle | 2 +- .../logins/DatabaseLoginsStorage.kt | 2 +- .../logins/DatabaseLoginsStorageTest.kt | 2 +- components/logins/src/db.rs | 2 +- components/logins/src/error.rs | 28 ++++----- components/logins/src/lib.rs | 11 ++-- components/logins/src/login.rs | 3 +- components/logins/src/store.rs | 8 +-- components/logins/src/sync/engine.rs | 2 +- components/logins/src/sync/merge.rs | 2 +- components/logins/src/sync/payload.rs | 2 +- components/logins/src/sync/update_plan.rs | 2 +- .../{encryption => db-crypto}/Cargo.toml | 3 +- .../android/build.gradle | 4 +- .../android/proguard-rules.pro | 0 .../android/src/main/AndroidManifest.xml | 0 .../{encryption => db-crypto}/build.rs | 2 +- .../src/db_crypto.udl} | 18 +++--- .../src/encryption.rs | 61 +++++++++---------- .../{encryption => db-crypto}/src/error.rs | 20 +++--- .../{encryption => db-crypto}/src/lib.rs | 2 +- .../{encryption => db-crypto}/uniffi.toml | 4 +- examples/sync-pass/Cargo.toml | 2 +- examples/sync-pass/src/sync-pass.rs | 10 +-- megazords/full/Cargo.toml | 2 +- megazords/full/src/lib.rs | 2 +- megazords/ios-rust/Cargo.toml | 2 +- megazords/ios-rust/src/lib.rs | 2 +- testing/sync-test/src/auth.rs | 4 +- testing/sync-test/src/logins.rs | 2 +- 34 files changed, 131 insertions(+), 131 deletions(-) rename components/support/{encryption => db-crypto}/Cargo.toml (92%) rename components/support/{encryption => db-crypto}/android/build.gradle (71%) rename components/support/{encryption => db-crypto}/android/proguard-rules.pro (100%) rename components/support/{encryption => db-crypto}/android/src/main/AndroidManifest.xml (100%) rename components/support/{encryption => db-crypto}/build.rs (76%) rename components/support/{encryption/src/encryption.udl => db-crypto/src/db_crypto.udl} (88%) rename components/support/{encryption => db-crypto}/src/encryption.rs (90%) rename components/support/{encryption => db-crypto}/src/error.rs (84%) rename components/support/{encryption => db-crypto}/src/lib.rs (93%) rename components/support/{encryption => db-crypto}/uniffi.toml (53%) diff --git a/.buildconfig-android.yml b/.buildconfig-android.yml index 41ecf7faf54..03903ddd844 100644 --- a/.buildconfig-android.yml +++ b/.buildconfig-android.yml @@ -166,10 +166,10 @@ projects: type: aar description: Client for Firefox Relay. - encryption: - path: components/support/encryption/android - artifactId: encryption + dbcrypto: + path: components/support/db-crypto/android + artifactId: dbcrypto publications: - - name: encryption + - name: dbcrypto type: aar - description: Credential encryption support + description: Database encryption support diff --git a/Cargo.lock b/Cargo.lock index 731f1eae7fb..e95dd3ddc6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -926,6 +926,22 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "db-crypto" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "error-support", + "futures", + "jwcrypto", + "nss-as", + "parking_lot", + "serde_json", + "thiserror 2.0.3", + "uniffi", +] + [[package]] name = "deflate64" version = "0.1.9" @@ -1105,22 +1121,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" -[[package]] -name = "encryption" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "error-support", - "futures", - "jwcrypto", - "nss-as", - "parking_lot", - "serde_json", - "thiserror 2.0.3", - "uniffi", -] - [[package]] name = "env_logger" version = "0.10.2" @@ -1309,7 +1309,7 @@ dependencies = [ "chrono", "clap", "cli-support", - "encryption", + "db-crypto", "fxa-client", "init_rust_components", "log", @@ -2550,7 +2550,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "encryption", + "db-crypto", "error-support", "futures", "interrupt-support", @@ -2624,7 +2624,7 @@ dependencies = [ "ads-client", "autofill", "crashtest", - "encryption", + "db-crypto", "error-support", "fxa-client", "init_rust_components", @@ -2669,7 +2669,7 @@ dependencies = [ "autofill", "context_id", "crashtest", - "encryption", + "db-crypto", "error-support", "fxa-client", "init_rust_components", diff --git a/Cargo.toml b/Cargo.toml index 9afc549b397..9a555a48bc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ members = [ "components/search", "components/suggest", "components/suggest/suggest-bench", - "components/support/encryption", + "components/support/db-crypto", "components/support/error", "components/support/error/tests", "components/support/find-places-db", diff --git a/components/logins/Cargo.toml b/components/logins/Cargo.toml index df60196f7f3..09894dfdfd8 100644 --- a/components/logins/Cargo.toml +++ b/components/logins/Cargo.toml @@ -12,7 +12,7 @@ default = [] # `key4.db` (wrapped with a key derived from the primary password, if set). # Used on Desktop to integrate with the existing NSS key store and primary # password flow. -keydb = ["nss-as/keydb", "encryption/keydb", "dep:async-trait", "dep:futures"] +keydb = ["nss-as/keydb", "db-crypto/keydb", "dep:async-trait", "dep:futures"] # Allows logins with empty passwords to be imported. Used on Desktop during # migration to accept existing logins that have empty passwords. allow_empty_passwords = [] @@ -53,7 +53,7 @@ anyhow = "1.0" uniffi = { version = "0.31" } async-trait = { version = "0.1", optional = true } futures = { version = "0.3", optional = true, features = ["executor"] } -encryption = { path = "../support/encryption", default-features = false } +db-crypto = { path = "../support/db-crypto", default-features = false } [build-dependencies] uniffi = { version = "0.31", features = ["build"] } diff --git a/components/logins/android/build.gradle b/components/logins/android/build.gradle index ee6e1839282..7acd48a6b64 100644 --- a/components/logins/android/build.gradle +++ b/components/logins/android/build.gradle @@ -44,7 +44,7 @@ dependencies { // Part of the public API. api project(':init_rust_components') api project(':sync15') - api project(':encryption') + api project(':dbcrypto') implementation project(':init_rust_components') diff --git a/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt b/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt index c1a4faade02..c92c27d6561 100644 --- a/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt +++ b/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt @@ -13,7 +13,7 @@ package mozilla.appservices.logins * on version updates. */ -import mozilla.appservices.encryption.KeyManager +import mozilla.appservices.dbcrypto.KeyManager import mozilla.telemetry.glean.private.CounterMetricType import mozilla.telemetry.glean.private.LabeledMetricType import org.mozilla.appservices.logins.GleanMetrics.LoginsStore as LoginsStoreMetrics diff --git a/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt b/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt index 7a690fb2399..2ced44544ec 100644 --- a/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt +++ b/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt @@ -6,7 +6,7 @@ package mozilla.appservices.logins import androidx.test.core.app.ApplicationProvider import mozilla.appservices.RustComponentsInitializer -import mozilla.appservices.encryption.KeyManager +import mozilla.appservices.dbcrypto.KeyManager import mozilla.appservices.syncmanager.SyncManager import mozilla.telemetry.glean.testing.GleanTestRule import org.junit.Assert.assertEquals diff --git a/components/logins/src/db.rs b/components/logins/src/db.rs index f7065c0d3ed..40bae0bde80 100644 --- a/components/logins/src/db.rs +++ b/components/logins/src/db.rs @@ -22,12 +22,12 @@ /// server. /// - After we sync, we move all records from loginsL to loginsM, overwriting any previous data. /// loginsL will be an empty table after this. See mark_as_synchronized() for the details. -use crate::encryption::EncryptorDecryptor; use crate::error::*; use crate::login::*; use crate::schema; use crate::sync::SyncStatus; use crate::util; +use db_crypto::EncryptorDecryptor; use interrupt_support::{SqlInterruptHandle, SqlInterruptScope}; use lazy_static::lazy_static; use rusqlite::{ diff --git a/components/logins/src/error.rs b/components/logins/src/error.rs index ac7f6e9aeae..2aa1418a11e 100644 --- a/components/logins/src/error.rs +++ b/components/logins/src/error.rs @@ -10,7 +10,7 @@ pub type ApiResult = std::result::Result; pub use error_support::{breadcrumb, handle_error, report_error}; pub use error_support::{debug, error, info, trace, warn}; -use encryption::EncryptionApiError; +use db_crypto::DbCryptoApiError; use error_support::{ErrorHandling, GetErrorHandling}; use jwcrypto::JwCryptoError; @@ -97,7 +97,7 @@ pub enum Error { InvalidPath(OsString), #[error("CryptoError({0})")] - CryptoError(#[from] EncryptionApiError), + CryptoError(#[from] DbCryptoApiError), #[error("CryptoError({0})")] JwCryptoError(#[from] JwCryptoError), @@ -219,27 +219,27 @@ impl From for LoginsApiError { } } -impl From for LoginsApiError { - fn from(error: EncryptionApiError) -> Self { +impl From for LoginsApiError { + fn from(error: DbCryptoApiError) -> Self { match error { - EncryptionApiError::NSSUninitialized => Self::NSSUninitialized, - EncryptionApiError::NSSAuthenticationError { reason: x } => { + DbCryptoApiError::NSSUninitialized => Self::NSSUninitialized, + DbCryptoApiError::NSSAuthenticationError { reason: x } => { Self::NSSAuthenticationError { reason: x } } - EncryptionApiError::AuthenticationError { reason: x } => { + DbCryptoApiError::AuthenticationError { reason: x } => { Self::AuthenticationError { reason: x } } - EncryptionApiError::AuthenticationCanceled => Self::AuthenticationCanceled, - EncryptionApiError::MissingKey => Self::MissingKey, - EncryptionApiError::InvalidKey => Self::InvalidKey, - EncryptionApiError::EncryptionFailed { reason: x } => { + DbCryptoApiError::AuthenticationCanceled => Self::AuthenticationCanceled, + DbCryptoApiError::MissingKey => Self::MissingKey, + DbCryptoApiError::InvalidKey => Self::InvalidKey, + DbCryptoApiError::EncryptionFailed { reason: x } => { Self::EncryptionFailed { reason: x } } - EncryptionApiError::DecryptionFailed { reason: x } => { + DbCryptoApiError::DecryptionFailed { reason: x } => { Self::DecryptionFailed { reason: x } } - EncryptionApiError::Interrupted { reason: x } => Self::Interrupted { reason: x }, - EncryptionApiError::UnexpectedEncryptionApiError { reason: x } => { + DbCryptoApiError::Interrupted { reason: x } => Self::Interrupted { reason: x }, + DbCryptoApiError::UnexpectedDbCryptoApiError { reason: x } => { Self::UnexpectedLoginsApiError { reason: x } } } diff --git a/components/logins/src/lib.rs b/components/logins/src/lib.rs index d7c70d324dd..97290250b51 100644 --- a/components/logins/src/lib.rs +++ b/components/logins/src/lib.rs @@ -10,17 +10,16 @@ mod error; mod login; mod db; -pub use encryption; mod schema; mod store; mod sync; mod util; -use encryption::{EncryptorDecryptor, KeyManager, ManagedEncryptorDecryptor, StaticKeyManager}; +use db_crypto::{EncryptorDecryptor, KeyManager, ManagedEncryptorDecryptor, StaticKeyManager}; uniffi::include_scaffolding!("logins"); #[cfg(feature = "keydb")] -pub use encryption::{NSSKeyManager, PrimaryPasswordAuthenticator}; +pub use db_crypto::{NSSKeyManager, PrimaryPasswordAuthenticator}; pub use crate::db::{LoginDb, LoginsDeletionMetrics}; pub use crate::error::*; @@ -60,16 +59,16 @@ pub fn create_login_store_with_static_key_manager(path: String, key: String) -> #[handle_error(Error)] pub fn create_canary(text: &str, key: &str) -> ApiResult { - Ok(encryption::create_canary(text, key)?) + Ok(db_crypto::create_canary(text, key)?) } pub fn check_canary(canary: &str, text: &str, key: &str) -> ApiResult { - Ok(encryption::check_canary(canary, text, key)?) + Ok(db_crypto::check_canary(canary, text, key)?) } #[handle_error(Error)] pub fn create_key() -> ApiResult { - Ok(encryption::create_key()?) + Ok(db_crypto::create_key()?) } // Create a LoginStore with NSSKeyManager by passing in a db path and a PrimaryPasswordAuthenticator. diff --git a/components/logins/src/login.rs b/components/logins/src/login.rs index 86b5992c8ac..c016d3279a8 100644 --- a/components/logins/src/login.rs +++ b/components/logins/src/login.rs @@ -280,7 +280,8 @@ //! - `Login::fixup()`: Returns either the existing login if it is valid, a clone with invalid fields //! fixed up if it was safe to do so, or an error if the login is irreparably invalid. -use crate::{encryption::EncryptorDecryptor, error::*}; +use crate::error::*; +use db_crypto::EncryptorDecryptor; use rusqlite::Row; use serde_derive::*; use sync_guid::Guid; diff --git a/components/logins/src/store.rs b/components/logins/src/store.rs index b0e12886512..7e511766b3b 100644 --- a/components/logins/src/store.rs +++ b/components/logins/src/store.rs @@ -2,12 +2,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::db::{LoginDb, LoginsDeletionMetrics}; -use crate::encryption::EncryptorDecryptor; use crate::error::*; use crate::login::{ BulkResultEntry, EncryptedLogin, Login, LoginCandidate, LoginEntry, LoginEntryWithMeta, }; use crate::LoginsSyncEngine; +use db_crypto::EncryptorDecryptor; use parking_lot::Mutex; use sql_support::run_maintenance; use std::path::Path; @@ -768,13 +768,13 @@ mod tests_keydb { #[async_trait] impl PrimaryPasswordAuthenticator for MockPrimaryPasswordAuthenticator { - async fn get_primary_password(&self) -> encryption::ApiResult { + async fn get_primary_password(&self) -> db_crypto::ApiResult { Ok(self.password.clone()) } - async fn on_authentication_success(&self) -> encryption::ApiResult<()> { + async fn on_authentication_success(&self) -> db_crypto::ApiResult<()> { Ok(()) } - async fn on_authentication_failure(&self) -> encryption::ApiResult<()> { + async fn on_authentication_failure(&self) -> db_crypto::ApiResult<()> { Ok(()) } } diff --git a/components/logins/src/sync/engine.rs b/components/logins/src/sync/engine.rs index 3eb62287ee9..becf5d62e22 100644 --- a/components/logins/src/sync/engine.rs +++ b/components/logins/src/sync/engine.rs @@ -6,13 +6,13 @@ use super::merge::{LocalLogin, MirrorLogin, SyncLoginData}; use super::update_plan::UpdatePlan; use super::SyncStatus; use crate::db::CLONE_ENTIRE_MIRROR_SQL; -use crate::encryption::EncryptorDecryptor; use crate::error::*; use crate::login::{EncryptedLogin, FXA_CREDENTIALS_ORIGIN}; use crate::schema; use crate::util; use crate::LoginDb; use crate::LoginStore; +use db_crypto::EncryptorDecryptor; use interrupt_support::SqlInterruptScope; use rusqlite::named_params; use sql_support::ConnExt; diff --git a/components/logins/src/sync/merge.rs b/components/logins/src/sync/merge.rs index 31929a8f604..66c2b234949 100644 --- a/components/logins/src/sync/merge.rs +++ b/components/logins/src/sync/merge.rs @@ -4,10 +4,10 @@ // Merging for Sync. use super::{IncomingLogin, LoginPayload}; -use crate::encryption::EncryptorDecryptor; use crate::error::*; use crate::login::EncryptedLogin; use crate::util; +use db_crypto::EncryptorDecryptor; use rusqlite::Row; use std::time::SystemTime; use sync15::bso::{IncomingBso, IncomingKind}; diff --git a/components/logins/src/sync/payload.rs b/components/logins/src/sync/payload.rs index d3c6eddee89..15244cdad3d 100644 --- a/components/logins/src/sync/payload.rs +++ b/components/logins/src/sync/payload.rs @@ -7,11 +7,11 @@ // This struct is used for fetching/sending login records to the server. There are a number // of differences between this and the top-level Login struct; some fields are renamed, some are // locally encrypted, etc. -use crate::encryption::EncryptorDecryptor; use crate::error::*; use crate::login::ValidateAndFixup; use crate::SecureLoginFields; use crate::{EncryptedLogin, LoginEntry, LoginFields, LoginMeta}; +use db_crypto::EncryptorDecryptor; use serde_derive::*; use sync15::bso::OutgoingBso; use sync_guid::Guid; diff --git a/components/logins/src/sync/update_plan.rs b/components/logins/src/sync/update_plan.rs index 3769614a4e9..ef145794c27 100644 --- a/components/logins/src/sync/update_plan.rs +++ b/components/logins/src/sync/update_plan.rs @@ -4,9 +4,9 @@ use super::merge::{LocalLogin, MirrorLogin}; use super::{IncomingLogin, SyncStatus}; -use crate::encryption::EncryptorDecryptor; use crate::error::*; use crate::util; +use db_crypto::EncryptorDecryptor; use interrupt_support::SqlInterruptScope; use rusqlite::{named_params, Connection}; use std::time::SystemTime; diff --git a/components/support/encryption/Cargo.toml b/components/support/db-crypto/Cargo.toml similarity index 92% rename from components/support/encryption/Cargo.toml rename to components/support/db-crypto/Cargo.toml index b7c4a61a862..e401a5acf22 100644 --- a/components/support/encryption/Cargo.toml +++ b/components/support/db-crypto/Cargo.toml @@ -1,8 +1,9 @@ [package] -name = "encryption" +name = "db-crypto" version = "0.1.0" edition = "2021" authors = ["Naomi Kirby "] +description = "Database cryptography support library" license = "MPL-2.0" [features] diff --git a/components/support/encryption/android/build.gradle b/components/support/db-crypto/android/build.gradle similarity index 71% rename from components/support/encryption/android/build.gradle rename to components/support/db-crypto/android/build.gradle index b7ac211ac67..0e2c0021558 100644 --- a/components/support/encryption/android/build.gradle +++ b/components/support/db-crypto/android/build.gradle @@ -2,9 +2,9 @@ apply from: "$appServicesRootDir/build-scripts/component-common.gradle" apply from: "$publishDir/publish.gradle" android { - namespace 'org.mozilla.appservices.encryption' + namespace 'org.mozilla.appservices.dbcrypto' } -ext.configureUniFFIBindgen("encryption") +ext.configureUniFFIBindgen("dbcrypto") ext.dependsOnTheMegazord() ext.configurePublish(appServicesGroupId, project.name, project.ext.description) diff --git a/components/support/encryption/android/proguard-rules.pro b/components/support/db-crypto/android/proguard-rules.pro similarity index 100% rename from components/support/encryption/android/proguard-rules.pro rename to components/support/db-crypto/android/proguard-rules.pro diff --git a/components/support/encryption/android/src/main/AndroidManifest.xml b/components/support/db-crypto/android/src/main/AndroidManifest.xml similarity index 100% rename from components/support/encryption/android/src/main/AndroidManifest.xml rename to components/support/db-crypto/android/src/main/AndroidManifest.xml diff --git a/components/support/encryption/build.rs b/components/support/db-crypto/build.rs similarity index 76% rename from components/support/encryption/build.rs rename to components/support/db-crypto/build.rs index 523c9cbf2aa..f7850440642 100644 --- a/components/support/encryption/build.rs +++ b/components/support/db-crypto/build.rs @@ -4,5 +4,5 @@ */ fn main() { - uniffi::generate_scaffolding("./src/encryption.udl").unwrap(); + uniffi::generate_scaffolding("./src/db_crypto.udl").unwrap(); } diff --git a/components/support/encryption/src/encryption.udl b/components/support/db-crypto/src/db_crypto.udl similarity index 88% rename from components/support/encryption/src/encryption.udl rename to components/support/db-crypto/src/db_crypto.udl index 6db7c4fd9ba..26a587f894d 100644 --- a/components/support/encryption/src/encryption.udl +++ b/components/support/db-crypto/src/db_crypto.udl @@ -2,27 +2,27 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -namespace encryption { +namespace db_crypto { /// We expose the crypto primitives on the namespace /// Create a new, random, encryption key. - [Throws=EncryptionApiError] + [Throws=DbCryptoApiError] string create_key(); /// Create a "canary" string, which can be used to test if the encryption //key is still valid for the logins data - [Throws=EncryptionApiError] + [Throws=DbCryptoApiError] string create_canary([ByRef]string text, [ByRef]string encryption_key); /// Check that key is still valid using the output of `create_canary`. //`text` much match the text you initially passed to `create_canary()` - [Throws=EncryptionApiError] + [Throws=DbCryptoApiError] boolean check_canary([ByRef]string canary, [ByRef]string text, [ByRef]string encryption_key); }; /// These are the errors returned by our public API. [Error] -interface EncryptionApiError { +interface DbCryptoApiError { /// NSS not initialized. NSSUninitialized(); @@ -54,21 +54,21 @@ interface EncryptionApiError { /// because the consuming app can not reasonably take any action to resolve it. /// The underlying error will have been logged and reported. /// (ideally would just be `Unexpected`, but that would be a breaking change) - UnexpectedEncryptionApiError(string reason); + UnexpectedDbCryptoApiError(string reason); }; [Trait, WithForeign] interface EncryptorDecryptor { - [Throws=EncryptionApiError] + [Throws=DbCryptoApiError] bytes encrypt(bytes cleartext); - [Throws=EncryptionApiError] + [Throws=DbCryptoApiError] bytes decrypt(bytes ciphertext); }; [Trait, WithForeign] interface KeyManager { - [Throws=EncryptionApiError] + [Throws=DbCryptoApiError] bytes get_key(); }; diff --git a/components/support/encryption/src/encryption.rs b/components/support/db-crypto/src/encryption.rs similarity index 90% rename from components/support/encryption/src/encryption.rs rename to components/support/db-crypto/src/encryption.rs index c8982b88b96..d28a414fadc 100644 --- a/components/support/encryption/src/encryption.rs +++ b/components/support/db-crypto/src/encryption.rs @@ -3,14 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -// This is the *local* encryption support - it has nothing to do with the -// encryption used by sync. +// This is the *local* database encryption support - it has nothing to do +// with the encryption used by sync. // For context, what "local encryption" means in this context is: // * We use regular sqlite, but ensure that sensitive data is encrypted in the DB in the // `secure_fields` column. The encryption key is managed by the app. -// * The `decrypt_struct` and `encrypt_struct` functions are used to convert between an encrypted -// `secure_fields` string and a decrypted `SecureFields` struct +// * The `decrypt` and `encrypt` functions are used to convert between an encrypted +// string and a decrypted string. // * Most API functions return `EncryptedLogin` which has its data encrypted. // // This makes life tricky for Sync - sync has its own encryption and its own @@ -27,7 +27,7 @@ // low level sync code. // To make life a little easier, we do that via a struct. // -// Consumers of the Login component have 3 options for setting up encryption: +// Consumers of the an encrypted database have 3 options for setting up encryption: // 1. Implement EncryptorDecryptor directly // eg `LoginStore::new(MyEncryptorDecryptor)` // 2. Implement KeyManager and use ManagedEncryptorDecryptor @@ -112,20 +112,20 @@ impl EncryptorDecryptor for ManagedEncryptorDecryptor { let keybytes = self .key_manager .get_key() - .map_err(|_| EncryptionApiError::MissingKey)?; - let key = std::str::from_utf8(&keybytes).map_err(|_| EncryptionApiError::InvalidKey)?; + .map_err(|_| DbCryptoApiError::MissingKey)?; + let key = std::str::from_utf8(&keybytes).map_err(|_| DbCryptoApiError::InvalidKey)?; let encdec = jwcrypto::EncryptorDecryptor::new(key) - .map_err(|_: jwcrypto::JwCryptoError| EncryptionApiError::InvalidKey)?; + .map_err(|_: jwcrypto::JwCryptoError| DbCryptoApiError::InvalidKey)?; let cleartext = - std::str::from_utf8(&clearbytes).map_err(|e| EncryptionApiError::EncryptionFailed { + std::str::from_utf8(&clearbytes).map_err(|e| DbCryptoApiError::EncryptionFailed { reason: e.to_string(), })?; encdec .encrypt(cleartext) .map_err( - |e: jwcrypto::JwCryptoError| EncryptionApiError::EncryptionFailed { + |e: jwcrypto::JwCryptoError| DbCryptoApiError::EncryptionFailed { reason: e.to_string(), }, ) @@ -136,21 +136,20 @@ impl EncryptorDecryptor for ManagedEncryptorDecryptor { let keybytes = self .key_manager .get_key() - .map_err(|_| EncryptionApiError::MissingKey)?; - let key = std::str::from_utf8(&keybytes).map_err(|_| EncryptionApiError::InvalidKey)?; + .map_err(|_| DbCryptoApiError::MissingKey)?; + let key = std::str::from_utf8(&keybytes).map_err(|_| DbCryptoApiError::InvalidKey)?; let encdec = jwcrypto::EncryptorDecryptor::new(key) - .map_err(|_: jwcrypto::JwCryptoError| EncryptionApiError::InvalidKey)?; + .map_err(|_: jwcrypto::JwCryptoError| DbCryptoApiError::InvalidKey)?; - let ciphertext = std::str::from_utf8(&cipherbytes).map_err(|e| { - EncryptionApiError::DecryptionFailed { + let ciphertext = + std::str::from_utf8(&cipherbytes).map_err(|e| DbCryptoApiError::DecryptionFailed { reason: e.to_string(), - } - })?; + })?; encdec .decrypt(ciphertext) .map_err( - |e: jwcrypto::JwCryptoError| EncryptionApiError::DecryptionFailed { + |e: jwcrypto::JwCryptoError| DbCryptoApiError::DecryptionFailed { reason: e.to_string(), }, ) @@ -213,26 +212,26 @@ pub trait PrimaryPasswordAuthenticator: Send + Sync { /// # Examples /// ```no_run /// use async_trait::async_trait; -/// use encryption::KeyManager; -/// use encryption::{PrimaryPasswordAuthenticator, EncryptionApiError, NSSKeyManager}; +/// use db_crypto::KeyManager; +/// use db_crypto::{PrimaryPasswordAuthenticator, DbCryptoApiError, NSSKeyManager}; /// use std::sync::Arc; /// /// struct MyPrimaryPasswordAuthenticator {} /// /// #[async_trait] /// impl PrimaryPasswordAuthenticator for MyPrimaryPasswordAuthenticator { -/// async fn get_primary_password(&self) -> Result { +/// async fn get_primary_password(&self) -> Result { /// // Most likely, you would want to prompt for a password. /// // let password = prompt_string("primary password").unwrap_or_default(); /// Ok("secret".to_string()) /// } /// -/// async fn on_authentication_success(&self) -> Result<(), EncryptionApiError> { +/// async fn on_authentication_success(&self) -> Result<(), DbCryptoApiError> { /// println!("success"); /// Ok(()) /// } /// -/// async fn on_authentication_failure(&self) -> Result<(), EncryptionApiError> { +/// async fn on_authentication_failure(&self) -> Result<(), DbCryptoApiError> { /// println!("this did not work, please try again:"); /// Ok(()) /// } @@ -276,7 +275,7 @@ impl NSSKeyManager { #[cfg(feature = "keydb")] fn api_authentication_with_primary_password_is_needed() -> ApiResult { authentication_with_primary_password_is_needed().map_err(|e: nss_as::Error| { - EncryptionApiError::NSSAuthenticationError { + DbCryptoApiError::NSSAuthenticationError { reason: e.to_string(), } }) @@ -286,7 +285,7 @@ fn api_authentication_with_primary_password_is_needed() -> ApiResult { #[cfg(feature = "keydb")] fn api_authenticate_with_primary_password(primary_password: &str) -> ApiResult { authenticate_with_primary_password(primary_password).map_err(|e: nss_as::Error| { - EncryptionApiError::NSSAuthenticationError { + DbCryptoApiError::NSSAuthenticationError { reason: e.to_string(), } }) @@ -332,7 +331,7 @@ impl KeyManager for NSSKeyManager { } let key = get_or_create_aes256_key(self.key_name.as_str()) - .map_err(|_| EncryptionApiError::MissingKey)?; + .map_err(|_| DbCryptoApiError::MissingKey)?; let mut bytes: Vec = Vec::new(); serde_json::to_writer( &mut bytes, @@ -351,7 +350,7 @@ pub fn create_canary(text: &str, key: &str) -> ApiResult { pub fn check_canary(canary: &str, text: &str, key: &str) -> ApiResult { let encdec = jwcrypto::EncryptorDecryptor::new(key) - .map_err(|_: jwcrypto::JwCryptoError| EncryptionApiError::InvalidKey)?; + .map_err(|_: jwcrypto::JwCryptoError| DbCryptoApiError::InvalidKey)?; Ok(encdec.check_canary(canary, text).unwrap_or(false)) } @@ -383,7 +382,7 @@ mod tests { let encdec = ManagedEncryptorDecryptor { key_manager }; assert!(matches!( encdec.encrypt("secret".as_bytes().into()).err().unwrap(), - EncryptionApiError::InvalidKey + DbCryptoApiError::InvalidKey )); } @@ -393,14 +392,14 @@ mod tests { struct MyKeyManager {} impl KeyManager for MyKeyManager { fn get_key(&self) -> ApiResult> { - Err(EncryptionApiError::MissingKey) + Err(DbCryptoApiError::MissingKey) } } let key_manager = Arc::new(MyKeyManager {}); let encdec = ManagedEncryptorDecryptor { key_manager }; assert!(matches!( encdec.encrypt("secret".as_bytes().into()).err().unwrap(), - EncryptionApiError::MissingKey + DbCryptoApiError::MissingKey )); } @@ -449,7 +448,7 @@ mod tests { let bad_key = "bad_key".to_owned(); assert!(matches!( check_canary(&canary, CANARY_TEXT, &bad_key).err().unwrap(), - EncryptionApiError::InvalidKey + DbCryptoApiError::InvalidKey )); } } diff --git a/components/support/encryption/src/error.rs b/components/support/db-crypto/src/error.rs similarity index 84% rename from components/support/encryption/src/error.rs rename to components/support/db-crypto/src/error.rs index 3e3d8a41d61..76bf4b2a61a 100644 --- a/components/support/encryption/src/error.rs +++ b/components/support/db-crypto/src/error.rs @@ -4,7 +4,7 @@ pub type Result = std::result::Result; // Functions which are part of the public API should use this Result. -pub type ApiResult = std::result::Result; +pub type ApiResult = std::result::Result; pub use error_support::{breadcrumb, handle_error, report_error}; pub use error_support::{debug, error, info, trace, warn}; @@ -14,7 +14,7 @@ use jwcrypto::JwCryptoError; // Errors we return via the public interface. #[derive(Debug, thiserror::Error)] -pub enum EncryptionApiError { +pub enum DbCryptoApiError { #[error("NSS not initialized")] NSSUninitialized, @@ -43,7 +43,7 @@ pub enum EncryptionApiError { Interrupted { reason: String }, #[error("Unexpected Error: {reason}")] - UnexpectedEncryptionApiError { reason: String }, + UnexpectedDbCryptoApiError { reason: String }, } /// Encryption error type @@ -67,7 +67,7 @@ pub enum Error { // Define how our internal errors are handled and converted to external errors // See `support/error/README.md` for how this works, especially the warning about PII. impl GetErrorHandling for Error { - type ExternalError = EncryptionApiError; + type ExternalError = DbCryptoApiError; fn get_error_handling(&self) -> ErrorHandling { // Unexpected errors that we report to Sentry. We should watch the reports for these @@ -75,27 +75,27 @@ impl GetErrorHandling for Error { // - Fix the underlying issue // - Add breadcrumbs or other context to help uncover the issue // - Decide that these are expected errors and move them to the above case - ErrorHandling::convert(EncryptionApiError::UnexpectedEncryptionApiError { + ErrorHandling::convert(DbCryptoApiError::UnexpectedDbCryptoApiError { reason: self.to_string(), }) - .report_error("encryption-unexpected") + .report_error("encdec-unexpected") } } // The bridged sync engine (`sync::bridge`) deals in `anyhow::Result`, as that's // what the `sync15` BridgedEngine traits use. This lets UniFFI map those errors // onto our public error type when the bridge methods are exposed via the UDL. -impl From for EncryptionApiError { +impl From for DbCryptoApiError { fn from(value: anyhow::Error) -> Self { - EncryptionApiError::UnexpectedEncryptionApiError { + DbCryptoApiError::UnexpectedDbCryptoApiError { reason: value.to_string(), } } } -impl From for EncryptionApiError { +impl From for DbCryptoApiError { fn from(error: uniffi::UnexpectedUniFFICallbackError) -> Self { - EncryptionApiError::UnexpectedEncryptionApiError { + DbCryptoApiError::UnexpectedDbCryptoApiError { reason: error.to_string(), } } diff --git a/components/support/encryption/src/lib.rs b/components/support/db-crypto/src/lib.rs similarity index 93% rename from components/support/encryption/src/lib.rs rename to components/support/db-crypto/src/lib.rs index 9dd64b94f23..fed43280bc8 100644 --- a/components/support/encryption/src/lib.rs +++ b/components/support/db-crypto/src/lib.rs @@ -13,7 +13,7 @@ mod encryption; pub use crate::encryption::{ EncryptorDecryptor, KeyManager, ManagedEncryptorDecryptor, StaticKeyManager, }; -uniffi::include_scaffolding!("encryption"); +uniffi::include_scaffolding!("db_crypto"); #[cfg(feature = "keydb")] pub use crate::encryption::{NSSKeyManager, PrimaryPasswordAuthenticator}; diff --git a/components/support/encryption/uniffi.toml b/components/support/db-crypto/uniffi.toml similarity index 53% rename from components/support/encryption/uniffi.toml rename to components/support/db-crypto/uniffi.toml index 1743e038695..5a2abda54f8 100644 --- a/components/support/encryption/uniffi.toml +++ b/components/support/db-crypto/uniffi.toml @@ -1,7 +1,7 @@ [bindings.kotlin] -package_name = "mozilla.appservices.encryption" +package_name = "mozilla.appservices.encdec" omit_checksums = true [bindings.swift] ffi_module_name = "MozillaRustComponents" -ffi_module_filename = "encryptionFFI" +ffi_module_filename = "encdecFFI" diff --git a/examples/sync-pass/Cargo.toml b/examples/sync-pass/Cargo.toml index 692e6e1abc6..a82d208aeea 100644 --- a/examples/sync-pass/Cargo.toml +++ b/examples/sync-pass/Cargo.toml @@ -13,7 +13,7 @@ path = "src/sync-pass.rs" [dev-dependencies] base64 = "0.21" logins = { path = "../../components/logins", features = ["keydb"] } -encryption = { path = "../../components/support/encryption", features = ["keydb"] } +db-crypto = { path = "../../components/support/db-crypto", features = ["keydb"] } sync15 = { path = "../../components/sync15" } sync-guid = { path = "../../components/support/guid" } log = "0.4" diff --git a/examples/sync-pass/src/sync-pass.rs b/examples/sync-pass/src/sync-pass.rs index fc18f3f5417..4af1ec083e2 100644 --- a/examples/sync-pass/src/sync-pass.rs +++ b/examples/sync-pass/src/sync-pass.rs @@ -7,8 +7,8 @@ use cli_support::fxa_creds::{get_default_fxa_config, CliFxa, SYNC_SCOPE}; use cli_support::prompt::{prompt_char, prompt_password, prompt_string, prompt_usize}; -use encryption::{ - EncryptionApiError, ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator, +use db_crypto::{ + DbCryptoApiError, ManagedEncryptorDecryptor, NSSKeyManager, PrimaryPasswordAuthenticator, }; use logins::{Login, LoginEntry, LoginStore, LoginsSyncEngine, ValidateAndFixup}; @@ -298,17 +298,17 @@ fn prompt_record_id(s: &LoginStore, action: &str) -> Result> { struct MyPrimaryPasswordAuthenticator {} #[async_trait] impl PrimaryPasswordAuthenticator for MyPrimaryPasswordAuthenticator { - async fn get_primary_password(&self) -> Result { + async fn get_primary_password(&self) -> Result { let password = prompt_password("primary password").unwrap_or_default(); Ok(password) } - async fn on_authentication_success(&self) -> Result<(), EncryptionApiError> { + async fn on_authentication_success(&self) -> Result<(), DbCryptoApiError> { println!("success"); Ok(()) } - async fn on_authentication_failure(&self) -> Result<(), EncryptionApiError> { + async fn on_authentication_failure(&self) -> Result<(), DbCryptoApiError> { println!("this did not work, please try again:"); Ok(()) } diff --git a/megazords/full/Cargo.toml b/megazords/full/Cargo.toml index f5b91c1752b..a136d6d59c4 100644 --- a/megazords/full/Cargo.toml +++ b/megazords/full/Cargo.toml @@ -40,7 +40,7 @@ mozilla-central-workspace-hack = { version = "0.1", features = ["megazord"], opt # NSS link chain. The megazord cdylib produces a self-contained Rust artifact # and needs static mozpkix + pure_virtual on top of the NSS dylibs. rc_crypto = { path = "../../components/support/rc_crypto" } -encryption = { path = "../../components/support/encryption" } +db-crypto = { path = "../../components/support/db-crypto" } [features] mozbuild-rustlib = ["rc_crypto/mozbuild-rustlib"] diff --git a/megazords/full/src/lib.rs b/megazords/full/src/lib.rs index 5f553483342..369bdd676af 100644 --- a/megazords/full/src/lib.rs +++ b/megazords/full/src/lib.rs @@ -12,7 +12,7 @@ use std::os::raw::c_char; pub use ads_client; pub use autofill; pub use crashtest; -pub use encryption; +pub use db_crypto; pub use error_support; pub use fxa_client; pub use init_rust_components; diff --git a/megazords/ios-rust/Cargo.toml b/megazords/ios-rust/Cargo.toml index dc340be9995..faa4ea21e24 100644 --- a/megazords/ios-rust/Cargo.toml +++ b/megazords/ios-rust/Cargo.toml @@ -23,7 +23,7 @@ places = { path = "../../components/places" } remote_settings = { path = "../../components/remote_settings", features=["telemetry-submission"] } suggest = { path = "../../components/suggest" } sync15 = { path = "../../components/sync15" } -encryption = { path = "../../components/support/encryption" } +db-crypto = { path = "../../components/support/db-crypto" } error-support = { path = "../../components/support/error" } tracing-support = { path = "../../components/support/tracing" } sync_manager = { path = "../../components/sync_manager" } diff --git a/megazords/ios-rust/src/lib.rs b/megazords/ios-rust/src/lib.rs index 95597f727a7..bff1167ffb9 100644 --- a/megazords/ios-rust/src/lib.rs +++ b/megazords/ios-rust/src/lib.rs @@ -10,7 +10,7 @@ pub use as_ohttp_client; pub use autofill; pub use context_id; pub use crashtest; -pub use encryption; +pub use db_crypto; pub use error_support; pub use fxa_client; pub use init_rust_components; diff --git a/testing/sync-test/src/auth.rs b/testing/sync-test/src/auth.rs index a24d60642f3..e619d777281 100644 --- a/testing/sync-test/src/auth.rs +++ b/testing/sync-test/src/auth.rs @@ -4,10 +4,10 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ use anyhow::Result; use autofill::db::store::Store as AutofillStore; use cli_support::fxa_creds::CliFxa; -use fxa_client::{Device, FxaConfig, FxaServer}; -use logins::encryption::{ +use db_crypto::{ create_key, EncryptorDecryptor, ManagedEncryptorDecryptor, StaticKeyManager, }; +use fxa_client::{Device, FxaConfig, FxaServer}; use logins::LoginStore; use std::collections::{hash_map::RandomState, HashMap}; use std::sync::Arc; diff --git a/testing/sync-test/src/logins.rs b/testing/sync-test/src/logins.rs index de478811d60..3ccc120fc7a 100644 --- a/testing/sync-test/src/logins.rs +++ b/testing/sync-test/src/logins.rs @@ -4,8 +4,8 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ use crate::auth::TestClient; use crate::testing::TestGroup; use anyhow::Result; +use db_crypto::{create_key, ManagedEncryptorDecryptor, StaticKeyManager}; use logins::{ - encryption::{create_key, ManagedEncryptorDecryptor, StaticKeyManager}, ApiResult as LoginResult, Login, LoginEntry, LoginStore, }; use std::sync::Arc; From ffde274ef611fea322598cf7952b4464e27b56b2 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 26 Aug 2026 18:07:58 -0700 Subject: [PATCH 27/32] Fix sync-test and lint --- Cargo.lock | 1 + testing/sync-test/Cargo.toml | 1 + testing/sync-test/src/auth.rs | 4 +--- testing/sync-test/src/logins.rs | 4 +--- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e95dd3ddc6f..b1f6a131b5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4310,6 +4310,7 @@ dependencies = [ "base64 0.21.2", "clap", "cli-support", + "db-crypto", "env_logger", "fxa-client", "interrupt-support", diff --git a/testing/sync-test/Cargo.toml b/testing/sync-test/Cargo.toml index 817f46d93cd..62084f01e9b 100644 --- a/testing/sync-test/Cargo.toml +++ b/testing/sync-test/Cargo.toml @@ -30,3 +30,4 @@ serde_json = "1.0" base64 = "0.21" cli-support = { path = "../../examples/cli-support" } nss-as = { path = "../../components/support/rc_crypto/nss" } +db-crypto = { path = "../../components/support/db-crypto" } diff --git a/testing/sync-test/src/auth.rs b/testing/sync-test/src/auth.rs index e619d777281..a78d3c91102 100644 --- a/testing/sync-test/src/auth.rs +++ b/testing/sync-test/src/auth.rs @@ -4,9 +4,7 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ use anyhow::Result; use autofill::db::store::Store as AutofillStore; use cli_support::fxa_creds::CliFxa; -use db_crypto::{ - create_key, EncryptorDecryptor, ManagedEncryptorDecryptor, StaticKeyManager, -}; +use db_crypto::{create_key, EncryptorDecryptor, ManagedEncryptorDecryptor, StaticKeyManager}; use fxa_client::{Device, FxaConfig, FxaServer}; use logins::LoginStore; use std::collections::{hash_map::RandomState, HashMap}; diff --git a/testing/sync-test/src/logins.rs b/testing/sync-test/src/logins.rs index 3ccc120fc7a..d0e1f614176 100644 --- a/testing/sync-test/src/logins.rs +++ b/testing/sync-test/src/logins.rs @@ -5,9 +5,7 @@ use crate::auth::TestClient; use crate::testing::TestGroup; use anyhow::Result; use db_crypto::{create_key, ManagedEncryptorDecryptor, StaticKeyManager}; -use logins::{ - ApiResult as LoginResult, Login, LoginEntry, LoginStore, -}; +use logins::{ApiResult as LoginResult, Login, LoginEntry, LoginStore}; use std::sync::Arc; use std::{collections::hash_map::RandomState, collections::HashMap}; From 9a43954c5d0aa5744b21e0d626734663e7eccacb Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 26 Aug 2026 18:19:29 -0700 Subject: [PATCH 28/32] Add a changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3751eee441b..67facd3c5d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ [Full Changelog](In progress) +### Logins + +- Refactor the database encryption support into a new support crate `db-crypto`, which provides all the same functions and traits as were previously available in `logins::encryption` module. However, this leads to two breaking changes: the functionality has been moved into a new `db_crypto` UniFFI namespace, and the error type has changed from `LoginsApiError` to `DbCryptoApiError`. ([#7542](https://github.com/mozilla/application-services/pull/7542)) + # v156.0 (_2026-08-27_) ## ✨ What's Changed ✨ From ef101dd8c71e1b3dc673346babae5cdcbb1a431c Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 26 Aug 2026 18:22:42 -0700 Subject: [PATCH 29/32] Fix external UniFFI reference --- .buildconfig-android.yml | 6 +++--- components/logins/android/build.gradle | 2 +- components/logins/src/logins.udl | 4 ++-- components/support/db-crypto/build.rs | 2 +- .../support/db-crypto/src/{db_crypto.udl => db-crypto.udl} | 0 components/support/db-crypto/src/lib.rs | 2 +- components/support/db-crypto/uniffi.toml | 4 ++-- megazords/fenix-dylib/megazord_stub.c | 3 ++- 8 files changed, 12 insertions(+), 11 deletions(-) rename components/support/db-crypto/src/{db_crypto.udl => db-crypto.udl} (100%) diff --git a/.buildconfig-android.yml b/.buildconfig-android.yml index 03903ddd844..41f5ad768f6 100644 --- a/.buildconfig-android.yml +++ b/.buildconfig-android.yml @@ -166,10 +166,10 @@ projects: type: aar description: Client for Firefox Relay. - dbcrypto: + db-crypto: path: components/support/db-crypto/android - artifactId: dbcrypto + artifactId: db-crypto publications: - - name: dbcrypto + - name: db-crypto type: aar description: Database encryption support diff --git a/components/logins/android/build.gradle b/components/logins/android/build.gradle index 7acd48a6b64..9608bd29ac7 100644 --- a/components/logins/android/build.gradle +++ b/components/logins/android/build.gradle @@ -44,7 +44,7 @@ dependencies { // Part of the public API. api project(':init_rust_components') api project(':sync15') - api project(':dbcrypto') + api project(':db-crypto') implementation project(':init_rust_components') diff --git a/components/logins/src/logins.udl b/components/logins/src/logins.udl index 33a65d3c39f..739fbd678cd 100644 --- a/components/logins/src/logins.udl +++ b/components/logins/src/logins.udl @@ -170,10 +170,10 @@ interface LoginsApiError { UnexpectedLoginsApiError(string reason); }; -[External = "encryption"] +[External = "db_crypto"] typedef trait_with_foreign EncryptorDecryptor; -[External = "encryption"] +[External = "db_crypto"] typedef trait_with_foreign KeyManager; interface LoginStore { diff --git a/components/support/db-crypto/build.rs b/components/support/db-crypto/build.rs index f7850440642..a516019b5a6 100644 --- a/components/support/db-crypto/build.rs +++ b/components/support/db-crypto/build.rs @@ -4,5 +4,5 @@ */ fn main() { - uniffi::generate_scaffolding("./src/db_crypto.udl").unwrap(); + uniffi::generate_scaffolding("./src/db-crypto.udl").unwrap(); } diff --git a/components/support/db-crypto/src/db_crypto.udl b/components/support/db-crypto/src/db-crypto.udl similarity index 100% rename from components/support/db-crypto/src/db_crypto.udl rename to components/support/db-crypto/src/db-crypto.udl diff --git a/components/support/db-crypto/src/lib.rs b/components/support/db-crypto/src/lib.rs index fed43280bc8..60f384da850 100644 --- a/components/support/db-crypto/src/lib.rs +++ b/components/support/db-crypto/src/lib.rs @@ -13,7 +13,7 @@ mod encryption; pub use crate::encryption::{ EncryptorDecryptor, KeyManager, ManagedEncryptorDecryptor, StaticKeyManager, }; -uniffi::include_scaffolding!("db_crypto"); +uniffi::include_scaffolding!("db-crypto"); #[cfg(feature = "keydb")] pub use crate::encryption::{NSSKeyManager, PrimaryPasswordAuthenticator}; diff --git a/components/support/db-crypto/uniffi.toml b/components/support/db-crypto/uniffi.toml index 5a2abda54f8..65c7a0a5933 100644 --- a/components/support/db-crypto/uniffi.toml +++ b/components/support/db-crypto/uniffi.toml @@ -1,7 +1,7 @@ [bindings.kotlin] -package_name = "mozilla.appservices.encdec" +package_name = "mozilla.appservices.dbcrypto" omit_checksums = true [bindings.swift] ffi_module_name = "MozillaRustComponents" -ffi_module_filename = "encdecFFI" +ffi_module_filename = "db_cryptoFFI" diff --git a/megazords/fenix-dylib/megazord_stub.c b/megazords/fenix-dylib/megazord_stub.c index 246344d380b..5984bca6931 100644 --- a/megazords/fenix-dylib/megazord_stub.c +++ b/megazords/fenix-dylib/megazord_stub.c @@ -13,7 +13,7 @@ extern int MOZ_EXPORT ffi_ads_client_uniffi_contract_version(); extern int MOZ_EXPORT ffi_autofill_uniffi_contract_version(); extern int MOZ_EXPORT ffi_crashtest_uniffi_contract_version(); -extern int MOZ_EXPORT ffi_encryption_uniffi_contract_version(); +extern int MOZ_EXPORT ffi_dbcrypto_uniffi_contract_version(); extern int MOZ_EXPORT ffi_fxa_client_uniffi_contract_version(); extern int MOZ_EXPORT ffi_init_rust_components_uniffi_contract_version(); extern int MOZ_EXPORT ffi_logins_uniffi_contract_version(); @@ -78,6 +78,7 @@ void _local_megazord_dummy_symbol() { ffi_ads_client_uniffi_contract_version(); ffi_autofill_uniffi_contract_version(); ffi_crashtest_uniffi_contract_version(); + ffi_dbcrypto_uniffi_contract_version(); ffi_fxa_client_uniffi_contract_version(); ffi_init_rust_components_uniffi_contract_version(); ffi_logins_uniffi_contract_version(); From cf77c1e5fabbc36cc1b6e307808cb4ad2e2cbdfc Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 27 Aug 2026 14:14:05 -0700 Subject: [PATCH 30/32] I am running out of ideas on this android UniFFI thing --- .../java/mozilla/appservices/logins/DatabaseLoginsStorage.kt | 2 +- .../mozilla/appservices/logins/DatabaseLoginsStorageTest.kt | 2 +- components/support/db-crypto/android/build.gradle | 4 ++-- components/support/db-crypto/uniffi.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt b/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt index c92c27d6561..f8a9f86b118 100644 --- a/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt +++ b/components/logins/android/src/main/java/mozilla/appservices/logins/DatabaseLoginsStorage.kt @@ -13,7 +13,7 @@ package mozilla.appservices.logins * on version updates. */ -import mozilla.appservices.dbcrypto.KeyManager +import mozilla.appservices.db_crypto.KeyManager import mozilla.telemetry.glean.private.CounterMetricType import mozilla.telemetry.glean.private.LabeledMetricType import org.mozilla.appservices.logins.GleanMetrics.LoginsStore as LoginsStoreMetrics diff --git a/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt b/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt index 2ced44544ec..c05465c099e 100644 --- a/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt +++ b/components/logins/android/src/test/java/mozilla/appservices/logins/DatabaseLoginsStorageTest.kt @@ -6,7 +6,7 @@ package mozilla.appservices.logins import androidx.test.core.app.ApplicationProvider import mozilla.appservices.RustComponentsInitializer -import mozilla.appservices.dbcrypto.KeyManager +import mozilla.appservices.db_crypto.KeyManager import mozilla.appservices.syncmanager.SyncManager import mozilla.telemetry.glean.testing.GleanTestRule import org.junit.Assert.assertEquals diff --git a/components/support/db-crypto/android/build.gradle b/components/support/db-crypto/android/build.gradle index 0e2c0021558..9afec5d5f69 100644 --- a/components/support/db-crypto/android/build.gradle +++ b/components/support/db-crypto/android/build.gradle @@ -2,9 +2,9 @@ apply from: "$appServicesRootDir/build-scripts/component-common.gradle" apply from: "$publishDir/publish.gradle" android { - namespace 'org.mozilla.appservices.dbcrypto' + namespace 'org.mozilla.appservices.db_crypto' } -ext.configureUniFFIBindgen("dbcrypto") +ext.configureUniFFIBindgen("db_crypto") ext.dependsOnTheMegazord() ext.configurePublish(appServicesGroupId, project.name, project.ext.description) diff --git a/components/support/db-crypto/uniffi.toml b/components/support/db-crypto/uniffi.toml index 65c7a0a5933..0cddacfb6ef 100644 --- a/components/support/db-crypto/uniffi.toml +++ b/components/support/db-crypto/uniffi.toml @@ -1,5 +1,5 @@ [bindings.kotlin] -package_name = "mozilla.appservices.dbcrypto" +package_name = "mozilla.appservices.db_crypto" omit_checksums = true [bindings.swift] From be4f0a1e91d1d0fb98c83c6ceacdf19db4587a4a Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 27 Aug 2026 20:23:15 -0700 Subject: [PATCH 31/32] Fix login tests after rebase --- components/logins/src/store.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/logins/src/store.rs b/components/logins/src/store.rs index 7e511766b3b..ab9ef921347 100644 --- a/components/logins/src/store.rs +++ b/components/logins/src/store.rs @@ -419,7 +419,7 @@ impl Default for RunMaintenanceOptions { #[cfg(test)] mod tests { use super::*; - use crate::encryption::{create_key, KeyManager, ManagedEncryptorDecryptor}; + use db_crypto::{create_key, KeyManager, ManagedEncryptorDecryptor}; use crate::util; use nss_as::ensure_initialized; use std::cmp::Reverse; @@ -614,7 +614,7 @@ mod tests { } impl KeyManager for CountingKeyManager { - fn get_key(&self) -> ApiResult> { + fn get_key(&self) -> db_crypto::ApiResult> { self.calls.fetch_add(1, Ordering::SeqCst); Ok(self.key.as_bytes().into()) } From b110afdbe1b614a675c7f0f9df5aa2ec9275e192 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 27 Aug 2026 20:45:31 -0700 Subject: [PATCH 32/32] Fix formatting --- components/logins/src/store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/logins/src/store.rs b/components/logins/src/store.rs index ab9ef921347..05d4af7d84d 100644 --- a/components/logins/src/store.rs +++ b/components/logins/src/store.rs @@ -419,8 +419,8 @@ impl Default for RunMaintenanceOptions { #[cfg(test)] mod tests { use super::*; - use db_crypto::{create_key, KeyManager, ManagedEncryptorDecryptor}; use crate::util; + use db_crypto::{create_key, KeyManager, ManagedEncryptorDecryptor}; use nss_as::ensure_initialized; use std::cmp::Reverse; use std::sync::atomic::{AtomicUsize, Ordering};