1use std::sync::Arc;
2
3use bitwarden_core::{Client, OrganizationId};
4use bitwarden_crypto::IdentifyKey;
5#[cfg(feature = "wasm")]
6use bitwarden_crypto::{CompositeEncryptable, SymmetricCryptoKey};
7#[cfg(feature = "wasm")]
8use bitwarden_encoding::B64;
9use bitwarden_state::repository::{Repository, RepositoryError};
10#[cfg(feature = "wasm")]
11use wasm_bindgen::prelude::*;
12
13use super::EncryptionContext;
14#[cfg(feature = "wasm")]
15use crate::Fido2CredentialFullView;
16use crate::{
17 Cipher, CipherError, CipherListView, CipherView, DecryptError, EncryptError,
18 cipher::cipher::{DecryptCipherListResult, DecryptCipherResult},
19 cipher_client::admin::CipherAdminClient,
20};
21
22mod admin;
23mod create;
24mod delete;
25mod delete_attachment;
26mod edit;
27mod get;
28mod restore;
29mod share_cipher;
30
31#[allow(missing_docs)]
32#[cfg_attr(feature = "wasm", wasm_bindgen)]
33pub struct CiphersClient {
34 pub(crate) client: Client,
35}
36
37#[cfg_attr(feature = "wasm", wasm_bindgen)]
38impl CiphersClient {
39 #[allow(missing_docs)]
40 pub fn encrypt(&self, mut cipher_view: CipherView) -> Result<EncryptionContext, EncryptError> {
41 let user_id = self
42 .client
43 .internal
44 .get_user_id()
45 .ok_or(EncryptError::MissingUserId)?;
46 let key_store = self.client.internal.get_key_store();
47
48 if cipher_view.key.is_none()
51 && self
52 .client
53 .internal
54 .get_flags()
55 .enable_cipher_key_encryption
56 {
57 let key = cipher_view.key_identifier();
58 cipher_view.generate_cipher_key(&mut key_store.context(), key)?;
59 }
60
61 let cipher = key_store.encrypt(cipher_view)?;
62 Ok(EncryptionContext {
63 cipher,
64 encrypted_for: user_id,
65 })
66 }
67
68 #[cfg(feature = "wasm")]
79 pub fn encrypt_cipher_for_rotation(
80 &self,
81 mut cipher_view: CipherView,
82 new_key: B64,
83 ) -> Result<EncryptionContext, CipherError> {
84 let new_key = SymmetricCryptoKey::try_from(new_key)?;
85
86 let user_id = self
87 .client
88 .internal
89 .get_user_id()
90 .ok_or(EncryptError::MissingUserId)?;
91 let key_store = self.client.internal.get_key_store();
92 let mut ctx = key_store.context();
93
94 let new_key_id = ctx.add_local_symmetric_key(new_key);
96
97 if cipher_view.key.is_none()
98 && self
99 .client
100 .internal
101 .get_flags()
102 .enable_cipher_key_encryption
103 {
104 cipher_view.generate_cipher_key(&mut ctx, new_key_id)?;
105 } else {
106 cipher_view.reencrypt_cipher_keys(&mut ctx, new_key_id)?;
107 }
108
109 let cipher = cipher_view.encrypt_composite(&mut ctx, new_key_id)?;
110
111 Ok(EncryptionContext {
112 cipher,
113 encrypted_for: user_id,
114 })
115 }
116
117 #[cfg(feature = "wasm")]
122 pub fn encrypt_list(
123 &self,
124 cipher_views: Vec<CipherView>,
125 ) -> Result<Vec<EncryptionContext>, EncryptError> {
126 let user_id = self
127 .client
128 .internal
129 .get_user_id()
130 .ok_or(EncryptError::MissingUserId)?;
131 let key_store = self.client.internal.get_key_store();
132 let enable_cipher_key = self
133 .client
134 .internal
135 .get_flags()
136 .enable_cipher_key_encryption;
137
138 let mut ctx = key_store.context();
139
140 let prepared_views: Vec<CipherView> = cipher_views
141 .into_iter()
142 .map(|mut cv| {
143 if cv.key.is_none() && enable_cipher_key {
144 let key = cv.key_identifier();
145 cv.generate_cipher_key(&mut ctx, key)?;
146 }
147 Ok(cv)
148 })
149 .collect::<Result<Vec<_>, bitwarden_crypto::CryptoError>>()?;
150
151 let ciphers: Vec<Cipher> = key_store.encrypt_list(&prepared_views)?;
152
153 Ok(ciphers
154 .into_iter()
155 .map(|cipher| EncryptionContext {
156 cipher,
157 encrypted_for: user_id,
158 })
159 .collect())
160 }
161
162 #[allow(missing_docs)]
163 pub fn decrypt(&self, cipher: Cipher) -> Result<CipherView, DecryptError> {
164 let key_store = self.client.internal.get_key_store();
165 let cipher_view = key_store.decrypt(&cipher)?;
166 Ok(cipher_view)
167 }
168
169 #[allow(missing_docs)]
170 pub fn decrypt_list(&self, ciphers: Vec<Cipher>) -> Result<Vec<CipherListView>, DecryptError> {
171 let key_store = self.client.internal.get_key_store();
172 let cipher_views = key_store.decrypt_list(&ciphers)?;
173 Ok(cipher_views)
174 }
175
176 pub fn decrypt_list_with_failures(&self, ciphers: Vec<Cipher>) -> DecryptCipherListResult {
179 let key_store = self.client.internal.get_key_store();
180 let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
181
182 DecryptCipherListResult {
183 successes,
184 failures: failures.into_iter().cloned().collect(),
185 }
186 }
187
188 #[cfg(feature = "wasm")]
191 pub fn decrypt_list_full_with_failures(&self, ciphers: Vec<Cipher>) -> DecryptCipherResult {
192 let key_store = self.client.internal.get_key_store();
193 let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
194
195 DecryptCipherResult {
196 successes,
197 failures: failures.into_iter().cloned().collect(),
198 }
199 }
200
201 #[allow(missing_docs)]
202 pub fn decrypt_fido2_credentials(
203 &self,
204 cipher_view: CipherView,
205 ) -> Result<Vec<crate::Fido2CredentialView>, DecryptError> {
206 let key_store = self.client.internal.get_key_store();
207 let credentials = cipher_view.decrypt_fido2_credentials(&mut key_store.context())?;
208 Ok(credentials)
209 }
210
211 #[cfg(feature = "wasm")]
217 pub fn set_fido2_credentials(
218 &self,
219 mut cipher_view: CipherView,
220 fido2_credentials: Vec<Fido2CredentialFullView>,
221 ) -> Result<CipherView, CipherError> {
222 let key_store = self.client.internal.get_key_store();
223
224 cipher_view.set_new_fido2_credentials(&mut key_store.context(), fido2_credentials)?;
225
226 Ok(cipher_view)
227 }
228
229 #[allow(missing_docs)]
230 pub fn move_to_organization(
231 &self,
232 mut cipher_view: CipherView,
233 organization_id: OrganizationId,
234 ) -> Result<CipherView, CipherError> {
235 let key_store = self.client.internal.get_key_store();
236 cipher_view.move_to_organization(&mut key_store.context(), organization_id)?;
237 Ok(cipher_view)
238 }
239
240 #[cfg(feature = "wasm")]
241 #[allow(missing_docs)]
242 pub fn decrypt_fido2_private_key(
243 &self,
244 cipher_view: CipherView,
245 ) -> Result<String, CipherError> {
246 let key_store = self.client.internal.get_key_store();
247 let decrypted_key = cipher_view.decrypt_fido2_private_key(&mut key_store.context())?;
248 Ok(decrypted_key)
249 }
250
251 pub fn admin(&self) -> CipherAdminClient {
254 CipherAdminClient {
255 client: self.client.clone(),
256 }
257 }
258}
259
260impl CiphersClient {
261 fn get_repository(&self) -> Result<Arc<dyn Repository<Cipher>>, RepositoryError> {
262 Ok(self.client.platform().state().get::<Cipher>()?)
263 }
264}
265
266#[cfg(test)]
267mod tests {
268
269 use bitwarden_core::client::test_accounts::test_bitwarden_com_account;
270 #[cfg(feature = "wasm")]
271 use bitwarden_crypto::CryptoError;
272
273 use super::*;
274 use crate::{Attachment, CipherRepromptType, CipherType, Login, LoginView, VaultClientExt};
275
276 fn test_cipher() -> Cipher {
277 Cipher {
278 id: Some("358f2b2b-9326-4e5e-94a8-b18100bb0908".parse().unwrap()),
279 organization_id: None,
280 folder_id: None,
281 collection_ids: vec![],
282 key: None,
283 name: "2.+oPT8B4xJhyhQRe1VkIx0A==|PBtC/bZkggXR+fSnL/pG7g==|UkjRD0VpnUYkjRC/05ZLdEBAmRbr3qWRyJey2bUvR9w=".parse().unwrap(),
284 notes: None,
285 r#type: CipherType::Login,
286 login: Some(Login{
287 username: None,
288 password: None,
289 password_revision_date: None,
290 uris:None,
291 totp: None,
292 autofill_on_page_load: None,
293 fido2_credentials: None,
294 }),
295 identity: None,
296 card: None,
297 secure_note: None,
298 ssh_key: None,
299 favorite: false,
300 reprompt: CipherRepromptType::None,
301 organization_use_totp: true,
302 edit: true,
303 permissions: None,
304 view_password: true,
305 local_data: None,
306 attachments: None,
307 fields: None,
308 password_history: None,
309 creation_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
310 deleted_date: None,
311 revision_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
312 archived_date: None,
313 data: None,
314 }
315 }
316
317 fn test_cipher_view() -> CipherView {
318 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
319 CipherView {
320 r#type: CipherType::Login,
321 login: Some(LoginView {
322 username: Some("test_username".to_string()),
323 password: Some("test_password".to_string()),
324 password_revision_date: None,
325 uris: None,
326 totp: None,
327 autofill_on_page_load: None,
328 fido2_credentials: None,
329 }),
330 id: Some(test_id),
331 organization_id: None,
332 folder_id: None,
333 collection_ids: vec![],
334 key: None,
335 name: "My test login".to_string(),
336 notes: None,
337 identity: None,
338 card: None,
339 secure_note: None,
340 ssh_key: None,
341 favorite: false,
342 reprompt: CipherRepromptType::None,
343 organization_use_totp: true,
344 edit: true,
345 permissions: None,
346 view_password: true,
347 local_data: None,
348 attachments: None,
349 attachment_decryption_failures: None,
350 fields: None,
351 password_history: None,
352 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
353 deleted_date: None,
354 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
355 archived_date: None,
356 }
357 }
358
359 fn test_attachment_legacy() -> Attachment {
360 Attachment {
361 id: Some("uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
362 url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
363 file_name: Some("2.mV50WiLq6duhwGbhM1TO0A==|dTufWNH8YTPP0EMlNLIpFA==|QHp+7OM8xHtEmCfc9QPXJ0Ro2BeakzvLgxJZ7NdLuDc=".parse().unwrap()),
364 key: None,
365 size: Some("65".to_string()),
366 size_name: Some("65 Bytes".to_string()),
367 }
368 }
369
370 fn test_attachment_v2() -> Attachment {
371 Attachment {
372 id: Some("a77m56oerrz5b92jm05lq5qoyj1xh2t9".to_string()),
373 url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
374 file_name: Some("2.GhazFdCYQcM5v+AtVwceQA==|98bMUToqC61VdVsSuXWRwA==|bsLByMht9Hy5QO9pPMRz0K4d0aqBiYnnROGM5YGbNu4=".parse().unwrap()),
375 key: Some("2.6TPEiYULFg/4+3CpDRwCqw==|6swweBHCJcd5CHdwBBWuRN33XRV22VoroDFDUmiM4OzjPEAhgZK57IZS1KkBlCcFvT+t+YbsmDcdv+Lqr+iJ3MmzfJ40MCB5TfYy+22HVRA=|rkgFDh2IWTfPC1Y66h68Diiab/deyi1p/X0Fwkva0NQ=".parse().unwrap()),
376 size: Some("65".to_string()),
377 size_name: Some("65 Bytes".to_string()),
378 }
379 }
380
381 #[tokio::test]
382 async fn test_decrypt_list() {
383 let client = Client::init_test_account(test_bitwarden_com_account()).await;
384
385 let dec = client
386 .vault()
387 .ciphers()
388 .decrypt_list(vec![Cipher {
389 id: Some("a1569f46-0797-4d3f-b859-b181009e2e49".parse().unwrap()),
390 organization_id: Some("1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap()),
391 folder_id: None,
392 collection_ids: vec!["66c5ca57-0868-4c7e-902f-b181009709c0".parse().unwrap()],
393 key: None,
394 name: "2.RTdUGVWYl/OZHUMoy68CMg==|sCaT5qHx8i0rIvzVrtJKww==|jB8DsRws6bXBtXNfNXUmFJ0JLDlB6GON6Y87q0jgJ+0=".parse().unwrap(),
395 notes: None,
396 r#type: CipherType::Login,
397 login: Some(Login{
398 username: Some("2.ouEYEk+SViUtqncesfe9Ag==|iXzEJq1zBeNdDbumFO1dUA==|RqMoo9soSwz/yB99g6YPqk8+ASWRcSdXsKjbwWzyy9U=".parse().unwrap()),
399 password: Some("2.6yXnOz31o20Z2kiYDnXueA==|rBxTb6NK9lkbfdhrArmacw==|ogZir8Z8nLgiqlaLjHH+8qweAtItS4P2iPv1TELo5a0=".parse().unwrap()),
400 password_revision_date: None, uris:None, totp: None, autofill_on_page_load: None, fido2_credentials: None }),
401 identity: None,
402 card: None,
403 secure_note: None,
404 ssh_key: None,
405 favorite: false,
406 reprompt: CipherRepromptType::None,
407 organization_use_totp: true,
408 edit: true,
409 permissions: None,
410 view_password: true,
411 local_data: None,
412 attachments: None,
413 fields: None,
414 password_history: None,
415 creation_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
416 deleted_date: None,
417 revision_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
418 archived_date: None,
419 data: None,
420 }])
421
422 .unwrap();
423
424 assert_eq!(dec[0].name, "Test item");
425 }
426
427 #[tokio::test]
428 async fn test_decrypt_list_with_failures_all_success() {
429 let client = Client::init_test_account(test_bitwarden_com_account()).await;
430
431 let valid_cipher = test_cipher();
432
433 let result = client
434 .vault()
435 .ciphers()
436 .decrypt_list_with_failures(vec![valid_cipher]);
437
438 assert_eq!(result.successes.len(), 1);
439 assert!(result.failures.is_empty());
440 assert_eq!(result.successes[0].name, "234234");
441 }
442
443 #[tokio::test]
444 async fn test_decrypt_list_with_failures_mixed_results() {
445 let client = Client::init_test_account(test_bitwarden_com_account()).await;
446 let valid_cipher = test_cipher();
447 let mut invalid_cipher = test_cipher();
448 invalid_cipher.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
450
451 let ciphers = vec![valid_cipher, invalid_cipher.clone()];
452
453 let result = client.vault().ciphers().decrypt_list_with_failures(ciphers);
454
455 assert_eq!(result.successes.len(), 1);
456 assert_eq!(result.failures.len(), 1);
457
458 assert_eq!(result.successes[0].name, "234234");
459 }
460
461 #[tokio::test]
462 async fn test_move_user_cipher_with_attachment_without_key_to_org_fails() {
463 let client = Client::init_test_account(test_bitwarden_com_account()).await;
464
465 let mut cipher = test_cipher();
466 cipher.attachments = Some(vec![test_attachment_legacy()]);
467
468 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
469
470 let res = client.vault().ciphers().move_to_organization(
472 view,
473 "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
474 );
475
476 assert!(res.is_err());
477 }
478
479 #[tokio::test]
480 async fn test_encrypt_cipher_with_legacy_attachment_without_key() {
481 let client = Client::init_test_account(test_bitwarden_com_account()).await;
482
483 let mut cipher = test_cipher();
484 let attachment = test_attachment_legacy();
485 cipher.attachments = Some(vec![attachment.clone()]);
486
487 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
488
489 assert!(cipher.key.is_none());
490
491 let EncryptionContext {
493 cipher: new_cipher,
494 encrypted_for: _,
495 } = client.vault().ciphers().encrypt(view).unwrap();
496 assert!(new_cipher.key.is_some());
497
498 let view = client.vault().ciphers().decrypt(new_cipher).unwrap();
499 let attachments = view.clone().attachments.unwrap();
500 let attachment_view = attachments.first().unwrap().clone();
501 assert!(attachment_view.key.is_none());
502
503 assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
504
505 let buf = vec![
506 2, 100, 205, 148, 152, 77, 184, 77, 53, 80, 38, 240, 83, 217, 251, 118, 254, 27, 117,
507 41, 148, 244, 216, 110, 216, 255, 104, 215, 23, 15, 176, 239, 208, 114, 95, 159, 23,
508 211, 98, 24, 145, 166, 60, 197, 42, 204, 131, 144, 253, 204, 195, 154, 27, 201, 215,
509 43, 10, 244, 107, 226, 152, 85, 167, 66, 185,
510 ];
511
512 let content = client
513 .vault()
514 .attachments()
515 .decrypt_buffer(cipher, attachment_view.clone(), buf.as_slice())
516 .unwrap();
517
518 assert_eq!(content, b"Hello");
519 }
520
521 #[tokio::test]
522 async fn test_encrypt_cipher_with_v1_attachment_without_key() {
523 let client = Client::init_test_account(test_bitwarden_com_account()).await;
524
525 let mut cipher = test_cipher();
526 let attachment = test_attachment_v2();
527 cipher.attachments = Some(vec![attachment.clone()]);
528
529 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
530
531 assert!(cipher.key.is_none());
532
533 let EncryptionContext {
535 cipher: new_cipher,
536 encrypted_for: _,
537 } = client.vault().ciphers().encrypt(view).unwrap();
538 assert!(new_cipher.key.is_some());
539
540 let view = client
541 .vault()
542 .ciphers()
543 .decrypt(new_cipher.clone())
544 .unwrap();
545 let attachments = view.clone().attachments.unwrap();
546 let attachment_view = attachments.first().unwrap().clone();
547 assert!(attachment_view.key.is_some());
548
549 assert_ne!(
551 attachment.clone().key.unwrap().to_string(),
552 attachment_view.clone().key.unwrap().to_string()
553 );
554
555 assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
556
557 let buf = vec![
558 2, 114, 53, 72, 20, 82, 18, 46, 48, 137, 97, 1, 100, 142, 120, 187, 28, 36, 180, 46,
559 189, 254, 133, 23, 169, 58, 73, 212, 172, 116, 185, 127, 111, 92, 112, 145, 99, 28,
560 158, 198, 48, 241, 121, 218, 66, 37, 152, 197, 122, 241, 110, 82, 245, 72, 47, 230, 95,
561 188, 196, 170, 127, 67, 44, 129, 90,
562 ];
563
564 let content = client
565 .vault()
566 .attachments()
567 .decrypt_buffer(new_cipher.clone(), attachment_view.clone(), buf.as_slice())
568 .unwrap();
569
570 assert_eq!(content, b"Hello");
571
572 let new_view = client
574 .vault()
575 .ciphers()
576 .move_to_organization(
577 view,
578 "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
579 )
580 .unwrap();
581 let EncryptionContext {
582 cipher: new_cipher,
583 encrypted_for: _,
584 } = client.vault().ciphers().encrypt(new_view).unwrap();
585
586 let attachment = new_cipher
587 .clone()
588 .attachments
589 .unwrap()
590 .first()
591 .unwrap()
592 .clone();
593
594 assert_eq!(
596 attachment.clone().key.as_ref().unwrap().to_string(),
597 attachment_view.key.as_ref().unwrap().to_string()
598 );
599
600 let content = client
601 .vault()
602 .attachments()
603 .decrypt_buffer(new_cipher, attachment_view, buf.as_slice())
604 .unwrap();
605
606 assert_eq!(content, b"Hello");
607 }
608
609 #[tokio::test]
610 #[cfg(feature = "wasm")]
611 async fn test_decrypt_list_full_with_failures_all_success() {
612 let client = Client::init_test_account(test_bitwarden_com_account()).await;
613
614 let valid_cipher = test_cipher();
615
616 let result = client
617 .vault()
618 .ciphers()
619 .decrypt_list_full_with_failures(vec![valid_cipher]);
620
621 assert_eq!(result.successes.len(), 1);
622 assert!(result.failures.is_empty());
623 assert_eq!(result.successes[0].name, "234234");
624 }
625
626 #[tokio::test]
627 #[cfg(feature = "wasm")]
628 async fn test_decrypt_list_full_with_failures_mixed_results() {
629 let client = Client::init_test_account(test_bitwarden_com_account()).await;
630 let valid_cipher = test_cipher();
631 let mut invalid_cipher = test_cipher();
632 invalid_cipher.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
634
635 let ciphers = vec![valid_cipher, invalid_cipher.clone()];
636
637 let result = client
638 .vault()
639 .ciphers()
640 .decrypt_list_full_with_failures(ciphers);
641
642 assert_eq!(result.successes.len(), 1);
643 assert_eq!(result.failures.len(), 1);
644
645 assert_eq!(result.successes[0].name, "234234");
646 }
647
648 #[tokio::test]
649 #[cfg(feature = "wasm")]
650 async fn test_decrypt_list_full_with_failures_all_failures() {
651 let client = Client::init_test_account(test_bitwarden_com_account()).await;
652 let mut invalid_cipher1 = test_cipher();
653 let mut invalid_cipher2 = test_cipher();
654 invalid_cipher1.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
656 invalid_cipher2.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
657
658 let ciphers = vec![invalid_cipher1, invalid_cipher2];
659
660 let result = client
661 .vault()
662 .ciphers()
663 .decrypt_list_full_with_failures(ciphers);
664
665 assert!(result.successes.is_empty());
666 assert_eq!(result.failures.len(), 2);
667 }
668
669 #[tokio::test]
670 #[cfg(feature = "wasm")]
671 async fn test_decrypt_list_full_with_failures_empty_list() {
672 let client = Client::init_test_account(test_bitwarden_com_account()).await;
673
674 let result = client
675 .vault()
676 .ciphers()
677 .decrypt_list_full_with_failures(vec![]);
678
679 assert!(result.successes.is_empty());
680 assert!(result.failures.is_empty());
681 }
682
683 #[tokio::test]
684 #[cfg(feature = "wasm")]
685 async fn test_encrypt_cipher_for_rotation() {
686 let client = Client::init_test_account(test_bitwarden_com_account()).await;
687
688 let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
689
690 let cipher_view = test_cipher_view();
691 let new_key_b64 = new_key.to_base64();
692
693 let ctx = client
694 .vault()
695 .ciphers()
696 .encrypt_cipher_for_rotation(cipher_view, new_key_b64)
697 .unwrap();
698
699 assert!(ctx.cipher.key.is_some());
700
701 assert!(matches!(
703 client.vault().ciphers().decrypt(ctx.cipher).err(),
704 Some(DecryptError::Crypto(CryptoError::Decrypt))
705 ));
706 }
707
708 #[tokio::test]
709 async fn test_encrypt_list() {
710 let client = Client::init_test_account(test_bitwarden_com_account()).await;
711
712 let cipher_views = vec![test_cipher_view(), test_cipher_view()];
713
714 let result = client.vault().ciphers().encrypt_list(cipher_views);
715
716 assert!(result.is_ok());
717 let contexts = result.unwrap();
718 assert_eq!(contexts.len(), 2);
719
720 for ctx in &contexts {
722 assert!(ctx.cipher.key.is_some());
723 }
724 }
725
726 #[tokio::test]
727 async fn test_encrypt_list_empty() {
728 let client = Client::init_test_account(test_bitwarden_com_account()).await;
729
730 let result = client.vault().ciphers().encrypt_list(vec![]);
731
732 assert!(result.is_ok());
733 assert!(result.unwrap().is_empty());
734 }
735
736 #[tokio::test]
737 async fn test_encrypt_list_roundtrip() {
738 let client = Client::init_test_account(test_bitwarden_com_account()).await;
739
740 let original_views = vec![test_cipher_view(), test_cipher_view()];
741 let original_names: Vec<_> = original_views.iter().map(|v| v.name.clone()).collect();
742
743 let contexts = client
744 .vault()
745 .ciphers()
746 .encrypt_list(original_views)
747 .unwrap();
748
749 for (ctx, original_name) in contexts.iter().zip(original_names.iter()) {
751 let decrypted = client
752 .vault()
753 .ciphers()
754 .decrypt(ctx.cipher.clone())
755 .unwrap();
756 assert_eq!(&decrypted.name, original_name);
757 }
758 }
759
760 #[tokio::test]
761 async fn test_encrypt_list_preserves_user_id() {
762 let client = Client::init_test_account(test_bitwarden_com_account()).await;
763
764 let expected_user_id = client.internal.get_user_id().unwrap();
765
766 let cipher_views = vec![test_cipher_view(), test_cipher_view(), test_cipher_view()];
767 let contexts = client.vault().ciphers().encrypt_list(cipher_views).unwrap();
768
769 for ctx in contexts {
770 assert_eq!(ctx.encrypted_for, expected_user_id);
771 }
772 }
773}