Skip to content

Commit b033edd

Browse files
authored
[PM-18046] Implement session storage (#547)
## 🎟️ Tracking <!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. --> ## 📔 Objective Add JS-compatible session repository implementation. I also added the ability to switch for situations where state providers might not make sense, for example if they will use insecure IPC to save the data, defeating the whole point of a secure session. ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes
1 parent c60a5d7 commit b033edd

File tree

4 files changed

+213
-10
lines changed

4 files changed

+213
-10
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//! Generic session repository abstraction allowing IPC clients to choose between
2+
//! SDK-managed (in-memory) and client-managed (JavaScript-backed) session storage.
3+
//!
4+
//! This is a workaround because wasm-bindgen does not handle generics.
5+
//!
6+
//! Use SDK-managed when state providers might not make sense, for example if they
7+
//! will use insecure IPC to save the data, defeating the whole point of a secure session.
8+
9+
use std::sync::Arc;
10+
11+
use crate::{
12+
traits::{InMemorySessionRepository, SessionRepository},
13+
wasm::JsSessionRepository,
14+
};
15+
16+
// TODO: Change session type when implementing encryption
17+
type Session = ();
18+
19+
pub enum GenericSessionRepository {
20+
InMemory(Arc<InMemorySessionRepository<Session>>),
21+
JsSessionRepository(Arc<JsSessionRepository>),
22+
}
23+
24+
impl SessionRepository<Session> for GenericSessionRepository {
25+
type GetError = String;
26+
type SaveError = String;
27+
type RemoveError = String;
28+
29+
async fn get(
30+
&self,
31+
endpoint: crate::endpoint::Endpoint,
32+
) -> Result<Option<Session>, Self::GetError> {
33+
match self {
34+
GenericSessionRepository::InMemory(repo) => repo
35+
.get(endpoint)
36+
.await
37+
.map_err(|_| "InMemorySessionRepository::get should never fail".to_owned()),
38+
GenericSessionRepository::JsSessionRepository(repo) => {
39+
<JsSessionRepository as SessionRepository<Session>>::get(repo.as_ref(), endpoint)
40+
.await
41+
}
42+
}
43+
}
44+
45+
async fn save(
46+
&self,
47+
endpoint: crate::endpoint::Endpoint,
48+
session: Session,
49+
) -> Result<(), Self::SaveError> {
50+
match self {
51+
GenericSessionRepository::InMemory(repo) => repo
52+
.save(endpoint, session)
53+
.await
54+
.map_err(|_| "InMemorySessionRepository::save should never fail".to_owned()),
55+
GenericSessionRepository::JsSessionRepository(repo) => {
56+
<JsSessionRepository as SessionRepository<Session>>::save(
57+
repo.as_ref(),
58+
endpoint,
59+
session,
60+
)
61+
.await
62+
}
63+
}
64+
}
65+
66+
async fn remove(&self, endpoint: crate::endpoint::Endpoint) -> Result<(), Self::RemoveError> {
67+
match self {
68+
GenericSessionRepository::InMemory(repo) => repo
69+
.remove(endpoint)
70+
.await
71+
.map_err(|_| "InMemorySessionRepository::remove should never fail".to_owned()),
72+
GenericSessionRepository::JsSessionRepository(repo) => {
73+
<JsSessionRepository as SessionRepository<Session>>::remove(repo.as_ref(), endpoint)
74+
.await
75+
}
76+
}
77+
}
78+
}

crates/bitwarden-ipc/src/wasm/ipc_client.rs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ use crate::{
99
ipc_client::{IpcClientSubscription, ReceiveError, SubscribeError},
1010
message::{IncomingMessage, OutgoingMessage},
1111
traits::{InMemorySessionRepository, NoEncryptionCryptoProvider},
12+
wasm::{
13+
JsSessionRepository, RawJsSessionRepository,
14+
generic_session_repository::GenericSessionRepository,
15+
},
1216
};
1317

1418
/// JavaScript wrapper around the IPC client. For more information, see the
@@ -21,12 +25,7 @@ pub struct JsIpcClient {
2125
/// send typed messages, etc. For examples see
2226
/// [wasm::ipc_register_discover_handler](crate::wasm::ipc_register_discover_handler).
2327
pub client: Arc<
24-
IpcClient<
25-
NoEncryptionCryptoProvider,
26-
JsCommunicationBackend,
27-
// TODO: Change session provider to a JS-implemented one
28-
InMemorySessionRepository<()>,
29-
>,
28+
IpcClient<NoEncryptionCryptoProvider, JsCommunicationBackend, GenericSessionRepository>,
3029
>,
3130
}
3231

@@ -51,14 +50,36 @@ impl JsIpcClientSubscription {
5150

5251
#[wasm_bindgen(js_class = IpcClient)]
5352
impl JsIpcClient {
54-
#[allow(missing_docs)]
55-
#[wasm_bindgen(constructor)]
56-
pub fn new(communication_provider: &JsCommunicationBackend) -> JsIpcClient {
53+
/// Create a new `IpcClient` instance with an in-memory session repository for saving
54+
/// sessions within the SDK.
55+
#[wasm_bindgen(js_name = newWithSdkInMemorySessions)]
56+
pub fn new_with_sdk_in_memory_sessions(
57+
communication_provider: &JsCommunicationBackend,
58+
) -> JsIpcClient {
59+
JsIpcClient {
60+
client: IpcClient::new(
61+
NoEncryptionCryptoProvider,
62+
communication_provider.clone(),
63+
GenericSessionRepository::InMemory(Arc::new(InMemorySessionRepository::new(
64+
HashMap::new(),
65+
))),
66+
),
67+
}
68+
}
69+
/// Create a new `IpcClient` instance with a client-managed session repository for saving
70+
/// sessions using State Provider.
71+
#[wasm_bindgen(js_name = newWithClientManagedSessions)]
72+
pub fn new_with_client_managed_sessions(
73+
communication_provider: &JsCommunicationBackend,
74+
session_repository: RawJsSessionRepository,
75+
) -> JsIpcClient {
5776
JsIpcClient {
5877
client: IpcClient::new(
5978
NoEncryptionCryptoProvider,
6079
communication_provider.clone(),
61-
InMemorySessionRepository::new(HashMap::new()),
80+
GenericSessionRepository::JsSessionRepository(Arc::new(JsSessionRepository::new(
81+
session_repository,
82+
))),
6283
),
6384
}
6485
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use bitwarden_threading::ThreadBoundRunner;
2+
use serde::{Serialize, de::DeserializeOwned};
3+
use tsify::serde_wasm_bindgen;
4+
use wasm_bindgen::prelude::*;
5+
6+
use crate::{endpoint::Endpoint, traits::SessionRepository};
7+
8+
#[wasm_bindgen(typescript_custom_section)]
9+
const TS_CUSTOM_TYPES: &'static str = r#"
10+
export interface IpcSessionRepository {
11+
get(endpoint: Endpoint): Promise<any | undefined>;
12+
save(endpoint: Endpoint, session: any): Promise<void>;
13+
remove(endpoint: Endpoint): Promise<void>;
14+
}
15+
"#;
16+
17+
#[wasm_bindgen]
18+
extern "C" {
19+
/// JavaScript interface for handling outgoing messages from the IPC framework.
20+
#[wasm_bindgen(js_name = IpcSessionRepository, typescript_type = "IpcSessionRepository")]
21+
pub type RawJsSessionRepository;
22+
23+
/// Used by the IPC framework to get a session for a specific endpoint.
24+
#[wasm_bindgen(catch, method, structural)]
25+
pub async fn get(this: &RawJsSessionRepository, endpoint: Endpoint)
26+
-> Result<JsValue, JsValue>;
27+
28+
/// Used by the IPC framework to save a session for a specific endpoint.
29+
#[wasm_bindgen(catch, method, structural)]
30+
pub async fn save(
31+
this: &RawJsSessionRepository,
32+
endpoint: Endpoint,
33+
session: JsValue,
34+
) -> Result<(), JsValue>;
35+
36+
/// Used by the IPC framework to remove a session for a specific endpoint.
37+
#[wasm_bindgen(catch, method, structural)]
38+
pub async fn remove(this: &RawJsSessionRepository, endpoint: Endpoint) -> Result<(), JsValue>;
39+
}
40+
41+
/// Thread safe JavaScript implementation of the `SessionRepository` trait for IPC sessions.
42+
pub struct JsSessionRepository(ThreadBoundRunner<RawJsSessionRepository>);
43+
44+
impl JsSessionRepository {
45+
/// Creates a new `JsSessionRepository` instance wrapping the raw JavaScript repository.
46+
pub fn new(repository: RawJsSessionRepository) -> Self {
47+
Self(ThreadBoundRunner::new(repository))
48+
}
49+
}
50+
51+
impl Clone for JsSessionRepository {
52+
fn clone(&self) -> Self {
53+
Self(self.0.clone())
54+
}
55+
}
56+
57+
impl<Session> SessionRepository<Session> for JsSessionRepository
58+
where
59+
Session: Serialize + DeserializeOwned + Send + Sync + 'static,
60+
{
61+
type GetError = String;
62+
type SaveError = String;
63+
type RemoveError = String;
64+
65+
async fn get(&self, endpoint: Endpoint) -> Result<Option<Session>, Self::GetError> {
66+
self.0
67+
.run_in_thread(move |repo| async move {
68+
let js_value = repo.get(endpoint).await.map_err(|e| format!("{e:?}"))?;
69+
if js_value.is_undefined() || js_value.is_null() {
70+
return Ok(None);
71+
}
72+
73+
Ok(Some(
74+
serde_wasm_bindgen::from_value(js_value).map_err(|e| e.to_string())?,
75+
))
76+
})
77+
.await
78+
.map_err(|e| e.to_string())?
79+
}
80+
81+
async fn save(&self, endpoint: Endpoint, session: Session) -> Result<(), Self::SaveError> {
82+
self.0
83+
.run_in_thread(move |repo| async move {
84+
let js_value = serde_wasm_bindgen::to_value(&session).map_err(|e| e.to_string())?;
85+
repo.save(endpoint, js_value)
86+
.await
87+
.map_err(|e| format!("{e:?}"))
88+
})
89+
.await
90+
.map_err(|e| e.to_string())?
91+
}
92+
93+
async fn remove(&self, endpoint: Endpoint) -> Result<(), Self::RemoveError> {
94+
self.0
95+
.run_in_thread(move |repo| async move {
96+
repo.remove(endpoint).await.map_err(|e| format!("{e:?}"))
97+
})
98+
.await
99+
.map_err(|e| e.to_string())?
100+
}
101+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
mod communication_backend;
22
mod discover;
3+
mod generic_session_repository;
34
mod ipc_client;
5+
mod js_session_repository;
46
mod message;
57

68
// Re-export types to make sure wasm_bindgen picks them up
79
pub use communication_backend::*;
810
pub use discover::*;
911
pub use ipc_client::*;
12+
pub use js_session_repository::*;

0 commit comments

Comments
 (0)