Skip to content

Commit 0d26728

Browse files
committed
POC state traits
1 parent 5cb4eb0 commit 0d26728

File tree

8 files changed

+276
-2
lines changed

8 files changed

+276
-2
lines changed

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ wasm = [
2828
] # WASM support
2929

3030
[dependencies]
31+
async-trait = ">=0.1.80, <0.2"
3132
base64 = ">=0.22.1, <0.23"
3233
bitwarden-api-api = { workspace = true }
3334
bitwarden-api-identity = { workspace = true }

crates/bitwarden-core/src/client/client.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ impl Client {
8282
})),
8383
external_client,
8484
key_store: KeyStore::default(),
85+
86+
cipher_store: RwLock::new(None),
8587
},
8688
}
8789
}

crates/bitwarden-core/src/client/internal.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ pub struct InternalClient {
6060
pub(crate) external_client: reqwest::Client,
6161

6262
pub(super) key_store: KeyStore<KeyIds>,
63+
64+
pub(super) cipher_store: RwLock<Option<Arc<dyn CipherStore>>>,
6365
}
6466

6567
impl InternalClient {
@@ -219,4 +221,27 @@ impl InternalClient {
219221
) -> Result<(), EncryptionSettingsError> {
220222
EncryptionSettings::set_org_keys(org_keys, &self.key_store)
221223
}
224+
225+
pub fn register_cipher_store(&self, store: Arc<dyn CipherStore>) {
226+
self.cipher_store
227+
.write()
228+
.expect("RwLock is not poisoned")
229+
.replace(store);
230+
}
231+
232+
pub fn get_cipher_store(&self) -> Option<Arc<dyn CipherStore>> {
233+
self.cipher_store
234+
.read()
235+
.expect("RwLock is not poisoned")
236+
.clone()
237+
}
238+
}
239+
240+
// TODO: We can't expose the store as returning a bitwarden_vault::Cipher to avoid a circular dependency, we'll fix that at some point somehow
241+
#[async_trait::async_trait]
242+
pub trait CipherStore: std::fmt::Debug + Send + Sync {
243+
async fn get(&self, key: &str) -> Option<String>;
244+
async fn list(&self) -> Vec<String>;
245+
async fn set(&self, key: &str, value: String);
246+
async fn remove(&self, key: &str);
222247
}

crates/bitwarden-uniffi/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ chrono = { workspace = true, features = ["std"] }
3232
env_logger = "0.11.1"
3333
log = { workspace = true }
3434
schemars = { workspace = true, optional = true }
35+
serde_json = { workspace = true }
3536
thiserror = { workspace = true }
3637
uniffi = { workspace = true }
3738
uuid = { workspace = true }

crates/bitwarden-uniffi/src/platform/mod.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::sync::Arc;
22

33
use bitwarden_core::platform::FingerprintRequest;
4+
use bitwarden_vault::Cipher;
45

56
use crate::{
67
error::{Error, Result},
@@ -44,4 +45,76 @@ impl PlatformClient {
4445
pub fn fido2(self: Arc<Self>) -> Arc<fido2::ClientFido2> {
4546
Arc::new(fido2::ClientFido2(self.0.clone()))
4647
}
48+
49+
pub fn store(self: Arc<Self>) -> StoreClient {
50+
StoreClient(self.0.clone())
51+
}
52+
}
53+
54+
#[derive(uniffi::Object)]
55+
pub struct StoreClient(pub(crate) Arc<Client>);
56+
57+
#[uniffi::export(with_foreign)]
58+
#[async_trait::async_trait]
59+
pub trait CipherStore: Send + Sync {
60+
async fn get(&self, id: String) -> Option<Cipher>;
61+
async fn list(&self) -> Vec<Cipher>;
62+
async fn set(&self, id: String, value: Cipher);
63+
async fn remove(&self, id: String);
64+
}
65+
66+
impl<T> std::fmt::Debug for UniffiTraitBridge<T> {
67+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68+
f.debug_struct("UniffiTraitBridge").finish()
69+
}
70+
}
71+
72+
struct UniffiTraitBridge<T>(T);
73+
74+
#[async_trait::async_trait]
75+
impl bitwarden_core::client::internal::CipherStore for UniffiTraitBridge<Arc<dyn CipherStore>> {
76+
async fn get(&self, key: &str) -> Option<String> {
77+
self.0
78+
.get(key.to_string())
79+
.await
80+
.map(|cipher| serde_json::to_string(&cipher).expect("Failed to serialize cipher"))
81+
}
82+
async fn list(&self) -> Vec<String> {
83+
self.0
84+
.list()
85+
.await
86+
.into_iter()
87+
.map(|cipher| serde_json::to_string(&cipher).expect("Failed to serialize cipher"))
88+
.collect()
89+
}
90+
async fn set(&self, key: &str, value: String) {
91+
self.0
92+
.set(key.to_string(), serde_json::from_str(&value).expect("msg"))
93+
.await
94+
}
95+
async fn remove(&self, key: &str) {
96+
self.0.remove(key.to_string()).await
97+
}
98+
}
99+
100+
#[uniffi::export(async_runtime = "tokio")]
101+
impl StoreClient {
102+
pub async fn print_the_ciphers(&self) -> String {
103+
let store = self.0 .0.internal.get_cipher_store().expect("msg");
104+
let mut result = String::new();
105+
let ciphers = store.list().await;
106+
for cipher in ciphers {
107+
result.push_str(&cipher);
108+
result.push('\n');
109+
}
110+
result
111+
}
112+
113+
pub fn register_cipher_store(&self, store: Arc<dyn CipherStore>) -> Result<()> {
114+
let store_internal: Arc<dyn bitwarden_core::client::internal::CipherStore> =
115+
Arc::new(UniffiTraitBridge(store));
116+
117+
self.0 .0.internal.register_cipher_store(store_internal);
118+
Ok(())
119+
}
47120
}

crates/bitwarden-wasm-internal/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ keywords.workspace = true
1616
crate-type = ["cdylib"]
1717

1818
[dependencies]
19+
async-trait = ">=0.1.80, <0.2"
1920
bitwarden-core = { workspace = true, features = ["wasm", "internal"] }
2021
bitwarden-crypto = { workspace = true, features = ["wasm"] }
2122
bitwarden-error = { workspace = true }
@@ -28,6 +29,8 @@ console_log = { version = "1.0.0", features = ["color"] }
2829
js-sys = "0.3.68"
2930
log = "0.4.20"
3031
serde_json = ">=1.0.96, <2.0"
32+
tokio = { features = ["sync", "rt"], workspace = true }
33+
tsify-next = { workspace = true }
3134
# When upgrading wasm-bindgen, make sure to update the version in the workflows!
3235
wasm-bindgen = { version = "=0.2.100", features = ["serde-serialize"] }
3336
wasm-bindgen-futures = "0.4.41"

crates/bitwarden-wasm-internal/src/client.rs

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
extern crate console_error_panic_hook;
2-
use std::{fmt::Display, rc::Rc};
2+
use std::{fmt::Display, rc::Rc, sync::Arc};
33

4-
use bitwarden_core::{Client, ClientSettings};
4+
use bitwarden_core::{client::internal::CipherStore, Client, ClientSettings};
55
use bitwarden_error::bitwarden_error;
6+
use bitwarden_vault::Cipher;
7+
use js_sys::{Array, JsString, Promise};
8+
use tokio::sync::{mpsc, oneshot};
69
use wasm_bindgen::prelude::*;
710

811
use crate::{CryptoClient, VaultClient};
@@ -47,6 +50,10 @@ impl BitwardenClient {
4750
pub fn vault(&self) -> VaultClient {
4851
VaultClient::new(self.0.clone())
4952
}
53+
54+
pub fn store(&self) -> StoreClient {
55+
StoreClient::new(self.0.clone())
56+
}
5057
}
5158

5259
#[bitwarden_error(basic)]
@@ -57,3 +64,160 @@ impl Display for TestError {
5764
write!(f, "{}", self.0)
5865
}
5966
}
67+
68+
#[wasm_bindgen]
69+
pub struct StoreClient(Rc<Client>);
70+
71+
impl StoreClient {
72+
pub fn new(client: Rc<Client>) -> Self {
73+
Self(client)
74+
}
75+
}
76+
77+
#[derive(Debug, Clone)]
78+
pub struct ChannelCipherStore {
79+
sender: mpsc::Sender<StoreCommand>,
80+
}
81+
82+
pub enum StoreCommand {
83+
Get {
84+
id: String,
85+
respond_to: oneshot::Sender<Option<String>>,
86+
},
87+
List {
88+
respond_to: oneshot::Sender<Vec<String>>,
89+
},
90+
Set {
91+
id: String,
92+
value: String,
93+
},
94+
Remove {
95+
id: String,
96+
},
97+
}
98+
99+
#[async_trait::async_trait]
100+
impl CipherStore for ChannelCipherStore {
101+
async fn get(&self, id: &str) -> Option<String> {
102+
let (tx, rx) = oneshot::channel();
103+
let cmd = StoreCommand::Get {
104+
id: id.to_string(),
105+
respond_to: tx,
106+
};
107+
let _ = self.sender.send(cmd).await;
108+
rx.await.expect("")
109+
}
110+
111+
async fn list(&self) -> Vec<String> {
112+
let (tx, rx) = oneshot::channel();
113+
let cmd = StoreCommand::List { respond_to: tx };
114+
let _ = self.sender.send(cmd).await;
115+
rx.await.expect("")
116+
}
117+
118+
async fn set(&self, id: &str, value: String) {
119+
let cmd = StoreCommand::Set {
120+
id: id.to_string(),
121+
value,
122+
};
123+
self.sender.send(cmd).await.expect("");
124+
}
125+
126+
async fn remove(&self, id: &str) {
127+
let cmd = StoreCommand::Remove { id: id.to_string() };
128+
self.sender.send(cmd).await.expect("");
129+
}
130+
}
131+
132+
#[wasm_bindgen(typescript_custom_section)]
133+
const CIPHER_STORE_CUSTOM_TS_TYPE: &'static str = r#"
134+
export interface CipherStore {
135+
get(id: string): Promise<string | null>;
136+
list(): Promise<string[]>;
137+
set(id: string, value: string): Promise<void>;
138+
remove(id: string): Promise<void>;
139+
}
140+
"#;
141+
142+
#[wasm_bindgen]
143+
extern "C" {
144+
#[wasm_bindgen(js_name = CipherStore, typescript_type = "CipherStore")]
145+
pub type JSCipherStore;
146+
147+
#[wasm_bindgen(method)]
148+
async fn get(this: &JSCipherStore, id: String) -> JsValue;
149+
150+
#[wasm_bindgen(method)]
151+
async fn list(this: &JSCipherStore) -> Array;
152+
153+
#[wasm_bindgen(method)]
154+
async fn set(this: &JSCipherStore, id: String, value: String);
155+
156+
#[wasm_bindgen(method)]
157+
async fn remove(this: &JSCipherStore, id: String);
158+
}
159+
160+
#[wasm_bindgen]
161+
impl StoreClient {
162+
pub async fn print_the_ciphers(&self) -> String {
163+
let store = self.0.internal.get_cipher_store().expect("msg");
164+
let mut result = String::new();
165+
let ciphers = store.list().await;
166+
for cipher in ciphers {
167+
result.push_str(&cipher);
168+
result.push('\n');
169+
}
170+
result
171+
}
172+
173+
pub fn register_cipher_store(
174+
&self,
175+
store: JSCipherStore,
176+
// get: js_sys::Function,
177+
// list: js_sys::Function,
178+
// save: js_sys::Function,
179+
// delete: js_sys::Function,
180+
) {
181+
let (tx, mut rx) = mpsc::channel::<StoreCommand>(32);
182+
183+
wasm_bindgen_futures::spawn_local(async move {
184+
fn resolve_value(val: JsValue) -> Option<JsValue> {
185+
if val.is_null() || val.is_undefined() {
186+
None
187+
} else {
188+
Some(val)
189+
}
190+
}
191+
192+
while let Some(cmd) = rx.recv().await {
193+
match cmd {
194+
StoreCommand::Get { id, respond_to } => {
195+
let result = store.get(id).await;
196+
let result = resolve_value(result).map(|v| v.try_into().expect(""));
197+
let _ = respond_to.send(result);
198+
}
199+
StoreCommand::List { respond_to } => {
200+
let result = store.list().await;
201+
202+
let result: Vec<String> = result
203+
.into_iter()
204+
.map(|v| v.as_string().expect("msg"))
205+
.collect();
206+
207+
let _ = respond_to.send(result);
208+
}
209+
StoreCommand::Set { id, value } => {
210+
store.set(id, value).await;
211+
}
212+
StoreCommand::Remove { id } => {
213+
store.remove(id).await;
214+
}
215+
}
216+
}
217+
});
218+
219+
let store = ChannelCipherStore { sender: tx };
220+
221+
self.0.internal.register_cipher_store(Arc::new(store));
222+
}
223+
}

0 commit comments

Comments
 (0)