Skip to main content

bitwarden_collections/
collection_client.rs

1use std::collections::HashMap;
2
3use bitwarden_core::{Client, FromClient};
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    collection::{Collection, CollectionId, CollectionView},
8    error::{CollectionDecryptError, CollectionEncryptError},
9    tree::{NodeItem, Tree},
10};
11
12/// Represents the result of decrypting a list of collections.
13///
14/// This struct contains two vectors: `successes` and `failures`.
15/// `successes` contains the decrypted `CollectionView` objects,
16/// while `failures` contains the original `Collection` objects that failed to decrypt.
17#[derive(Serialize, Deserialize)]
18#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
19#[bitwarden_ffi::wasm_record]
20pub struct DecryptCollectionListResult {
21    /// The decrypted `CollectionView` objects.
22    pub successes: Vec<CollectionView>,
23    /// The original `Collection` objects that failed to decrypt.
24    pub failures: Vec<Collection>,
25}
26
27#[allow(missing_docs)]
28#[bitwarden_ffi::wasm_object]
29#[derive(Clone)]
30pub struct CollectionsClient {
31    pub(crate) client: Client,
32}
33
34impl FromClient for CollectionsClient {
35    fn from_client(client: &Client) -> Self {
36        Self {
37            client: client.clone(),
38        }
39    }
40}
41
42#[bitwarden_ffi::wasm_export]
43impl CollectionsClient {
44    /// Encrypts a [CollectionView] into an encrypted [Collection] using the organization key.
45    pub fn encrypt(
46        &self,
47        collection_view: CollectionView,
48    ) -> Result<Collection, CollectionEncryptError> {
49        let key_store = self.client.internal.get_key_store();
50        let collection = key_store.encrypt(collection_view)?;
51        Ok(collection)
52    }
53
54    /// Encrypts a list of [CollectionView]s into encrypted [Collection]s using the organization
55    /// key.
56    pub fn encrypt_list(
57        &self,
58        collection_views: Vec<CollectionView>,
59    ) -> Result<Vec<Collection>, CollectionEncryptError> {
60        let key_store = self.client.internal.get_key_store();
61        let collections = key_store.encrypt_list(&collection_views)?;
62        Ok(collections)
63    }
64
65    #[allow(missing_docs)]
66    pub fn decrypt(
67        &self,
68        collection: Collection,
69    ) -> Result<CollectionView, CollectionDecryptError> {
70        let key_store = self.client.internal.get_key_store();
71        let view = key_store.decrypt(&collection)?;
72        Ok(view)
73    }
74
75    #[allow(missing_docs)]
76    pub fn decrypt_list(
77        &self,
78        collections: Vec<Collection>,
79    ) -> Result<Vec<CollectionView>, CollectionDecryptError> {
80        let key_store = self.client.internal.get_key_store();
81        let views = key_store.decrypt_list(&collections)?;
82        Ok(views)
83    }
84
85    /// Decrypts a list of collections, returning successes and failures separately.
86    ///
87    /// Unlike `decrypt_list`, a single collection that fails to decrypt (e.g. due to a missing
88    /// organization key) does not abort the entire batch — it is returned in `failures` instead.
89    pub fn decrypt_list_with_failures(
90        &self,
91        collections: Vec<Collection>,
92    ) -> DecryptCollectionListResult {
93        let key_store = self.client.internal.get_key_store();
94        let (successes, failures) = key_store.decrypt_list_with_failures(&collections);
95        DecryptCollectionListResult {
96            successes,
97            failures: failures.into_iter().cloned().collect(),
98        }
99    }
100
101    ///
102    /// Returns the vector of CollectionView objects in a tree structure based on its implemented
103    /// path().
104    pub fn get_collection_tree(&self, collections: Vec<CollectionView>) -> CollectionViewTree {
105        CollectionViewTree {
106            tree: Tree::from_items(collections),
107        }
108    }
109}
110
111#[bitwarden_ffi::wasm_object]
112pub struct CollectionViewTree {
113    tree: Tree<CollectionView>,
114}
115
116#[bitwarden_ffi::wasm_object]
117pub struct CollectionViewNodeItem {
118    node_item: NodeItem<CollectionView>,
119}
120
121#[derive(Serialize, Deserialize)]
122#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
123#[bitwarden_ffi::wasm_record]
124pub struct AncestorMap {
125    pub ancestors: HashMap<CollectionId, String>,
126}
127
128#[bitwarden_ffi::wasm_export]
129impl CollectionViewNodeItem {
130    pub fn get_item(&self) -> CollectionView {
131        self.node_item.item.clone()
132    }
133
134    pub fn get_parent(&self) -> Option<CollectionView> {
135        self.node_item.parent.clone()
136    }
137
138    pub fn get_children(&self) -> Vec<CollectionView> {
139        self.node_item.children.clone()
140    }
141
142    pub fn get_ancestors(&self) -> AncestorMap {
143        AncestorMap {
144            ancestors: self
145                .node_item
146                .ancestors
147                .iter()
148                .map(|(&uuid, name)| (CollectionId::new(uuid), name.clone()))
149                .collect(),
150        }
151    }
152}
153
154#[bitwarden_ffi::wasm_export]
155impl CollectionViewTree {
156    pub fn get_item_for_view(
157        &self,
158        collection_view: CollectionView,
159    ) -> Option<CollectionViewNodeItem> {
160        self.tree
161            .get_item_by_id(collection_view.id.unwrap_or_default().into())
162            .map(|n| CollectionViewNodeItem { node_item: n })
163    }
164
165    pub fn get_root_items(&self) -> Vec<CollectionViewNodeItem> {
166        self.tree
167            .get_root_items()
168            .into_iter()
169            .map(|n| CollectionViewNodeItem { node_item: n })
170            .collect()
171    }
172
173    pub fn get_flat_items(&self) -> Vec<CollectionViewNodeItem> {
174        self.tree
175            .get_flat_items()
176            .into_iter()
177            .map(|n| CollectionViewNodeItem { node_item: n })
178            .collect()
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use bitwarden_core::{OrganizationId, client::test_accounts::test_bitwarden_com_account};
185
186    use super::*;
187    use crate::collection::CollectionType;
188
189    fn test_collection() -> Collection {
190        Collection {
191            id: Some("66c5ca57-0868-4c7e-902f-b181009709c0".parse().unwrap()),
192            organization_id: "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
193            name: "2.EI9Km5BfrIqBa1W+WCccfA==|laWxNnx+9H3MZww4zm7cBSLisjpi81zreaQntRhegVI=|x42+qKFf5ga6DIL0OW5pxCdLrC/gm8CXJvf3UASGteI=".parse().unwrap(),
194            external_id: None,
195            hide_passwords: false,
196            read_only: false,
197            manage: false,
198            default_user_collection_email: None,
199            r#type: CollectionType::SharedCollection,
200        }
201    }
202
203    async fn test_collections_client() -> CollectionsClient {
204        let client = Client::init_test_account(test_bitwarden_com_account()).await;
205        CollectionsClient::from_client(&client)
206    }
207
208    #[tokio::test]
209    async fn test_decrypt_list() {
210        let collections = test_collections_client().await;
211
212        let dec = collections.decrypt_list(vec![test_collection()]).unwrap();
213
214        assert_eq!(dec[0].name, "Default collection");
215    }
216
217    #[tokio::test]
218    async fn test_decrypt() {
219        let collections = test_collections_client().await;
220
221        let dec = collections.decrypt(test_collection()).unwrap();
222
223        assert_eq!(dec.name, "Default collection");
224    }
225
226    #[tokio::test]
227    async fn test_decrypt_list_with_failures_all_success() {
228        let collections = test_collections_client().await;
229
230        let result = collections.decrypt_list_with_failures(vec![test_collection()]);
231
232        assert_eq!(result.successes.len(), 1);
233        assert!(result.failures.is_empty());
234        assert_eq!(result.successes[0].name, "Default collection");
235    }
236
237    #[tokio::test]
238    async fn test_decrypt_list_with_failures_mixed_results() {
239        let client = test_collections_client().await;
240
241        let valid_collection = test_collection();
242        let mut invalid_collection = test_collection();
243        // No organization key exists in the test account's key store for this id, so
244        // decryption of this single item must fail without affecting the others.
245        invalid_collection.organization_id = OrganizationId::new_v4();
246
247        let collections = vec![valid_collection, invalid_collection.clone()];
248
249        let result = client.decrypt_list_with_failures(collections);
250
251        assert_eq!(result.successes.len(), 1);
252        assert_eq!(result.successes[0].name, "Default collection");
253
254        assert_eq!(result.failures.len(), 1);
255        // The failed item must be returned unchanged (still ciphertext) — decryption
256        // failures must never leak partially-decrypted or plaintext data.
257        assert_eq!(result.failures[0].id, invalid_collection.id);
258        assert_eq!(result.failures[0].name, invalid_collection.name);
259    }
260
261    #[tokio::test]
262    async fn test_decrypt_list_with_failures_empty_list() {
263        let collections = test_collections_client().await;
264
265        let result = collections.decrypt_list_with_failures(vec![]);
266
267        assert!(result.successes.is_empty());
268        assert!(result.failures.is_empty());
269    }
270
271    #[tokio::test]
272    async fn test_encrypt_decrypt_roundtrip() {
273        let collections = test_collections_client().await;
274
275        let view = collections.decrypt(test_collection()).unwrap();
276
277        assert_eq!(view.name, "Default collection");
278
279        // Re-encrypt the decrypted view, then decrypt again
280        let expected_id = view.id;
281        let expected_org_id = view.organization_id;
282        let re_encrypted = collections.encrypt(view).unwrap();
283        let re_decrypted = collections.decrypt(re_encrypted).unwrap();
284
285        assert_eq!(re_decrypted.name, "Default collection");
286        assert_eq!(re_decrypted.id, expected_id);
287        assert_eq!(re_decrypted.organization_id, expected_org_id);
288    }
289
290    #[tokio::test]
291    async fn test_encrypt_list_decrypt_list_roundtrip() {
292        let collections = test_collections_client().await;
293
294        let views = collections.decrypt_list(vec![test_collection()]).unwrap();
295
296        assert_eq!(views.len(), 1);
297        assert_eq!(views[0].name, "Default collection");
298
299        let expected_id = views[0].id;
300        let expected_org_id = views[0].organization_id;
301
302        let re_encrypted = collections.encrypt_list(views).unwrap();
303
304        assert_eq!(re_encrypted.len(), 1);
305
306        let re_decrypted = collections.decrypt_list(re_encrypted).unwrap();
307
308        assert_eq!(re_decrypted.len(), 1);
309        assert_eq!(re_decrypted[0].name, "Default collection");
310        assert_eq!(re_decrypted[0].id, expected_id);
311        assert_eq!(re_decrypted[0].organization_id, expected_org_id);
312    }
313}