From 7947d70eeb7f7e710a6ec3e401bfc9d46a700080 Mon Sep 17 00:00:00 2001 From: jrmccannon Date: Thu, 31 Jul 2025 09:43:23 -0500 Subject: [PATCH 1/6] Added default_user_collection_email to Collection and tests for decrypt --- .../bitwarden-collections/src/collection.rs | 137 +++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden-collections/src/collection.rs b/crates/bitwarden-collections/src/collection.rs index 6d03c14a3..9a3911ece 100644 --- a/crates/bitwarden-collections/src/collection.rs +++ b/crates/bitwarden-collections/src/collection.rs @@ -24,6 +24,7 @@ pub struct Collection { pub hide_passwords: bool, pub read_only: bool, pub manage: bool, + pub default_user_collection_email: Option, } #[allow(missing_docs)] @@ -48,10 +49,17 @@ 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) + .ok() + .unwrap_or_default(); + 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, @@ -73,6 +81,9 @@ 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: EncString::try_from_optional( + collection.default_user_collection_email + )?, }) } } @@ -103,3 +114,127 @@ impl TreeItem for CollectionView { const DELIMITER: char = '/'; } + +#[cfg(test)] +mod tests { + use super::*; + use bitwarden_core::key_management::{KeyIds, SymmetricKeyId}; + use bitwarden_crypto::{KeyStore, PrimitiveEncryptable, SymmetricCryptoKey}; + use uuid::Uuid; + + 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::try_from("sJnO8rVi0dTwND43n0T9x7665s8mVUYNAaJ4nm7gx1iia1I7947URL60nwfIHaf9QJePO4VkNN0oT9jh4iC6aA==".to_string()).unwrap(); + 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, + }; + + let decrypted = collection.decrypt(&mut ctx, key).unwrap(); + + 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, collection.external_id); + assert_eq!(decrypted.hide_passwords, collection.hide_passwords); + assert_eq!(decrypted.read_only, collection.read_only); + assert_eq!(decrypted.manage, collection.manage); + } + + #[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: &str = "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.encrypt(&mut ctx, key).unwrap()), // Different encrypted value + }; + + let decrypted = collection.decrypt(&mut ctx, key).unwrap(); + + assert_eq!(decrypted.id, collection.id); + assert_eq!(decrypted.organization_id, collection.organization_id); + assert_ne!(decrypted.name, collection_name); + assert_eq!(decrypted.name, default_user_collection_email); + assert_eq!(decrypted.external_id, collection.external_id); + assert_eq!(decrypted.hide_passwords, collection.hide_passwords); + assert_eq!(decrypted.read_only, collection.read_only); + assert_eq!(decrypted.manage, collection.manage); + } + + #[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 = 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, + }; + + 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, org_id); + assert_eq!(decrypted.name, collection_name); + assert_eq!(decrypted.external_id, external_id); + assert!(decrypted.hide_passwords); + assert!(decrypted.read_only); + assert!(decrypted.manage); + } +} From a859e8d558d35ca86ce6ea742e608e9305e0bde7 Mon Sep 17 00:00:00 2001 From: jrmccannon Date: Thu, 31 Jul 2025 09:52:14 -0500 Subject: [PATCH 2/6] Added key generation to test --- crates/bitwarden-collections/src/collection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-collections/src/collection.rs b/crates/bitwarden-collections/src/collection.rs index 9a3911ece..57dfc669a 100644 --- a/crates/bitwarden-collections/src/collection.rs +++ b/crates/bitwarden-collections/src/collection.rs @@ -128,7 +128,7 @@ mod tests { // 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::try_from("sJnO8rVi0dTwND43n0T9x7665s8mVUYNAaJ4nm7gx1iia1I7947URL60nwfIHaf9QJePO4VkNN0oT9jh4iC6aA==".to_string()).unwrap(); + let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); let org_id = Uuid::parse_str(ORGANIZATION_ID).unwrap(); #[allow(deprecated)] From 8e043e22b20f577fda4b35fc006696fe487f23dd Mon Sep 17 00:00:00 2001 From: jrmccannon Date: Thu, 31 Jul 2025 10:22:41 -0500 Subject: [PATCH 3/6] fixed client tests --- crates/bitwarden-vault/src/collection_client.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bitwarden-vault/src/collection_client.rs b/crates/bitwarden-vault/src/collection_client.rs index 72e58a507..185b70e5a 100644 --- a/crates/bitwarden-vault/src/collection_client.rs +++ b/crates/bitwarden-vault/src/collection_client.rs @@ -127,6 +127,7 @@ mod tests { hide_passwords: false, read_only: false, manage: false, + default_user_collection_email: None }]).unwrap(); assert_eq!(dec[0].name, "Default collection"); @@ -144,6 +145,7 @@ mod tests { hide_passwords: false, read_only: false, manage: false, + default_user_collection_email: None }).unwrap(); assert_eq!(dec.name, "Default collection"); From 9bbd232104610f31028db76d11f987cbfe09d64e Mon Sep 17 00:00:00 2001 From: jrmccannon Date: Thu, 31 Jul 2025 10:54:55 -0500 Subject: [PATCH 4/6] Added collection type and fixed up tests. --- Cargo.lock | 1 + crates/bitwarden-collections/Cargo.toml | 1 + .../bitwarden-collections/src/collection.rs | 74 +++++++++++++------ .../bitwarden-vault/src/collection_client.rs | 7 +- 4 files changed, 60 insertions(+), 23 deletions(-) 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 57dfc669a..8ea6fe6aa 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::*}; @@ -25,6 +26,7 @@ pub struct Collection { pub read_only: bool, pub manage: bool, pub default_user_collection_email: Option, + pub r#type: CollectionType, } #[allow(missing_docs)] @@ -40,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)] @@ -49,7 +69,8 @@ impl Decryptable for Collection { ctx: &mut KeyStoreContext, key: SymmetricKeyId, ) -> Result { - let name = self.default_user_collection_email + let name = self + .default_user_collection_email .as_ref() .unwrap_or(&self.name) .decrypt(ctx, key) @@ -64,6 +85,7 @@ impl Decryptable for Collection { hide_passwords: self.hide_passwords, read_only: self.read_only, manage: self.manage, + r#type: self.r#type.clone(), }) } } @@ -82,8 +104,9 @@ impl TryFrom for Collection { read_only: collection.read_only.unwrap_or(false), manage: collection.manage.unwrap_or(false), default_user_collection_email: EncString::try_from_optional( - collection.default_user_collection_email + collection.default_user_collection_email, )?, + r#type: require!(collection.r#type).into(), }) } } @@ -115,13 +138,25 @@ 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 super::*; 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"; @@ -158,17 +193,12 @@ mod tests { 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.id, collection.id); - assert_eq!(decrypted.organization_id, collection.organization_id); assert_eq!(decrypted.name, collection_name); - assert_eq!(decrypted.external_id, collection.external_id); - assert_eq!(decrypted.hide_passwords, collection.hide_passwords); - assert_eq!(decrypted.read_only, collection.read_only); - assert_eq!(decrypted.manage, collection.manage); } #[test] @@ -189,19 +219,18 @@ mod tests { hide_passwords: false, read_only: true, manage: false, - default_user_collection_email: Some(default_user_collection_email.encrypt(&mut ctx, key).unwrap()), // Different encrypted value + default_user_collection_email: Some( + default_user_collection_email + .encrypt(&mut ctx, key) + .unwrap(), + ), // Different encrypted value + r#type: CollectionType::SharedCollection, }; let decrypted = collection.decrypt(&mut ctx, key).unwrap(); - assert_eq!(decrypted.id, collection.id); - assert_eq!(decrypted.organization_id, collection.organization_id); assert_ne!(decrypted.name, collection_name); assert_eq!(decrypted.name, default_user_collection_email); - assert_eq!(decrypted.external_id, collection.external_id); - assert_eq!(decrypted.hide_passwords, collection.hide_passwords); - assert_eq!(decrypted.read_only, collection.read_only); - assert_eq!(decrypted.manage, collection.manage); } #[test] @@ -214,6 +243,7 @@ mod tests { 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, @@ -224,17 +254,19 @@ mod tests { 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, org_id); + 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!(decrypted.hide_passwords); - assert!(decrypted.read_only); - assert!(decrypted.manage); + 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 185b70e5a..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,7 +128,8 @@ mod tests { hide_passwords: false, read_only: false, manage: false, - default_user_collection_email: None + default_user_collection_email: None, + r#type: CollectionType::SharedCollection, }]).unwrap(); assert_eq!(dec[0].name, "Default collection"); @@ -145,7 +147,8 @@ mod tests { hide_passwords: false, read_only: false, manage: false, - default_user_collection_email: None + default_user_collection_email: None, + r#type: CollectionType::SharedCollection, }).unwrap(); assert_eq!(dec.name, "Default collection"); From 56fe3ebcc0039f87e139343b809aa78139d2aaf8 Mon Sep 17 00:00:00 2001 From: jrmccannon Date: Fri, 1 Aug 2025 11:21:36 -0500 Subject: [PATCH 5/6] Default user collection email is not encrypted. --- .../bitwarden-collections/src/collection.rs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/crates/bitwarden-collections/src/collection.rs b/crates/bitwarden-collections/src/collection.rs index 8ea6fe6aa..23f3048aa 100644 --- a/crates/bitwarden-collections/src/collection.rs +++ b/crates/bitwarden-collections/src/collection.rs @@ -25,7 +25,7 @@ pub struct Collection { pub hide_passwords: bool, pub read_only: bool, pub manage: bool, - pub default_user_collection_email: Option, + pub default_user_collection_email: Option, pub r#type: CollectionType, } @@ -72,10 +72,8 @@ impl Decryptable for Collection { let name = self .default_user_collection_email .as_ref() - .unwrap_or(&self.name) - .decrypt(ctx, key) - .ok() - .unwrap_or_default(); + .unwrap_or(&self.name.decrypt(ctx, key)?) + .clone(); Ok(CollectionView { id: self.id, @@ -103,9 +101,7 @@ 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: EncString::try_from_optional( - collection.default_user_collection_email, - )?, + default_user_collection_email: collection.default_user_collection_email, r#type: require!(collection.r#type).into(), }) } @@ -209,7 +205,7 @@ mod tests { let key = SymmetricKeyId::Organization(org_id); let collection_name: &str = "Collection Name"; - let default_user_collection_email: &str = "test-user@bitwarden.com"; + let default_user_collection_email= String::from("test-user@bitwarden.com"); let collection = Collection { id: Some(Uuid::parse_str(COLLECTION_ID).unwrap()), @@ -219,11 +215,7 @@ mod tests { hide_passwords: false, read_only: true, manage: false, - default_user_collection_email: Some( - default_user_collection_email - .encrypt(&mut ctx, key) - .unwrap(), - ), // Different encrypted value + default_user_collection_email: Some(default_user_collection_email.clone()), r#type: CollectionType::SharedCollection, }; From 335b5739a6535c43910fad7f5acf53a2e04b40af Mon Sep 17 00:00:00 2001 From: jrmccannon Date: Fri, 1 Aug 2025 11:22:19 -0500 Subject: [PATCH 6/6] Linting --- crates/bitwarden-collections/src/collection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-collections/src/collection.rs b/crates/bitwarden-collections/src/collection.rs index 23f3048aa..b0ba100a1 100644 --- a/crates/bitwarden-collections/src/collection.rs +++ b/crates/bitwarden-collections/src/collection.rs @@ -205,7 +205,7 @@ mod tests { let key = SymmetricKeyId::Organization(org_id); let collection_name: &str = "Collection Name"; - let default_user_collection_email= String::from("test-user@bitwarden.com"); + let default_user_collection_email = String::from("test-user@bitwarden.com"); let collection = Collection { id: Some(Uuid::parse_str(COLLECTION_ID).unwrap()),