diff --git a/Cargo.lock b/Cargo.lock index 04f97f266..14943a298 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -378,6 +378,7 @@ dependencies = [ "bitwarden-crypto", "bitwarden-error", "serde", + "serde_repr", "thiserror 1.0.69", "tsify", "uniffi", diff --git a/crates/bitwarden-collections/Cargo.toml b/crates/bitwarden-collections/Cargo.toml index c7a748f8f..774e5ce77 100644 --- a/crates/bitwarden-collections/Cargo.toml +++ b/crates/bitwarden-collections/Cargo.toml @@ -28,6 +28,7 @@ bitwarden-core = { workspace = true, features = ["internal"] } bitwarden-crypto = { workspace = true } bitwarden-error = { workspace = true } serde = { workspace = true } +serde_repr = { workspace = true } thiserror = { workspace = true } tsify = { workspace = true, optional = true } uniffi = { workspace = true, optional = true } diff --git a/crates/bitwarden-collections/src/collection.rs b/crates/bitwarden-collections/src/collection.rs index 6d03c14a3..b0ba100a1 100644 --- a/crates/bitwarden-collections/src/collection.rs +++ b/crates/bitwarden-collections/src/collection.rs @@ -5,6 +5,7 @@ use bitwarden_core::{ }; use bitwarden_crypto::{CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext}; use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use uuid::Uuid; #[cfg(feature = "wasm")] use {tsify::Tsify, wasm_bindgen::prelude::*}; @@ -24,6 +25,8 @@ pub struct Collection { pub hide_passwords: bool, pub read_only: bool, pub manage: bool, + pub default_user_collection_email: Option, + pub r#type: CollectionType, } #[allow(missing_docs)] @@ -39,6 +42,24 @@ pub struct CollectionView { pub hide_passwords: bool, pub read_only: bool, pub manage: bool, + pub r#type: CollectionType, +} + +/// Type of collection +#[derive(Serialize_repr, Deserialize_repr, Debug, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +#[repr(u8)] +pub enum CollectionType { + /// Default collection type. Can be assigned by an organization to user(s) or group(s) + SharedCollection = 0, + /// Default collection assigned to a user for an organization that has + /// OrganizationDataOwnership (formerly PersonalOwnership) policy enabled. + DefaultUserCollection = 1, } #[allow(missing_docs)] @@ -48,14 +69,21 @@ impl Decryptable for Collection { ctx: &mut KeyStoreContext, key: SymmetricKeyId, ) -> Result { + let name = self + .default_user_collection_email + .as_ref() + .unwrap_or(&self.name.decrypt(ctx, key)?) + .clone(); + Ok(CollectionView { id: self.id, organization_id: self.organization_id, - name: self.name.decrypt(ctx, key).ok().unwrap_or_default(), + name, external_id: self.external_id.clone(), hide_passwords: self.hide_passwords, read_only: self.read_only, manage: self.manage, + r#type: self.r#type.clone(), }) } } @@ -73,6 +101,8 @@ impl TryFrom for Collection { hide_passwords: collection.hide_passwords.unwrap_or(false), read_only: collection.read_only.unwrap_or(false), manage: collection.manage.unwrap_or(false), + default_user_collection_email: collection.default_user_collection_email, + r#type: require!(collection.r#type).into(), }) } } @@ -103,3 +133,132 @@ impl TreeItem for CollectionView { const DELIMITER: char = '/'; } + +impl From for CollectionType { + fn from(collection_type: bitwarden_api_api::models::CollectionType) -> Self { + match collection_type { + bitwarden_api_api::models::CollectionType::SharedCollection => Self::SharedCollection, + bitwarden_api_api::models::CollectionType::DefaultUserCollection => { + Self::DefaultUserCollection + } + } + } +} + +#[cfg(test)] +mod tests { + use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; + use bitwarden_crypto::{KeyStore, PrimitiveEncryptable, SymmetricCryptoKey}; + use uuid::Uuid; + + use super::*; + + const ORGANIZATION_ID: &str = "12345678-1234-1234-1234-123456789012"; + const COLLECTION_ID: &str = "87654321-4321-4321-4321-210987654321"; + + // Helper function to create a test key store with a symmetric key + fn create_test_key_store() -> KeyStore { + let store = KeyStore::::default(); + let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + let org_id = Uuid::parse_str(ORGANIZATION_ID).unwrap(); + + #[allow(deprecated)] + store + .context_mut() + .set_symmetric_key(SymmetricKeyId::Organization(org_id), key) + .unwrap(); + + store + } + + #[test] + fn test_decrypt_with_name_only() { + let store = create_test_key_store(); + let mut ctx = store.context(); + let org_id = Uuid::parse_str(ORGANIZATION_ID).unwrap(); + let key = SymmetricKeyId::Organization(org_id); + + let collection_name: &str = "Collection Name"; + + let collection = Collection { + id: Some(Uuid::parse_str(COLLECTION_ID).unwrap()), + organization_id: org_id, + name: collection_name.encrypt(&mut ctx, key).unwrap(), + external_id: Some("external-id".to_string()), + hide_passwords: true, + read_only: false, + manage: true, + default_user_collection_email: None, + r#type: CollectionType::SharedCollection, + }; + + let decrypted = collection.decrypt(&mut ctx, key).unwrap(); + + assert_eq!(decrypted.name, collection_name); + } + + #[test] + fn test_decrypt_with_default_user_collection_email() { + let store = create_test_key_store(); + let mut ctx = store.context(); + let org_id = Uuid::parse_str(ORGANIZATION_ID).unwrap(); + let key = SymmetricKeyId::Organization(org_id); + + let collection_name: &str = "Collection Name"; + let default_user_collection_email = String::from("test-user@bitwarden.com"); + + let collection = Collection { + id: Some(Uuid::parse_str(COLLECTION_ID).unwrap()), + organization_id: org_id, + name: collection_name.encrypt(&mut ctx, key).unwrap(), + external_id: None, + hide_passwords: false, + read_only: true, + manage: false, + default_user_collection_email: Some(default_user_collection_email.clone()), + r#type: CollectionType::SharedCollection, + }; + + let decrypted = collection.decrypt(&mut ctx, key).unwrap(); + + assert_ne!(decrypted.name, collection_name); + assert_eq!(decrypted.name, default_user_collection_email); + } + + #[test] + fn test_decrypt_all_fields_preserved() { + let store = create_test_key_store(); + let mut ctx = store.context(); + let org_id = Uuid::parse_str(ORGANIZATION_ID).unwrap(); + let key = SymmetricKeyId::Organization(org_id); + + let collection_id = Some(Uuid::parse_str(COLLECTION_ID).unwrap()); + let external_id = Some("external-test-id".to_string()); + let collection_name: &str = "Collection Name"; + let collection_type = CollectionType::SharedCollection; + + let collection = Collection { + id: collection_id, + organization_id: org_id, + name: collection_name.encrypt(&mut ctx, key).unwrap(), + external_id: external_id.clone(), + hide_passwords: true, + read_only: true, + manage: true, + default_user_collection_email: None, + r#type: collection_type.clone(), + }; + + let decrypted = collection.decrypt(&mut ctx, key).unwrap(); + + // Verify all fields are correctly transferred + assert_eq!(decrypted.id, collection.id); + assert_eq!(decrypted.organization_id, collection.organization_id); + assert_eq!(decrypted.name, collection_name); + assert_eq!(decrypted.external_id, external_id); + assert_eq!(decrypted.hide_passwords, collection.hide_passwords); + assert_eq!(decrypted.read_only, collection.read_only); + assert_eq!(decrypted.manage, collection.manage); + assert_eq!(decrypted.r#type, collection_type); + } +} diff --git a/crates/bitwarden-vault/src/collection_client.rs b/crates/bitwarden-vault/src/collection_client.rs index 72e58a507..4856c84d0 100644 --- a/crates/bitwarden-vault/src/collection_client.rs +++ b/crates/bitwarden-vault/src/collection_client.rs @@ -110,6 +110,7 @@ impl CollectionViewTree { #[cfg(test)] mod tests { + use bitwarden_collections::collection::CollectionType; use bitwarden_core::client::test_accounts::test_bitwarden_com_account; use super::*; @@ -127,6 +128,8 @@ mod tests { hide_passwords: false, read_only: false, manage: false, + default_user_collection_email: None, + r#type: CollectionType::SharedCollection, }]).unwrap(); assert_eq!(dec[0].name, "Default collection"); @@ -144,6 +147,8 @@ mod tests { hide_passwords: false, read_only: false, manage: false, + default_user_collection_email: None, + r#type: CollectionType::SharedCollection, }).unwrap(); assert_eq!(dec.name, "Default collection");