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