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