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