Skip to content
Open
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
2 changes: 2 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions crates/bitwarden-collections/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,9 @@ uniffi = { workspace = true, optional = true }
uuid = { workspace = true }
wasm-bindgen = { workspace = true, optional = true }

[dev-dependencies]
bitwarden-core = { workspace = true, features = ["internal", "test-fixtures"] }
tokio = { workspace = true, features = ["rt"] }

[lints]
workspace = true
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
use std::collections::HashMap;

use bitwarden_collections::{
collection::{Collection, CollectionId, CollectionView},
tree::{NodeItem, Tree},
};
use bitwarden_core::Client;
use bitwarden_core::{Client, FromClient};
#[cfg(feature = "wasm")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm")]
use tsify::Tsify;
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::wasm_bindgen;

use crate::{DecryptError, EncryptError};
use crate::{
collection::{Collection, CollectionId, CollectionView},
error::{CollectionDecryptError, CollectionEncryptError},
tree::{NodeItem, Tree},
};

#[allow(missing_docs)]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
Expand All @@ -21,10 +21,21 @@ pub struct CollectionsClient {
pub(crate) client: Client,
}

impl FromClient for CollectionsClient {
fn from_client(client: &Client) -> Self {
Self {
client: client.clone(),
}
}
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl CollectionsClient {
/// Encrypts a [CollectionView] into an encrypted [Collection] using the organization key.
pub fn encrypt(&self, collection_view: CollectionView) -> Result<Collection, EncryptError> {
pub fn encrypt(
&self,
collection_view: CollectionView,
) -> Result<Collection, CollectionEncryptError> {
let key_store = self.client.internal.get_key_store();
let collection = key_store.encrypt(collection_view)?;
Ok(collection)
Expand All @@ -35,14 +46,17 @@ impl CollectionsClient {
pub fn encrypt_list(
&self,
collection_views: Vec<CollectionView>,
) -> Result<Vec<Collection>, EncryptError> {
) -> Result<Vec<Collection>, CollectionEncryptError> {
let key_store = self.client.internal.get_key_store();
let collections = key_store.encrypt_list(&collection_views)?;
Ok(collections)
}

#[allow(missing_docs)]
pub fn decrypt(&self, collection: Collection) -> Result<CollectionView, DecryptError> {
pub fn decrypt(
&self,
collection: Collection,
) -> Result<CollectionView, CollectionDecryptError> {
let key_store = self.client.internal.get_key_store();
let view = key_store.decrypt(&collection)?;
Ok(view)
Expand All @@ -52,7 +66,7 @@ impl CollectionsClient {
pub fn decrypt_list(
&self,
collections: Vec<Collection>,
) -> Result<Vec<CollectionView>, DecryptError> {
) -> Result<Vec<CollectionView>, CollectionDecryptError> {
let key_store = self.client.internal.get_key_store();
let views = key_store.decrypt_list(&collections)?;
Ok(views)
Expand Down Expand Up @@ -144,11 +158,10 @@ impl CollectionViewTree {

#[cfg(test)]
mod tests {
use bitwarden_collections::collection::CollectionType;
use bitwarden_core::client::test_accounts::test_bitwarden_com_account;

use super::*;
use crate::VaultClientExt;
use crate::collection::CollectionType;

fn test_collection() -> Collection {
Collection {
Expand All @@ -164,49 +177,42 @@ mod tests {
}
}

async fn test_collections_client() -> CollectionsClient {
let client = Client::init_test_account(test_bitwarden_com_account()).await;
CollectionsClient::from_client(&client)
}

#[tokio::test]
async fn test_decrypt_list() {
let client = Client::init_test_account(test_bitwarden_com_account()).await;
let collections = test_collections_client().await;

let dec = client
.vault()
.collections()
.decrypt_list(vec![test_collection()])
.unwrap();
let dec = collections.decrypt_list(vec![test_collection()]).unwrap();

assert_eq!(dec[0].name, "Default collection");
}

#[tokio::test]
async fn test_decrypt() {
let client = Client::init_test_account(test_bitwarden_com_account()).await;
let collections = test_collections_client().await;

let dec = client
.vault()
.collections()
.decrypt(test_collection())
.unwrap();
let dec = collections.decrypt(test_collection()).unwrap();

assert_eq!(dec.name, "Default collection");
}

#[tokio::test]
async fn test_encrypt_decrypt_roundtrip() {
let client = Client::init_test_account(test_bitwarden_com_account()).await;
let collections = test_collections_client().await;

let view = client
.vault()
.collections()
.decrypt(test_collection())
.unwrap();
let view = collections.decrypt(test_collection()).unwrap();

assert_eq!(view.name, "Default collection");

// Re-encrypt the decrypted view, then decrypt again
let expected_id = view.id;
let expected_org_id = view.organization_id;
let re_encrypted = client.vault().collections().encrypt(view).unwrap();
let re_decrypted = client.vault().collections().decrypt(re_encrypted).unwrap();
let re_encrypted = collections.encrypt(view).unwrap();
let re_decrypted = collections.decrypt(re_encrypted).unwrap();

assert_eq!(re_decrypted.name, "Default collection");
assert_eq!(re_decrypted.id, expected_id);
Expand All @@ -215,29 +221,21 @@ mod tests {

#[tokio::test]
async fn test_encrypt_list_decrypt_list_roundtrip() {
let client = Client::init_test_account(test_bitwarden_com_account()).await;
let collections = test_collections_client().await;

let views = client
.vault()
.collections()
.decrypt_list(vec![test_collection()])
.unwrap();
let views = collections.decrypt_list(vec![test_collection()]).unwrap();

assert_eq!(views.len(), 1);
assert_eq!(views[0].name, "Default collection");

let expected_id = views[0].id;
let expected_org_id = views[0].organization_id;

let re_encrypted = client.vault().collections().encrypt_list(views).unwrap();
let re_encrypted = collections.encrypt_list(views).unwrap();

assert_eq!(re_encrypted.len(), 1);

let re_decrypted = client
.vault()
.collections()
.decrypt_list(re_encrypted)
.unwrap();
let re_decrypted = collections.decrypt_list(re_encrypted).unwrap();

assert_eq!(re_decrypted.len(), 1);
assert_eq!(re_decrypted[0].name, "Default collection");
Expand Down
13 changes: 13 additions & 0 deletions crates/bitwarden-collections/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ pub enum CollectionDecryptError {
Crypto(#[from] bitwarden_crypto::CryptoError),
}

/// Generic error type for collection encryption errors.
///
/// This intentionally mirrors `bitwarden_vault::EncryptError` rather than depending on it, to
/// avoid creating a circular dependency between the `bitwarden-collections` and `bitwarden-vault`
/// crates.
#[allow(missing_docs)]
#[bitwarden_error(flat)]
#[derive(Debug, Error)]
pub enum CollectionEncryptError {
#[error(transparent)]
Crypto(#[from] bitwarden_crypto::CryptoError),
}
Comment on lines +12 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: Finding 1: Swapping the collection client's error types silently changes the error identity that TS and mobile consumers match on.

Details and impact

CollectionsClient::{encrypt,encrypt_list} moved from bitwarden_vault::EncryptError to CollectionEncryptError, and {decrypt,decrypt_list} from bitwarden_vault::DecryptError to CollectionDecryptError. Because #[bitwarden_error(flat)] derives the binding surface from the enum name, this is consumer-visible on both bindings:

  • WASM/TS: the thrown error's name becomes "CollectionEncryptError" / "CollectionDecryptError", and the generated guard is now isCollectionEncryptError() / isCollectionDecryptError(). Existing isEncryptError(e) / isDecryptError(e) checks around collection calls now return false with no compile error.
  • UniFFI: bitwarden-uniffi/src/error.rs routes these through new BitwardenError::CollectionEncrypt / CollectionDecrypt variants, so catch (e: BitwardenException.Encrypt) / BitwardenException.Decrypt in Kotlin/Swift stops matching β€” also without a compile error.

The accessor move in this PR keeps vault().collections() around for a staged migration, but the error rename has no equivalent shim, so client-side error handling breaks the moment this version is consumed. Renaming to keep EncryptError via export_as is not viable (it would collide with bitwarden_vault::EncryptError in the same wasm module), so this likely needs a coordinated client change β€” worth calling out in the PR description / linked ticket so web and mobile pick it up.


#[allow(missing_docs)]
#[derive(Debug, Error)]
pub enum CollectionsParseError {
Expand Down
5 changes: 5 additions & 0 deletions crates/bitwarden-collections/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ mod uniffi_support;
/// Encryptable, TryFrom, and TreeItem
pub mod collection;
///
/// Module containing the [CollectionsClient](collection_client::CollectionsClient), which exposes
/// encrypt/decrypt operations for collections.
#[allow(missing_docs)]
pub mod collection_client;
///
/// Module containing the error types.
pub mod error;
///
Expand Down
3 changes: 3 additions & 0 deletions crates/bitwarden-pm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ keywords.workspace = true
cli = ["bitwarden-unlock/cli"]
no-memory-hardening = ["bitwarden-core/no-memory-hardening"]
uniffi = [
"bitwarden-collections/uniffi",
"bitwarden-core/uniffi",
"bitwarden-crypto-cipher-suite/uniffi",
"bitwarden-crypto-sync-handler/uniffi",
Expand All @@ -33,6 +34,7 @@ uniffi = [
]
wasm = [
"bitwarden-auth/wasm",
"bitwarden-collections/wasm",
"bitwarden-commercial-vault/wasm",
"bitwarden-core/wasm",
"bitwarden-crypto-cipher-suite/wasm",
Expand All @@ -56,6 +58,7 @@ bitwarden-license = ["dep:bitwarden-commercial-vault", "dep:bitwarden-pam"]
[dependencies]
async-trait = { workspace = true }
bitwarden-auth = { workspace = true }
bitwarden-collections = { workspace = true }
bitwarden-commercial-vault = { workspace = true, optional = true }
bitwarden-core = { workspace = true, features = ["internal"] }
bitwarden-crypto = { workspace = true }
Expand Down
10 changes: 10 additions & 0 deletions crates/bitwarden-pm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ uniffi::setup_scaffolding!();
/// Re-export subclients for easier access
pub mod clients {
pub use bitwarden_auth::AuthClient;
pub use bitwarden_collections::collection_client::CollectionsClient;
pub use bitwarden_core::key_management::CryptoClient;
pub use bitwarden_crypto_cipher_suite::CryptoCipherSuiteClient;
pub use bitwarden_crypto_sync_handler::CryptoSyncHandlerClient;
Expand Down Expand Up @@ -147,6 +148,15 @@ impl PasswordManagerClient {
self.0.vault()
}

/// Collection related operations.
///
/// This is registered directly on the top-level client in addition to being nested under
/// [`vault`](Self::vault); once all consumers have migrated to this accessor, the nested one
/// will be removed.
pub fn collections(&self) -> bitwarden_collections::collection_client::CollectionsClient {
bitwarden_collections::collection_client::CollectionsClient::from_client(&self.0)
}

/// Exporter operations
pub fn exporters(&self) -> bitwarden_exporters::ExporterClient {
self.0.exporters()
Expand Down
6 changes: 6 additions & 0 deletions crates/bitwarden-uniffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ pub enum BitwardenError {
#[error(transparent)]
EncryptFile(#[from] bitwarden_vault::EncryptFileError),

// Collections
#[error(transparent)]
CollectionDecrypt(#[from] bitwarden_collections::error::CollectionDecryptError),
#[error(transparent)]
CollectionEncrypt(#[from] bitwarden_collections::error::CollectionEncryptError),

// Send
#[error(transparent)]
SendDecrypt(#[from] bitwarden_send::SendDecryptError),
Expand Down
9 changes: 9 additions & 0 deletions crates/bitwarden-uniffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ impl Client {
VaultClient(self.0.vault())
}

/// Collection related operations.
///
/// This is registered directly on the top-level client in addition to being nested under
/// [`vault`](Self::vault). Once mobile clients have migrated to this accessor, the nested one
/// will be removed.
pub fn collections(&self) -> vault::collections::CollectionsClient {
vault::collections::CollectionsClient(self.0.collections())
}

#[allow(missing_docs)]
pub fn platform(&self) -> PlatformClient {
PlatformClient(self.0.0.clone())
Expand Down
6 changes: 4 additions & 2 deletions crates/bitwarden-uniffi/src/vault/collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ use std::sync::Arc;

use bitwarden_collections::{
collection::{Collection, CollectionId, CollectionView},
collection_client::AncestorMap,
tree::{NodeItem, Tree},
};
use bitwarden_vault::collection_client::AncestorMap;

use crate::Result;

#[allow(missing_docs)]
#[derive(uniffi::Object)]
pub struct CollectionsClient(pub(crate) bitwarden_vault::collection_client::CollectionsClient);
pub struct CollectionsClient(
pub(crate) bitwarden_collections::collection_client::CollectionsClient,
);

#[uniffi::export]
impl CollectionsClient {
Expand Down
2 changes: 0 additions & 2 deletions crates/bitwarden-vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ pub use error::{DecryptError, EncryptError, VaultParseError};
mod vault_client;
pub use vault_client::{VaultClient, VaultClientExt};

#[allow(missing_docs)]
pub mod collection_client;
mod totp_client;

pub use totp_client::TotpClient;
10 changes: 6 additions & 4 deletions crates/bitwarden-vault/src/vault_client.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use bitwarden_collections::collection_client::CollectionsClient;
use bitwarden_core::{Client, FromClient};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;

use crate::{
AttachmentsClient, CipherRiskClient, CiphersClient, FoldersClient, PasswordHistoryClient,
TotpClient, collection_client::CollectionsClient,
TotpClient,
};

#[allow(missing_docs)]
Expand Down Expand Up @@ -52,10 +53,11 @@ impl VaultClient {
}

/// Collection related operations.
///
/// This nested accessor is kept for backwards compatibility. New callers should prefer the
/// `collections()` accessor registered directly on the top-level Password Manager client.
pub fn collections(&self) -> CollectionsClient {
CollectionsClient {
client: self.client.clone(),
}
CollectionsClient::from_client(&self.client)
}

/// Cipher risk evaluation operations.
Expand Down
9 changes: 9 additions & 0 deletions crates/bitwarden-wasm-internal/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ impl PasswordManagerClient {
self.0.vault()
}

/// Collection related operations.
///
/// This is registered directly on the top-level client in addition to being nested under
/// [`vault`](Self::vault). Once consumers have migrated to this accessor, the nested one will
/// be removed.
pub fn collections(&self) -> CollectionsClient {
self.0.collections()
}

/// Constructs a specific client for platform-specific functionality
pub fn platform(&self) -> PlatformClient {
PlatformClient::new(self.0.0.clone())
Expand Down
Loading