Skip to content

[PM-19479] Client-managed Repository traits #213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ 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-send = { path = "crates/bitwarden-send", version = "=1.0.0" }
bitwarden-state = { path = "crates/bitwarden-state", version = "=1.0.0" }
bitwarden-threading = { path = "crates/bitwarden-threading", 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" }
Expand All @@ -39,6 +40,7 @@ bitwarden-uuid-macro = { path = "crates/bitwarden-uuid-macro", version = "=1.0.0
bitwarden-vault = { path = "crates/bitwarden-vault", version = "=1.0.0" }

# External crates that are expected to maintain a consistent version across all crates
async-trait = ">=0.1.80, <0.2"
chrono = { version = ">=0.4.26, <0.5", features = [
"clock",
"serde",
Expand Down
1 change: 1 addition & 0 deletions crates/bitwarden-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ bitwarden-api-api = { workspace = true }
bitwarden-api-identity = { workspace = true }
bitwarden-crypto = { workspace = true }
bitwarden-error = { workspace = true }
bitwarden-state = { workspace = true }
bitwarden-uuid = { workspace = true }
chrono = { workspace = true, features = ["std"] }
# We don't use this directly (it's used by rand), but we need it here to enable WASM support
Expand Down
4 changes: 4 additions & 0 deletions crates/bitwarden-core/src/client/client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::sync::{Arc, OnceLock, RwLock};

use bitwarden_crypto::KeyStore;
#[cfg(feature = "internal")]
use bitwarden_state::registry::StateRegistry;
use reqwest::header::{self, HeaderValue};

use super::internal::InternalClient;
Expand Down Expand Up @@ -88,6 +90,8 @@ impl Client {
})),
external_client,
key_store: KeyStore::default(),
#[cfg(feature = "internal")]
repository_map: StateRegistry::new(),
}),
}
}
Expand Down
5 changes: 5 additions & 0 deletions crates/bitwarden-core/src/client/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use bitwarden_crypto::KeyStore;
use bitwarden_crypto::SymmetricCryptoKey;
#[cfg(feature = "internal")]
use bitwarden_crypto::{EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey};
#[cfg(feature = "internal")]
use bitwarden_state::registry::StateRegistry;
use chrono::Utc;
use uuid::Uuid;

Expand Down Expand Up @@ -62,6 +64,9 @@ pub struct InternalClient {
pub(crate) external_client: reqwest::Client,

pub(super) key_store: KeyStore<KeyIds>,

#[cfg(feature = "internal")]
pub(crate) repository_map: StateRegistry,
}

impl InternalClient {
Expand Down
2 changes: 2 additions & 0 deletions crates/bitwarden-core/src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod generate_fingerprint;
mod get_user_api_key;
mod platform_client;
mod secret_verification_request;
mod state_client;

pub use generate_fingerprint::{
FingerprintError, FingerprintRequest, FingerprintResponse, UserFingerprintError,
Expand All @@ -14,3 +15,4 @@ pub(crate) use get_user_api_key::get_user_api_key;
pub use get_user_api_key::{UserApiKeyError, UserApiKeyResponse};
pub use platform_client::PlatformClient;
pub use secret_verification_request::SecretVerificationRequest;
pub use state_client::StateClient;
7 changes: 7 additions & 0 deletions crates/bitwarden-core/src/platform/platform_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@
) -> Result<UserApiKeyResponse, UserApiKeyError> {
get_user_api_key(&self.client, &input).await
}

/// Access to state functionality.
pub fn state(&self) -> super::StateClient {
super::StateClient {
client: self.client.clone(),
}
}

Check warning on line 42 in crates/bitwarden-core/src/platform/platform_client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/platform/platform_client.rs#L38-L42

Added lines #L38 - L42 were not covered by tests
}

impl Client {
Expand Down
28 changes: 28 additions & 0 deletions crates/bitwarden-core/src/platform/state_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use std::sync::Arc;

use bitwarden_state::repository::{Repository, RepositoryItem};

use crate::Client;

/// Wrapper for state specific functionality.
pub struct StateClient {
pub(crate) client: Client,
}

impl StateClient {
/// Register a client managed state repository for a specific type.
pub fn register_client_managed<T: 'static + Repository<V>, V: RepositoryItem>(
&self,
store: Arc<T>,
) {
self.client
.internal
.repository_map
.register_client_managed(store)
}

Check warning on line 22 in crates/bitwarden-core/src/platform/state_client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/platform/state_client.rs#L14-L22

Added lines #L14 - L22 were not covered by tests

/// Get a client managed state repository for a specific type, if it exists.
pub fn get_client_managed<T: RepositoryItem>(&self) -> Option<Arc<dyn Repository<T>>> {
self.client.internal.repository_map.get_client_managed()
}

Check warning on line 27 in crates/bitwarden-core/src/platform/state_client.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/platform/state_client.rs#L25-L27

Added lines #L25 - L27 were not covered by tests
}
2 changes: 1 addition & 1 deletion crates/bitwarden-fido/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ keywords.workspace = true
uniffi = ["dep:uniffi", "bitwarden-core/uniffi", "bitwarden-vault/uniffi"]

[dependencies]
async-trait = ">=0.1.80, <0.2"
async-trait = { workspace = true }
base64 = ">=0.22.1, <0.23"
bitwarden-core = { workspace = true }
bitwarden-crypto = { workspace = true }
Expand Down
24 changes: 24 additions & 0 deletions crates/bitwarden-state/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "bitwarden-state"
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

[features]
uniffi = []
wasm = []

[dependencies]
async-trait = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
tokio = { workspace = true, features = ["rt"] }

[lints]
workspace = true
175 changes: 175 additions & 0 deletions crates/bitwarden-state/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# bitwarden-state

This crate contains the core state handling code of the Bitwarden SDK. Its primary feature is a
namespaced key-value store, accessible via the typed [Repository](crate::repository::Repository)
trait.

To make use of the `Repository` trait, the first thing to do is to ensure the data to be used with
it is registered to do so:

```rust
struct Cipher {
// Cipher fields
};

// Register `Cipher` for use with a `Repository`.
// This should be done in the crate where `Cipher` is defined.
bitwarden_state::register_repository_item!(Cipher, "Cipher");
```

With the registration complete, the next important decision is to select where will the data be
stored:

- If the application using the SDK is responsible for storing the data, it must provide its own
implementation of the `Repository` trait. We call this approach `Client-Managed State` or
`Application-Managed State`. See the next section for details on how to implement this.

- If the SDK itself will handle data storage, we call that approach `SDK-Managed State`. The
implementation of this is will a work in progress.

## Client-Managed State

With `Client-Managed State` the application and SDK will both access the same data pool, which
simplifies the initial migration and development. Using this approach requires manual setup, as we
need to define some functions in `bitwarden-wasm-internal` and `bitwarden-uniffi` to allow the
applications to provide their `Repository` implementations. The implementations themselves will be
very simple as we provide macros that take care of the brunt of the work.

### Client-Managed State in WASM

For WASM, we need to define a new `Repository` for our type and provide a function that will accept
it. This is done in the file `crates/bitwarden-wasm-internal/src/platform/mod.rs`, you can check the
provided example:

```rust,ignore
repository::create_wasm_repository!(CipherRepository, Cipher, "Repository<Cipher>");

#[wasm_bindgen]
impl StateClient {
pub fn register_cipher_repository(&self, store: CipherRepository) {
let store = store.into_channel_impl();
self.0.platform().state().register_client_managed(store)
}
}
```

#### How to use it on web clients

Once we have the function defined in `bitwarden-wasm-internal`, we can use it from the web clients.
For that, the first thing we need to do is create a mapper between the client and SDK types. This
mapper will also contain the `UserKeyDefinition` for the `StateProvider` API and should be created
in the folder of the team that owns the model:

```typescript
export class CipherRecordMapper implements SdkRecordMapper<CipherData, SdkCipher> {
userKeyDefinition(): UserKeyDefinition<Record<string, CipherData>> {
return ENCRYPTED_CIPHERS;
}

toSdk(value: CipherData): SdkCipher {
return new Cipher(value).toSdkCipher();
}

fromSdk(value: SdkCipher): CipherData {
throw new Error("Cipher.fromSdk is not implemented yet");
}
}
```

Once that is done, we should be able to register the mapper in the
`libs/common/src/platform/services/sdk/client-managed-state.ts` file, inside the `initializeState`
function:

```typescript
export async function initializeState(
userId: UserId,
stateClient: StateClient,
stateProvider: StateProvider,
): Promise<void> {
await stateClient.register_cipher_repository(
new RepositoryRecord(userId, stateProvider, new CipherRecordMapper()),
);
}
```

### Client-Managed State in UniFFI

For UniFFI, we need to define a new `Repository` for our type and provide a function that will
accept it. This is done in the file `crates/bitwarden-uniffi/src/platform/mod.rs`, you can check the
provided example:

```rust,ignore
repository::create_uniffi_repository!(CipherRepository, Cipher);

#[uniffi::export]
impl StateClient {
pub fn register_cipher_repository(&self, store: Arc<dyn CipherRepository>) {
let store_internal = UniffiRepositoryBridge::new(store);
self.0
.platform()
.state()
.register_client_managed(store_internal)
}
}
```

#### How to use it on iOS

Once we have the function defined in `bitwarden-uniffi`, we can use it from the iOS application:

```swift
class CipherStoreImpl: CipherStore {
private var cipherDataStore: CipherDataStore
private var userId: String

init(cipherDataStore: CipherDataStore, userId: String) {
self.cipherDataStore = cipherDataStore
self.userId = userId
}

func get(id: String) async -> Cipher? {
return try await cipherDataStore.fetchCipher(withId: id, userId: userId)
}

func list() async -> [Cipher] {
return try await cipherDataStore.fetchAllCiphers(userId: userId)
}

func set(id: String, value: Cipher) async { }

func remove(id: String) async { }
}

let store = CipherStoreImpl(cipherDataStore: self.cipherDataStore, userId: userId);
try await self.clientService.platform().store().registerCipherStore(store: store);
```

### How to use it on Android

Once we have the function defined in `bitwarden-uniffi`, we can use it from the Android application:

```kotlin
val vaultDiskSource: VaultDiskSource ;

class CipherStoreImpl: CipherStore {
override suspend fun get(id: String): Cipher? {
return vaultDiskSource.getCiphers(userId).firstOrNull()
.orEmpty().firstOrNull { it.id == id }?.toEncryptedSdkCipher()
}

override suspend fun list(): List<Cipher> {
return vaultDiskSource.getCiphers(userId).firstOrNull()
.orEmpty().map { it.toEncryptedSdkCipher() }
}

override suspend fun set(id: String, value: Cipher) {
TODO("Not yet implemented")
}

override suspend fun remove(id: String) {
TODO("Not yet implemented")
}
}

getClient(userId = userId).platform().store().registerCipherStore(CipherStoreImpl());
```
7 changes: 7 additions & 0 deletions crates/bitwarden-state/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#![doc = include_str!("../README.md")]

/// This module provides a generic repository interface for storing and retrieving items.
pub mod repository;

/// This module provides a registry for managing repositories of different types.
pub mod registry;
Loading
Loading