1use bitwarden_core::{Client, OrganizationId};
2use bitwarden_crypto::IdentifyKey;
3#[cfg(feature = "wasm")]
4use wasm_bindgen::prelude::*;
5
6use super::EncryptionContext;
7use crate::{
8 cipher::cipher::DecryptCipherListResult, Cipher, CipherError, CipherListView, CipherView,
9 DecryptError, EncryptError, Fido2CredentialFullView,
10};
11
12#[allow(missing_docs)]
13#[cfg_attr(feature = "wasm", wasm_bindgen)]
14pub struct CiphersClient {
15 pub(crate) client: Client,
16}
17
18#[cfg_attr(feature = "wasm", wasm_bindgen)]
19impl CiphersClient {
20 #[allow(missing_docs)]
21 pub fn encrypt(&self, mut cipher_view: CipherView) -> Result<EncryptionContext, EncryptError> {
22 let user_id = self
23 .client
24 .internal
25 .get_user_id()
26 .ok_or(EncryptError::MissingUserId)?;
27 let key_store = self.client.internal.get_key_store();
28
29 if cipher_view.key.is_none()
32 && self
33 .client
34 .internal
35 .get_flags()
36 .enable_cipher_key_encryption
37 {
38 let key = cipher_view.key_identifier();
39 cipher_view.generate_cipher_key(&mut key_store.context(), key)?;
40 }
41
42 let cipher = key_store.encrypt(cipher_view)?;
43 Ok(EncryptionContext {
44 cipher,
45 encrypted_for: user_id,
46 })
47 }
48
49 #[allow(missing_docs)]
50 pub fn decrypt(&self, cipher: Cipher) -> Result<CipherView, DecryptError> {
51 let key_store = self.client.internal.get_key_store();
52 let cipher_view = key_store.decrypt(&cipher)?;
53 Ok(cipher_view)
54 }
55
56 #[allow(missing_docs)]
57 pub fn decrypt_list(&self, ciphers: Vec<Cipher>) -> Result<Vec<CipherListView>, DecryptError> {
58 let key_store = self.client.internal.get_key_store();
59 let cipher_views = key_store.decrypt_list(&ciphers)?;
60 Ok(cipher_views)
61 }
62
63 pub fn decrypt_list_with_failures(&self, ciphers: Vec<Cipher>) -> DecryptCipherListResult {
66 let key_store = self.client.internal.get_key_store();
67 let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
68
69 DecryptCipherListResult {
70 successes,
71 failures: failures.into_iter().cloned().collect(),
72 }
73 }
74
75 #[allow(missing_docs)]
76 pub fn decrypt_fido2_credentials(
77 &self,
78 cipher_view: CipherView,
79 ) -> Result<Vec<crate::Fido2CredentialView>, DecryptError> {
80 let key_store = self.client.internal.get_key_store();
81 let credentials = cipher_view.decrypt_fido2_credentials(&mut key_store.context())?;
82 Ok(credentials)
83 }
84
85 #[cfg(feature = "wasm")]
91 pub fn set_fido2_credentials(
92 &self,
93 mut cipher_view: CipherView,
94 fido2_credentials: Vec<Fido2CredentialFullView>,
95 ) -> Result<CipherView, CipherError> {
96 let key_store = self.client.internal.get_key_store();
97
98 cipher_view.set_new_fido2_credentials(&mut key_store.context(), fido2_credentials)?;
99
100 Ok(cipher_view)
101 }
102
103 #[allow(missing_docs)]
104 pub fn move_to_organization(
105 &self,
106 mut cipher_view: CipherView,
107 organization_id: OrganizationId,
108 ) -> Result<CipherView, CipherError> {
109 let key_store = self.client.internal.get_key_store();
110 cipher_view.move_to_organization(&mut key_store.context(), organization_id.into())?;
111 Ok(cipher_view)
112 }
113
114 #[cfg(feature = "wasm")]
115 #[allow(missing_docs)]
116 pub fn decrypt_fido2_private_key(
117 &self,
118 cipher_view: CipherView,
119 ) -> Result<String, CipherError> {
120 let key_store = self.client.internal.get_key_store();
121 let decrypted_key = cipher_view.decrypt_fido2_private_key(&mut key_store.context())?;
122 Ok(decrypted_key)
123 }
124}
125
126#[cfg(test)]
127mod tests {
128
129 use bitwarden_core::client::test_accounts::test_bitwarden_com_account;
130
131 use super::*;
132 use crate::{Attachment, CipherRepromptType, CipherType, Login, VaultClientExt};
133
134 fn test_cipher() -> Cipher {
135 Cipher {
136 id: Some("358f2b2b-9326-4e5e-94a8-b18100bb0908".parse().unwrap()),
137 organization_id: None,
138 folder_id: None,
139 collection_ids: vec![],
140 key: None,
141 name: "2.+oPT8B4xJhyhQRe1VkIx0A==|PBtC/bZkggXR+fSnL/pG7g==|UkjRD0VpnUYkjRC/05ZLdEBAmRbr3qWRyJey2bUvR9w=".parse().unwrap(),
142 notes: None,
143 r#type: CipherType::Login,
144 login: Some(Login{
145 username: None,
146 password: None,
147 password_revision_date: None,
148 uris:None,
149 totp: None,
150 autofill_on_page_load: None,
151 fido2_credentials: None,
152 }),
153 identity: None,
154 card: None,
155 secure_note: None,
156 ssh_key: None,
157 favorite: false,
158 reprompt: CipherRepromptType::None,
159 organization_use_totp: true,
160 edit: true,
161 permissions: None,
162 view_password: true,
163 local_data: None,
164 attachments: None,
165 fields: None,
166 password_history: None,
167 creation_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
168 deleted_date: None,
169 revision_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
170 }
171 }
172
173 fn test_attachment_legacy() -> Attachment {
174 Attachment {
175 id: Some("uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
176 url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
177 file_name: Some("2.mV50WiLq6duhwGbhM1TO0A==|dTufWNH8YTPP0EMlNLIpFA==|QHp+7OM8xHtEmCfc9QPXJ0Ro2BeakzvLgxJZ7NdLuDc=".parse().unwrap()),
178 key: None,
179 size: Some("65".to_string()),
180 size_name: Some("65 Bytes".to_string()),
181 }
182 }
183
184 fn test_attachment_v2() -> Attachment {
185 Attachment {
186 id: Some("a77m56oerrz5b92jm05lq5qoyj1xh2t9".to_string()),
187 url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
188 file_name: Some("2.GhazFdCYQcM5v+AtVwceQA==|98bMUToqC61VdVsSuXWRwA==|bsLByMht9Hy5QO9pPMRz0K4d0aqBiYnnROGM5YGbNu4=".parse().unwrap()),
189 key: Some("2.6TPEiYULFg/4+3CpDRwCqw==|6swweBHCJcd5CHdwBBWuRN33XRV22VoroDFDUmiM4OzjPEAhgZK57IZS1KkBlCcFvT+t+YbsmDcdv+Lqr+iJ3MmzfJ40MCB5TfYy+22HVRA=|rkgFDh2IWTfPC1Y66h68Diiab/deyi1p/X0Fwkva0NQ=".parse().unwrap()),
190 size: Some("65".to_string()),
191 size_name: Some("65 Bytes".to_string()),
192 }
193 }
194
195 #[tokio::test]
196 async fn test_decrypt_list() {
197 let client = Client::init_test_account(test_bitwarden_com_account()).await;
198
199 let dec = client
200 .vault()
201 .ciphers()
202 .decrypt_list(vec![Cipher {
203 id: Some("a1569f46-0797-4d3f-b859-b181009e2e49".parse().unwrap()),
204 organization_id: Some("1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap()),
205 folder_id: None,
206 collection_ids: vec!["66c5ca57-0868-4c7e-902f-b181009709c0".parse().unwrap()],
207 key: None,
208 name: "2.RTdUGVWYl/OZHUMoy68CMg==|sCaT5qHx8i0rIvzVrtJKww==|jB8DsRws6bXBtXNfNXUmFJ0JLDlB6GON6Y87q0jgJ+0=".parse().unwrap(),
209 notes: None,
210 r#type: CipherType::Login,
211 login: Some(Login{
212 username: Some("2.ouEYEk+SViUtqncesfe9Ag==|iXzEJq1zBeNdDbumFO1dUA==|RqMoo9soSwz/yB99g6YPqk8+ASWRcSdXsKjbwWzyy9U=".parse().unwrap()),
213 password: Some("2.6yXnOz31o20Z2kiYDnXueA==|rBxTb6NK9lkbfdhrArmacw==|ogZir8Z8nLgiqlaLjHH+8qweAtItS4P2iPv1TELo5a0=".parse().unwrap()),
214 password_revision_date: None, uris:None, totp: None, autofill_on_page_load: None, fido2_credentials: None }),
215 identity: None,
216 card: None,
217 secure_note: None,
218 ssh_key: None,
219 favorite: false,
220 reprompt: CipherRepromptType::None,
221 organization_use_totp: true,
222 edit: true,
223 permissions: None,
224 view_password: true,
225 local_data: None,
226 attachments: None,
227 fields: None,
228 password_history: None,
229 creation_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
230 deleted_date: None,
231 revision_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
232 }])
233
234 .unwrap();
235
236 assert_eq!(dec[0].name, "Test item");
237 }
238
239 #[tokio::test]
240 async fn test_decrypt_list_with_failures_all_success() {
241 let client = Client::init_test_account(test_bitwarden_com_account()).await;
242
243 let valid_cipher = test_cipher();
244
245 let result = client
246 .vault()
247 .ciphers()
248 .decrypt_list_with_failures(vec![valid_cipher]);
249
250 assert_eq!(result.successes.len(), 1);
251 assert!(result.failures.is_empty());
252 assert_eq!(result.successes[0].name, "234234");
253 }
254
255 #[tokio::test]
256 async fn test_decrypt_list_with_failures_mixed_results() {
257 let client = Client::init_test_account(test_bitwarden_com_account()).await;
258 let valid_cipher = test_cipher();
259 let mut invalid_cipher = test_cipher();
260 invalid_cipher.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
262
263 let ciphers = vec![valid_cipher, invalid_cipher.clone()];
264
265 let result = client.vault().ciphers().decrypt_list_with_failures(ciphers);
266
267 assert_eq!(result.successes.len(), 1);
268 assert_eq!(result.failures.len(), 1);
269
270 assert_eq!(result.successes[0].name, "234234");
271 }
272
273 #[tokio::test]
274 async fn test_move_user_cipher_with_attachment_without_key_to_org_fails() {
275 let client = Client::init_test_account(test_bitwarden_com_account()).await;
276
277 let mut cipher = test_cipher();
278 cipher.attachments = Some(vec![test_attachment_legacy()]);
279
280 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
281
282 let res = client.vault().ciphers().move_to_organization(
284 view,
285 "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
286 );
287
288 assert!(res.is_err());
289 }
290
291 #[tokio::test]
292 async fn test_encrypt_cipher_with_legacy_attachment_without_key() {
293 let client = Client::init_test_account(test_bitwarden_com_account()).await;
294
295 let mut cipher = test_cipher();
296 let attachment = test_attachment_legacy();
297 cipher.attachments = Some(vec![attachment.clone()]);
298
299 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
300
301 assert!(cipher.key.is_none());
302
303 let EncryptionContext {
305 cipher: new_cipher,
306 encrypted_for: _,
307 } = client.vault().ciphers().encrypt(view).unwrap();
308 assert!(new_cipher.key.is_some());
309
310 let view = client.vault().ciphers().decrypt(new_cipher).unwrap();
311 let attachments = view.clone().attachments.unwrap();
312 let attachment_view = attachments.first().unwrap().clone();
313 assert!(attachment_view.key.is_none());
314
315 assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
316
317 let buf = vec![
318 2, 100, 205, 148, 152, 77, 184, 77, 53, 80, 38, 240, 83, 217, 251, 118, 254, 27, 117,
319 41, 148, 244, 216, 110, 216, 255, 104, 215, 23, 15, 176, 239, 208, 114, 95, 159, 23,
320 211, 98, 24, 145, 166, 60, 197, 42, 204, 131, 144, 253, 204, 195, 154, 27, 201, 215,
321 43, 10, 244, 107, 226, 152, 85, 167, 66, 185,
322 ];
323
324 let content = client
325 .vault()
326 .attachments()
327 .decrypt_buffer(cipher, attachment_view.clone(), buf.as_slice())
328 .unwrap();
329
330 assert_eq!(content, b"Hello");
331 }
332
333 #[tokio::test]
334 async fn test_encrypt_cipher_with_v1_attachment_without_key() {
335 let client = Client::init_test_account(test_bitwarden_com_account()).await;
336
337 let mut cipher = test_cipher();
338 let attachment = test_attachment_v2();
339 cipher.attachments = Some(vec![attachment.clone()]);
340
341 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
342
343 assert!(cipher.key.is_none());
344
345 let EncryptionContext {
347 cipher: new_cipher,
348 encrypted_for: _,
349 } = client.vault().ciphers().encrypt(view).unwrap();
350 assert!(new_cipher.key.is_some());
351
352 let view = client
353 .vault()
354 .ciphers()
355 .decrypt(new_cipher.clone())
356 .unwrap();
357 let attachments = view.clone().attachments.unwrap();
358 let attachment_view = attachments.first().unwrap().clone();
359 assert!(attachment_view.key.is_some());
360
361 assert_ne!(
363 attachment.clone().key.unwrap().to_string(),
364 attachment_view.clone().key.unwrap().to_string()
365 );
366
367 assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
368
369 let buf = vec![
370 2, 114, 53, 72, 20, 82, 18, 46, 48, 137, 97, 1, 100, 142, 120, 187, 28, 36, 180, 46,
371 189, 254, 133, 23, 169, 58, 73, 212, 172, 116, 185, 127, 111, 92, 112, 145, 99, 28,
372 158, 198, 48, 241, 121, 218, 66, 37, 152, 197, 122, 241, 110, 82, 245, 72, 47, 230, 95,
373 188, 196, 170, 127, 67, 44, 129, 90,
374 ];
375
376 let content = client
377 .vault()
378 .attachments()
379 .decrypt_buffer(new_cipher.clone(), attachment_view.clone(), buf.as_slice())
380 .unwrap();
381
382 assert_eq!(content, b"Hello");
383
384 let new_view = client
386 .vault()
387 .ciphers()
388 .move_to_organization(
389 view,
390 "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
391 )
392 .unwrap();
393 let EncryptionContext {
394 cipher: new_cipher,
395 encrypted_for: _,
396 } = client.vault().ciphers().encrypt(new_view).unwrap();
397
398 let attachment = new_cipher
399 .clone()
400 .attachments
401 .unwrap()
402 .first()
403 .unwrap()
404 .clone();
405
406 assert_eq!(
408 attachment.clone().key.as_ref().unwrap().to_string(),
409 attachment_view.key.as_ref().unwrap().to_string()
410 );
411
412 let content = client
413 .vault()
414 .attachments()
415 .decrypt_buffer(new_cipher, attachment_view, buf.as_slice())
416 .unwrap();
417
418 assert_eq!(content, b"Hello");
419 }
420}