Skip to main content

bitwarden_vault/cipher/cipher_client/
restore.rs

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