Skip to main content

bitwarden_vault/cipher/cipher_client/admin/
restore.rs

1use bitwarden_api_api::{apis::ApiClient, models::CipherBulkRestoreRequestModel};
2use bitwarden_core::{ApiError, OrganizationId, key_management::KeySlotIds};
3use bitwarden_crypto::{CryptoError, KeyStore};
4use bitwarden_error::bitwarden_error;
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::wasm_bindgen;
8
9use crate::{
10    Cipher, CipherId, CipherView, DecryptCipherListResult, VaultParseError,
11    cipher::cipher::{PartialCipher, StrictDecrypt},
12    cipher_client::admin::CipherAdminClient,
13};
14
15#[allow(missing_docs)]
16#[bitwarden_error(flat)]
17#[derive(Debug, Error)]
18pub enum RestoreCipherAdminError {
19    #[error(transparent)]
20    Api(#[from] ApiError),
21    #[error(transparent)]
22    VaultParse(#[from] VaultParseError),
23    #[error(transparent)]
24    Crypto(#[from] CryptoError),
25}
26
27impl<T> From<bitwarden_api_api::apis::Error<T>> for RestoreCipherAdminError {
28    fn from(val: bitwarden_api_api::apis::Error<T>) -> Self {
29        Self::Api(val.into())
30    }
31}
32
33/// Restores a soft-deleted cipher on the server, using the admin endpoint.
34pub async fn restore_as_admin(
35    cipher_id: CipherId,
36    api_client: &ApiClient,
37    key_store: &KeyStore<KeySlotIds>,
38    use_strict_decryption: bool,
39) -> Result<CipherView, RestoreCipherAdminError> {
40    let api = api_client.ciphers_api();
41
42    let cipher: Cipher = api
43        .put_restore_admin(cipher_id.into())
44        .await?
45        .merge_with_cipher(None)?;
46
47    if use_strict_decryption {
48        Ok(key_store.decrypt(&StrictDecrypt(cipher))?)
49    } else {
50        Ok(key_store.decrypt(&cipher)?)
51    }
52}
53
54/// Restores multiple soft-deleted ciphers on the server.
55pub async fn restore_many_as_admin(
56    cipher_ids: Vec<CipherId>,
57    org_id: OrganizationId,
58    api_client: &ApiClient,
59    key_store: &KeyStore<KeySlotIds>,
60) -> Result<DecryptCipherListResult, RestoreCipherAdminError> {
61    let api = api_client.ciphers_api();
62
63    let ciphers: Vec<Cipher> = api
64        .put_restore_many_admin(Some(CipherBulkRestoreRequestModel {
65            ids: cipher_ids.into_iter().map(|id| id.to_string()).collect(),
66            organization_id: Some(org_id.into()),
67        }))
68        .await?
69        .data
70        .into_iter()
71        .flatten()
72        .map(|c| c.merge_with_cipher(None))
73        .collect::<Result<Vec<_>, _>>()?;
74
75    let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
76    Ok(DecryptCipherListResult {
77        successes,
78        failures: failures.into_iter().cloned().collect(),
79    })
80}
81
82#[cfg_attr(feature = "wasm", wasm_bindgen)]
83impl CipherAdminClient {
84    /// Restores a soft-deleted cipher on the server, using the admin endpoint.
85    pub async fn restore(
86        &self,
87        cipher_id: CipherId,
88    ) -> Result<CipherView, RestoreCipherAdminError> {
89        let api_client = &self.client.internal.get_api_configurations().api_client;
90        let key_store = self.client.internal.get_key_store();
91
92        restore_as_admin(
93            cipher_id,
94            api_client,
95            key_store,
96            self.is_strict_decrypt().await,
97        )
98        .await
99    }
100    /// Restores multiple soft-deleted ciphers on the server.
101    pub async fn restore_many(
102        &self,
103        cipher_ids: Vec<CipherId>,
104        org_id: OrganizationId,
105    ) -> Result<DecryptCipherListResult, RestoreCipherAdminError> {
106        let api_client = &self.client.internal.get_api_configurations().api_client;
107        let key_store = self.client.internal.get_key_store();
108
109        restore_many_as_admin(cipher_ids, org_id, api_client, key_store).await
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use bitwarden_api_api::{
116        apis::ApiClient,
117        models::{CipherMiniResponseModel, CipherMiniResponseModelListResponseModel},
118    };
119    use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
120    use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
121    use chrono::Utc;
122
123    use super::*;
124    use crate::{Cipher, CipherId, Login};
125
126    const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
127    const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
128    const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
129
130    fn generate_test_cipher() -> Cipher {
131        Cipher {
132            id: TEST_CIPHER_ID.parse().ok(),
133            name: "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap(),
134            r#type: crate::CipherType::Login,
135            notes: Default::default(),
136            organization_id: Default::default(),
137            folder_id: Default::default(),
138            favorite: Default::default(),
139            reprompt: Default::default(),
140            fields: Default::default(),
141            collection_ids: Default::default(),
142            key: Default::default(),
143            login: Some(Login{
144                username: None,
145                password: None,
146                password_revision_date: None,
147                uris: None, totp: None,
148                autofill_on_page_load: None,
149                fido2_credentials: None,
150            }),
151            identity: Default::default(),
152            card: Default::default(),
153            secure_note: Default::default(),
154            ssh_key: Default::default(),
155            bank_account: Default::default(),
156            organization_use_totp: Default::default(),
157            edit: Default::default(),
158            permissions: Default::default(),
159            view_password: Default::default(),
160            local_data: Default::default(),
161            attachments: Default::default(),
162            password_history: Default::default(),
163            creation_date: Default::default(),
164            deleted_date: Default::default(),
165            revision_date: Default::default(),
166            archived_date: Default::default(),
167            data: Default::default(),
168        }
169    }
170
171    #[tokio::test]
172    async fn test_restore_as_admin() {
173        let mut cipher = generate_test_cipher();
174        cipher.deleted_date = Some(Utc::now());
175
176        let api_client = {
177            let cipher = cipher.clone();
178            ApiClient::new_mocked(move |mock| {
179                mock.ciphers_api
180                    .expect_put_restore_admin()
181                    .returning(move |_model| {
182                        Ok(CipherMiniResponseModel {
183                            id: Some(TEST_CIPHER_ID.try_into().unwrap()),
184                            name: Some(cipher.name.to_string()),
185                            r#type: Some(cipher.r#type.into()),
186                            creation_date: Some(cipher.creation_date.to_string()),
187                            revision_date: Some(Utc::now().to_rfc3339()),
188                            login: cipher.login.clone().map(|l| Box::new(l.into())),
189                            ..Default::default()
190                        })
191                    });
192            })
193        };
194
195        let store: KeyStore<KeySlotIds> = KeyStore::default();
196        #[allow(deprecated)]
197        let _ = store.context_mut().set_symmetric_key(
198            SymmetricKeySlotId::User,
199            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
200        );
201        let start_time = Utc::now();
202        let updated_cipher =
203            restore_as_admin(TEST_CIPHER_ID.parse().unwrap(), &api_client, &store, false)
204                .await
205                .unwrap();
206        let end_time = Utc::now();
207
208        assert!(updated_cipher.deleted_date.is_none());
209        assert!(
210            updated_cipher.revision_date >= start_time && updated_cipher.revision_date <= end_time
211        );
212    }
213
214    #[tokio::test]
215    async fn test_restore_many_as_admin() {
216        let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
217        let mut cipher_1 = generate_test_cipher();
218        cipher_1.deleted_date = Some(Utc::now());
219        let mut cipher_2 = generate_test_cipher();
220        cipher_2.deleted_date = Some(Utc::now());
221        cipher_2.id = Some(cipher_id_2);
222
223        let api_client = ApiClient::new_mocked(move |mock| {
224            mock.ciphers_api
225                .expect_put_restore_many_admin()
226                .returning(move |_model| {
227                    Ok(CipherMiniResponseModelListResponseModel {
228                        object: None,
229                        data: Some(vec![
230                            CipherMiniResponseModel {
231                                id: cipher_1.id.map(|id| id.into()),
232                                name: Some(cipher_1.name.to_string()),
233                                r#type: Some(cipher_1.r#type.into()),
234                                login: cipher_1.login.clone().map(|l| Box::new(l.into())),
235                                creation_date: cipher_1.creation_date.to_string().into(),
236                                deleted_date: None,
237                                revision_date: Some(Utc::now().to_rfc3339()),
238                                ..Default::default()
239                            },
240                            CipherMiniResponseModel {
241                                id: cipher_2.id.map(|id| id.into()),
242                                name: Some(cipher_2.name.to_string()),
243                                r#type: Some(cipher_2.r#type.into()),
244                                login: cipher_2.login.clone().map(|l| Box::new(l.into())),
245                                creation_date: cipher_2.creation_date.to_string().into(),
246                                deleted_date: None,
247                                revision_date: Some(Utc::now().to_rfc3339()),
248                                ..Default::default()
249                            },
250                        ]),
251                        continuation_token: None,
252                    })
253                });
254        });
255        let store: KeyStore<KeySlotIds> = KeyStore::default();
256        #[allow(deprecated)]
257        let _ = store.context_mut().set_symmetric_key(
258            SymmetricKeySlotId::User,
259            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
260        );
261
262        let start_time = Utc::now();
263        let ciphers = restore_many_as_admin(
264            vec![
265                TEST_CIPHER_ID.parse().unwrap(),
266                TEST_CIPHER_ID_2.parse().unwrap(),
267            ],
268            TEST_ORG_ID.parse().unwrap(),
269            &api_client,
270            &store,
271        )
272        .await
273        .unwrap();
274        let end_time = Utc::now();
275
276        assert_eq!(ciphers.successes.len(), 2,);
277        assert_eq!(ciphers.failures.len(), 0,);
278        assert_eq!(
279            ciphers.successes[0].id,
280            Some(TEST_CIPHER_ID.parse().unwrap()),
281        );
282        assert_eq!(
283            ciphers.successes[1].id,
284            Some(TEST_CIPHER_ID_2.parse().unwrap()),
285        );
286        assert_eq!(ciphers.successes[0].deleted_date, None,);
287        assert_eq!(ciphers.successes[1].deleted_date, None,);
288
289        assert!(
290            ciphers.successes[0].revision_date >= start_time
291                && ciphers.successes[0].revision_date <= end_time
292        );
293        assert!(
294            ciphers.successes[1].revision_date >= start_time
295                && ciphers.successes[1].revision_date <= end_time
296        );
297    }
298}