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