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, cipher_client::admin::CipherAdminClient,
17};
18#[cfg(feature = "wasm")]
19use crate::{Fido2CredentialFullView, cipher::cipher::DecryptCipherResult};
20
21mod admin;
22mod create;
23mod delete;
24mod delete_attachment;
25mod edit;
26mod get;
27mod restore;
28mod share_cipher;
29
30#[allow(missing_docs)]
31#[cfg_attr(feature = "wasm", wasm_bindgen)]
32pub struct CiphersClient {
33 pub(crate) client: Client,
34}
35
36#[cfg_attr(feature = "wasm", wasm_bindgen)]
37impl CiphersClient {
38 #[allow(missing_docs)]
39 pub fn encrypt(&self, mut cipher_view: CipherView) -> Result<EncryptionContext, EncryptError> {
40 let user_id = self
41 .client
42 .internal
43 .get_user_id()
44 .ok_or(EncryptError::MissingUserId)?;
45 let key_store = self.client.internal.get_key_store();
46
47 if cipher_view.key.is_none()
50 && self
51 .client
52 .internal
53 .get_flags()
54 .enable_cipher_key_encryption
55 {
56 let key = cipher_view.key_identifier();
57 cipher_view.generate_cipher_key(&mut key_store.context(), key)?;
58 }
59
60 let cipher = key_store.encrypt(cipher_view)?;
61 Ok(EncryptionContext {
62 cipher,
63 encrypted_for: user_id,
64 })
65 }
66
67 #[cfg(feature = "wasm")]
78 pub fn encrypt_cipher_for_rotation(
79 &self,
80 mut cipher_view: CipherView,
81 new_key: B64,
82 ) -> Result<EncryptionContext, CipherError> {
83 let new_key = SymmetricCryptoKey::try_from(new_key)?;
84
85 let user_id = self
86 .client
87 .internal
88 .get_user_id()
89 .ok_or(EncryptError::MissingUserId)?;
90 let key_store = self.client.internal.get_key_store();
91 let mut ctx = key_store.context();
92
93 let new_key_id = ctx.add_local_symmetric_key(new_key);
95
96 if cipher_view.key.is_none()
97 && self
98 .client
99 .internal
100 .get_flags()
101 .enable_cipher_key_encryption
102 {
103 cipher_view.generate_cipher_key(&mut ctx, new_key_id)?;
104 } else {
105 cipher_view.reencrypt_cipher_keys(&mut ctx, new_key_id)?;
106 }
107
108 let cipher = cipher_view.encrypt_composite(&mut ctx, new_key_id)?;
109
110 Ok(EncryptionContext {
111 cipher,
112 encrypted_for: user_id,
113 })
114 }
115
116 #[cfg(feature = "wasm")]
121 pub fn encrypt_list(
122 &self,
123 cipher_views: Vec<CipherView>,
124 ) -> Result<Vec<EncryptionContext>, EncryptError> {
125 let user_id = self
126 .client
127 .internal
128 .get_user_id()
129 .ok_or(EncryptError::MissingUserId)?;
130 let key_store = self.client.internal.get_key_store();
131 let enable_cipher_key = self
132 .client
133 .internal
134 .get_flags()
135 .enable_cipher_key_encryption;
136
137 let mut ctx = key_store.context();
138
139 let prepared_views: Vec<CipherView> = cipher_views
140 .into_iter()
141 .map(|mut cv| {
142 if cv.key.is_none() && enable_cipher_key {
143 let key = cv.key_identifier();
144 cv.generate_cipher_key(&mut ctx, key)?;
145 }
146 Ok(cv)
147 })
148 .collect::<Result<Vec<_>, bitwarden_crypto::CryptoError>>()?;
149
150 let ciphers: Vec<Cipher> = key_store.encrypt_list(&prepared_views)?;
151
152 Ok(ciphers
153 .into_iter()
154 .map(|cipher| EncryptionContext {
155 cipher,
156 encrypted_for: user_id,
157 })
158 .collect())
159 }
160
161 #[allow(missing_docs)]
162 pub fn decrypt(&self, cipher: Cipher) -> Result<CipherView, DecryptError> {
163 let key_store = self.client.internal.get_key_store();
164 let cipher_view = key_store.decrypt(&cipher)?;
165 Ok(cipher_view)
166 }
167
168 #[allow(missing_docs)]
169 pub fn decrypt_list(&self, ciphers: Vec<Cipher>) -> Result<Vec<CipherListView>, DecryptError> {
170 let key_store = self.client.internal.get_key_store();
171 let cipher_views = key_store.decrypt_list(&ciphers)?;
172 Ok(cipher_views)
173 }
174
175 pub fn decrypt_list_with_failures(&self, ciphers: Vec<Cipher>) -> DecryptCipherListResult {
178 let key_store = self.client.internal.get_key_store();
179 let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
180
181 DecryptCipherListResult {
182 successes,
183 failures: failures.into_iter().cloned().collect(),
184 }
185 }
186
187 #[cfg(feature = "wasm")]
190 pub fn decrypt_list_full_with_failures(&self, ciphers: Vec<Cipher>) -> DecryptCipherResult {
191 let key_store = self.client.internal.get_key_store();
192 let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
193
194 DecryptCipherResult {
195 successes,
196 failures: failures.into_iter().cloned().collect(),
197 }
198 }
199
200 #[allow(missing_docs)]
201 pub fn decrypt_fido2_credentials(
202 &self,
203 cipher_view: CipherView,
204 ) -> Result<Vec<crate::Fido2CredentialView>, DecryptError> {
205 let key_store = self.client.internal.get_key_store();
206 let credentials = cipher_view.decrypt_fido2_credentials(&mut key_store.context())?;
207 Ok(credentials)
208 }
209
210 #[cfg(feature = "wasm")]
216 pub fn set_fido2_credentials(
217 &self,
218 mut cipher_view: CipherView,
219 fido2_credentials: Vec<Fido2CredentialFullView>,
220 ) -> Result<CipherView, CipherError> {
221 let key_store = self.client.internal.get_key_store();
222
223 cipher_view.set_new_fido2_credentials(&mut key_store.context(), fido2_credentials)?;
224
225 Ok(cipher_view)
226 }
227
228 #[allow(missing_docs)]
229 pub fn move_to_organization(
230 &self,
231 mut cipher_view: CipherView,
232 organization_id: OrganizationId,
233 ) -> Result<CipherView, CipherError> {
234 let key_store = self.client.internal.get_key_store();
235 cipher_view.move_to_organization(&mut key_store.context(), organization_id)?;
236 Ok(cipher_view)
237 }
238
239 #[cfg(feature = "wasm")]
240 #[allow(missing_docs)]
241 pub fn decrypt_fido2_private_key(
242 &self,
243 cipher_view: CipherView,
244 ) -> Result<String, CipherError> {
245 let key_store = self.client.internal.get_key_store();
246 let decrypted_key = cipher_view.decrypt_fido2_private_key(&mut key_store.context())?;
247 Ok(decrypted_key)
248 }
249
250 pub fn admin(&self) -> CipherAdminClient {
253 CipherAdminClient {
254 client: self.client.clone(),
255 }
256 }
257}
258
259impl CiphersClient {
260 fn get_repository(&self) -> Result<Arc<dyn Repository<Cipher>>, RepositoryError> {
261 Ok(self.client.platform().state().get::<Cipher>()?)
262 }
263}
264
265#[cfg(test)]
266mod tests {
267
268 use bitwarden_core::client::test_accounts::test_bitwarden_com_account;
269 #[cfg(feature = "wasm")]
270 use bitwarden_crypto::CryptoError;
271
272 use super::*;
273 use crate::{Attachment, CipherRepromptType, CipherType, Login, VaultClientExt};
274
275 fn test_cipher() -> Cipher {
276 Cipher {
277 id: Some("358f2b2b-9326-4e5e-94a8-b18100bb0908".parse().unwrap()),
278 organization_id: None,
279 folder_id: None,
280 collection_ids: vec![],
281 key: None,
282 name: "2.+oPT8B4xJhyhQRe1VkIx0A==|PBtC/bZkggXR+fSnL/pG7g==|UkjRD0VpnUYkjRC/05ZLdEBAmRbr3qWRyJey2bUvR9w=".parse().unwrap(),
283 notes: None,
284 r#type: CipherType::Login,
285 login: Some(Login{
286 username: None,
287 password: None,
288 password_revision_date: None,
289 uris:None,
290 totp: None,
291 autofill_on_page_load: None,
292 fido2_credentials: None,
293 }),
294 identity: None,
295 card: None,
296 secure_note: None,
297 ssh_key: None,
298 favorite: false,
299 reprompt: CipherRepromptType::None,
300 organization_use_totp: true,
301 edit: true,
302 permissions: None,
303 view_password: true,
304 local_data: None,
305 attachments: None,
306 fields: None,
307 password_history: None,
308 creation_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
309 deleted_date: None,
310 revision_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
311 archived_date: None,
312 data: None,
313 }
314 }
315
316 #[cfg(feature = "wasm")]
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(crate::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 #[cfg(feature = "wasm")]
709 #[tokio::test]
710 async fn test_encrypt_list() {
711 let client = Client::init_test_account(test_bitwarden_com_account()).await;
712
713 let cipher_views = vec![test_cipher_view(), test_cipher_view()];
714
715 let result = client.vault().ciphers().encrypt_list(cipher_views);
716
717 assert!(result.is_ok());
718 let contexts = result.unwrap();
719 assert_eq!(contexts.len(), 2);
720
721 for ctx in &contexts {
723 assert!(ctx.cipher.key.is_some());
724 }
725 }
726
727 #[cfg(feature = "wasm")]
728 #[tokio::test]
729 async fn test_encrypt_list_empty() {
730 let client = Client::init_test_account(test_bitwarden_com_account()).await;
731
732 let result = client.vault().ciphers().encrypt_list(vec![]);
733
734 assert!(result.is_ok());
735 assert!(result.unwrap().is_empty());
736 }
737
738 #[cfg(feature = "wasm")]
739 #[tokio::test]
740 async fn test_encrypt_list_roundtrip() {
741 let client = Client::init_test_account(test_bitwarden_com_account()).await;
742
743 let original_views = vec![test_cipher_view(), test_cipher_view()];
744 let original_names: Vec<_> = original_views.iter().map(|v| v.name.clone()).collect();
745
746 let contexts = client
747 .vault()
748 .ciphers()
749 .encrypt_list(original_views)
750 .unwrap();
751
752 for (ctx, original_name) in contexts.iter().zip(original_names.iter()) {
754 let decrypted = client
755 .vault()
756 .ciphers()
757 .decrypt(ctx.cipher.clone())
758 .unwrap();
759 assert_eq!(&decrypted.name, original_name);
760 }
761 }
762
763 #[cfg(feature = "wasm")]
764 #[tokio::test]
765 async fn test_encrypt_list_preserves_user_id() {
766 let client = Client::init_test_account(test_bitwarden_com_account()).await;
767
768 let expected_user_id = client.internal.get_user_id().unwrap();
769
770 let cipher_views = vec![test_cipher_view(), test_cipher_view(), test_cipher_view()];
771 let contexts = client.vault().ciphers().encrypt_list(cipher_views).unwrap();
772
773 for ctx in contexts {
774 assert_eq!(ctx.encrypted_for, expected_user_id);
775 }
776 }
777}