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