diff --git a/Cargo.lock b/Cargo.lock index 04f97f266..746f84176 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -525,6 +525,8 @@ dependencies = [ "base64", "bitwarden-core", "bitwarden-crypto", + "bitwarden-error", + "bitwarden-iter", "bitwarden-vault", "chrono", "coset", @@ -537,8 +539,11 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.12", + "tsify", "uniffi", "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", ] [[package]] @@ -582,6 +587,15 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "bitwarden-iter" +version = "1.0.0" +dependencies = [ + "futures", + "wasm-bindgen", + "wasm-bindgen-futures", +] + [[package]] name = "bitwarden-send" version = "1.0.0" @@ -737,6 +751,7 @@ dependencies = [ "bitwarden-core", "bitwarden-crypto", "bitwarden-error", + "bitwarden-iter", "bitwarden-state", "bitwarden-test", "chrono", @@ -770,14 +785,17 @@ dependencies = [ "bitwarden-crypto", "bitwarden-error", "bitwarden-exporters", + "bitwarden-fido", "bitwarden-generators", "bitwarden-ipc", + "bitwarden-iter", "bitwarden-ssh", "bitwarden-state", "bitwarden-threading", "bitwarden-vault", "console_error_panic_hook", "console_log", + "js-sys", "log", "serde", "tsify", diff --git a/Cargo.toml b/Cargo.toml index 778a91e39..242f4c211 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ bitwarden-exporters = { path = "crates/bitwarden-exporters", version = "=1.0.0" bitwarden-fido = { path = "crates/bitwarden-fido", version = "=1.0.0" } bitwarden-generators = { path = "crates/bitwarden-generators", version = "=1.0.0" } bitwarden-ipc = { path = "crates/bitwarden-ipc", version = "=1.0.0" } +bitwarden-iter = { path = "crates/bitwarden-iter", version = "=1.0.0" } bitwarden-send = { path = "crates/bitwarden-send", version = "=1.0.0" } bitwarden-sm = { path = "bitwarden_license/bitwarden-sm", version = "=1.0.0" } bitwarden-ssh = { path = "crates/bitwarden-ssh", version = "=1.0.0" } diff --git a/crates/bitwarden-fido/Cargo.toml b/crates/bitwarden-fido/Cargo.toml index 0f880a945..2382a12fb 100644 --- a/crates/bitwarden-fido/Cargo.toml +++ b/crates/bitwarden-fido/Cargo.toml @@ -16,12 +16,20 @@ keywords.workspace = true [features] uniffi = ["dep:uniffi", "bitwarden-core/uniffi", "bitwarden-vault/uniffi"] +wasm = [ + "dep:wasm-bindgen", + "dep:wasm-bindgen-futures", + "dep:tsify", + "bitwarden-vault/wasm", +] [dependencies] async-trait = { workspace = true } base64 = ">=0.22.1, <0.23" bitwarden-core = { workspace = true } bitwarden-crypto = { workspace = true } +bitwarden-error = { workspace = true } +bitwarden-iter = { workspace = true } bitwarden-vault = { workspace = true } chrono = { workspace = true } coset = ">=0.3.7, <0.4" @@ -38,6 +46,9 @@ serde_json = { workspace = true } thiserror = { workspace = true } uniffi = { workspace = true, optional = true } uuid = { workspace = true } +tsify = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } +wasm-bindgen-futures = { workspace = true, optional = true } [lints] workspace = true diff --git a/crates/bitwarden-fido/src/authenticator.rs b/crates/bitwarden-fido/src/authenticator.rs index 8216bb16b..2fba5b7f2 100644 --- a/crates/bitwarden-fido/src/authenticator.rs +++ b/crates/bitwarden-fido/src/authenticator.rs @@ -2,7 +2,9 @@ use std::sync::Mutex; use bitwarden_core::{Client, VaultLockedError}; use bitwarden_crypto::CryptoError; -use bitwarden_vault::{CipherError, CipherView, EncryptionContext}; +use bitwarden_error::bitwarden_error; +use bitwarden_iter::BwIterator; +use bitwarden_vault::{CipherError, CipherView, DecryptError, EncryptionContext}; use itertools::Itertools; use log::error; use passkey::{ @@ -84,8 +86,11 @@ pub enum SilentlyDiscoverCredentialsError { } #[allow(missing_docs)] +#[bitwarden_error(flat)] #[derive(Debug, Error)] pub enum CredentialsForAutofillError { + #[error(transparent)] + DecryptError(#[from] DecryptError), #[error(transparent)] CipherError(#[from] CipherError), #[error(transparent)] @@ -291,6 +296,29 @@ impl<'a> Fido2Authenticator<'a> { .collect() } + #[allow(missing_docs)] + pub async fn credentials_for_autofill_stream( + &mut self, + ) -> Result< + BwIterator>, + CredentialsForAutofillError, + > { + let all_credentials = self.credential_store.all_credentials_stream().await?; + + let iter = all_credentials + .into_iter() + .map( + |cipher| -> Result, CredentialsForAutofillError> { + Ok(Fido2CredentialAutofillView::from_cipher_list_view( + &cipher?, + )?) + }, + ) + .flatten_ok(); + + Ok(BwIterator::new(iter)) + } + pub(super) fn get_authenticator( &self, create_credential: bool, diff --git a/crates/bitwarden-fido/src/lib.rs b/crates/bitwarden-fido/src/lib.rs index 79a431daa..da3eecd41 100644 --- a/crates/bitwarden-fido/src/lib.rs +++ b/crates/bitwarden-fido/src/lib.rs @@ -20,6 +20,11 @@ mod client_fido; mod crypto; mod traits; mod types; + +#[cfg(feature = "wasm")] +#[allow(missing_docs)] +pub mod wasm; + pub use authenticator::{ CredentialsForAutofillError, Fido2Authenticator, GetAssertionError, MakeCredentialError, SilentlyDiscoverCredentialsError, diff --git a/crates/bitwarden-fido/src/traits.rs b/crates/bitwarden-fido/src/traits.rs index 7a5e5d071..4e1206c29 100644 --- a/crates/bitwarden-fido/src/traits.rs +++ b/crates/bitwarden-fido/src/traits.rs @@ -1,4 +1,6 @@ -use bitwarden_vault::{CipherListView, CipherView, EncryptionContext, Fido2CredentialNewView}; +use bitwarden_vault::{ + CipherListView, CipherListViewIterator, CipherView, EncryptionContext, Fido2CredentialNewView, +}; use passkey::authenticator::UIHint; use thiserror::Error; @@ -47,6 +49,8 @@ pub trait Fido2CredentialStore: Send + Sync { async fn all_credentials(&self) -> Result, Fido2CallbackError>; async fn save_credential(&self, cred: EncryptionContext) -> Result<(), Fido2CallbackError>; + + async fn all_credentials_stream(&self) -> Result; } #[allow(missing_docs)] diff --git a/crates/bitwarden-fido/src/types.rs b/crates/bitwarden-fido/src/types.rs index 5d69e6436..c320b7a07 100644 --- a/crates/bitwarden-fido/src/types.rs +++ b/crates/bitwarden-fido/src/types.rs @@ -8,6 +8,8 @@ use passkey::types::webauthn::UserVerificationRequirement; use reqwest::Url; use serde::{Deserialize, Serialize}; use thiserror::Error; +#[cfg(feature = "wasm")] +use tsify::Tsify; use super::{ get_enum_from_string_name, string_to_guid_bytes, InvalidGuid, SelectedCredential, UnknownEnum, @@ -18,6 +20,7 @@ use super::{ #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct Fido2CredentialAutofillView { pub credential_id: Vec, pub cipher_id: uuid::Uuid, diff --git a/crates/bitwarden-fido/src/wasm.rs b/crates/bitwarden-fido/src/wasm.rs new file mode 100644 index 000000000..87575d63b --- /dev/null +++ b/crates/bitwarden-fido/src/wasm.rs @@ -0,0 +1,41 @@ +use bitwarden_iter::BwIterator; +use bitwarden_vault::CipherListViewIterator; +use itertools::Itertools; +use wasm_bindgen::prelude::*; + +use crate::{CredentialsForAutofillError, Fido2CredentialAutofillView}; + +#[wasm_bindgen] +#[allow(missing_docs)] +pub struct Fido2CredentialAutofillViewIterator( + BwIterator>, +); + +#[wasm_bindgen] +impl Fido2CredentialAutofillViewIterator { + #[allow(missing_docs)] + pub fn next(&mut self) -> Option { + // Simplify the return because wasm_bindgen doesn't like Option> and I'm just + // proving the concept + self.0.iter.next().transpose().ok().flatten() + } +} + +#[wasm_bindgen] +#[allow(missing_docs)] +pub async fn credentials_for_autofill_stream( + all_credentials: CipherListViewIterator, +) -> Result { + let iter = all_credentials + .into_iter() + .map( + |cipher| -> Result, CredentialsForAutofillError> { + Ok(Fido2CredentialAutofillView::from_cipher_list_view( + &cipher?, + )?) + }, + ) + .flatten_ok(); + + Ok(Fido2CredentialAutofillViewIterator(BwIterator::new(iter))) +} diff --git a/crates/bitwarden-iter/.cargo/config b/crates/bitwarden-iter/.cargo/config new file mode 100644 index 000000000..4ec2f3b86 --- /dev/null +++ b/crates/bitwarden-iter/.cargo/config @@ -0,0 +1,2 @@ +[target.wasm32-unknown-unknown] +runner = 'wasm-bindgen-test-runner' diff --git a/crates/bitwarden-iter/Cargo.toml b/crates/bitwarden-iter/Cargo.toml new file mode 100644 index 000000000..85067f9f4 --- /dev/null +++ b/crates/bitwarden-iter/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "bitwarden-iter" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +keywords.workspace = true + +[package.metadata.cargo-udeps.ignore] +development = ["tokio-test"] # only used in doc-tests + +[features] +wasm = ["dep:wasm-bindgen", "dep:wasm-bindgen-futures"] + +[dependencies] +futures = "0.3.31" +wasm-bindgen = { workspace = true, optional = true } +wasm-bindgen-futures = { workspace = true, optional = true } + +[target.'cfg(target_arch="wasm32")'.dependencies] +wasm-bindgen-futures = { workspace = true, optional = true } + +[lints] +workspace = true diff --git a/crates/bitwarden-iter/README.md b/crates/bitwarden-iter/README.md new file mode 100644 index 000000000..44394dd1c --- /dev/null +++ b/crates/bitwarden-iter/README.md @@ -0,0 +1,3 @@ +# bitwarden-iter + +Utility crate for providing streamed calculation using iterators over Bitwarden data structures. diff --git a/crates/bitwarden-iter/src/iter.rs b/crates/bitwarden-iter/src/iter.rs new file mode 100644 index 000000000..813e4736f --- /dev/null +++ b/crates/bitwarden-iter/src/iter.rs @@ -0,0 +1,36 @@ +#![allow(missing_docs)] + +use std::pin::Pin; + +pub struct BwIterator { + pub iter: Box>, +} + +impl BwIterator { + pub fn new(iter: impl Iterator + 'static) -> Self { + Self { + iter: Box::new(iter), + } + } +} + +impl IntoIterator for BwIterator { + type Item = T; + type IntoIter = Box>; + + fn into_iter(self) -> Self::IntoIter { + self.iter + } +} + +pub struct BwStream { + pub stream: Pin>>, +} + +impl BwStream { + pub fn new(stream: impl futures::stream::Stream + 'static) -> Self { + Self { + stream: Box::pin(stream), + } + } +} diff --git a/crates/bitwarden-iter/src/lib.rs b/crates/bitwarden-iter/src/lib.rs new file mode 100644 index 000000000..aae79c993 --- /dev/null +++ b/crates/bitwarden-iter/src/lib.rs @@ -0,0 +1,7 @@ +#![doc = include_str!("../README.md")] + +mod iter; + +pub mod wasm; + +pub use iter::{BwIterator, BwStream}; diff --git a/crates/bitwarden-iter/src/wasm.rs b/crates/bitwarden-iter/src/wasm.rs new file mode 100644 index 000000000..8fa0e6853 --- /dev/null +++ b/crates/bitwarden-iter/src/wasm.rs @@ -0,0 +1,90 @@ +#![allow(missing_docs)] + +use std::pin::Pin; + +use futures::StreamExt; +use wasm_bindgen::prelude::*; + +// #[wasm_bindgen] +// impl IteratorClient { +// pub fn state(&self) -> IteratorClient { +// IteratorClient::new(self.0.clone()) +// } + +// pub fn create_js_iterator(&self) -> JsRustIterator { +// to_js_iterator(get_test_iteration()) +// } + +// pub fn create_js_async_iterator(&self) -> JsRustAsyncIterator { +// JsRustAsyncIterator::new( +// stream::iter(get_test_iteration()).then(|x| async move { async_operation(x).await }), +// ) +// } +// } + +// fn get_test_iteration() -> impl Iterator { +// (0..10).map(|x| x * 2) +// } + +// async fn async_operation(input: i32) -> i32 { +// input * 2 +// } + +// #[wasm_bindgen] +// pub fn into_iterable>() { + +// } + +#[wasm_bindgen] +pub struct JsRustAsyncIterator { + iter: Pin>>, +} + +impl JsRustAsyncIterator { + pub fn new(iter: impl futures::stream::Stream + 'static) -> Self { + Self { + iter: Box::pin(iter), + } + } +} + +#[wasm_bindgen] +impl JsRustAsyncIterator { + pub async fn next(&mut self) -> Option { + self.iter.next().await + } +} + +#[wasm_bindgen] +/// An iterable that wraps a Rust iterator and can be used in JavaScript +pub struct JsRustIterator { + iter: Box>, + // next_value: Option, +} + +impl JsRustIterator { + pub fn new(iter: impl Iterator + 'static) -> Self { + Self { + iter: Box::new(iter), + } + } +} + +#[wasm_bindgen] +impl JsRustIterator { + pub fn next(&mut self) -> Option { + self.iter.next() + } +} + +// extern "C" { +// #[wasm_bindgen(js_name = "getTestIteration")] +// fn get_test_iteration_js() -> js_sys::Iterator; +// } + +pub fn to_js_iterator(iter: impl Iterator + 'static) -> JsRustIterator { + JsRustIterator::new(iter) +} + +// #[wasm_bindgen] +// pub struct PlatformClient(Client); diff --git a/crates/bitwarden-uniffi/src/platform/fido2.rs b/crates/bitwarden-uniffi/src/platform/fido2.rs index 812d3ae6c..ee8c37ebe 100644 --- a/crates/bitwarden-uniffi/src/platform/fido2.rs +++ b/crates/bitwarden-uniffi/src/platform/fido2.rs @@ -7,7 +7,9 @@ use bitwarden_fido::{ PublicKeyCredentialAuthenticatorAttestationResponse, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; -use bitwarden_vault::{CipherListView, CipherView, EncryptionContext, Fido2CredentialNewView}; +use bitwarden_vault::{ + CipherListView, CipherListViewIterator, CipherView, EncryptionContext, Fido2CredentialNewView, +}; use crate::error::{Error, Result}; @@ -274,6 +276,12 @@ impl bitwarden_fido::Fido2CredentialStore for UniffiTraitBridge<&dyn Fido2Creden async fn save_credential(&self, cred: EncryptionContext) -> Result<(), BitFido2CallbackError> { self.0.save_credential(cred).await.map_err(Into::into) } + + async fn all_credentials_stream( + &self, + ) -> Result { + unimplemented!() + } } // Uniffi seems to have trouble generating code for Android when a local trait returns a type from diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index 5ceab6aaa..ac99e728e 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -18,13 +18,14 @@ keywords.workspace = true uniffi = [ "bitwarden-core/uniffi", "bitwarden-crypto/uniffi", - "dep:uniffi" + "dep:uniffi", ] # Uniffi bindings wasm = [ "bitwarden-core/wasm", + "bitwarden-iter/wasm", "dep:tsify", "dep:wasm-bindgen", - "dep:wasm-bindgen-futures" + "dep:wasm-bindgen-futures", ] # WASM support [dependencies] @@ -34,6 +35,7 @@ bitwarden-collections = { workspace = true, features = ["wasm"] } bitwarden-core = { workspace = true, features = ["internal"] } bitwarden-crypto = { workspace = true } bitwarden-error = { workspace = true } +bitwarden-iter = { workspace = true } bitwarden-state = { workspace = true } chrono = { workspace = true } data-encoding = ">=2.0, <3" diff --git a/crates/bitwarden-vault/src/cipher/cipher_client.rs b/crates/bitwarden-vault/src/cipher/cipher_client.rs index 45e9d846a..00bd0133d 100644 --- a/crates/bitwarden-vault/src/cipher/cipher_client.rs +++ b/crates/bitwarden-vault/src/cipher/cipher_client.rs @@ -1,5 +1,6 @@ use bitwarden_core::{key_management::SymmetricKeyId, Client, OrganizationId}; use bitwarden_crypto::{CompositeEncryptable, IdentifyKey, SymmetricCryptoKey}; +use bitwarden_iter::BwIterator; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; @@ -15,6 +16,54 @@ pub struct CiphersClient { pub(crate) client: Client, } +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[allow(missing_docs)] +pub struct CipherIterator(BwIterator); + +impl Into> for CipherIterator { + fn into(self) -> BwIterator { + self.0 + } +} + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[allow(missing_docs)] +pub struct CipherViewIterator(BwIterator>); + +impl Into>> for CipherViewIterator { + fn into(self) -> BwIterator> { + self.0 + } +} + +impl IntoIterator for CipherViewIterator { + type Item = Result; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[allow(missing_docs)] +pub struct CipherListViewIterator(BwIterator>); + +impl Into>> for CipherListViewIterator { + fn into(self) -> BwIterator> { + self.0 + } +} + +impl IntoIterator for CipherListViewIterator { + type Item = Result; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + #[cfg_attr(feature = "wasm", wasm_bindgen)] impl CiphersClient { #[allow(missing_docs)] @@ -104,6 +153,16 @@ impl CiphersClient { Ok(cipher_view) } + #[allow(missing_docs)] + pub fn decrypt_stream(&self, cipher_iter: CipherIterator) -> CipherViewIterator { + let client = self.client.clone(); + let iter = cipher_iter.0.iter.map(move |cipher| { + let key_store = client.internal.get_key_store(); + key_store.decrypt(&cipher).map_err(DecryptError::from) + }); + CipherViewIterator(BwIterator::new(iter)) + } + #[allow(missing_docs)] pub fn decrypt_list(&self, ciphers: Vec) -> Result, DecryptError> { let key_store = self.client.internal.get_key_store(); @@ -111,6 +170,16 @@ impl CiphersClient { Ok(cipher_views) } + #[allow(missing_docs)] + pub fn decrypt_list_stream(&self, cipher_iter: CipherIterator) -> CipherListViewIterator { + let client = self.client.clone(); + let iter = cipher_iter.0.iter.map(move |cipher| { + let key_store = client.internal.get_key_store(); + key_store.decrypt(&cipher).map_err(DecryptError::from) + }); + CipherListViewIterator(BwIterator::new(iter)) + } + /// Decrypt cipher list with failures /// Returns both successfully decrypted ciphers and any that failed to decrypt pub fn decrypt_list_with_failures(&self, ciphers: Vec) -> DecryptCipherListResult { diff --git a/crates/bitwarden-vault/src/cipher/mod.rs b/crates/bitwarden-vault/src/cipher/mod.rs index ee36cc1b7..29fd5230c 100644 --- a/crates/bitwarden-vault/src/cipher/mod.rs +++ b/crates/bitwarden-vault/src/cipher/mod.rs @@ -22,7 +22,9 @@ pub use cipher::{ Cipher, CipherError, CipherListView, CipherListViewType, CipherRepromptType, CipherType, CipherView, DecryptCipherListResult, EncryptionContext, }; -pub use cipher_client::CiphersClient; +pub use cipher_client::{ + CipherIterator, CipherListViewIterator, CipherViewIterator, CiphersClient, +}; pub use field::{FieldType, FieldView}; pub use identity::IdentityView; pub use login::{ diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index a85384f6e..cc56fc79d 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -23,14 +23,17 @@ bitwarden-core = { workspace = true, features = ["wasm", "internal"] } bitwarden-crypto = { workspace = true, features = ["wasm"] } bitwarden-error = { workspace = true } bitwarden-exporters = { workspace = true, features = ["wasm"] } +bitwarden-fido = { workspace = true, features = ["wasm"] } bitwarden-generators = { workspace = true, features = ["wasm"] } bitwarden-ipc = { workspace = true, features = ["wasm"] } +bitwarden-iter = { workspace = true, features = ["wasm"] } bitwarden-ssh = { workspace = true, features = ["wasm"] } bitwarden-state = { workspace = true, features = ["wasm"] } bitwarden-threading = { workspace = true } bitwarden-vault = { workspace = true, features = ["wasm"] } console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } +js-sys = { workspace = true, optional = true } log = "0.4.20" serde = { workspace = true } tsify = { workspace = true } diff --git a/crates/bitwarden-wasm-internal/src/lib.rs b/crates/bitwarden-wasm-internal/src/lib.rs index 253ec8ffd..77f013462 100644 --- a/crates/bitwarden-wasm-internal/src/lib.rs +++ b/crates/bitwarden-wasm-internal/src/lib.rs @@ -7,6 +7,8 @@ mod platform; mod pure_crypto; mod ssh; +pub use bitwarden_fido::wasm::*; pub use bitwarden_ipc::wasm::*; +pub use bitwarden_iter::wasm::*; pub use client::BitwardenClient; pub use init::init_sdk;