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 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, LoginView, 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 fn test_cipher_view() -> CipherView {
317 let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
318 CipherView {
319 r#type: CipherType::Login,
320 login: Some(LoginView {
321 username: Some("test_username".to_string()),
322 password: Some("test_password".to_string()),
323 password_revision_date: None,
324 uris: None,
325 totp: None,
326 autofill_on_page_load: None,
327 fido2_credentials: None,
328 }),
329 id: Some(test_id),
330 organization_id: None,
331 folder_id: None,
332 collection_ids: vec![],
333 key: None,
334 name: "My test login".to_string(),
335 notes: None,
336 identity: None,
337 card: None,
338 secure_note: None,
339 ssh_key: None,
340 favorite: false,
341 reprompt: CipherRepromptType::None,
342 organization_use_totp: true,
343 edit: true,
344 permissions: None,
345 view_password: true,
346 local_data: None,
347 attachments: None,
348 attachment_decryption_failures: None,
349 fields: None,
350 password_history: None,
351 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
352 deleted_date: None,
353 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
354 archived_date: None,
355 }
356 }
357
358 fn test_attachment_legacy() -> Attachment {
359 Attachment {
360 id: Some("uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
361 url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
362 file_name: Some("2.mV50WiLq6duhwGbhM1TO0A==|dTufWNH8YTPP0EMlNLIpFA==|QHp+7OM8xHtEmCfc9QPXJ0Ro2BeakzvLgxJZ7NdLuDc=".parse().unwrap()),
363 key: None,
364 size: Some("65".to_string()),
365 size_name: Some("65 Bytes".to_string()),
366 }
367 }
368
369 fn test_attachment_v2() -> Attachment {
370 Attachment {
371 id: Some("a77m56oerrz5b92jm05lq5qoyj1xh2t9".to_string()),
372 url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
373 file_name: Some("2.GhazFdCYQcM5v+AtVwceQA==|98bMUToqC61VdVsSuXWRwA==|bsLByMht9Hy5QO9pPMRz0K4d0aqBiYnnROGM5YGbNu4=".parse().unwrap()),
374 key: Some("2.6TPEiYULFg/4+3CpDRwCqw==|6swweBHCJcd5CHdwBBWuRN33XRV22VoroDFDUmiM4OzjPEAhgZK57IZS1KkBlCcFvT+t+YbsmDcdv+Lqr+iJ3MmzfJ40MCB5TfYy+22HVRA=|rkgFDh2IWTfPC1Y66h68Diiab/deyi1p/X0Fwkva0NQ=".parse().unwrap()),
375 size: Some("65".to_string()),
376 size_name: Some("65 Bytes".to_string()),
377 }
378 }
379
380 #[tokio::test]
381 async fn test_decrypt_list() {
382 let client = Client::init_test_account(test_bitwarden_com_account()).await;
383
384 let dec = client
385 .vault()
386 .ciphers()
387 .decrypt_list(vec![Cipher {
388 id: Some("a1569f46-0797-4d3f-b859-b181009e2e49".parse().unwrap()),
389 organization_id: Some("1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap()),
390 folder_id: None,
391 collection_ids: vec!["66c5ca57-0868-4c7e-902f-b181009709c0".parse().unwrap()],
392 key: None,
393 name: "2.RTdUGVWYl/OZHUMoy68CMg==|sCaT5qHx8i0rIvzVrtJKww==|jB8DsRws6bXBtXNfNXUmFJ0JLDlB6GON6Y87q0jgJ+0=".parse().unwrap(),
394 notes: None,
395 r#type: CipherType::Login,
396 login: Some(Login{
397 username: Some("2.ouEYEk+SViUtqncesfe9Ag==|iXzEJq1zBeNdDbumFO1dUA==|RqMoo9soSwz/yB99g6YPqk8+ASWRcSdXsKjbwWzyy9U=".parse().unwrap()),
398 password: Some("2.6yXnOz31o20Z2kiYDnXueA==|rBxTb6NK9lkbfdhrArmacw==|ogZir8Z8nLgiqlaLjHH+8qweAtItS4P2iPv1TELo5a0=".parse().unwrap()),
399 password_revision_date: None, uris:None, totp: None, autofill_on_page_load: None, fido2_credentials: None }),
400 identity: None,
401 card: None,
402 secure_note: None,
403 ssh_key: None,
404 favorite: false,
405 reprompt: CipherRepromptType::None,
406 organization_use_totp: true,
407 edit: true,
408 permissions: None,
409 view_password: true,
410 local_data: None,
411 attachments: None,
412 fields: None,
413 password_history: None,
414 creation_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
415 deleted_date: None,
416 revision_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
417 archived_date: None,
418 data: None,
419 }])
420
421 .unwrap();
422
423 assert_eq!(dec[0].name, "Test item");
424 }
425
426 #[tokio::test]
427 async fn test_decrypt_list_with_failures_all_success() {
428 let client = Client::init_test_account(test_bitwarden_com_account()).await;
429
430 let valid_cipher = test_cipher();
431
432 let result = client
433 .vault()
434 .ciphers()
435 .decrypt_list_with_failures(vec![valid_cipher]);
436
437 assert_eq!(result.successes.len(), 1);
438 assert!(result.failures.is_empty());
439 assert_eq!(result.successes[0].name, "234234");
440 }
441
442 #[tokio::test]
443 async fn test_decrypt_list_with_failures_mixed_results() {
444 let client = Client::init_test_account(test_bitwarden_com_account()).await;
445 let valid_cipher = test_cipher();
446 let mut invalid_cipher = test_cipher();
447 invalid_cipher.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
449
450 let ciphers = vec![valid_cipher, invalid_cipher.clone()];
451
452 let result = client.vault().ciphers().decrypt_list_with_failures(ciphers);
453
454 assert_eq!(result.successes.len(), 1);
455 assert_eq!(result.failures.len(), 1);
456
457 assert_eq!(result.successes[0].name, "234234");
458 }
459
460 #[tokio::test]
461 async fn test_move_user_cipher_with_attachment_without_key_to_org_fails() {
462 let client = Client::init_test_account(test_bitwarden_com_account()).await;
463
464 let mut cipher = test_cipher();
465 cipher.attachments = Some(vec![test_attachment_legacy()]);
466
467 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
468
469 let res = client.vault().ciphers().move_to_organization(
471 view,
472 "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
473 );
474
475 assert!(res.is_err());
476 }
477
478 #[tokio::test]
479 async fn test_encrypt_cipher_with_legacy_attachment_without_key() {
480 let client = Client::init_test_account(test_bitwarden_com_account()).await;
481
482 let mut cipher = test_cipher();
483 let attachment = test_attachment_legacy();
484 cipher.attachments = Some(vec![attachment.clone()]);
485
486 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
487
488 assert!(cipher.key.is_none());
489
490 let EncryptionContext {
492 cipher: new_cipher,
493 encrypted_for: _,
494 } = client.vault().ciphers().encrypt(view).unwrap();
495 assert!(new_cipher.key.is_some());
496
497 let view = client.vault().ciphers().decrypt(new_cipher).unwrap();
498 let attachments = view.clone().attachments.unwrap();
499 let attachment_view = attachments.first().unwrap().clone();
500 assert!(attachment_view.key.is_none());
501
502 assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
503
504 let buf = vec![
505 2, 100, 205, 148, 152, 77, 184, 77, 53, 80, 38, 240, 83, 217, 251, 118, 254, 27, 117,
506 41, 148, 244, 216, 110, 216, 255, 104, 215, 23, 15, 176, 239, 208, 114, 95, 159, 23,
507 211, 98, 24, 145, 166, 60, 197, 42, 204, 131, 144, 253, 204, 195, 154, 27, 201, 215,
508 43, 10, 244, 107, 226, 152, 85, 167, 66, 185,
509 ];
510
511 let content = client
512 .vault()
513 .attachments()
514 .decrypt_buffer(cipher, attachment_view.clone(), buf.as_slice())
515 .unwrap();
516
517 assert_eq!(content, b"Hello");
518 }
519
520 #[tokio::test]
521 async fn test_encrypt_cipher_with_v1_attachment_without_key() {
522 let client = Client::init_test_account(test_bitwarden_com_account()).await;
523
524 let mut cipher = test_cipher();
525 let attachment = test_attachment_v2();
526 cipher.attachments = Some(vec![attachment.clone()]);
527
528 let view = client.vault().ciphers().decrypt(cipher.clone()).unwrap();
529
530 assert!(cipher.key.is_none());
531
532 let EncryptionContext {
534 cipher: new_cipher,
535 encrypted_for: _,
536 } = client.vault().ciphers().encrypt(view).unwrap();
537 assert!(new_cipher.key.is_some());
538
539 let view = client
540 .vault()
541 .ciphers()
542 .decrypt(new_cipher.clone())
543 .unwrap();
544 let attachments = view.clone().attachments.unwrap();
545 let attachment_view = attachments.first().unwrap().clone();
546 assert!(attachment_view.key.is_some());
547
548 assert_ne!(
550 attachment.clone().key.unwrap().to_string(),
551 attachment_view.clone().key.unwrap().to_string()
552 );
553
554 assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
555
556 let buf = vec![
557 2, 114, 53, 72, 20, 82, 18, 46, 48, 137, 97, 1, 100, 142, 120, 187, 28, 36, 180, 46,
558 189, 254, 133, 23, 169, 58, 73, 212, 172, 116, 185, 127, 111, 92, 112, 145, 99, 28,
559 158, 198, 48, 241, 121, 218, 66, 37, 152, 197, 122, 241, 110, 82, 245, 72, 47, 230, 95,
560 188, 196, 170, 127, 67, 44, 129, 90,
561 ];
562
563 let content = client
564 .vault()
565 .attachments()
566 .decrypt_buffer(new_cipher.clone(), attachment_view.clone(), buf.as_slice())
567 .unwrap();
568
569 assert_eq!(content, b"Hello");
570
571 let new_view = client
573 .vault()
574 .ciphers()
575 .move_to_organization(
576 view,
577 "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
578 )
579 .unwrap();
580 let EncryptionContext {
581 cipher: new_cipher,
582 encrypted_for: _,
583 } = client.vault().ciphers().encrypt(new_view).unwrap();
584
585 let attachment = new_cipher
586 .clone()
587 .attachments
588 .unwrap()
589 .first()
590 .unwrap()
591 .clone();
592
593 assert_eq!(
595 attachment.clone().key.as_ref().unwrap().to_string(),
596 attachment_view.key.as_ref().unwrap().to_string()
597 );
598
599 let content = client
600 .vault()
601 .attachments()
602 .decrypt_buffer(new_cipher, attachment_view, buf.as_slice())
603 .unwrap();
604
605 assert_eq!(content, b"Hello");
606 }
607
608 #[tokio::test]
609 #[cfg(feature = "wasm")]
610 async fn test_decrypt_list_full_with_failures_all_success() {
611 let client = Client::init_test_account(test_bitwarden_com_account()).await;
612
613 let valid_cipher = test_cipher();
614
615 let result = client
616 .vault()
617 .ciphers()
618 .decrypt_list_full_with_failures(vec![valid_cipher]);
619
620 assert_eq!(result.successes.len(), 1);
621 assert!(result.failures.is_empty());
622 assert_eq!(result.successes[0].name, "234234");
623 }
624
625 #[tokio::test]
626 #[cfg(feature = "wasm")]
627 async fn test_decrypt_list_full_with_failures_mixed_results() {
628 let client = Client::init_test_account(test_bitwarden_com_account()).await;
629 let valid_cipher = test_cipher();
630 let mut invalid_cipher = test_cipher();
631 invalid_cipher.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
633
634 let ciphers = vec![valid_cipher, invalid_cipher.clone()];
635
636 let result = client
637 .vault()
638 .ciphers()
639 .decrypt_list_full_with_failures(ciphers);
640
641 assert_eq!(result.successes.len(), 1);
642 assert_eq!(result.failures.len(), 1);
643
644 assert_eq!(result.successes[0].name, "234234");
645 }
646
647 #[tokio::test]
648 #[cfg(feature = "wasm")]
649 async fn test_decrypt_list_full_with_failures_all_failures() {
650 let client = Client::init_test_account(test_bitwarden_com_account()).await;
651 let mut invalid_cipher1 = test_cipher();
652 let mut invalid_cipher2 = test_cipher();
653 invalid_cipher1.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
655 invalid_cipher2.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
656
657 let ciphers = vec![invalid_cipher1, invalid_cipher2];
658
659 let result = client
660 .vault()
661 .ciphers()
662 .decrypt_list_full_with_failures(ciphers);
663
664 assert!(result.successes.is_empty());
665 assert_eq!(result.failures.len(), 2);
666 }
667
668 #[tokio::test]
669 #[cfg(feature = "wasm")]
670 async fn test_decrypt_list_full_with_failures_empty_list() {
671 let client = Client::init_test_account(test_bitwarden_com_account()).await;
672
673 let result = client
674 .vault()
675 .ciphers()
676 .decrypt_list_full_with_failures(vec![]);
677
678 assert!(result.successes.is_empty());
679 assert!(result.failures.is_empty());
680 }
681
682 #[tokio::test]
683 #[cfg(feature = "wasm")]
684 async fn test_encrypt_cipher_for_rotation() {
685 let client = Client::init_test_account(test_bitwarden_com_account()).await;
686
687 let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
688
689 let cipher_view = test_cipher_view();
690 let new_key_b64 = new_key.to_base64();
691
692 let ctx = client
693 .vault()
694 .ciphers()
695 .encrypt_cipher_for_rotation(cipher_view, new_key_b64)
696 .unwrap();
697
698 assert!(ctx.cipher.key.is_some());
699
700 assert!(matches!(
702 client.vault().ciphers().decrypt(ctx.cipher).err(),
703 Some(DecryptError::Crypto(CryptoError::Decrypt))
704 ));
705 }
706
707 #[tokio::test]
708 async fn test_encrypt_list() {
709 let client = Client::init_test_account(test_bitwarden_com_account()).await;
710
711 let cipher_views = vec![test_cipher_view(), test_cipher_view()];
712
713 let result = client.vault().ciphers().encrypt_list(cipher_views);
714
715 assert!(result.is_ok());
716 let contexts = result.unwrap();
717 assert_eq!(contexts.len(), 2);
718
719 for ctx in &contexts {
721 assert!(ctx.cipher.key.is_some());
722 }
723 }
724
725 #[tokio::test]
726 async fn test_encrypt_list_empty() {
727 let client = Client::init_test_account(test_bitwarden_com_account()).await;
728
729 let result = client.vault().ciphers().encrypt_list(vec![]);
730
731 assert!(result.is_ok());
732 assert!(result.unwrap().is_empty());
733 }
734
735 #[tokio::test]
736 async fn test_encrypt_list_roundtrip() {
737 let client = Client::init_test_account(test_bitwarden_com_account()).await;
738
739 let original_views = vec![test_cipher_view(), test_cipher_view()];
740 let original_names: Vec<_> = original_views.iter().map(|v| v.name.clone()).collect();
741
742 let contexts = client
743 .vault()
744 .ciphers()
745 .encrypt_list(original_views)
746 .unwrap();
747
748 for (ctx, original_name) in contexts.iter().zip(original_names.iter()) {
750 let decrypted = client
751 .vault()
752 .ciphers()
753 .decrypt(ctx.cipher.clone())
754 .unwrap();
755 assert_eq!(&decrypted.name, original_name);
756 }
757 }
758
759 #[tokio::test]
760 async fn test_encrypt_list_preserves_user_id() {
761 let client = Client::init_test_account(test_bitwarden_com_account()).await;
762
763 let expected_user_id = client.internal.get_user_id().unwrap();
764
765 let cipher_views = vec![test_cipher_view(), test_cipher_view(), test_cipher_view()];
766 let contexts = client.vault().ciphers().encrypt_list(cipher_views).unwrap();
767
768 for ctx in contexts {
769 assert_eq!(ctx.encrypted_for, expected_user_id);
770 }
771 }
772}