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