Skip to main content

bitwarden_vault/cipher/cipher_client/
mod.rs

1use std::sync::Arc;
2
3use bitwarden_core::{
4    Client, FromClient, OrganizationId,
5    client::{ApiConfigurations, FromClientPart},
6    key_management::{BLOB_SECURITY_VERSION, KeySlotIds},
7};
8#[cfg(feature = "wasm")]
9use bitwarden_crypto::{CompositeEncryptable, SymmetricCryptoKey};
10use bitwarden_crypto::{IdentifyKey, KeyStore};
11#[cfg(feature = "wasm")]
12use bitwarden_encoding::B64;
13use bitwarden_state::repository::{Repository, RepositoryError};
14#[cfg(feature = "wasm")]
15use wasm_bindgen::prelude::*;
16
17use super::EncryptionContext;
18use crate::{
19    Cipher, CipherError, CipherListView, CipherView, DecryptError, EncryptError,
20    cipher::cipher::{DecryptCipherListResult, 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 delete_attachment;
33mod edit;
34mod get;
35mod move_many;
36mod restore;
37mod share_cipher;
38
39#[allow(missing_docs)]
40#[cfg_attr(feature = "wasm", wasm_bindgen)]
41pub struct CiphersClient {
42    #[allow(dead_code)]
43    pub(crate) key_store: KeyStore<KeySlotIds>,
44    pub(crate) api_configurations: Arc<ApiConfigurations>,
45    pub(crate) repository: Option<Arc<dyn Repository<Cipher>>>,
46    #[deprecated(
47        note = "Use the component fields (key_store, api_configurations, repository) for new operations"
48    )]
49    pub(crate) client: Client,
50}
51
52impl FromClient for CiphersClient {
53    fn from_client(client: &Client) -> Self {
54        #[allow(deprecated)]
55        Self {
56            key_store: client.get_part(),
57            api_configurations: client.get_part(),
58            repository: client.get_part(),
59            client: client.clone(),
60        }
61    }
62}
63
64#[allow(deprecated)]
65#[cfg_attr(feature = "wasm", wasm_bindgen)]
66impl CiphersClient {
67    /// Returns `true` when cipher data for the given scope should be written in the
68    /// blob-encrypted format. Individual-vault ciphers qualify once the user's security state has
69    /// reached [`BLOB_SECURITY_VERSION`]. Organization-vault support is tracked in PM-32430.
70    #[allow(dead_code)] // Consumed by the encrypt/decrypt wiring ticket.
71    pub(crate) fn should_use_blob_encryption(
72        &self,
73        organization_id: Option<OrganizationId>,
74    ) -> bool {
75        if organization_id.is_some() {
76            return false;
77        }
78        self.client
79            .internal
80            .get_key_store()
81            .context()
82            .get_security_state_version()
83            >= BLOB_SECURITY_VERSION
84    }
85
86    #[allow(missing_docs)]
87    pub async fn encrypt(
88        &self,
89        mut cipher_view: CipherView,
90    ) -> Result<EncryptionContext, EncryptError> {
91        let user_id = self
92            .client
93            .internal
94            .get_user_id()
95            .ok_or(EncryptError::MissingUserId)?;
96        let key_store = self.client.internal.get_key_store();
97
98        // TODO: Once this flag is removed, the key generation logic should
99        // be moved directly into the KeyEncryptable implementation
100        if cipher_view.key.is_none()
101            && self
102                .client
103                .internal
104                .get_flags()
105                .await
106                .enable_cipher_key_encryption
107        {
108            let key = cipher_view.key_identifier();
109            cipher_view.generate_cipher_key(&mut key_store.context(), key)?;
110        }
111
112        let cipher = key_store.encrypt(cipher_view)?;
113        Ok(EncryptionContext {
114            cipher,
115            encrypted_for: user_id,
116        })
117    }
118
119    /// Encrypt a cipher with the provided key. This should only be used when rotating encryption
120    /// keys in the Web client.
121    ///
122    /// Until key rotation is fully implemented in the SDK, this method must be provided the new
123    /// symmetric key in base64 format. See PM-23084
124    ///
125    /// If the cipher has a CipherKey, it will be re-encrypted with the new key.
126    /// If the cipher does not have a CipherKey and CipherKeyEncryption is enabled, one will be
127    /// generated using the new key. Otherwise, the cipher's data will be encrypted with the new
128    /// key directly.
129    #[cfg(feature = "wasm")]
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        let enable_cipher_key_encryption = self
143            .client
144            .internal
145            .get_flags()
146            .await
147            .enable_cipher_key_encryption;
148
149        let key_store = self.client.internal.get_key_store();
150        let mut ctx = key_store.context();
151
152        // Set the new key in the key store context
153        let new_key_id = ctx.add_local_symmetric_key(new_key);
154
155        if cipher_view.key.is_none() && enable_cipher_key_encryption {
156            cipher_view.generate_cipher_key(&mut ctx, new_key_id)?;
157        } else {
158            cipher_view.reencrypt_cipher_keys(&mut ctx, new_key_id)?;
159        }
160
161        let cipher = cipher_view.encrypt_composite(&mut ctx, new_key_id)?;
162
163        Ok(EncryptionContext {
164            cipher,
165            encrypted_for: user_id,
166        })
167    }
168
169    /// Encrypt a list of cipher views.
170    ///
171    /// This method attempts to encrypt all ciphers in the list. If any cipher
172    /// fails to encrypt, the entire operation fails and an error is returned.
173    #[cfg(feature = "wasm")]
174    pub async fn encrypt_list(
175        &self,
176        cipher_views: Vec<CipherView>,
177    ) -> Result<Vec<EncryptionContext>, EncryptError> {
178        let user_id = self
179            .client
180            .internal
181            .get_user_id()
182            .ok_or(EncryptError::MissingUserId)?;
183        let key_store = self.client.internal.get_key_store();
184        let enable_cipher_key = self
185            .client
186            .internal
187            .get_flags()
188            .await
189            .enable_cipher_key_encryption;
190
191        let mut ctx = key_store.context();
192
193        let prepared_views: Vec<CipherView> = cipher_views
194            .into_iter()
195            .map(|mut cv| {
196                if cv.key.is_none() && enable_cipher_key {
197                    let key = cv.key_identifier();
198                    cv.generate_cipher_key(&mut ctx, key)?;
199                }
200                Ok(cv)
201            })
202            .collect::<Result<Vec<_>, bitwarden_crypto::CryptoError>>()?;
203
204        let ciphers: Vec<Cipher> = key_store.encrypt_list(&prepared_views)?;
205
206        Ok(ciphers
207            .into_iter()
208            .map(|cipher| EncryptionContext {
209                cipher,
210                encrypted_for: user_id,
211            })
212            .collect())
213    }
214
215    #[allow(missing_docs)]
216    pub async fn decrypt(&self, cipher: Cipher) -> Result<CipherView, DecryptError> {
217        let key_store = self.client.internal.get_key_store();
218        if self.is_strict_decrypt().await {
219            Ok(key_store.decrypt(&StrictDecrypt(cipher))?)
220        } else {
221            Ok(key_store.decrypt(&cipher)?)
222        }
223    }
224
225    #[allow(missing_docs)]
226    pub async fn decrypt_list(
227        &self,
228        ciphers: Vec<Cipher>,
229    ) -> Result<Vec<CipherListView>, DecryptError> {
230        let key_store = self.client.internal.get_key_store();
231        if self.is_strict_decrypt().await {
232            let strict: Vec<StrictDecrypt<Cipher>> =
233                ciphers.into_iter().map(StrictDecrypt).collect();
234            Ok(key_store.decrypt_list(&strict)?)
235        } else {
236            Ok(key_store.decrypt_list(&ciphers)?)
237        }
238    }
239
240    /// Decrypt cipher list with failures
241    /// Returns both successfully decrypted ciphers and any that failed to decrypt
242    pub async fn decrypt_list_with_failures(
243        &self,
244        ciphers: Vec<Cipher>,
245    ) -> DecryptCipherListResult {
246        let key_store = self.client.internal.get_key_store();
247        if self.is_strict_decrypt().await {
248            let strict: Vec<StrictDecrypt<Cipher>> =
249                ciphers.into_iter().map(StrictDecrypt).collect();
250            let (successes, failures) = key_store.decrypt_list_with_failures(&strict);
251            DecryptCipherListResult {
252                successes,
253                failures: failures.into_iter().map(|f| f.0.clone()).collect(),
254            }
255        } else {
256            let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
257            DecryptCipherListResult {
258                successes,
259                failures: failures.into_iter().cloned().collect(),
260            }
261        }
262    }
263
264    /// Decrypt full cipher list
265    /// Returns both successfully fully decrypted ciphers and any that failed to decrypt
266    #[cfg(feature = "wasm")]
267    pub async fn decrypt_list_full_with_failures(
268        &self,
269        ciphers: Vec<Cipher>,
270    ) -> DecryptCipherResult {
271        let key_store = self.client.internal.get_key_store();
272        if self.is_strict_decrypt().await {
273            let strict: Vec<StrictDecrypt<Cipher>> =
274                ciphers.into_iter().map(StrictDecrypt).collect();
275            let (successes, failures) = key_store.decrypt_list_with_failures(&strict);
276            return DecryptCipherResult {
277                successes,
278                failures: failures.into_iter().map(|f| f.0.clone()).collect(),
279            };
280        }
281        let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
282
283        DecryptCipherResult {
284            successes,
285            failures: failures.into_iter().cloned().collect(),
286        }
287    }
288
289    #[allow(missing_docs)]
290    pub fn decrypt_fido2_credentials(
291        &self,
292        cipher_view: CipherView,
293    ) -> Result<Vec<crate::Fido2CredentialView>, DecryptError> {
294        let key_store = self.client.internal.get_key_store();
295        let credentials = cipher_view.decrypt_fido2_credentials(&mut key_store.context())?;
296        Ok(credentials)
297    }
298
299    /// Temporary method used to re-encrypt FIDO2 credentials for a cipher view.
300    /// Necessary until the TS clients utilize the SDK entirely for FIDO2 credentials management.
301    /// TS clients create decrypted FIDO2 credentials that need to be encrypted manually when
302    /// encrypting the rest of the CipherView.
303    /// TODO: Remove once TS passkey provider implementation uses SDK - PM-8313
304    #[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        let key_store = self.client.internal.get_key_store();
311
312        cipher_view.set_new_fido2_credentials(&mut key_store.context(), fido2_credentials)?;
313
314        Ok(cipher_view)
315    }
316
317    #[allow(missing_docs)]
318    pub fn move_to_organization(
319        &self,
320        mut cipher_view: CipherView,
321        organization_id: OrganizationId,
322    ) -> Result<CipherView, CipherError> {
323        let key_store = self.client.internal.get_key_store();
324        cipher_view.move_to_organization(&mut key_store.context(), organization_id)?;
325        Ok(cipher_view)
326    }
327
328    #[cfg(feature = "wasm")]
329    #[allow(missing_docs)]
330    pub fn decrypt_fido2_private_key(
331        &self,
332        cipher_view: CipherView,
333    ) -> Result<String, CipherError> {
334        let key_store = self.client.internal.get_key_store();
335        let decrypted_key = cipher_view.decrypt_fido2_private_key(&mut key_store.context())?;
336        Ok(decrypted_key)
337    }
338
339    /// Returns a new client for performing admin operations.
340    /// Uses the admin server API endpoints and does not modify local state.
341    pub fn admin(&self) -> CipherAdminClient {
342        CipherAdminClient::from_client(&self.client)
343    }
344}
345
346#[allow(deprecated)]
347impl CiphersClient {
348    fn get_repository(&self) -> Result<Arc<dyn Repository<Cipher>>, RepositoryError> {
349        Ok(self.client.platform().state().get::<Cipher>()?)
350    }
351
352    async fn is_strict_decrypt(&self) -> bool {
353        self.client
354            .internal
355            .get_flags()
356            .await
357            .strict_cipher_decryption
358    }
359}
360
361#[cfg(test)]
362mod tests {
363
364    use bitwarden_core::client::test_accounts::test_bitwarden_com_account;
365    #[cfg(feature = "wasm")]
366    use bitwarden_crypto::CryptoError;
367
368    use super::*;
369    use crate::{Attachment, CipherRepromptType, CipherType, Login, VaultClientExt};
370
371    fn test_cipher() -> Cipher {
372        Cipher {
373            id: Some("358f2b2b-9326-4e5e-94a8-b18100bb0908".parse().unwrap()),
374            organization_id: None,
375            folder_id: None,
376            collection_ids: vec![],
377            key: None,
378            name: "2.+oPT8B4xJhyhQRe1VkIx0A==|PBtC/bZkggXR+fSnL/pG7g==|UkjRD0VpnUYkjRC/05ZLdEBAmRbr3qWRyJey2bUvR9w=".parse().unwrap(),
379            notes: None,
380            r#type: CipherType::Login,
381            login: Some(Login{
382                username: None,
383                password: None,
384                password_revision_date: None,
385                uris:None,
386                totp: None,
387                autofill_on_page_load: None,
388                fido2_credentials: None,
389            }),
390            identity: None,
391            card: None,
392            secure_note: None,
393            ssh_key: None,
394            bank_account: None,
395            drivers_license: None,
396            passport: None,
397            favorite: false,
398            reprompt: CipherRepromptType::None,
399            organization_use_totp: true,
400            edit: true,
401            permissions: None,
402            view_password: true,
403            local_data: None,
404            attachments: None,
405            fields:  None,
406            password_history: None,
407            creation_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
408            deleted_date: None,
409            revision_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
410            archived_date: None,
411            data: None,
412        }
413    }
414
415    #[cfg(feature = "wasm")]
416    fn test_cipher_view() -> CipherView {
417        let test_id = "fd411a1a-fec8-4070-985d-0e6560860e69".parse().unwrap();
418        CipherView {
419            r#type: CipherType::Login,
420            login: Some(crate::LoginView {
421                username: Some("test_username".to_string()),
422                password: Some("test_password".to_string()),
423                password_revision_date: None,
424                uris: None,
425                totp: None,
426                autofill_on_page_load: None,
427                fido2_credentials: None,
428            }),
429            id: Some(test_id),
430            organization_id: None,
431            folder_id: None,
432            collection_ids: vec![],
433            key: None,
434            name: "My test login".to_string(),
435            notes: None,
436            identity: None,
437            card: None,
438            secure_note: None,
439            ssh_key: None,
440            bank_account: None,
441            drivers_license: None,
442            passport: None,
443            favorite: false,
444            reprompt: CipherRepromptType::None,
445            organization_use_totp: true,
446            edit: true,
447            permissions: None,
448            view_password: true,
449            local_data: None,
450            attachments: None,
451            attachment_decryption_failures: None,
452            fields: None,
453            password_history: None,
454            creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
455            deleted_date: None,
456            revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
457            archived_date: None,
458        }
459    }
460
461    fn test_attachment_legacy() -> Attachment {
462        Attachment {
463            id: Some("uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
464            url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
465            file_name: Some("2.mV50WiLq6duhwGbhM1TO0A==|dTufWNH8YTPP0EMlNLIpFA==|QHp+7OM8xHtEmCfc9QPXJ0Ro2BeakzvLgxJZ7NdLuDc=".parse().unwrap()),
466            key: None,
467            size: Some("65".to_string()),
468            size_name: Some("65 Bytes".to_string()),
469        }
470    }
471
472    fn test_attachment_v2() -> Attachment {
473        Attachment {
474            id: Some("a77m56oerrz5b92jm05lq5qoyj1xh2t9".to_string()),
475            url: Some("http://localhost:4000/attachments//358f2b2b-9326-4e5e-94a8-b18100bb0908/uf7bkexzag04d3cw04jsbqqkbpbwhxs0".to_string()),
476            file_name: Some("2.GhazFdCYQcM5v+AtVwceQA==|98bMUToqC61VdVsSuXWRwA==|bsLByMht9Hy5QO9pPMRz0K4d0aqBiYnnROGM5YGbNu4=".parse().unwrap()),
477            key: Some("2.6TPEiYULFg/4+3CpDRwCqw==|6swweBHCJcd5CHdwBBWuRN33XRV22VoroDFDUmiM4OzjPEAhgZK57IZS1KkBlCcFvT+t+YbsmDcdv+Lqr+iJ3MmzfJ40MCB5TfYy+22HVRA=|rkgFDh2IWTfPC1Y66h68Diiab/deyi1p/X0Fwkva0NQ=".parse().unwrap()),
478            size: Some("65".to_string()),
479            size_name: Some("65 Bytes".to_string()),
480        }
481    }
482
483    #[tokio::test]
484    async fn test_decrypt_list() {
485        let client = Client::init_test_account(test_bitwarden_com_account()).await;
486
487        let dec = client
488            .vault()
489            .ciphers()
490            .decrypt_list(vec![Cipher {
491                id: Some("a1569f46-0797-4d3f-b859-b181009e2e49".parse().unwrap()),
492                organization_id: Some("1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap()),
493                folder_id: None,
494                collection_ids: vec!["66c5ca57-0868-4c7e-902f-b181009709c0".parse().unwrap()],
495                key: None,
496                name: "2.RTdUGVWYl/OZHUMoy68CMg==|sCaT5qHx8i0rIvzVrtJKww==|jB8DsRws6bXBtXNfNXUmFJ0JLDlB6GON6Y87q0jgJ+0=".parse().unwrap(),
497                notes: None,
498                r#type: CipherType::Login,
499                login: Some(Login{
500                    username: Some("2.ouEYEk+SViUtqncesfe9Ag==|iXzEJq1zBeNdDbumFO1dUA==|RqMoo9soSwz/yB99g6YPqk8+ASWRcSdXsKjbwWzyy9U=".parse().unwrap()),
501                    password: Some("2.6yXnOz31o20Z2kiYDnXueA==|rBxTb6NK9lkbfdhrArmacw==|ogZir8Z8nLgiqlaLjHH+8qweAtItS4P2iPv1TELo5a0=".parse().unwrap()),
502                    password_revision_date: None, uris:None, totp: None, autofill_on_page_load: None, fido2_credentials: None }),
503                identity: None,
504                card: None,
505                secure_note: None,
506                ssh_key: None,
507                bank_account: None,
508                drivers_license: None,
509                passport: None,
510                favorite: false,
511                reprompt: CipherRepromptType::None,
512                organization_use_totp: true,
513                edit: true,
514                permissions: None,
515                view_password: true,
516                local_data: None,
517                attachments: None,
518                fields:  None,
519                password_history: None,
520                creation_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
521                deleted_date: None,
522                revision_date: "2024-05-31T09:35:55.12Z".parse().unwrap(),
523                archived_date: None,
524                data: None,
525            }])
526            .await
527            .unwrap();
528
529        assert_eq!(dec[0].name, "Test item");
530    }
531
532    #[tokio::test]
533    async fn test_decrypt_list_with_failures_all_success() {
534        let client = Client::init_test_account(test_bitwarden_com_account()).await;
535
536        let valid_cipher = test_cipher();
537
538        let result = client
539            .vault()
540            .ciphers()
541            .decrypt_list_with_failures(vec![valid_cipher])
542            .await;
543
544        assert_eq!(result.successes.len(), 1);
545        assert!(result.failures.is_empty());
546        assert_eq!(result.successes[0].name, "234234");
547    }
548
549    #[tokio::test]
550    async fn test_decrypt_list_with_failures_mixed_results() {
551        let client = Client::init_test_account(test_bitwarden_com_account()).await;
552        let valid_cipher = test_cipher();
553        let mut invalid_cipher = test_cipher();
554        // Set an invalid encryptedkey to cause decryption failure
555        invalid_cipher.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
556
557        let ciphers = vec![valid_cipher, invalid_cipher.clone()];
558
559        let result = client
560            .vault()
561            .ciphers()
562            .decrypt_list_with_failures(ciphers)
563            .await;
564
565        assert_eq!(result.successes.len(), 1);
566        assert_eq!(result.failures.len(), 1);
567
568        assert_eq!(result.successes[0].name, "234234");
569    }
570
571    #[tokio::test]
572    async fn test_move_user_cipher_with_attachment_without_key_to_org_fails() {
573        let client = Client::init_test_account(test_bitwarden_com_account()).await;
574
575        let mut cipher = test_cipher();
576        cipher.attachments = Some(vec![test_attachment_legacy()]);
577
578        let view = client
579            .vault()
580            .ciphers()
581            .decrypt(cipher.clone())
582            .await
583            .unwrap();
584
585        //  Move cipher to organization
586        let res = client.vault().ciphers().move_to_organization(
587            view,
588            "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
589        );
590
591        assert!(res.is_err());
592    }
593
594    #[tokio::test]
595    async fn test_encrypt_cipher_with_legacy_attachment_without_key() {
596        let client = Client::init_test_account(test_bitwarden_com_account()).await;
597
598        let mut cipher = test_cipher();
599        let attachment = test_attachment_legacy();
600        cipher.attachments = Some(vec![attachment.clone()]);
601
602        let view = client
603            .vault()
604            .ciphers()
605            .decrypt(cipher.clone())
606            .await
607            .unwrap();
608
609        assert!(cipher.key.is_none());
610
611        // Assert the cipher has a key, and the attachment is still readable
612        let EncryptionContext {
613            cipher: new_cipher,
614            encrypted_for: _,
615        } = client.vault().ciphers().encrypt(view).await.unwrap();
616        assert!(new_cipher.key.is_some());
617
618        let view = client.vault().ciphers().decrypt(new_cipher).await.unwrap();
619        let attachments = view.clone().attachments.unwrap();
620        let attachment_view = attachments.first().unwrap().clone();
621        assert!(attachment_view.key.is_none());
622
623        assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
624
625        let buf = vec![
626            2, 100, 205, 148, 152, 77, 184, 77, 53, 80, 38, 240, 83, 217, 251, 118, 254, 27, 117,
627            41, 148, 244, 216, 110, 216, 255, 104, 215, 23, 15, 176, 239, 208, 114, 95, 159, 23,
628            211, 98, 24, 145, 166, 60, 197, 42, 204, 131, 144, 253, 204, 195, 154, 27, 201, 215,
629            43, 10, 244, 107, 226, 152, 85, 167, 66, 185,
630        ];
631
632        let content = client
633            .vault()
634            .attachments()
635            .decrypt_buffer(cipher, attachment_view.clone(), buf.as_slice())
636            .unwrap();
637
638        assert_eq!(content, b"Hello");
639    }
640
641    #[tokio::test]
642    async fn test_encrypt_cipher_with_v1_attachment_without_key() {
643        let client = Client::init_test_account(test_bitwarden_com_account()).await;
644
645        let mut cipher = test_cipher();
646        let attachment = test_attachment_v2();
647        cipher.attachments = Some(vec![attachment.clone()]);
648
649        let view = client
650            .vault()
651            .ciphers()
652            .decrypt(cipher.clone())
653            .await
654            .unwrap();
655
656        assert!(cipher.key.is_none());
657
658        // Assert the cipher has a key, and the attachment is still readable
659        let EncryptionContext {
660            cipher: new_cipher,
661            encrypted_for: _,
662        } = client.vault().ciphers().encrypt(view).await.unwrap();
663        assert!(new_cipher.key.is_some());
664
665        let view = client
666            .vault()
667            .ciphers()
668            .decrypt(new_cipher.clone())
669            .await
670            .unwrap();
671        let attachments = view.clone().attachments.unwrap();
672        let attachment_view = attachments.first().unwrap().clone();
673        assert!(attachment_view.key.is_some());
674
675        // Ensure attachment key is updated since it's now protected by the cipher key
676        assert_ne!(
677            attachment.clone().key.unwrap().to_string(),
678            attachment_view.clone().key.unwrap().to_string()
679        );
680
681        assert_eq!(attachment_view.file_name.as_deref(), Some("h.txt"));
682
683        let buf = vec![
684            2, 114, 53, 72, 20, 82, 18, 46, 48, 137, 97, 1, 100, 142, 120, 187, 28, 36, 180, 46,
685            189, 254, 133, 23, 169, 58, 73, 212, 172, 116, 185, 127, 111, 92, 112, 145, 99, 28,
686            158, 198, 48, 241, 121, 218, 66, 37, 152, 197, 122, 241, 110, 82, 245, 72, 47, 230, 95,
687            188, 196, 170, 127, 67, 44, 129, 90,
688        ];
689
690        let content = client
691            .vault()
692            .attachments()
693            .decrypt_buffer(new_cipher.clone(), attachment_view.clone(), buf.as_slice())
694            .unwrap();
695
696        assert_eq!(content, b"Hello");
697
698        // Move cipher to organization
699        let new_view = client
700            .vault()
701            .ciphers()
702            .move_to_organization(
703                view,
704                "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
705            )
706            .unwrap();
707        let EncryptionContext {
708            cipher: new_cipher,
709            encrypted_for: _,
710        } = client.vault().ciphers().encrypt(new_view).await.unwrap();
711
712        let attachment = new_cipher
713            .clone()
714            .attachments
715            .unwrap()
716            .first()
717            .unwrap()
718            .clone();
719
720        // Ensure attachment key is still the same since it's protected by the cipher key
721        assert_eq!(
722            attachment.clone().key.as_ref().unwrap().to_string(),
723            attachment_view.key.as_ref().unwrap().to_string()
724        );
725
726        let content = client
727            .vault()
728            .attachments()
729            .decrypt_buffer(new_cipher, attachment_view, buf.as_slice())
730            .unwrap();
731
732        assert_eq!(content, b"Hello");
733    }
734
735    #[tokio::test]
736    #[cfg(feature = "wasm")]
737    async fn test_decrypt_list_full_with_failures_all_success() {
738        let client = Client::init_test_account(test_bitwarden_com_account()).await;
739
740        let valid_cipher = test_cipher();
741
742        let result = client
743            .vault()
744            .ciphers()
745            .decrypt_list_full_with_failures(vec![valid_cipher])
746            .await;
747
748        assert_eq!(result.successes.len(), 1);
749        assert!(result.failures.is_empty());
750        assert_eq!(result.successes[0].name, "234234");
751    }
752
753    #[tokio::test]
754    #[cfg(feature = "wasm")]
755    async fn test_decrypt_list_full_with_failures_mixed_results() {
756        let client = Client::init_test_account(test_bitwarden_com_account()).await;
757        let valid_cipher = test_cipher();
758        let mut invalid_cipher = test_cipher();
759        // Set an invalid encrypted key to cause decryption failure
760        invalid_cipher.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
761
762        let ciphers = vec![valid_cipher, invalid_cipher.clone()];
763
764        let result = client
765            .vault()
766            .ciphers()
767            .decrypt_list_full_with_failures(ciphers)
768            .await;
769
770        assert_eq!(result.successes.len(), 1);
771        assert_eq!(result.failures.len(), 1);
772
773        assert_eq!(result.successes[0].name, "234234");
774    }
775
776    #[tokio::test]
777    #[cfg(feature = "wasm")]
778    async fn test_decrypt_list_full_with_failures_all_failures() {
779        let client = Client::init_test_account(test_bitwarden_com_account()).await;
780        let mut invalid_cipher1 = test_cipher();
781        let mut invalid_cipher2 = test_cipher();
782        // Set invalid encrypted keys to cause decryption failures
783        invalid_cipher1.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
784        invalid_cipher2.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".parse().unwrap());
785
786        let ciphers = vec![invalid_cipher1, invalid_cipher2];
787
788        let result = client
789            .vault()
790            .ciphers()
791            .decrypt_list_full_with_failures(ciphers)
792            .await;
793
794        assert!(result.successes.is_empty());
795        assert_eq!(result.failures.len(), 2);
796    }
797
798    #[tokio::test]
799    #[cfg(feature = "wasm")]
800    async fn test_decrypt_list_full_with_failures_empty_list() {
801        let client = Client::init_test_account(test_bitwarden_com_account()).await;
802
803        let result = client
804            .vault()
805            .ciphers()
806            .decrypt_list_full_with_failures(vec![])
807            .await;
808
809        assert!(result.successes.is_empty());
810        assert!(result.failures.is_empty());
811    }
812
813    #[tokio::test]
814    #[cfg(feature = "wasm")]
815    async fn test_encrypt_cipher_for_rotation() {
816        let client = Client::init_test_account(test_bitwarden_com_account()).await;
817
818        let new_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
819
820        let cipher_view = test_cipher_view();
821        let new_key_b64 = new_key.to_base64();
822
823        let ctx = client
824            .vault()
825            .ciphers()
826            .encrypt_cipher_for_rotation(cipher_view, new_key_b64)
827            .await
828            .unwrap();
829
830        assert!(ctx.cipher.key.is_some());
831
832        // Decrypting the cipher "normally" will fail because it was encrypted with a new key
833        assert!(matches!(
834            client.vault().ciphers().decrypt(ctx.cipher).await.err(),
835            Some(DecryptError::Crypto(CryptoError::Decrypt))
836        ));
837    }
838
839    #[cfg(feature = "wasm")]
840    #[tokio::test]
841    async fn test_encrypt_list() {
842        let client = Client::init_test_account(test_bitwarden_com_account()).await;
843
844        let cipher_views = vec![test_cipher_view(), test_cipher_view()];
845
846        let result = client.vault().ciphers().encrypt_list(cipher_views).await;
847
848        assert!(result.is_ok());
849        let contexts = result.unwrap();
850        assert_eq!(contexts.len(), 2);
851
852        // Verify each encrypted cipher has a key (cipher key encryption is enabled)
853        for ctx in &contexts {
854            assert!(ctx.cipher.key.is_some());
855        }
856    }
857
858    #[cfg(feature = "wasm")]
859    #[tokio::test]
860    async fn test_encrypt_list_empty() {
861        let client = Client::init_test_account(test_bitwarden_com_account()).await;
862
863        let result = client.vault().ciphers().encrypt_list(vec![]).await;
864
865        assert!(result.is_ok());
866        assert!(result.unwrap().is_empty());
867    }
868
869    #[cfg(feature = "wasm")]
870    #[tokio::test]
871    async fn test_encrypt_list_roundtrip() {
872        let client = Client::init_test_account(test_bitwarden_com_account()).await;
873
874        let original_views = vec![test_cipher_view(), test_cipher_view()];
875        let original_names: Vec<_> = original_views.iter().map(|v| v.name.clone()).collect();
876
877        let contexts = client
878            .vault()
879            .ciphers()
880            .encrypt_list(original_views)
881            .await
882            .unwrap();
883
884        // Decrypt each cipher and verify the name matches
885        for (ctx, original_name) in contexts.iter().zip(original_names.iter()) {
886            let decrypted = client
887                .vault()
888                .ciphers()
889                .decrypt(ctx.cipher.clone())
890                .await
891                .unwrap();
892            assert_eq!(&decrypted.name, original_name);
893        }
894    }
895
896    #[cfg(feature = "wasm")]
897    #[tokio::test]
898    async fn test_encrypt_list_preserves_user_id() {
899        let client = Client::init_test_account(test_bitwarden_com_account()).await;
900
901        let expected_user_id = client.internal.get_user_id().unwrap();
902
903        let cipher_views = vec![test_cipher_view(), test_cipher_view(), test_cipher_view()];
904        let contexts = client
905            .vault()
906            .ciphers()
907            .encrypt_list(cipher_views)
908            .await
909            .unwrap();
910
911        for ctx in contexts {
912            assert_eq!(ctx.encrypted_for, expected_user_id);
913        }
914    }
915
916    #[tokio::test]
917    async fn should_use_blob_encryption_individual_above_threshold_returns_true() {
918        let client = Client::init_test_account(test_bitwarden_com_account()).await;
919        client
920            .internal
921            .get_key_store()
922            .set_security_state_version(BLOB_SECURITY_VERSION);
923
924        assert!(client.vault().ciphers().should_use_blob_encryption(None));
925    }
926
927    #[tokio::test]
928    async fn should_use_blob_encryption_individual_below_threshold_returns_false() {
929        let client = Client::init_test_account(test_bitwarden_com_account()).await;
930        // Default KeyStore security_state_version is 1, below BLOB_SECURITY_VERSION (2).
931
932        assert!(!client.vault().ciphers().should_use_blob_encryption(None));
933    }
934
935    #[tokio::test]
936    async fn should_use_blob_encryption_organization_returns_false() {
937        let client = Client::init_test_account(test_bitwarden_com_account()).await;
938        client
939            .internal
940            .get_key_store()
941            .set_security_state_version(BLOB_SECURITY_VERSION);
942        let org_id: OrganizationId = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap();
943
944        assert!(
945            !client
946                .vault()
947                .ciphers()
948                .should_use_blob_encryption(Some(org_id))
949        );
950    }
951}