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