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