diff --git a/Cargo.lock b/Cargo.lock index c8a6973cb..b5705a4da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,6 +385,7 @@ dependencies = [ name = "bitwarden-core" version = "1.0.0" dependencies = [ + "async-trait", "base64", "bitwarden-api-api", "bitwarden-api-identity", @@ -501,6 +502,21 @@ dependencies = [ "uuid", ] +[[package]] +name = "bitwarden-ffi-macros" +version = "1.0.0" +dependencies = [ + "darling", + "js-sys", + "proc-macro2", + "quote", + "serde", + "syn", + "thiserror 1.0.69", + "tsify-next", + "wasm-bindgen", +] + [[package]] name = "bitwarden-fido" version = "1.0.0" @@ -641,6 +657,7 @@ dependencies = [ "oslog", "rustls-platform-verifier", "schemars", + "serde_json", "thiserror 2.0.12", "uniffi", "uuid", @@ -679,9 +696,11 @@ dependencies = [ name = "bitwarden-wasm-internal" version = "0.1.0" dependencies = [ + "async-trait", "bitwarden-core", "bitwarden-crypto", "bitwarden-error", + "bitwarden-ffi-macros", "bitwarden-generators", "bitwarden-ipc", "bitwarden-ssh", @@ -692,6 +711,8 @@ dependencies = [ "js-sys", "log", "serde_json", + "tokio", + "tsify-next", "wasm-bindgen", "wasm-bindgen-futures", ] diff --git a/Cargo.toml b/Cargo.toml index d16db728f..db015fff1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ bitwarden-cli = { path = "crates/bitwarden-cli", version = "=1.0.0" } bitwarden-core = { path = "crates/bitwarden-core", version = "=1.0.0" } bitwarden-crypto = { path = "crates/bitwarden-crypto", version = "=1.0.0" } bitwarden-exporters = { path = "crates/bitwarden-exporters", version = "=1.0.0" } +bitwarden-ffi-macros = { path = "crates/bitwarden-ffi-macros", 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" } diff --git a/crates/bitwarden-core/Cargo.toml b/crates/bitwarden-core/Cargo.toml index 464905398..eb2b3dd88 100644 --- a/crates/bitwarden-core/Cargo.toml +++ b/crates/bitwarden-core/Cargo.toml @@ -28,6 +28,7 @@ wasm = [ ] # WASM support [dependencies] +async-trait = ">=0.1.80, <0.2" base64 = ">=0.22.1, <0.23" bitwarden-api-api = { workspace = true } bitwarden-api-identity = { workspace = true } diff --git a/crates/bitwarden-core/src/client/client.rs b/crates/bitwarden-core/src/client/client.rs index b4a05e6f2..73d8b44fb 100644 --- a/crates/bitwarden-core/src/client/client.rs +++ b/crates/bitwarden-core/src/client/client.rs @@ -8,6 +8,7 @@ use super::internal::InternalClient; use crate::client::flags::Flags; use crate::client::{ client_settings::ClientSettings, + data_store::DataStoreMap, internal::{ApiConfigurations, Tokens}, }; @@ -86,6 +87,8 @@ impl Client { })), external_client, key_store: KeyStore::default(), + #[cfg(feature = "internal")] + data_store_map: RwLock::new(DataStoreMap::default()), }), } } diff --git a/crates/bitwarden-core/src/client/data_store.rs b/crates/bitwarden-core/src/client/data_store.rs new file mode 100644 index 000000000..030b9e8a9 --- /dev/null +++ b/crates/bitwarden-core/src/client/data_store.rs @@ -0,0 +1,113 @@ +use std::{ + any::{Any, TypeId}, + collections::HashMap, + sync::Arc, +}; + +#[async_trait::async_trait] +pub trait DataStore: Send + Sync { + async fn get(&self, key: String) -> Option; + async fn list(&self) -> Vec; + async fn set(&self, key: String, value: T); + async fn remove(&self, key: String); +} + +#[derive(Default)] +pub struct DataStoreMap { + stores: HashMap>, +} + +impl std::fmt::Debug for DataStoreMap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DataStoreMap") + .field("stores", &self.stores.keys()) + .finish() + } +} + +impl DataStoreMap { + pub fn new() -> Self { + DataStoreMap { + stores: HashMap::new(), + } + } + + pub fn insert(&mut self, value: Arc>) { + self.stores.insert(TypeId::of::(), Box::new(value)); + } + + pub fn get(&self) -> Option>> { + self.stores + .get(&TypeId::of::()) + .and_then(|boxed| boxed.downcast_ref::>>()) + .map(Arc::clone) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! impl_data_store { + ($name:ident, $ty:ty) => { + #[async_trait::async_trait] + impl DataStore<$ty> for $name { + async fn get(&self, _key: String) -> Option<$ty> { + Some(self.0.clone()) + } + async fn list(&self) -> Vec<$ty> { + unimplemented!() + } + async fn set(&self, _key: String, _value: $ty) { + unimplemented!() + } + async fn remove(&self, _key: String) { + unimplemented!() + } + } + }; + } + + #[derive(PartialEq, Eq, Debug)] + struct TestA(usize); + #[derive(PartialEq, Eq, Debug)] + struct TestB(String); + #[derive(PartialEq, Eq, Debug)] + struct TestC(Vec); + + impl_data_store!(TestA, usize); + impl_data_store!(TestB, String); + impl_data_store!(TestC, Vec); + + #[tokio::test] + async fn test_data_store_map() { + let a = Arc::new(TestA(145832)); + let b = Arc::new(TestB("test".to_string())); + let c = Arc::new(TestC(vec![1, 2, 3, 4, 5, 6, 7, 8, 9])); + + let mut map = DataStoreMap::new(); + + async fn get(map: &DataStoreMap) -> Option { + map.get::().unwrap().get(String::new()).await + } + + assert!(map.get::().is_none()); + assert!(map.get::().is_none()); + assert!(map.get::>().is_none()); + + map.insert(a.clone()); + assert_eq!(get(&map).await, Some(a.0)); + assert!(map.get::().is_none()); + assert!(map.get::>().is_none()); + + map.insert(b.clone()); + assert_eq!(get(&map).await, Some(a.0)); + assert_eq!(get(&map).await, Some(b.0.clone())); + assert!(map.get::>().is_none()); + + map.insert(c.clone()); + assert_eq!(get(&map).await, Some(a.0)); + assert_eq!(get(&map).await, Some(b.0.clone())); + assert_eq!(get(&map).await, Some(c.0.clone())); + } +} diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index e365f0250..8abfae97f 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -18,6 +18,7 @@ use crate::{ }; #[cfg(feature = "internal")] use crate::{ + client::data_store::{DataStore, DataStoreMap}, client::encryption_settings::EncryptionSettingsError, client::{flags::Flags, login_method::UserLoginMethod}, error::NotAuthenticatedError, @@ -60,6 +61,9 @@ pub struct InternalClient { pub(crate) external_client: reqwest::Client, pub(super) key_store: KeyStore, + + #[cfg(feature = "internal")] + pub(super) data_store_map: RwLock, } impl InternalClient { @@ -219,4 +223,20 @@ impl InternalClient { ) -> Result<(), EncryptionSettingsError> { EncryptionSettings::set_org_keys(org_keys, &self.key_store) } + + #[cfg(feature = "internal")] + pub fn register_data_store, V: 'static>(&self, store: Arc) { + self.data_store_map + .write() + .expect("RwLock is not poisoned") + .insert(store); + } + + #[cfg(feature = "internal")] + pub fn get_data_store(&self) -> Option>> { + self.data_store_map + .read() + .expect("RwLock is not poisoned") + .get() + } } diff --git a/crates/bitwarden-core/src/client/mod.rs b/crates/bitwarden-core/src/client/mod.rs index 1ef7d9357..ad521c7ad 100644 --- a/crates/bitwarden-core/src/client/mod.rs +++ b/crates/bitwarden-core/src/client/mod.rs @@ -18,3 +18,6 @@ pub use client_settings::{ClientSettings, DeviceType}; #[cfg(feature = "internal")] pub mod test_accounts; + +#[cfg(feature = "internal")] +pub mod data_store; diff --git a/crates/bitwarden-ffi-macros/Cargo.toml b/crates/bitwarden-ffi-macros/Cargo.toml new file mode 100644 index 000000000..03ae75210 --- /dev/null +++ b/crates/bitwarden-ffi-macros/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "bitwarden-ffi-macros" +description = """ +Internal crate for the bitwarden crate. Do not use. +""" + +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +keywords.workspace = true + +[features] +wasm = [] + +[dependencies] +darling = "0.20.10" +proc-macro2 = "1.0.89" +quote = "1.0.37" +syn = "2.0.87" + +[lints] +workspace = true + +[lib] +proc-macro = true + +[dev-dependencies] +js-sys.workspace = true +serde.workspace = true +thiserror.workspace = true +tsify-next.workspace = true +wasm-bindgen.workspace = true diff --git a/crates/bitwarden-ffi-macros/README.md b/crates/bitwarden-ffi-macros/README.md new file mode 100644 index 000000000..f5d967e91 --- /dev/null +++ b/crates/bitwarden-ffi-macros/README.md @@ -0,0 +1,3 @@ +# Bitwarden WASM Macros + +Provides utility macros for simplifying FFI with WebAssembly and UniFFI. diff --git a/crates/bitwarden-ffi-macros/src/extern_wasm_channel.rs b/crates/bitwarden-ffi-macros/src/extern_wasm_channel.rs new file mode 100644 index 000000000..c38870e54 --- /dev/null +++ b/crates/bitwarden-ffi-macros/src/extern_wasm_channel.rs @@ -0,0 +1,310 @@ +use darling::{ast::NestedMeta, FromMeta}; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{ + parse_quote, spanned::Spanned, Attribute, Error, FnArg, ForeignItem, ForeignItemFn, Ident, + ItemForeignMod, Pat, ReturnType, Type, +}; + +#[derive(FromMeta)] +struct WasmIpcArgs { + trait_impl: Option, + #[darling(default)] + async_trait: bool, +} + +pub(crate) fn extern_wasm_channel_internal( + attr: TokenStream, + item: TokenStream, +) -> Result { + let mut input = syn::parse2::(item)?; + let attr_args = NestedMeta::parse_meta_list(attr)?; + let attr_args = WasmIpcArgs::from_list(&attr_args)?; + + // Validate the ABI + match input.abi.name { + Some(ref name) if name.value() == "C" => Ok(()), + _ => Err(Error::new(input.abi.span(), "Only C ABI is supported")), + }?; + + // Check that the extern mod is marked with #[wasm_bindgen] + get_bindgen_attr(&input, &input.attrs)?; + + // Extract the type and functions from the foreign module. + // This will also transform the functions to use JsValue as the return type for the extern + // block. + let Items { ident, functions } = Items::process_items(&mut input)?; + + // Prepend the ident with Channel + let channel_ident = quote::format_ident!("Channel{ident}"); + let channel_command_ident = quote::format_ident!("Channel{ident}Command"); + + // Generate the command struct used in the sent messages between the WASM implementation and the + // IPC implementation + let command_struct = generate_command_struct(&channel_command_ident, &functions); + + // Generate all the functions in the IPC implementation that will call the WASM impl through + // channels + let channel_impl = generate_channel_impl( + &channel_ident, + &channel_command_ident, + &functions, + &attr_args, + ); + + // Generate the function that, given a WASM instance, creates the channel implementation, and + // starts the message passing task + let channel_init = + generate_channel_init(&ident, &channel_ident, &channel_command_ident, &functions); + + // Define the channel based IPC struct + let channel_struct = quote::quote! { + #[derive(Debug, Clone)] + pub struct #channel_ident { + sender: ::tokio::sync::mpsc::Sender<#channel_command_ident>, + } + }; + + Ok([ + input.into_token_stream(), + command_struct, + channel_struct, + channel_init, + channel_impl, + ] + .into_iter() + .collect()) +} + +fn generate_command_struct(channel_ident: &Ident, functions: &[Func]) -> TokenStream { + // Create one variant per function, with all the arguments, plus a return channel + let enum_variants = functions.iter().map(|f| { + let name = &f.name; + let ret = &f.return_type; + let args_decls = &f.args_decls; + + quote::quote! { #name { + // This is the tokio channel used to send the response back to the caller. + // Using an _internal_ prefix to try to avoid collisions. + _internal_respond_to: ::tokio::sync::oneshot::Sender<#ret>, + #( #args_decls ),* + } } + }); + + quote::quote! { + #[allow(non_camel_case_types, clippy::large_enum_variant)] + enum #channel_ident { + #( #enum_variants ),* + } + } +} + +fn generate_channel_impl( + channel_ident: &Ident, + channel_command_ident: &Ident, + functions: &[Func], + attr_args: &WasmIpcArgs, +) -> TokenStream { + let impls = functions.iter().map(|f| { + let name = &f.name; + let ret = &f.return_type; + let arg_idents = &f.arg_idents; + let args_decls = &f.args_decls; + + let vis = attr_args.trait_impl.is_none().then(|| { + quote::quote! { pub } + }); + + quote::quote! { + // TODO: Should these return a result? + #vis async fn #name(&self, #( #args_decls ),*) -> #ret { + let (tx, rx) = ::tokio::sync::oneshot::channel(); + + self.sender.send(#channel_command_ident::#name { + _internal_respond_to: tx, + #( #arg_idents ),* + }).await.expect("Failed to send command"); + + rx.await.expect("Failed to receive response") + } + } + }); + + if let Some(trait_impl) = &attr_args.trait_impl { + let async_trait = attr_args.async_trait.then(|| { + quote::quote! { #[async_trait::async_trait] } + }); + + quote! { + #async_trait + impl #trait_impl for #channel_ident { + #( #impls )* + } + } + } else { + quote! { + impl #channel_ident { + #( #impls )* + } + } + } +} + +fn generate_channel_init( + ident: &Ident, + channel_ident: &Ident, + channel_command_ident: &Ident, + functions: &[Func], +) -> TokenStream { + let matches = functions.iter().map(|f| { + let name = &f.name; + let arg_idents = &f.arg_idents; + + if f.returns_value { + // TODO: Should these return a result? + quote::quote! { + #channel_command_ident::#name { _internal_respond_to, #( #arg_idents ),* } => { + let result = self.#name(#( #arg_idents ),*).await; + let result = serde_wasm_bindgen::from_value(result).expect("Couldn't convert to value"); + _internal_respond_to.send(result).expect("Failed to send response"); + } + } + } else { + quote::quote! { + #channel_command_ident::#name { _internal_respond_to, #( #arg_idents ),* } => { + self.#name(#( #arg_idents ),*).await; + _internal_respond_to.send(()).expect("Failed to send response"); + } + } + } + }); + + quote::quote! { + // Define the function that creates the channel impl and the message passing task + impl #ident { + fn create_channel_impl(self) -> #channel_ident { + let (tx, mut rx) = mpsc::channel::<#channel_command_ident>(16); + + wasm_bindgen_futures::spawn_local(async move { + while let Some(cmd) = rx.recv().await { + match cmd { + #( #matches )* + } + } + }); + + #channel_ident { sender: tx } + } + } + } +} + +fn get_bindgen_attr(item: impl ToTokens, attrs: &[Attribute]) -> Result<&Attribute, Error> { + attrs + .iter() + .find(|a| a.path().is_ident("wasm_bindgen")) + .ok_or_else(|| Error::new(item.span(), "This item needs to use #[wasm_bindgen]")) +} + +struct Items { + ident: Ident, + functions: Vec, +} + +struct Func { + name: Ident, + arg_idents: Vec, + args_decls: Vec, + return_type: Type, + returns_value: bool, +} + +impl Items { + fn process_items(input: &mut ItemForeignMod) -> Result { + let mut ident = None; + let mut functions = Vec::new(); + + // Collect and parse the items (one type + multiple functions) + // For functions that return a value, we need to change the return type to JsValue in the + // #[wasm_bindgen] extern block, as only types that implement JsCast can be returned. + // The functions in the Channel struct will return the original values, and just use + // `serde_wasm_bindgen::from_value`. + for item in &mut input.items { + match item { + ForeignItem::Type(typ) => { + if ident.is_some() { + return Err(Error::new( + typ.span(), + "Only one type is allowed in a foreign module", + )); + } + let _bindgen = get_bindgen_attr(&typ, &typ.attrs)?; + + // TODO: Get js_name from the attribute? + ident = Some(typ.ident.clone()); + } + ForeignItem::Fn(func) => { + if let Ok(_bindgen) = get_bindgen_attr(&func, &func.attrs) { + // Collect the function info first, then modify the function return type + functions.push(Func::from_item(func)?); + if let ReturnType::Type(_, ty) = &mut func.sig.output { + *ty = parse_quote! { ::wasm_bindgen::JsValue }; + } + } + } + _ => { + return Err(Error::new( + item.span(), + "Only functions and types are supported", + )); + } + } + } + + let Some(ident) = ident else { + return Err(Error::new(input.span(), "No type found")); + }; + + Ok(Items { ident, functions }) + } +} + +impl Func { + fn from_item(func: &ForeignItemFn) -> Result { + let name = func.sig.ident.clone(); + + let mut args = func.sig.inputs.iter(); + let _this_arg = args.next().expect("Expected a this argument"); + + let mut arg_idents = Vec::new(); + let mut args_decls = Vec::new(); + + for arg in args { + match arg { + FnArg::Typed(arg) => match *arg.pat { + Pat::Ident(ref pat) => { + let ident = pat.ident.clone(); + let ty = &arg.ty; + args_decls.push(quote::quote! { #ident: #ty }); + arg_idents.push(ident); + } + _ => return Err(Error::new(arg.span(), "Expected an Ident argument")), + }, + _ => return Err(Error::new(arg.span(), "Expected a typed argument")), + } + } + + let (return_type, returns_value) = match &func.sig.output { + ReturnType::Default => (parse_quote! { () }, false), + ReturnType::Type(_, ty) => ((**ty).clone(), true), + }; + + Ok(Func { + name, + arg_idents, + args_decls, + return_type, + returns_value, + }) + } +} diff --git a/crates/bitwarden-ffi-macros/src/lib.rs b/crates/bitwarden-ffi-macros/src/lib.rs new file mode 100644 index 000000000..96c5a13d1 --- /dev/null +++ b/crates/bitwarden-ffi-macros/src/lib.rs @@ -0,0 +1,14 @@ +#![doc = include_str!("../README.md")] + +mod extern_wasm_channel; + +#[proc_macro_attribute] +pub fn extern_wasm_channel( + args: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + match extern_wasm_channel::extern_wasm_channel_internal(args.into(), item.into()) { + Ok(v) => v.into(), + Err(e) => proc_macro::TokenStream::from(e.to_compile_error()), + } +} diff --git a/crates/bitwarden-uniffi/Cargo.toml b/crates/bitwarden-uniffi/Cargo.toml index a8bd77ab0..afa8865da 100644 --- a/crates/bitwarden-uniffi/Cargo.toml +++ b/crates/bitwarden-uniffi/Cargo.toml @@ -32,6 +32,7 @@ chrono = { workspace = true, features = ["std"] } env_logger = "0.11.1" log = { workspace = true } schemars = { workspace = true, optional = true } +serde_json = { workspace = true } thiserror = { workspace = true } uniffi = { workspace = true } uuid = { workspace = true } diff --git a/crates/bitwarden-uniffi/src/platform/mod.rs b/crates/bitwarden-uniffi/src/platform/mod.rs index 9b0dc5574..1d418f7d3 100644 --- a/crates/bitwarden-uniffi/src/platform/mod.rs +++ b/crates/bitwarden-uniffi/src/platform/mod.rs @@ -1,5 +1,8 @@ -use bitwarden_core::platform::FingerprintRequest; +use std::sync::Arc; + +use bitwarden_core::{platform::FingerprintRequest, Client}; use bitwarden_fido::ClientFido2Ext; +use bitwarden_vault::Cipher; use crate::error::{Error, Result}; @@ -38,4 +41,66 @@ impl PlatformClient { pub fn fido2(&self) -> fido2::ClientFido2 { fido2::ClientFido2(self.0.fido2()) } + + pub fn store(&self) -> StoreClient { + StoreClient(self.0.clone()) + } +} + +#[derive(uniffi::Object)] +pub struct StoreClient(Client); + +#[uniffi::export(with_foreign)] +#[async_trait::async_trait] +pub trait CipherStore: Send + Sync { + async fn get(&self, id: String) -> Option; + async fn list(&self) -> Vec; + async fn set(&self, id: String, value: Cipher); + async fn remove(&self, id: String); +} + +impl std::fmt::Debug for UniffiTraitBridge { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UniffiTraitBridge").finish() + } +} + +struct UniffiTraitBridge(T); + +#[async_trait::async_trait] +impl bitwarden_core::client::data_store::DataStore + for UniffiTraitBridge> +{ + async fn get(&self, key: String) -> Option { + self.0.get(key).await + } + async fn list(&self) -> Vec { + self.0.list().await + } + async fn set(&self, key: String, value: Cipher) { + self.0.set(key, value).await + } + async fn remove(&self, key: String) { + self.0.remove(key).await + } +} + +#[uniffi::export(async_runtime = "tokio")] +impl StoreClient { + pub async fn print_the_ciphers(&self) -> String { + let store = self.0.internal.get_data_store::().expect("msg"); + let mut result = String::new(); + let ciphers = store.list().await; + for cipher in ciphers { + result.push_str(&serde_json::to_string(&cipher).expect("msg")); + result.push('\n'); + } + result + } + + pub fn register_cipher_store(&self, store: Arc) -> Result<()> { + let store_internal = Arc::new(UniffiTraitBridge(store)); + self.0.internal.register_data_store(store_internal); + Ok(()) + } } diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index 3e1aaf307..14165b3a7 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -16,9 +16,11 @@ keywords.workspace = true crate-type = ["cdylib"] [dependencies] +async-trait = ">=0.1.80, <0.2" bitwarden-core = { workspace = true, features = ["wasm", "internal"] } bitwarden-crypto = { workspace = true, features = ["wasm"] } bitwarden-error = { workspace = true } +bitwarden-ffi-macros = { workspace = true } bitwarden-generators = { workspace = true, features = ["wasm"] } bitwarden-ipc = { workspace = true, features = ["wasm"] } bitwarden-ssh = { workspace = true, features = ["wasm"] } @@ -29,6 +31,8 @@ console_log = { version = "1.0.0", features = ["color"] } js-sys = "0.3.68" log = "0.4.20" serde_json = ">=1.0.96, <2.0" +tokio = { features = ["sync", "rt"], workspace = true } +tsify-next = { workspace = true } # When upgrading wasm-bindgen, make sure to update the version in the workflows! wasm-bindgen = { version = "=0.2.100", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.41" diff --git a/crates/bitwarden-wasm-internal/src/client.rs b/crates/bitwarden-wasm-internal/src/client.rs index 09da673bb..aa1c2deee 100644 --- a/crates/bitwarden-wasm-internal/src/client.rs +++ b/crates/bitwarden-wasm-internal/src/client.rs @@ -1,9 +1,12 @@ extern crate console_error_panic_hook; -use std::fmt::Display; +use std::{fmt::Display, sync::Arc}; -use bitwarden_core::{Client, ClientSettings}; +use bitwarden_core::{client::data_store::DataStore, Client, ClientSettings}; use bitwarden_error::bitwarden_error; +use bitwarden_vault::Cipher; use bitwarden_vault::VaultClientExt; +use tokio::sync::mpsc; +use tsify_next::serde_wasm_bindgen; use wasm_bindgen::prelude::*; use crate::{CryptoClient, GeneratorClient, VaultClient}; @@ -51,6 +54,10 @@ impl BitwardenClient { pub fn generator(&self) -> GeneratorClient { GeneratorClient::new(self.0.clone()) } + + pub fn store(&self) -> StoreClient { + StoreClient::new(self.0.clone()) + } } #[bitwarden_error(basic)] @@ -61,3 +68,60 @@ impl Display for TestError { write!(f, "{}", self.0) } } + +#[wasm_bindgen] +pub struct StoreClient(Client); + +impl StoreClient { + pub fn new(client: Client) -> Self { + Self(client) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const CIPHER_STORE_CUSTOM_TS_TYPE: &'static str = r#" +export interface CipherStore { + get(id: string): Promise; + list(): Promise; + set(id: string, value: Cipher): Promise; + remove(id: string): Promise; +} +"#; + +#[bitwarden_ffi_macros::extern_wasm_channel(trait_impl = "DataStore", async_trait = true)] +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = CipherStore, typescript_type = "CipherStore")] + pub type JSCipherStore; + + #[wasm_bindgen(method)] + async fn get(this: &JSCipherStore, id: String) -> Option; + + #[wasm_bindgen(method)] + async fn list(this: &JSCipherStore) -> Vec; + + #[wasm_bindgen(method)] + async fn set(this: &JSCipherStore, id: String, value: Cipher); + + #[wasm_bindgen(method)] + async fn remove(this: &JSCipherStore, id: String); +} + +#[wasm_bindgen] +impl StoreClient { + pub async fn print_the_ciphers(&self) -> String { + let store = self.0.internal.get_data_store::().expect("msg"); + let mut result = String::new(); + let ciphers = store.list().await; + for cipher in ciphers { + result.push_str(format!("{:?}", cipher).as_str()); + result.push('\n'); + } + result + } + + pub fn register_cipher_store(&self, store: JSCipherStore) { + let store = store.create_channel_impl(); + self.0.internal.register_data_store(Arc::new(store)); + } +}