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