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